[med-svn] [libjloda-java] 01/02: New upstream version 0.0+20161018

Andreas Tille tille at debian.org
Wed Nov 2 20:56:19 UTC 2016


This is an automated email from the git hooks/post-receive script.

tille pushed a commit to branch master
in repository libjloda-java.

commit 9771605f2e4e2a018cbd552b884fe57987d7bd20
Author: Andreas Tille <tille at debian.org>
Date:   Wed Nov 2 21:54:35 2016 +0100

    New upstream version 0.0+20161018
---
 .gitignore                                         |   27 +
 LICENSE                                            |    5 +
 README.md                                          |    7 +
 antbuild/build.xml                                 |   65 +
 jars/gnujpdf-license.txt                           |  460 +++
 jars/gnujpdf-src.jar                               |  Bin 0 -> 75488 bytes
 jars/gnujpdf.jar                                   |  Bin 0 -> 59551 bytes
 src/jloda/export/EPSExportType.java                |  182 +
 src/jloda/export/EPSGraphics.java                  |  888 +++++
 src/jloda/export/ExportGraphicType.java            |   93 +
 src/jloda/export/ExportImageDialog.java            |  370 ++
 src/jloda/export/ExportManager.java                |  146 +
 src/jloda/export/GIFExportType.java                |  162 +
 src/jloda/export/GraphicsFileFilters.java          |  206 +
 src/jloda/export/JPGExportType.java                |  172 +
 src/jloda/export/PDFExportType.java                |  186 +
 src/jloda/export/PNGExportType.java                |  165 +
 src/jloda/export/RenderedExportType.java           |  163 +
 src/jloda/export/SVGExportType.java                |  180 +
 src/jloda/export/SVGStringExportType.java          |  124 +
 src/jloda/export/SaveImageDialog.java              |  228 ++
 src/jloda/export/TransferableGraphic.java          |  140 +
 src/jloda/export/gifEncode/DirectGif89Frame.java   |  101 +
 src/jloda/export/gifEncode/Gif89Encoder.java       |  732 ++++
 src/jloda/export/gifEncode/Gif89Frame.java         |  603 +++
 src/jloda/export/gifEncode/IndexGif89Frame.java    |   70 +
 src/jloda/export/gifEncode/Put.java                |   61 +
 src/jloda/graph/Dijkstra.java                      |  130 +
 src/jloda/graph/DirectedCycleDetector.java         |  111 +
 src/jloda/graph/Edge.java                          |  433 +++
 src/jloda/graph/EdgeArray.java                     |  198 +
 src/jloda/graph/EdgeAssociation.java               |   85 +
 src/jloda/graph/EdgeDoubleArray.java               |  106 +
 src/jloda/graph/EdgeDoubleMap.java                 |  106 +
 src/jloda/graph/EdgeIntegerArray.java              |  106 +
 src/jloda/graph/EdgeIntegerMap.java                |  107 +
 src/jloda/graph/EdgeMap.java                       |  168 +
 src/jloda/graph/EdgeSet.java                       |  340 ++
 src/jloda/graph/FruchtermanReingoldLayout.java     |  212 ++
 src/jloda/graph/Graph.java                         | 1735 +++++++++
 src/jloda/graph/GraphBase.java                     |   74 +
 src/jloda/graph/GraphUpdateAdapter.java            |   69 +
 src/jloda/graph/GraphUpdateListener.java           |   64 +
 src/jloda/graph/IllegalSelfEdgeException.java      |   27 +
 src/jloda/graph/MaxClique.java                     |  335 ++
 src/jloda/graph/Node.java                          |  639 ++++
 src/jloda/graph/NodeArray.java                     |  207 +
 src/jloda/graph/NodeAssociation.java               |   84 +
 src/jloda/graph/NodeData.java                      |  136 +
 src/jloda/graph/NodeDoubleArray.java               |  119 +
 src/jloda/graph/NodeDoubleMap.java                 |  106 +
 src/jloda/graph/NodeEdge.java                      |  132 +
 src/jloda/graph/NodeEdgeEnumeration.java           |   49 +
 src/jloda/graph/NodeIntegerArray.java              |  105 +
 src/jloda/graph/NodeIntegerMap.java                |  106 +
 src/jloda/graph/NodeMap.java                       |  167 +
 src/jloda/graph/NodeSet.java                       |  390 ++
 src/jloda/graph/Num2EdgeArray.java                 |  109 +
 src/jloda/graph/Num2NodeArray.java                 |  110 +
 src/jloda/graphview/DefaultGraphDrawer.java        |  652 ++++
 src/jloda/graphview/DefaultNodeDrawer.java         |  353 ++
 src/jloda/graphview/EdgeActionAdapter.java         |  108 +
 src/jloda/graphview/EdgeActionListener.java        |  108 +
 src/jloda/graphview/EdgeView.java                  | 1141 ++++++
 src/jloda/graphview/GraphEditor.java               |  545 +++
 src/jloda/graphview/GraphView.java                 | 3847 +++++++++++++++++++
 src/jloda/graphview/GraphViewBase.java             |   32 +
 src/jloda/graphview/GraphViewListener.java         | 1247 ++++++
 src/jloda/graphview/IGraphDrawer.java              |  225 ++
 src/jloda/graphview/IGraphViewListener.java        |   27 +
 src/jloda/graphview/INodeDrawer.java               |   61 +
 src/jloda/graphview/INodeEdgeFormatable.java       |  143 +
 src/jloda/graphview/IPopupListener.java            |   70 +
 src/jloda/graphview/ITransformChangeListener.java  |   28 +
 src/jloda/graphview/LabelLayoutRTree.java          |   87 +
 src/jloda/graphview/LabelLayouter.java             |  267 ++
 src/jloda/graphview/LabelOverlapAvoider.java       |  185 +
 src/jloda/graphview/Magnifier.java                 |  491 +++
 src/jloda/graphview/MagnifierUtil.java             |  124 +
 src/jloda/graphview/NodeActionAdapter.java         |  121 +
 src/jloda/graphview/NodeActionListener.java        |  117 +
 src/jloda/graphview/NodeImage.java                 |  263 ++
 src/jloda/graphview/NodeView.java                  | 1001 +++++
 src/jloda/graphview/PanelActionListener.java       |   30 +
 src/jloda/graphview/ScrollPaneAdjuster.java        |  104 +
 src/jloda/graphview/Transform.java                 |  823 ++++
 src/jloda/graphview/ViewBase.java                  |  397 ++
 src/jloda/gui/About.java                           |  275 ++
 src/jloda/gui/ActionJList.java                     |   73 +
 src/jloda/gui/AppleStuff.java                      |  109 +
 src/jloda/gui/ChooseColorDialog.java               |   67 +
 src/jloda/gui/ChooseFileDialog.java                |  299 ++
 src/jloda/gui/ChooseFontDialog.java                |  237 ++
 src/jloda/gui/ColorTable.java                      |  179 +
 src/jloda/gui/ColorTableManager.java               |  221 ++
 src/jloda/gui/DefaultLabelGetter.java              |   37 +
 src/jloda/gui/GraphViewPopupListener.java          |  141 +
 src/jloda/gui/HistogramPanel.java                  |  502 +++
 src/jloda/gui/ILabelGetter.java                    |   34 +
 src/jloda/gui/IMenuModifier.java                   |   32 +
 src/jloda/gui/IPopupMenuModifier.java              |   32 +
 src/jloda/gui/IToolBarModifier.java                |   32 +
 src/jloda/gui/ListTransferHandler.java             |  184 +
 src/jloda/gui/MemoryUsageManager.java              |   80 +
 src/jloda/gui/MenuBar.java                         |  150 +
 src/jloda/gui/MenuConfiguration.java               |   69 +
 src/jloda/gui/Message.java                         |  211 ++
 src/jloda/gui/PopupMenu.java                       |   89 +
 src/jloda/gui/ProgressDialog.java                  |  566 +++
 src/jloda/gui/ReorderListDialog.java               |  483 +++
 src/jloda/gui/StatusBar.java                       |  185 +
 src/jloda/gui/ToolBar.java                         |  150 +
 src/jloda/gui/TwoInputOptionsPanel.java            |   72 +
 src/jloda/gui/WindowListenerAdapter.java           |   52 +
 src/jloda/gui/WrapLayout.java                      |  189 +
 src/jloda/gui/commands/CommandBase.java            |  298 ++
 src/jloda/gui/commands/CommandManager.java         |  890 +++++
 src/jloda/gui/commands/ICheckBoxCommand.java       |   41 +
 src/jloda/gui/commands/ICommand.java               |  199 +
 src/jloda/gui/commands/MenuCreator.java            |  333 ++
 src/jloda/gui/commands/TeXGenerator.java           |  158 +
 src/jloda/gui/commands/WrappedCheckBoxCommand.java |   57 +
 src/jloda/gui/commands/WrappedCommand.java         |  305 ++
 src/jloda/gui/director/IDirectableViewer.java      |   69 +
 src/jloda/gui/director/IDirector.java              |  164 +
 src/jloda/gui/director/IDirectorListener.java      |   72 +
 src/jloda/gui/director/IMainViewer.java            |   42 +
 .../gui/director/IProjectsChangedListener.java     |   31 +
 src/jloda/gui/director/IUpdateableView.java        |   30 +
 src/jloda/gui/director/IViewerWithFindToolBar.java |   47 +
 src/jloda/gui/director/IViewerWithLegend.java      |   36 +
 src/jloda/gui/director/ProjectManager.java         |  402 ++
 src/jloda/gui/find/CompositeObjectSearchers.java   |  279 ++
 src/jloda/gui/find/EdgeLabelSearcher.java          |  327 ++
 src/jloda/gui/find/EmptySearcher.java              |  106 +
 src/jloda/gui/find/FindToolBar.java                |  469 +++
 src/jloda/gui/find/FindWindow.java                 |  363 ++
 src/jloda/gui/find/IFindDialog.java                |   47 +
 src/jloda/gui/find/IObjectSearcher.java            |   98 +
 src/jloda/gui/find/ISearcher.java                  |   83 +
 src/jloda/gui/find/ITextSearcher.java              |   90 +
 src/jloda/gui/find/JListSearcher.java              |  271 ++
 src/jloda/gui/find/JTableSearcher.java             |  325 ++
 src/jloda/gui/find/JTreeSearcher.java              |  295 ++
 src/jloda/gui/find/NodeLabelSearcher.java          |  327 ++
 src/jloda/gui/find/SearchActions.java              |  445 +++
 src/jloda/gui/find/SearchManager.java              | 1204 ++++++
 src/jloda/gui/find/TableSearcher.java              |  244 ++
 src/jloda/gui/find/TextAreaSearcher.java           |  328 ++
 src/jloda/gui/format/Formatter.java                |  804 ++++
 src/jloda/gui/format/FormatterActions.java         |  871 +++++
 src/jloda/gui/format/FormatterMenuBar.java         |  113 +
 src/jloda/gui/format/IFormatterListener.java       |   33 +
 src/jloda/gui/message/MessageWindow.java           |  476 +++
 src/jloda/gui/message/MessageWindowActions.java    |  422 +++
 src/jloda/gui/message/MessageWindowMenuBar.java    |   97 +
 src/jloda/phylo/HomoplasyScore.java                |  181 +
 src/jloda/phylo/PhyloGraph.java                    | 1188 ++++++
 src/jloda/phylo/PhyloGraphView.java                |  649 ++++
 src/jloda/phylo/PhyloTree.java                     | 1271 +++++++
 src/jloda/phylo/PhyloTreeUtils.java                |  326 ++
 src/jloda/phylo/PhyloTreeView.java                 |  402 ++
 src/jloda/phylo/TreeDrawerAngled.java              |  236 ++
 src/jloda/phylo/TreeDrawerCircular.java            |  371 ++
 src/jloda/phylo/TreeDrawerParallel.java            |  242 ++
 src/jloda/phylo/TreeDrawerRadial.java              |  332 ++
 src/jloda/phylo/TreeParseException.java            |   44 +
 src/jloda/progs/ApproximateBinaryExpansion.java    |   67 +
 src/jloda/progs/ApproximateSquareRootOf2.java      |   61 +
 src/jloda/progs/CoverDigraph.java                  |  315 ++
 src/jloda/progs/Date2Number.java                   |   57 +
 src/jloda/progs/GeneEvolutionSimulator.java        |  289 ++
 src/jloda/progs/GraphPather.java                   |  209 ++
 src/jloda/progs/GraphViewDemo.java                 |  124 +
 src/jloda/progs/Gunzip.java                        |   46 +
 src/jloda/progs/ImageProcessor.java                |  154 +
 src/jloda/progs/JTableWithRowHeaders.java          |  106 +
 src/jloda/progs/Lines2FastA.java                   |   82 +
 src/jloda/progs/MABlocker.java                     |  617 +++
 src/jloda/progs/MASampler.java                     |   75 +
 src/jloda/progs/NewMABlocker.java                  |  509 +++
 src/jloda/progs/NextMABlocker.java                 |  648 ++++
 src/jloda/progs/QuasiMedianClosure.java            |  224 ++
 src/jloda/progs/QuasiMedianNetwork.java            |  872 +++++
 src/jloda/progs/RandomDNAGenerator.java            |  127 +
 src/jloda/progs/RandomizeLines.java                |   51 +
 src/jloda/progs/ReadTrimmer.java                   |   62 +
 src/jloda/progs/SharedGenesDistance.java           |  162 +
 src/jloda/progs/Tree2MeganCSV.java                 |  121 +
 src/jloda/progs/TreeViewDemo.java                  |  101 +
 src/jloda/progs/seq4.txt                           |    5 +
 src/jloda/util/Alert.java                          |   59 +
 src/jloda/util/ArgsOptions.java                    |  664 ++++
 src/jloda/util/Basic.java                          | 3960 ++++++++++++++++++++
 src/jloda/util/BlastFileFilter.java                |   51 +
 src/jloda/util/Cache.java                          |   67 +
 src/jloda/util/CanceledException.java              |   36 +
 src/jloda/util/Colors.java                         |  704 ++++
 src/jloda/util/CommandLineOptions.java             |  898 +++++
 src/jloda/util/ConvexHull.java                     |  175 +
 src/jloda/util/Correlation.java                    |  124 +
 src/jloda/util/Counter.java                        |  104 +
 src/jloda/util/Cursors.java                        |  149 +
 src/jloda/util/DNAComplexityMeasure.java           |  110 +
 src/jloda/util/DrawOval.java                       |  165 +
 src/jloda/util/EditDistance.java                   |  320 ++
 src/jloda/util/FastA.java                          |  231 ++
 src/jloda/util/FastaFileFilter.java                |   76 +
 src/jloda/util/FileFilter.java                     |   41 +
 src/jloda/util/FileFilterBase.java                 |  184 +
 src/jloda/util/FileInputIterator.java              |  294 ++
 src/jloda/util/FileIterator.java                   |  187 +
 src/jloda/util/GZipUtils.java                      |  100 +
 src/jloda/util/Geometry.java                       |  419 +++
 src/jloda/util/GrowlNetwork.java                   |  198 +
 src/jloda/util/HeatSpectrum.java                   |  560 +++
 src/jloda/util/ICloseableIterator.java             |   50 +
 src/jloda/util/IFileIterator.java                  |   33 +
 src/jloda/util/IStateChecker.java                  |   12 +
 src/jloda/util/IteratorAdapter.java                |   75 +
 src/jloda/util/License.java                        |  353 ++
 src/jloda/util/ListOfLongs.java                    |   69 +
 src/jloda/util/MenuMnemonics.java                  |   92 +
 src/jloda/util/MultiLineCellRenderer.java          |   82 +
 src/jloda/util/NexusFileFilter.java                |   48 +
 src/jloda/util/NotOwnerException.java              |   58 +
 src/jloda/util/Pair.java                           |  188 +
 src/jloda/util/PeakMemoryUsageMonitor.java         |   87 +
 src/jloda/util/PhylipUtils.java                    |  162 +
 src/jloda/util/PluginClassLoader.java              |  152 +
 src/jloda/util/PolygonDouble.java                  |  165 +
 src/jloda/util/ProgramProperties.java              |  549 +++
 src/jloda/util/ProgressCmdLine.java                |  136 +
 src/jloda/util/ProgressListener.java               |  102 +
 src/jloda/util/ProgressPercentage.java             |  208 +
 src/jloda/util/ProgressSilent.java                 |  127 +
 src/jloda/util/PropertiesListListener.java         |   45 +
 src/jloda/util/ProteinComplexityMeasure.java       |  158 +
 src/jloda/util/RTFFileFilter.java                  |   95 +
 src/jloda/util/RTree.java                          |  487 +++
 src/jloda/util/RandomGaussian.java                 |  160 +
 src/jloda/util/RememberingComboBox.java            |  181 +
 src/jloda/util/ResourceManager.java                |  417 +++
 src/jloda/util/RunLater.java                       |   63 +
 src/jloda/util/SequenceUtils.java                  |  574 +++
 src/jloda/util/Signer.java                         |  244 ++
 src/jloda/util/Single.java                         |  107 +
 src/jloda/util/State.java                          |   28 +
 src/jloda/util/Statistics.java                     |  107 +
 src/jloda/util/StreamGobbler.java                  |   75 +
 src/jloda/util/StringParser.java                   |  161 +
 src/jloda/util/Task.java                           |  103 +
 src/jloda/util/TemporaryFileSet.java               |   70 +
 src/jloda/util/TextFileFilter.java                 |   61 +
 src/jloda/util/TextPrinter.java                    |  131 +
 src/jloda/util/TextWindow.java                     |  329 ++
 src/jloda/util/TimeStamp.java                      |   39 +
 src/jloda/util/ToolTipHelper.java                  |   85 +
 src/jloda/util/Triplet.java                        |  150 +
 src/jloda/util/UsageException.java                 |   43 +
 src/jloda/util/lang/Language.java                  |   98 +
 src/jloda/util/lang/Translator.java                |  148 +
 src/jloda/util/parse/NexusStreamParser.java        | 1500 ++++++++
 src/jloda/util/parse/NexusStreamTokenizer.java     |  447 +++
 src/jloda/util/shapes/TaxaSetShape.java            |   76 +
 265 files changed, 70171 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..91a534f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+# Class files
+class/
+*.class
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# intellij 
+*.iml
+.idea
+
+# MacOS
+.DS_Store
+
+# LaTeX auxiliary files 
+*.aux
+*.blg
+*.idx
+*.ilg
+*.ind
+*.log
+*.out
+*.toc
+
+# antbuild:
+antbuild/
+!antbuild/build.xml
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..31ec3a5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,5 @@
+Copyright (c) 2015 Daniel H. Huson
+
+All rights reserved. This program and the accompanying materials are made available under the terms of the GNU Public License v3.0 which accompanies this distribution, and is available at http://www.gnu.org/licenses/gpl.html
+
+For distributors of proprietary software, other licensing is possible on request: Daniel.Huson at uni-tuebingen.de
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9156a29
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+# jloda
+
+The jloda library provides some basic data structures and algorithms used by SplitsTree, Dendroscope and MEGAN
+
+jloda requires the following jars:
+batik-1.8
+Jama-1.0.3
diff --git a/antbuild/build.xml b/antbuild/build.xml
new file mode 100644
index 0000000..73bc28e
--- /dev/null
+++ b/antbuild/build.xml
@@ -0,0 +1,65 @@
+<project name="JLODA" default="compile" basedir=".">
+
+
+	<path id="build.classpath">
+	    <fileset dir="../../jloda/jars"  includes="*.jar"/>
+    	    <fileset dir="../../jloda/jars/batik-1.8"  includes="*.jar"/>
+
+    </path>
+
+
+    <!-- set global properties for this build -->
+    <property name="src" value="../src"/>
+    <property name="build" value="class"/>
+    <property name="doc" value="doc"/>
+    <target name="init">
+        <mkdir dir="${build}"/>
+        <mkdir dir="${doc}"/>
+        <!-- Create the time stamp -->
+        <tstamp/>
+    </target>
+
+    <target name="compile" depends="init">
+	    <!-- Compile the java code from ${src} into ${build} -->
+	     <javac  includeantruntime="false"
+        	srcdir="${src}"
+               destdir="${build}"
+               debug="on"
+	       classpathref="build.classpath">
+	       <compilerarg value="-XDignore.symbol.file=true"/> 
+        </javac>
+    </target>
+
+    <target name="jar" depends="compile">
+        <!-- Put everything in ${build} into a jar file -->
+        <jar jarfile="jloda.jar"
+             basedir="${build}"
+                />
+    </target>
+
+    <target name="clean">
+        <!-- Delete the ${build} directory tree -->
+        <delete dir="${build}"/>
+    </target>
+
+    <target name="aseptic_clean">
+        <!-- Delete the ${build} directory tree -->
+        <delete dir="${build}"/>
+        <!-- Delete the ${doc} directory tree -->
+        <delete dir="${doc}"/>
+    </target>
+
+    <target name="doc" depends="init">
+        <javadoc packagenames=
+                "jloda.*"
+                 sourcepath="${src}"
+                 destdir="${doc}"
+                 author="true"
+                 version="true"
+                 verbose="true"
+                 use="true"
+                 windowtitle="JLODA">
+            <doctitle><![CDATA[<h1>JLODA API</h1>]]></doctitle>
+        </javadoc>
+    </target>
+</project>
diff --git a/jars/gnujpdf-license.txt b/jars/gnujpdf-license.txt
new file mode 100644
index 0000000..01d72e7
--- /dev/null
+++ b/jars/gnujpdf-license.txt
@@ -0,0 +1,460 @@
+gnujpdf license:
+
+		  GNU LESSER GENERAL PUBLIC LICENSE
+		       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+     59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+

+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+

+		  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+

+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+

+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+

+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+

+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+

+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+

+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+			    NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
diff --git a/jars/gnujpdf-src.jar b/jars/gnujpdf-src.jar
new file mode 100644
index 0000000..81f7970
Binary files /dev/null and b/jars/gnujpdf-src.jar differ
diff --git a/jars/gnujpdf.jar b/jars/gnujpdf.jar
new file mode 100644
index 0000000..d0d8819
Binary files /dev/null and b/jars/gnujpdf.jar differ
diff --git a/src/jloda/export/EPSExportType.java b/src/jloda/export/EPSExportType.java
new file mode 100644
index 0000000..1a05527
--- /dev/null
+++ b/src/jloda/export/EPSExportType.java
@@ -0,0 +1,182 @@
+/**
+ * EPSExportType.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import jloda.util.Basic;
+
+import javax.swing.*;
+import javax.swing.filechooser.FileFilter;
+import java.awt.datatransfer.DataFlavor;
+import java.io.*;
+
+/**
+ * Export using the <i>encapsulated postscript</i> file format.
+ * The export itself is done by {@link jloda.export.EPSGraphics}.
+ *
+ * @author Daniel Huson, Michael Schroeder
+ * @see jloda.export.EPSGraphics
+ */
+public class EPSExportType extends FileFilter implements ExportGraphicType {
+
+    /**
+     * the mime type of this exportfile type
+     */
+    private final String mimeType = "image/x-eps";
+    /**
+     * the DataFlavor supported by this exportfile type
+     */
+    private final DataFlavor flavor;
+
+    private boolean drawTextAsOutlines = false;
+
+
+    public EPSExportType() {
+        flavor = new DataFlavor(mimeType + ";class=jloda.export.EPSExportType", "EPS graphic");
+    }
+
+    public String getMimeType() {
+        return mimeType;
+    }
+
+    public DataFlavor getDataFlavor() {
+        return flavor;
+    }
+
+    public Object getData(JPanel panel) {
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        stream(panel, out);
+        return new ByteArrayInputStream(out.toByteArray());
+    }
+
+
+    /**
+     * stream the image data to a given <code>ByteArrayOutputStream</code>.
+     *
+     * @param panel the panel which paints the image.
+     * @param out   the OutputStream.
+     */
+    public static void stream(JPanel panel, OutputStream out) {
+        EPSExportType eps = new EPSExportType();
+        eps.setDrawTextAsOutlines(true);
+        eps.stream(panel, null, false, out);
+    }
+
+    /**
+     * stream the image data to a given <code>ByteArrayOutputStream</code>.
+     *
+     * @param imagePanel the panel which paints the image.
+     * @param out        the ByteArrayOutputStream.
+     */
+    public void stream(JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage, OutputStream out) {
+        JPanel panel;
+        if (showWholeImage || imageScrollPane == null)
+            panel = imagePanel;
+        else
+            panel = ExportManager.makePanelFromScrollPane(imagePanel, imageScrollPane);
+        EPSGraphics epsGraphics = new EPSGraphics(panel.getWidth(), panel.getHeight(), out, getDrawTextAsOutlines());
+        panel.paint(epsGraphics);
+        epsGraphics.finish();
+    }
+
+    /**
+     * writes image to file. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param file
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @throws IOException
+     */
+    public void writeToFile(File file, final JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage) throws IOException {
+        FileOutputStream fos = new FileOutputStream(file);
+        stream(imagePanel, imageScrollPane, showWholeImage, fos);
+        fos.close();
+    }
+
+    /**
+     * write the image into an eps file.
+     *
+     * @param file  the file to write to.
+     * @param panel the panel which paints the image.
+     */
+    public static void writeToFile(File file, JPanel panel) throws IOException {
+        writeToFile(file, panel, true);
+    }
+
+    /**
+     * write the image into an eps file.
+     *
+     * @param file     the file to write to.
+     * @param panel    the panel which paints the image.
+     * @param fontMode whether font are converted to outlines
+     */
+    public static void writeToFile(File file, JPanel panel, boolean fontMode) throws IOException {
+        EPSExportType eps = new EPSExportType();
+        eps.setDrawTextAsOutlines(fontMode);
+        eps.writeToFile(file, panel, null, false);
+    }
+
+    public boolean accept(File f) {
+        if (f.isDirectory()) {
+            return true;
+        }
+        String extension = Basic.getSuffix(f.getName());
+        if (extension != null) {
+            if (extension.equalsIgnoreCase(".eps"))
+                return true;
+        } else {
+            return false;
+        }
+        return false;
+    }
+
+    public String getDescription() {
+        return "EPS (*.eps)";
+    }
+
+    public String toString() {
+        return getDescription();
+    }
+
+    /**
+     * gets the associated file filter and filename filter
+     *
+     * @return filename filter
+     */
+    public jloda.util.FileFilter getFileFilter() {
+        jloda.util.FileFilter filter = new jloda.util.FileFilter(getFileExtension());
+        filter.getFileExtensions().add(".ps");
+        return filter;
+    }
+
+    public String getFileExtension() {
+        return ".eps";
+    }
+
+    public boolean getDrawTextAsOutlines() {
+        return drawTextAsOutlines;
+    }
+
+    public void setDrawTextAsOutlines(boolean drawTextAsOutlines) {
+        this.drawTextAsOutlines = drawTextAsOutlines;
+    }
+}
diff --git a/src/jloda/export/EPSGraphics.java b/src/jloda/export/EPSGraphics.java
new file mode 100644
index 0000000..e32aa24
--- /dev/null
+++ b/src/jloda/export/EPSGraphics.java
@@ -0,0 +1,888 @@
+/**
+ * EPSGraphics.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import jloda.util.Basic;
+
+import java.awt.*;
+import java.awt.font.FontRenderContext;
+import java.awt.font.GlyphVector;
+import java.awt.font.TextLayout;
+import java.awt.geom.*;
+import java.awt.image.*;
+import java.awt.image.renderable.RenderableImage;
+import java.io.*;
+import java.text.AttributedCharacterIterator;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+
+/**
+ * A basic implementation of Graphics2D for output of the
+ * <i>encapsulated post script</i> file type.
+ *
+ * @author Daniel Huson, Michael Schroeder
+ */
+public class EPSGraphics extends Graphics2D {
+
+    /**
+     * the writer which writes the eps document in a givenOutputStream.
+     */
+    private final Writer w;
+    /**
+     * a Map of currently supported mappings of java symbolic fontnames
+     * to postscript fontnames.
+     */
+    private Map fonts;
+
+    /**
+     * width of the eps page.
+     */
+    private final int width;
+    /**
+     * height of the eps page
+     */
+    private final int height;
+
+    /**
+     * the current Font.
+     */
+    private Font font;
+    /**
+     * the current FontRenderContext
+     */
+    private final FontRenderContext fontRenderContext;
+    /**
+     * the current Color.
+     */
+    private Color color;
+    /**
+     * the current backgorund color.
+     */
+    private Color background;
+    /**
+     * the current Stroke
+     */
+    private BasicStroke stroke;
+    /**
+     * the current clipping bounds
+     */
+    private Shape clip = null;
+    /**
+     * the current transformation matrix
+     */
+    private AffineTransform tx;
+
+    private final boolean drawTextAsOutlines;
+
+    private static final int DRAW_SHAPE = 0;
+    private static final int FILL_SHAPE = 1;
+    private static final int CLIP_SHAPE = 2;
+
+    public static final boolean FONT_OUTLINES = true;
+    public static final boolean FONT_TEXT = false;
+
+    /**
+     * CONSTRUCTORS
+     */
+
+    /**
+     * the eps document will be written directly to the given
+     * <code>OutputStream</code>.
+     * after writing to EPSGraphics is finished, {@link #finish() finish()} needs to be called in order to
+     * close the BufferedWriter explicitly.
+     *
+     * @param width  the width of the eps document,
+     * @param height the height of the eps document.
+     * @param stream the <code>OutputStream</code> to write to, e.g. <code>FileOutputStream</code>
+     *               or <code>ByteArrayOutputStream</code>.
+     */
+    public EPSGraphics(int width, int height, OutputStream stream) {
+
+        this(width, height, stream, FONT_TEXT);
+    }
+
+    public EPSGraphics(int width, int height, OutputStream stream, boolean drawTextAsOutlines) {
+
+        this.w = new BufferedWriter(new OutputStreamWriter(stream));
+        this.width = width;
+        this.height = height;
+        this.clip = new Rectangle(width, height);
+        this.tx = new AffineTransform();
+        this.background = Color.WHITE;
+        this.fontRenderContext = new FontRenderContext(null, false, true);
+        this.drawTextAsOutlines = drawTextAsOutlines;
+
+        initFonts();
+        writeHeader();
+    }
+
+
+    /**
+     * creates a new EPSGraphics with the same
+     * configuration as the given EPSGraphics
+     *
+     * @param g the EPSGraphics to copy field values from
+     */
+    protected EPSGraphics(EPSGraphics g) {
+
+        this.w = g.w;
+        this.width = g.width;
+        this.height = g.height;
+        this.clip = g.clip;
+        this.font = g.font;
+        this.fontRenderContext = g.fontRenderContext;
+        this.fonts = g.fonts;
+        this.color = g.color;
+        this.background = g.background;
+        this.tx = g.tx;
+        this.drawTextAsOutlines = g.drawTextAsOutlines;
+
+    }
+
+    public Graphics create() {
+        return new EPSGraphics(this);
+    }
+
+
+    /**
+     * overriden methods of <code>java.awt.Graphics</code>
+     */
+
+
+    /**
+     * methods of <code>java.awt.Graphics</code> supported by <code>EPSGraphics</code>
+     */
+
+
+    public Color getColor() {
+        return color;
+    }
+
+    public Font getFont() {
+        return font;
+    }
+
+    /**
+     * maps ranges of 256 integer rgb color values to
+     * the interval between 0 and 1 before writing postscript.
+     *
+     * @param c the color
+     */
+    public void setColor(Color c) {
+        this.color = c;
+        float r = c.getRed() / 255.0f;
+        float g = c.getGreen() / 255.0f;
+        float b = c.getBlue() / 255.0f;
+        writeToFile(r + " " + g + " " + b + " setrgbcolor\r\n");
+    }
+
+    /**
+     * sets the current font.
+     *
+     * @param font the font
+     */
+    public void setFont(Font font) {
+
+        if (!drawTextAsOutlines) {
+
+            if (fonts.containsKey(font.getFontName())) {
+                this.font = font;
+                writeToFile("/" + fonts.get(font.getFontName()) + " findfont\r\n" +
+                        font.getSize() + " scalefont\r\n" +
+                        "setfont\r\n");
+            } else {
+                this.font = font;
+                writeToFile("/" + font.getPSName() + " findfont\r\n" +
+                        font.getSize() + " scalefont\r\n" +
+                        "setfont\r\n");
+            }
+        } else {
+            this.font = font;
+        }
+    }
+
+    public void drawLine(int x1, int y1, int x2, int y2) {
+
+        Shape s = new Line2D.Float(x1, y1, x2, y2);
+        draw(s, DRAW_SHAPE);
+    }
+
+    public void fillRect(int ulx, int uly, int width, int height) {
+
+        Rectangle2D rect = new Rectangle2D.Float(ulx, uly, width, height);
+        draw(rect, FILL_SHAPE);
+    }
+
+
+    /**
+     * draws an oval.
+     *
+     * @param x      the x-coordinate of the upper left corner of the oval's bounding box.
+     * @param y      the y-coordinate of the upper left corner of the oval's bounding box.
+     * @param width  the width of the oval's bounding box
+     * @param height the height of the oval's bounding box
+     */
+    public void drawOval(int x, int y, int width, int height) {
+
+        Ellipse2D ellipse = new Ellipse2D.Float(x, y, width, height);
+        draw(ellipse, DRAW_SHAPE);
+
+    }
+
+    /**
+     * draws a filled oval.
+     * see {@link #drawOval(int x, int y, int width, int height)} for details.
+     *
+     * @param x      the x-coordinate of the upper left corner of the oval's bounding box.
+     * @param y      the y-coordinate of the upper left corner of the oval's bounding box.
+     * @param width  the width of the oval's bounding box
+     * @param height the height of the oval's bounding box
+     */
+    public void fillOval(int x, int y, int width, int height) {
+
+        Ellipse2D ellipse = new Ellipse2D.Float(x, y, width, height);
+        draw(ellipse, FILL_SHAPE);
+    }
+
+    public void drawArc(int x, int y, int width, int height,
+                        int startAngle, int arcAngle) {
+
+        Arc2D arc = new Arc2D.Float(x, y, width, height, startAngle, arcAngle, Arc2D.OPEN);
+        draw(arc, DRAW_SHAPE);
+
+    }
+
+    public void fillArc(int x, int y, int width, int height,
+                        int startAngle, int arcAngle) {
+
+        Arc2D arc = new Arc2D.Float(x, y, width, height, startAngle, arcAngle, Arc2D.PIE);
+        draw(arc, FILL_SHAPE);
+
+    }
+
+    public void drawPolyline(int xPoints[], int yPoints[],
+                             int nPoints) {
+        if (0 < nPoints) {
+            GeneralPath path = new GeneralPath();
+            path.moveTo(xPoints[0], yPoints[0]);
+            for (int i = 1; i < nPoints; i++) {
+                path.lineTo(xPoints[i], yPoints[i]);
+            }
+            draw(path, DRAW_SHAPE);
+        }
+    }
+
+    public void drawPolygon(int xPoints[], int yPoints[],
+                            int nPoints) {
+        if (nPoints > 1) {
+            Polygon poly = new Polygon(xPoints, yPoints, nPoints);
+            draw(poly, DRAW_SHAPE);
+        }
+    }
+
+    public void fillPolygon(int xPoints[], int yPoints[],
+                            int nPoints) {
+        if (0 < nPoints) {
+            GeneralPath path = new GeneralPath();
+            path.moveTo(xPoints[0], yPoints[0]);
+            for (int i = 1; i < nPoints; i++) {
+                path.lineTo(xPoints[i], yPoints[i]);
+            }
+            draw(path, FILL_SHAPE);
+        }
+    }
+
+    public void drawRoundRect(int x, int y, int width, int height,
+                              int arcWidth, int arcHeight) {
+        notSupported("drawRoundRect(int x, int y, int width, int height,int arcWidth, int arcHeight)");
+    }
+
+    public void fillRoundRect(int x, int y, int width, int height,
+                              int arcWidth, int arcHeight) {
+        notSupported("fillRoundRect(int x, int y, int width, int height,int arcWidth, int arcHeight)");
+    }
+
+    public void translate(int x, int y) {
+
+        this.tx.concatenate(AffineTransform.getTranslateInstance(x, y));
+    }
+
+    public void setClip(int ulx, int uly, int width, int height) {
+        clip = new Rectangle(ulx, uly, width, height);
+
+        int[] p = {ulx, uly + height};
+        toPsCoords(p);
+        writeToFile(ulx + " " + uly + " " + width + " " + height + " rectclip\r\n");
+
+    }
+
+    public Shape getClip() {
+        return clip;
+    }
+
+    public Rectangle getClipBounds() {
+        if (clip == null) return null;
+        return clip.getBounds();
+    }
+
+    public void setClip(Shape clip) {
+        draw(clip, CLIP_SHAPE);
+    }
+
+    /**
+     * methods of <code>java.awt.Graphics</code> NOT supported by <code>EPSGraphics</code>
+     */
+
+
+    public void setPaintMode() {
+        notSupported("setPaintMode()");
+    }
+
+    public void setXORMode(Color c1) {
+        notSupported("setXORMode(Color c1)");
+    }
+
+
+    public FontMetrics getFontMetrics(Font f) {
+        BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
+        Graphics g = image.getGraphics();
+        return g.getFontMetrics(f);
+    }
+
+    public void clipRect(int x, int y, int width, int height) {
+        Rectangle2D rect = new Rectangle2D.Float(x, y, width, height);
+        draw(rect, CLIP_SHAPE);
+    }
+
+    public void copyArea(int x, int y, int width, int height,
+                         int dx, int dy) {
+        notSupported("copyArea(int x, int y, int width, int height,int dx, int dy)");
+    }
+
+    public void clearRect(int x, int y, int width, int height) {
+        float[] bg = background.getColorComponents(null);
+        float[] c = color.getColorComponents(null);
+        writeToFile(bg[0] + " " + bg[1] + " " + bg[2] + " setcolor\r\n");
+        Rectangle2D rect = new Rectangle2D.Float(x, y, width, height);
+        draw(rect, FILL_SHAPE);
+        writeToFile(c[0] + " " + c[1] + " " + c[2] + " setcolor\r\n");
+    }
+
+    public Paint getPaint() {
+        notSupported("getPaint()");
+        return null;
+    }
+
+    public Composite getComposite() {
+        notSupported("getComposite()");
+        return null;
+    }
+
+    public void setBackground(Color color) {
+        this.background = color;
+    }
+
+    public Color getBackground() {
+        return this.background;
+    }
+
+    public Stroke getStroke() {
+        return stroke;
+    }
+
+    public void clip(Shape s) {
+        draw(s, CLIP_SHAPE);
+    }
+
+    public FontRenderContext getFontRenderContext() {
+
+        return this.fontRenderContext;
+    }
+
+
+    /**
+     * overriden methods of <code>java.awt.Graphics2D</code>
+     */
+
+
+    /**
+     * methods of <code>java.awt.Graphics2D</code> supported by <code>EPSGraphics</code>
+     */
+
+    /**
+     * draws a string at (<code>x,y</code>).
+     * since the postscript user space is flipped horizontically in order
+     * to draw in java coordinates, the string itself has to be flipped again.
+     *
+     * @param str the string to be drawn.
+     * @param x   the x-coordinate of the lower left corner
+     * @param y   the y-coordinate of the lower left corner
+     */
+
+    public void drawString(String str, int x, int y) {
+
+        drawString(str, (float) x, (float) y);
+    }
+
+    public void drawString(String str, float x, float y) {
+
+        if (drawTextAsOutlines) {
+            //System.err.println("rendering outlines: font="+font);
+            TextLayout layout = new TextLayout(str, font, fontRenderContext);
+            Shape s = layout.getOutline(AffineTransform.getTranslateInstance(x, y));
+            fill(s);
+        } else if (!drawTextAsOutlines) {
+            //System.err.println("rendering text");
+            AffineTransform m = getTransform();
+            m.rotate(Math.PI);
+            double[] gm = new double[6];
+            m.getMatrix(gm);
+            writeToFile("gsave\r\n");
+            writeToFile("[" + -gm[0] + " " + gm[1] + " " + gm[2] + " " + -gm[3] + " " + gm[4] + " " + (height - gm[5]) + "] concat\r\n");
+            writeToFile(x + " " + -y + " m\r\n");
+            writeToFile("(" + str + ") show\r\n");
+            writeToFile("grestore\r\n");
+        }
+    }
+
+    public void translate(double tx, double ty) {
+        this.tx.concatenate(AffineTransform.getTranslateInstance(tx, ty));
+    }
+
+    public void rotate(double theta) {
+        this.tx.concatenate(AffineTransform.getRotateInstance(theta));
+    }
+
+    public void rotate(double theta, double x, double y) {
+        this.tx.concatenate(AffineTransform.getRotateInstance(theta, x, y));
+    }
+
+    public void scale(double sx, double sy) {
+        this.tx.concatenate(AffineTransform.getScaleInstance(sx, sy));
+    }
+
+    public void shear(double shx, double shy) {
+        this.tx.concatenate(AffineTransform.getShearInstance(shx, shy));
+    }
+
+    public void transform(AffineTransform Tx) {
+        this.tx.concatenate(Tx);
+
+    }
+
+    public void setTransform(AffineTransform Tx) {
+        this.tx = new AffineTransform(Tx);
+
+    }
+
+    public AffineTransform getTransform() {
+        return new AffineTransform(this.tx);
+    }
+
+    /**
+     * sets the current linewidth.
+     *
+     * @param s a stroke object to get the linewidth from.
+     */
+    public void setStroke(Stroke s) {
+
+        this.stroke = (BasicStroke) s;
+
+        // endcap
+        int endCap = stroke.getEndCap();
+        int psEndCap = -1;
+        switch (endCap) {
+            case BasicStroke.CAP_BUTT:
+                psEndCap = 0;
+                break;
+            case BasicStroke.CAP_ROUND:
+                psEndCap = 1;
+                break;
+            case BasicStroke.CAP_SQUARE:
+                psEndCap = 2;
+                break;
+        }
+        if (-1 != psEndCap) writeToFile(psEndCap + " setlinecap\r\n");
+
+        // line join
+        int lineJoin = stroke.getLineJoin();
+        int psLineJoin = -1;
+        switch (lineJoin) {
+            case BasicStroke.JOIN_BEVEL:
+            case BasicStroke.JOIN_MITER:
+            case BasicStroke.JOIN_ROUND:
+                psLineJoin = 1;
+        }
+        if (-1 != psLineJoin) writeToFile(psLineJoin + " setlinejoin\r\n");
+        if (1 <= stroke.getMiterLimit()) writeToFile(stroke.getMiterLimit() + " setmiterlimit\r\n");
+        writeToFile(stroke.getLineWidth() + " setlinewidth\r\n");
+    }
+
+
+    /**
+     * methods of <code>java.awt.Graphics2D</code> NOT supported by <code>EPSGraphics</code>
+     */
+
+    public void dispose() {
+        //notSupported("dispose()");
+    }
+
+    public void draw(Shape s) {
+
+        draw(s, DRAW_SHAPE);
+    }
+
+    public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) {
+        AffineTransform at = getTransform();
+        transform(xform);
+        boolean st = drawImage(img, 0, 0, obs);
+        setTransform(at);
+        return st;
+    }
+
+
+    public void drawImage(BufferedImage img,
+                          BufferedImageOp op,
+                          int x,
+                          int y) {
+        notSupported("drawImage(BufferedImage img,BufferedImageOp op,int x,int y)");
+    }
+
+    public void drawRenderedImage(RenderedImage img, AffineTransform xform) {
+        Hashtable properties = new Hashtable();
+        String[] names = img.getPropertyNames();
+        for (String name : names) {
+            properties.put(name, img.getProperty(name));
+        }
+
+        ColorModel cm = img.getColorModel();
+        WritableRaster wr = img.copyData(null);
+        BufferedImage img1 = new BufferedImage(cm, wr, cm.isAlphaPremultiplied(), properties);
+        AffineTransform at = AffineTransform.getTranslateInstance(img.getMinX(), img.getMinY());
+        at.preConcatenate(xform);
+        drawImage(img1, at, null);
+    }
+
+
+    public void drawRenderableImage(RenderableImage img,
+                                    AffineTransform xform) {
+        notSupported("drawRenderableImage(RenderableImage img,AffineTransform xform)");
+    }
+
+    public void drawString(AttributedCharacterIterator iterator,
+                           int x, int y) {
+        notSupported("drawString(AttributedCharacterIterator iterator,int x, int y)");
+    }
+
+    public boolean drawImage(Image img, int x, int y,
+                             ImageObserver observer) {
+        return drawImage(img, x, y, Color.WHITE, observer);
+    }
+
+    public boolean drawImage(Image img, int x, int y,
+                             Color bgcolor,
+                             ImageObserver observer) {
+        int width = img.getWidth(observer);
+        int height = img.getHeight(observer);
+        return drawImage(img, x, y, width, height, bgcolor, observer);
+    }
+
+    public boolean drawImage(Image img, int x, int y,
+                             int width, int height,
+                             Color bgcolor,
+                             ImageObserver observer) {
+        return drawImage(img, x, y, x + width, y + height, 0, 0, width, height, observer);
+    }
+
+    public boolean drawImage(Image img, int x, int y,
+                             int width, int height,
+                             ImageObserver observer) {
+        return drawImage(img, x, y, width, height, Color.WHITE, observer);
+    }
+
+
+    public boolean drawImage(Image img,
+                             int dx1, int dy1, int dx2, int dy2,
+                             int sx1, int sy1, int sx2, int sy2,
+                             ImageObserver observer) {
+        return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, Color.WHITE, observer);
+    }
+
+    public boolean drawImage(Image img,
+                             int dx1, int dy1, int dx2, int dy2,
+                             int sx1, int sy1, int sx2, int sy2,
+                             Color bgcolor,
+                             ImageObserver observer) {
+        notSupported("drawImage");
+        return false;
+    }
+
+    public void drawString(AttributedCharacterIterator iterator,
+                           float x, float y) {
+        notSupported("drawString(AttributedCharacterIterator iterator,float x, float y)");
+    }
+
+    public void drawGlyphVector(GlyphVector g, float x, float y) {
+        notSupported("drawGlyphVector(GlyphVector g, float x, float y)");
+    }
+
+    public void fill(Shape s) {
+        draw(s, FILL_SHAPE);
+    }
+
+    public void draw(Shape s, int operator) {
+        Shape st = tx.createTransformedShape(s);
+
+        PathIterator it = st.getPathIterator(null);
+
+        writeToFile("newpath\r\n");
+        float[] pts = new float[6];
+        float p0x = 0f;
+        float p0y = 0f;
+
+        while (!it.isDone()) {
+
+            int type = it.currentSegment(pts);
+
+            float p1x = pts[0];
+            float p1y = height - pts[1];
+            float p2x = pts[2];
+            float p2y = height - pts[3];
+            float p3x = pts[4];
+            float p3y = height - pts[5];
+
+            switch (type) {
+
+                case PathIterator.SEG_MOVETO:
+
+                    writeToFile(p1x + " " + p1y + " m\r\n");
+                    p0x = p1x;
+                    p0y = p1y;
+                    break;
+
+                case PathIterator.SEG_LINETO:
+
+                    writeToFile(p1x + " " + p1y + " l\r\n");
+                    p0x = p1x;
+                    p0y = p1y;
+                    break;
+
+                case PathIterator.SEG_CUBICTO:
+
+                    writeToFile(p1x + " " + p1y + " " + p2x + " " + p2y + " " + p3x + " " + p3y + " c\r\n");
+                    p0x = p3x;
+                    p0y = p3y;
+                    break;
+
+                case PathIterator.SEG_QUADTO: // @todo
+
+
+                    float c1x = p0x + 2f / 3f * (p1x - p0x);
+                    float c1y = p0y + 2f / 3f * (p1y - p0y);
+                    float c2x = p1x + 1f / 3f * (p2x - p1x);
+                    float c2y = p1y + 1f / 3f * (p2y - p1y);
+
+                    writeToFile(c1x + " " + c1y + " " + c2x + " " + c2y + " " + p2x + " " + p2y + " c\r\n");
+                    p0x = p2x;
+                    p0y = p2y;
+
+                    break;
+
+                case PathIterator.SEG_CLOSE:
+                    writeToFile("closepath\r\n");
+                    break;
+
+            }
+
+            it.next();
+
+        }
+        switch (operator) {
+            case DRAW_SHAPE:
+                writeToFile("stroke\r\n");
+                break;
+            case FILL_SHAPE:
+                writeToFile("fill\r\n");
+                break;
+            case CLIP_SHAPE:
+                writeToFile("clip\r\n");
+                break;
+        }
+    }
+
+    public boolean hit(Rectangle rect,
+                       Shape s,
+                       boolean onStroke) {
+        return s.intersects(rect);
+    }
+
+    public GraphicsConfiguration getDeviceConfiguration() {
+        GraphicsConfiguration gc = null;
+        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
+        GraphicsDevice[] gds = ge.getScreenDevices();
+        for (GraphicsDevice gd : gds) {
+            GraphicsConfiguration[] gcs = gd.getConfigurations();
+            if (gcs.length > 0) {
+                return gcs[0];
+            }
+        }
+        return gc;
+    }
+
+    public void setComposite(Composite comp) {
+        notSupported("setComposite(Composite comp)");
+    }
+
+    public void setPaint(Paint paint) {
+        notSupported("setPaint(Paint paint)");
+    }
+
+    public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) {
+        notSupported("setRenderingHint(RenderingHints.Key hintKey, Object hintValue)");
+    }
+
+    public Object getRenderingHint(RenderingHints.Key hintKey) {
+        notSupported("getRenderingHint(RenderingHints.Key hintKey)");
+        return null;
+    }
+
+    public void setRenderingHints(Map hints) {
+        notSupported("setRenderingHints(Map hints)");
+    }
+
+    public void addRenderingHints(Map hints) {
+        notSupported("addRenderingHints(Map hints)");
+    }
+
+    public RenderingHints getRenderingHints() {
+        notSupported("RenderingHints getRenderingHints()");
+        return null;
+    }
+
+
+    /**
+     * additional methods
+     */
+
+    private void writeToFile(String s) {
+        try {
+            w.write(s);
+        } catch (IOException e) {
+            Basic.caught(e);
+        }
+    }
+
+    private void toPsCoords(int[] p) {
+        p[1] = height - p[1];
+    }
+
+
+    private void psUserPathStart(Shape s) {
+
+        Rectangle2D.Float bounds = (Rectangle2D.Float) s.getBounds2D();
+        //ul = tx.transform(bounds.getLocation(),ul);
+
+        float llx = bounds.x;
+        float lly = height - (bounds.y + bounds.height);
+        float urx = bounds.x + bounds.width;
+        float ury = height - bounds.y;
+
+        writeToFile("{" + llx + " " + lly + " " + urx + " " + ury + " setbbox\r\n");
+
+    }
+
+
+    /**
+     * write the eps header.
+     */
+    private void writeHeader() {
+        writeToFile("%!PS-Adobe-3.0 EPSF-3.0\r\n" +
+                "%%BoundingBox: 0 0 " + width + " " + height + "\r\n" +
+                "%%Creator: jloda\r\n" +
+                "%%EndComments\r\n" +
+                "/c {curveto} bind def\r\n" +
+                "/m {moveto} bind def\r\n" +
+                "/l {lineto} bind def\r\n");
+    }
+
+    /**
+     * write the eps trailer
+     */
+    private void writeTrailer() {
+        writeToFile("showpage\r\n%%EOF");
+    }
+
+    /**
+     * finish writing the document.
+     * this method needs to be called after a Component has
+     * painted on <code>EPSGraphics</code>.
+     */
+    public void finish() {
+        writeTrailer();
+        try {
+            w.close();
+        } catch (IOException e) {
+            Basic.caught(e);
+        }
+    }
+
+    /**
+     * initialize a mappping of symbolic java fontnames
+     * to postscript fontnames
+     */
+    private void initFonts() {
+
+        /*String defaultFont = "Default";
+
+        fonts = new HashMap();
+        fonts.put(defaultFont + ".plain","SansSerif");
+        fonts.put(defaultFont + ".italic","SansSerifOblique");
+        fonts.put(defaultFont + ".bold","SansSerifBold");
+
+        this.font = new Font("Default",Font.PLAIN,12); */
+
+        Font defaultPlain = new Font("Default", Font.PLAIN, 10);
+
+        fonts = new HashMap();
+        fonts.put("Default.plain", "SansSerif");
+        fonts.put("Default.italic", "SansSerifOblique");
+        fonts.put("Default.bold", "SansSerifBold");
+
+        this.font = defaultPlain;
+    }
+
+    /**
+     * add a mapping of a symbolic java fontname
+     * to a postscript fontname
+     *
+     * @param fontName java symbolic fontname
+     * @param psName   postscript fontname
+     */
+    public void addFont(String fontName, String psName) {
+        fonts.put(fontName, psName);
+    }
+
+    private void notSupported(String methodName) {
+        System.err.println("Method not supported by " + this.getClass().getName() + ": " + methodName);
+    }
+
+}
diff --git a/src/jloda/export/ExportGraphicType.java b/src/jloda/export/ExportGraphicType.java
new file mode 100644
index 0000000..7ae0344
--- /dev/null
+++ b/src/jloda/export/ExportGraphicType.java
@@ -0,0 +1,93 @@
+/**
+ * ExportGraphicType.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import javax.swing.*;
+import java.awt.datatransfer.DataFlavor;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * @author huson, schroeder
+ *         interface for export graphics classes, 2004, 5.2006
+ */
+public interface ExportGraphicType {
+
+    /**
+     * get the mime type of this exportfile type.
+     *
+     * @return the mime type.
+     */
+    String getMimeType();
+
+    /**
+     * get the <code>DataFlavor</code> supported by this exportfile type.
+     *
+     * @return the supported <code>DataFlavor</code>
+     */
+    DataFlavor getDataFlavor();
+
+    /**
+     * return the image data in a specific format.
+     *
+     * @param panel the <code>JPanel</code> which paints the image data.
+     * @return the image data in a specific format.
+     */
+    Object getData(JPanel panel);
+
+    /**
+     * gets the associated file filter
+     *
+     * @return filter
+     */
+    jloda.util.FileFilter getFileFilter();
+
+    /**
+     * gets the associated file extension for this file
+     *
+     * @return extension
+     */
+    String getFileExtension();
+
+    /**
+     * writes image to a stream. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @param out
+     * @throws IOException
+     */
+    void stream(JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage, OutputStream out) throws IOException;
+
+    /**
+     * writes image to file. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param file
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @throws IOException
+     */
+    void writeToFile(File file, JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage) throws IOException;
+}
diff --git a/src/jloda/export/ExportImageDialog.java b/src/jloda/export/ExportImageDialog.java
new file mode 100644
index 0000000..59c4ecc
--- /dev/null
+++ b/src/jloda/export/ExportImageDialog.java
@@ -0,0 +1,370 @@
+/**
+ * ExportImageDialog.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import jloda.gui.ChooseFileDialog;
+import jloda.util.Basic;
+import jloda.util.ProgramProperties;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.io.File;
+import java.util.LinkedList;
+
+/**
+ * dialog for setting up image export
+ * Daniel Huson, 5.2011
+ */
+public class ExportImageDialog extends JDialog {
+    private final JTextField fileField = new JTextField();
+    private final JComboBox formatComboBox = new JComboBox();
+    private final JRadioButton saveVisibleOnlyButton;
+    private final JCheckBox textAsOutlinesButton;
+    private final JButton applyButton;
+
+    private boolean fileNameChangedByText = true; // if file name changed by text, need to check for overwriting of file
+
+    private static final java.util.List<ExportGraphicType> graphicTypes = new LinkedList<>();
+
+    final public static String GRAPHICSFORMAT = "GraphicsFormat";
+    final public static String GRAPHICSDIR = "GraphicsDir";
+
+    private boolean inUpdate = false; // use this to prevent bouncing between update of format and update of file name
+
+    private String command = null;
+
+    /**
+     * constructs a dialog for exporting an image
+     *
+     * @param parent
+     * @param documentFileName
+     * @param allowVisible
+     * @param allowWhole
+     * @param allowEPS
+     * @param event
+     */
+    public ExportImageDialog(JFrame parent, String documentFileName, boolean allowVisible, boolean allowWhole, boolean allowEPS, final ActionEvent event) {
+        super(parent, "Export Image" + (ProgramProperties.getProgramName() != null ? " - " + ProgramProperties.getProgramName() : ""));
+        setModal(true);
+        setSize(new Dimension(420, 210));
+        setLocationRelativeTo(parent);
+
+        getContentPane().setLayout(new BorderLayout());
+
+        JPanel topPanel = new JPanel();
+        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
+        topPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEtchedBorder(),
+                BorderFactory.createEmptyBorder(4, 2, 2, 2)));
+
+        JPanel filePanel = new JPanel();
+        filePanel.setMaximumSize(new Dimension(1000, 20));
+        filePanel.setLayout(new BoxLayout(filePanel, BoxLayout.X_AXIS));
+        filePanel.add(new JLabel("File: "));
+        fileField.setMinimumSize(new Dimension(100, 20));
+        filePanel.add(fileField);
+        filePanel.add(new JButton(new AbstractAction("Browse...") {
+            public void actionPerformed(ActionEvent actionEvent) {
+                File file = new File(fileField.getText());
+                file = ChooseFileDialog.chooseFileToSave(ExportImageDialog.this, file, getExportGraphicType().getFileFilter(), getExportGraphicType().getFileFilter(), event, "Save image", getFileExtension());
+                if (file != null) {
+                    fileNameChangedByText = false;
+                    String fileName = file.getPath();
+                    String suffix = Basic.getSuffix(fileName);
+                    if (suffix == null || suffix.length() == 0) {
+                        file = new File(file.getPath() + getFileExtension());
+                    }
+                    if (suffix != null && !suffix.equals(getFileExtension())) {
+                        setFormat(Basic.getSuffix(file.getPath()));
+                    }
+                    setFile(file);
+                }
+            }
+        }));
+        fileField.getDocument().addDocumentListener(new DocumentListener() {
+            public void insertUpdate(DocumentEvent documentEvent) {
+                updateEnabledState();
+                fileNameChangedByText = true;
+                if (!inUpdate) {
+                    String extension = Basic.getFileSuffix(fileField.getText());
+                    if (extension != null && extension.length() > 0) {
+                        extension = extension.trim();
+                        if (extension.startsWith(".")) {
+                            extension = extension.substring(1);
+                            if (extension.length() >= 3) {
+                                inUpdate = true;
+                                setFormat(extension);
+                                inUpdate = false;
+                            }
+                        }
+                    }
+                }
+            }
+
+            public void removeUpdate(DocumentEvent documentEvent) {
+                insertUpdate(documentEvent);
+            }
+
+            public void changedUpdate(DocumentEvent documentEvent) {
+                insertUpdate(documentEvent);
+            }
+        });
+
+        String directory = ProgramProperties.get(GRAPHICSDIR, new File(documentFileName).getParent());
+
+        topPanel.add(filePanel);
+
+        JPanel formatPanel = new JPanel();
+        formatPanel.setLayout(new BoxLayout(formatPanel, BoxLayout.X_AXIS));
+        formatPanel.add(new JLabel("Format:"));
+
+        // setup format combo list:
+        for (ExportGraphicType exportGraphicType : getGraphicTypes()) {
+            if (allowEPS || !exportGraphicType.getFileExtension().endsWith("eps")) {
+                formatComboBox.addItem(exportGraphicType);
+            }
+        }
+        setFormat(ProgramProperties.get(GRAPHICSFORMAT, (new EPSExportType()).getFileExtension()));
+        setFile(new File(directory, Basic.replaceFileSuffix(Basic.getFileNameWithoutPath(documentFileName), getFileExtension())));
+
+        formatComboBox.addItemListener(new ItemListener() {
+            public void itemStateChanged(ItemEvent e) {
+                ExportGraphicType exportGraphicType = (ExportGraphicType) e.getItem();
+                if (textAsOutlinesButton != null)
+                    textAsOutlinesButton.setEnabled(exportGraphicType instanceof EPSExportType);
+                if (!inUpdate) {
+                    String fileName = Basic.replaceFileSuffix(fileField.getText(), exportGraphicType.getFileExtension());
+                    if (!fileName.equals(exportGraphicType.getFileExtension())) {
+                        inUpdate = true;
+                        fileField.setText(fileName);
+                        inUpdate = false;
+                    }
+                }
+                fileNameChangedByText = true;
+            }
+        });
+        formatPanel.add(formatComboBox);
+        formatPanel.add(Box.createHorizontalGlue());
+        formatPanel.add(Box.createHorizontalGlue());
+
+        topPanel.add(formatPanel);
+        getContentPane().add(topPanel, BorderLayout.NORTH);
+
+        JPanel middlePanel = new JPanel();
+        middlePanel.setLayout(new BoxLayout(middlePanel, BoxLayout.Y_AXIS));
+
+        saveVisibleOnlyButton = new JRadioButton(new AbstractAction("Visible region") {
+            public void actionPerformed(ActionEvent actionEvent) {
+            }
+        });
+        saveVisibleOnlyButton.setEnabled(allowVisible);
+        middlePanel.add(saveVisibleOnlyButton);
+
+        JRadioButton saveWholeImageButton = new JRadioButton(new AbstractAction("Whole image") {
+            public void actionPerformed(ActionEvent actionEvent) {
+            }
+        });
+        saveWholeImageButton.setEnabled(allowWhole);
+        ButtonGroup group = new ButtonGroup();
+        group.add(saveVisibleOnlyButton);
+        group.add(saveWholeImageButton);
+
+        boolean preSelectWholeImage = ProgramProperties.get("graphicsVisibleOnly", true);
+        if (preSelectWholeImage && allowWhole)
+            saveWholeImageButton.setSelected(true);
+        else
+            saveVisibleOnlyButton.setSelected(true);
+
+        middlePanel.add(saveWholeImageButton);
+
+        if (allowEPS) {
+            textAsOutlinesButton = new JCheckBox(new AbstractAction("Text as outlines (EPS)") {
+                public void actionPerformed(ActionEvent actionEvent) {
+                }
+            });
+            textAsOutlinesButton.setEnabled(allowEPS && getFormat().equals("eps"));
+            textAsOutlinesButton.setSelected(ProgramProperties.get("graphicsConvertText", true));
+            middlePanel.add(textAsOutlinesButton);
+        } else
+            textAsOutlinesButton = null;
+
+        getContentPane().add(middlePanel, BorderLayout.CENTER);
+
+        JPanel bottomPanel = new JPanel();
+        bottomPanel.setBorder(BorderFactory.createEmptyBorder(2, 20, 2, 20));
+        bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.X_AXIS));
+        bottomPanel.setBorder(BorderFactory.createEtchedBorder());
+        bottomPanel.add(Box.createHorizontalGlue());
+
+        bottomPanel.add(new JButton(new AbstractAction("Cancel") {
+            public void actionPerformed(ActionEvent actionEvent) {
+                setVisible(false);
+            }
+        }));
+
+        applyButton = new JButton(new AbstractAction("Apply") {
+            public void actionPerformed(ActionEvent actionEvent) {
+                String fileName = getFileName();
+                String suffix = Basic.getSuffix(fileName);
+                if (suffix == null || suffix.length() == 0) {
+                    fileName += getFileExtension();
+                    fileField.setText(fileName);
+                }
+                if (fileNameChangedByText && !checkOkToWriteFile(fileName))
+                    return;
+                setVisible(false);
+                ProgramProperties.put("graphicsSaveVisibleOnly", isSaveVisibleOnly());
+                ProgramProperties.put("graphicsConvertText", isTextAsOutlinesEPS());
+                File tmpFile = new File(fileName);
+                if (tmpFile.getParentFile() != null)
+                    ProgramProperties.put(GRAPHICSDIR, tmpFile.getParentFile());
+                ProgramProperties.put(GRAPHICSFORMAT, getFormat());
+
+                command = "exportImage file='" + fileName + "' format=" + getFormat() + " visibleOnly=" + isSaveVisibleOnly()
+                        + (isTextAsOutlinesEPS() ? " textAsShapes=true" : "") + " title=none replace=true;";
+            }
+        });
+        bottomPanel.add(applyButton);
+        getRootPane().setDefaultButton(applyButton);
+
+        getContentPane().add(bottomPanel, BorderLayout.SOUTH);
+        getContentPane().validate();
+    }
+
+    /**
+     * ok to write file?
+     *
+     * @param fileName
+     * @return true, if ok to write file
+     */
+    private boolean checkOkToWriteFile(String fileName) {
+        File file = new File(fileName);
+        if (file.exists()) {
+            switch (
+                    JOptionPane.showConfirmDialog(this,
+                            "This file already exists. Overwrite the existing file?", "Save File", JOptionPane.YES_NO_CANCEL_OPTION)) {
+                case JOptionPane.YES_OPTION:
+                    return true;
+                case JOptionPane.NO_OPTION:
+                case JOptionPane.CANCEL_OPTION:
+                    return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * update the enable state
+     */
+    private void updateEnabledState() {
+        String fileName = getFileName();
+        if (applyButton != null)
+            applyButton.setEnabled(fileName.length() > 0);
+    }
+
+    /**
+     * sets the format
+     *
+     * @param format
+     */
+    private void setFormat(String format) {
+        for (int i = 0; i < formatComboBox.getItemCount(); i++) {
+            ExportGraphicType exportGraphicType = (ExportGraphicType) formatComboBox.getItemAt(i);
+            if (exportGraphicType.getFileExtension().endsWith(format))
+                formatComboBox.setSelectedItem(exportGraphicType);
+        }
+    }
+
+    /**
+     * displays the dialog. Returns null, if user canceled, otherwise returns command string
+     * that specifies image file, format and other options
+     *
+     * @return
+     */
+    public String displayDialog() {
+        setVisible(true);
+        return command;
+    }
+
+    /**
+     * get the file
+     *
+     * @return filename
+     */
+    public String getFileName() {
+        return fileField.getText().trim();
+    }
+
+    /**
+     * set the file
+     *
+     * @param file
+     */
+    public void setFile(File file) {
+        if (file == null)
+            fileField.setText("");
+        else
+            fileField.setText(file.getPath());
+        updateEnabledState();
+    }
+
+    public String getFormat() {
+        return getFileExtension().substring(1);
+    }
+
+    public String getFileExtension() {
+        return ((ExportGraphicType) formatComboBox.getSelectedItem()).getFileExtension();
+    }
+
+    public ExportGraphicType getExportGraphicType() {
+        return (ExportGraphicType) formatComboBox.getSelectedItem();
+    }
+
+    public boolean isSaveVisibleOnly() {
+        return saveVisibleOnlyButton.isSelected();
+    }
+
+    public boolean isTextAsOutlinesEPS() {
+        return textAsOutlinesButton != null && textAsOutlinesButton.isSelected();
+    }
+
+    /**
+     * get list of known graphics types
+     *
+     * @return list of graphic types
+     */
+    public static java.util.List<ExportGraphicType> getGraphicTypes() {
+        if (graphicTypes.size() == 0) {
+            // TODO: use plugin-mechanism to load from directory
+            graphicTypes.add(new RenderedExportType());
+            graphicTypes.add(new EPSExportType());
+            graphicTypes.add(new GIFExportType());
+            graphicTypes.add(new JPGExportType());
+            graphicTypes.add(new PDFExportType());
+            graphicTypes.add(new PNGExportType());
+            graphicTypes.add(new SVGExportType());
+        }
+        return graphicTypes;
+    }
+}
diff --git a/src/jloda/export/ExportManager.java b/src/jloda/export/ExportManager.java
new file mode 100644
index 0000000..71b0d93
--- /dev/null
+++ b/src/jloda/export/ExportManager.java
@@ -0,0 +1,146 @@
+/**
+ * ExportManager.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import javax.swing.*;
+import javax.swing.filechooser.FileFilter;
+import java.awt.*;
+import java.io.File;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * manages graphics-export
+ * Daniel Huson  , 5.2006
+ */
+public class ExportManager {
+    private static ExportManager instance;
+    private final List<ExportGraphicType> graphicTypes;
+
+    private ExportManager() {
+        graphicTypes = new LinkedList<>();
+        loadGraphicTypes();
+    }
+
+    static public ExportManager getInstance() {
+        if (instance == null)
+            instance = new ExportManager();
+        return instance;
+    }
+
+    private void loadGraphicTypes() {
+        // TODO: use plugin-mechanism to load from directory
+        graphicTypes.add(new EPSExportType());
+        graphicTypes.add(new GIFExportType());
+        graphicTypes.add(new JPGExportType());
+        graphicTypes.add(new PNGExportType());
+        graphicTypes.add(new SVGExportType());
+        graphicTypes.add(new RenderedExportType());
+        graphicTypes.add(new PDFExportType());
+        // graphicTypes.add(new PDFExportType2());
+    }
+
+    public List<ExportGraphicType> getGraphicTypes() {
+        return graphicTypes;
+    }
+
+    private FileFilter allFileFilter = null;
+
+    public FileFilter getAllFileFilter() {
+        if (allFileFilter == null) {
+            allFileFilter = new FileFilter() {
+                public boolean accept(File f) {
+                    for (ExportGraphicType ext : getGraphicTypes()) {
+                        if (ext.getFileFilter().accept(f))
+                            return true;
+                    }
+                    return false;
+                }
+
+                public String getDescription() {
+                    StringBuilder buf = new StringBuilder();
+                    buf.append("Supported image file types: ");
+                    boolean first = true;
+                    for (ExportGraphicType exportGraphicType : getGraphicTypes()) {
+                        if (first)
+                            first = false;
+                        else
+                            buf.append(", ");
+                        buf.append(exportGraphicType.getFileFilter().getDescription());
+                    }
+                    return buf.toString();
+                }
+            };
+        }
+        return allFileFilter;
+    }
+
+    /**
+     * creates a panel whose paint method draws only what is currently visible in the given scrollpane
+     *
+     * @param imagePanel
+     * @param imageScrollPane
+     * @return panel clipped to region visible in scroll pane
+     */
+    public static JPanel makePanelFromScrollPane(final JPanel imagePanel, JScrollPane imageScrollPane) {
+        final Point apt = imageScrollPane.getViewport().getViewPosition();
+        final Dimension extent = (Dimension) imageScrollPane.getViewport().getExtentSize().clone();
+
+        final JPanel panel = new JPanel() {
+            public void paint(Graphics g0) {
+                doPaint(g0, imagePanel, apt, extent);
+            }
+        };
+        panel.setSize(extent);
+        return panel;
+    }
+
+    static private void doPaint(Graphics g0, JPanel imagePanel, Point apt, Dimension extent) {
+        //System.err.println("apt: " + apt);
+        //System.err.println("Extent: " + extent);
+        g0.translate(-apt.x, -apt.y);
+        g0.setClip(apt.x, apt.y, extent.width, extent.height);
+        g0.setColor(imagePanel.getBackground());
+        g0.fillRect(apt.x, apt.y, extent.width, extent.height);
+        imagePanel.paint(g0);
+        g0.translate(apt.x, apt.y);
+
+    }
+
+
+    /**
+     * result is true, if we are currently in a writeToFile call.
+     * This is used in paint to determine whether we are drawing to the screen or
+     * writing to a file
+     *
+     * @return true, if in writeToFile or getData
+     */
+    public static boolean inWriteToFileOrGetData() {
+        Throwable throwable = new Throwable();
+        throwable.fillInStackTrace();
+        StackTraceElement[] ste = throwable.getStackTrace();
+        for (StackTraceElement aSte : ste)
+            if (aSte.getMethodName().equals("writeToFile") || aSte.getMethodName().equals("getData"))
+                return true;
+        return false;
+    }
+
+
+}
diff --git a/src/jloda/export/GIFExportType.java b/src/jloda/export/GIFExportType.java
new file mode 100644
index 0000000..f9714c2
--- /dev/null
+++ b/src/jloda/export/GIFExportType.java
@@ -0,0 +1,162 @@
+/**
+ * GIFExportType.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import jloda.export.gifEncode.Gif89Encoder;
+import jloda.util.Basic;
+
+import javax.swing.*;
+import javax.swing.filechooser.FileFilter;
+import java.awt.*;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.image.BufferedImage;
+import java.io.*;
+
+/**
+ * @author Daniel Huson, Michael Schroeder
+ */
+public class GIFExportType extends FileFilter implements ExportGraphicType {
+
+    /**
+     * the mime type of this exportfile type
+     */
+    private final String mimeType = "image/gif";
+    /**
+     * the DataFlavor supported by this exportfile type
+     */
+    private final DataFlavor flavor;
+
+    public GIFExportType() {
+        flavor = new DataFlavor(mimeType + ";class=jloda.export.GIFExportType", "gif89 image");
+    }
+
+    public String getMimeType() {
+        return mimeType;
+    }
+
+    public DataFlavor getDataFlavor() {
+        return flavor;
+    }
+
+    /**
+     * <code>getData</code>: <i>currently not implemented since clipboard export of
+     * gif images is not intended.</i>
+     *
+     * @param panel
+     * @return
+     */
+    public Object getData(JPanel panel) {
+        return null;
+    }
+
+    public static void stream(JPanel panel, ByteArrayOutputStream out) throws IOException {
+        (new GIFExportType()).stream(panel, null, false, out);
+    }
+
+
+    /**
+     * writes image to a stream. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @param out
+     * @throws IOException
+     */
+    public void stream(JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage, OutputStream out) throws IOException {
+        JPanel panel;
+        if (showWholeImage || imageScrollPane == null)
+            panel = imagePanel;
+        else
+            panel = ExportManager.makePanelFromScrollPane(imagePanel, imageScrollPane);
+        BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
+        panel.paint(img.getGraphics());
+
+        BufferedOutputStream bos = new BufferedOutputStream(out);
+        Gif89Encoder enc = new Gif89Encoder(img);
+        enc.setTransparentIndex(-1);
+        enc.encode(bos);
+        bos.close();
+    }
+
+
+    public void writeToFile(File file, final JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage) throws IOException {
+        JPanel panel;
+        if (showWholeImage || imageScrollPane == null)
+            panel = imagePanel;
+        else
+            panel = ExportManager.makePanelFromScrollPane(imagePanel, imageScrollPane);
+
+        Image img = imagePanel.getGraphicsConfiguration().createCompatibleImage(panel.getWidth(), panel.getHeight());
+        Graphics2D g = (Graphics2D) img.getGraphics();
+        panel.paint(g);
+        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
+        Gif89Encoder enc = new Gif89Encoder(img);
+        enc.setTransparentIndex(-1);
+        enc.encode(bos);
+        bos.close();
+    }
+
+    /**
+     * writes the image as a gif file.
+     *
+     * @param file  the file to write to.
+     * @param panel the panel which paints the image.
+     */
+    public static void writeToFile(File file, JPanel panel) throws IOException {
+        (new GIFExportType()).writeToFile(file, panel, null, false);
+    }
+
+    public boolean accept(File f) {
+        if (f.isDirectory()) {
+            return true;
+        }
+        String extension = Basic.getSuffix(f.getName());
+        if (extension != null) {
+            if (extension.equalsIgnoreCase("gif"))
+                return true;
+        } else {
+            return false;
+        }
+        return false;
+    }
+
+    public String getDescription() {
+        return "GIF (*.gif)";
+    }
+
+    public String toString() {
+        return getDescription();
+    }
+
+    /**
+     * gets the associated file filter and filename filter
+     *
+     * @return filename filter
+     */
+    public jloda.util.FileFilter getFileFilter() {
+        return new jloda.util.FileFilter(getFileExtension());
+    }
+
+    public String getFileExtension() {
+        return ".gif";
+    }
+}
diff --git a/src/jloda/export/GraphicsFileFilters.java b/src/jloda/export/GraphicsFileFilters.java
new file mode 100644
index 0000000..4c8c92c
--- /dev/null
+++ b/src/jloda/export/GraphicsFileFilters.java
@@ -0,0 +1,206 @@
+/**
+ * GraphicsFileFilters.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import javax.swing.filechooser.FileFilter;
+import java.io.File;
+
+/**
+ * @author Daniel Huson, Michael Schroeder
+ */
+public class GraphicsFileFilters {
+
+
+    static class AllTypesFilter extends FileFilter {
+
+        public boolean accept(File f) {
+            if (f.isDirectory()) {
+                return true;
+            }
+
+            String extension = getExtension(f);
+            if (extension != null) {
+                if (
+                        extension.equalsIgnoreCase("jpeg") ||
+                                extension.equalsIgnoreCase("jpg") ||
+                                extension.equalsIgnoreCase("eps") ||
+                                extension.equalsIgnoreCase("svg") ||
+                                extension.equalsIgnoreCase("gif") ||
+                                extension.equalsIgnoreCase("png")) {
+                    return true;
+                }
+            } else {
+                return false;
+            }
+
+            return false;
+        }
+
+        public String getDescription() {
+            return "supported image file types (*.jpg,*.eps,*.svg,*.gif,*.png)";
+        }
+    }
+
+    static class JpgFilter extends FileFilter {
+
+        public boolean accept(File f) {
+            if (f.isDirectory()) {
+                return true;
+            }
+
+            String extension = getExtension(f);
+            if (extension != null) {
+                if (extension.equalsIgnoreCase("jpeg") ||
+                        extension.equalsIgnoreCase("jpg")) {
+                    return true;
+                }
+            } else {
+                return false;
+            }
+            return false;
+        }
+
+        public String getDescription() {
+            return "JPG";
+        }
+
+        public String getString() {
+            return getDescription();
+        }
+    }
+
+    static class EpsFilter extends FileFilter {
+
+        public boolean accept(File f) {
+            if (f.isDirectory()) {
+                return true;
+            }
+            String extension = getExtension(f);
+            if (extension != null) {
+                if (extension.equalsIgnoreCase("eps"))
+                    return true;
+            } else {
+                return false;
+            }
+            return false;
+        }
+
+        public String getDescription() {
+            return "Encapsulated Postscript (*.eps)";
+        }
+
+        public String getString() {
+            return getDescription();
+        }
+    }
+
+    static class SvgFilter extends FileFilter {
+
+        public boolean accept(File f) {
+            if (f.isDirectory()) {
+                return true;
+            }
+            String extension = getExtension(f);
+            if (extension != null) {
+                if (extension.equalsIgnoreCase("svg"))
+                    return true;
+            } else {
+                return false;
+            }
+            return false;
+        }
+
+        public String getDescription() {
+            return "Scalable Vector Graphics (*.svg)";
+        }
+
+        public String getString() {
+            return getDescription();
+        }
+    }
+
+    static class GifFilter extends FileFilter {
+
+        public boolean accept(File f) {
+            if (f.isDirectory()) {
+                return true;
+            }
+            String extension = getExtension(f);
+            if (extension != null) {
+                if (extension.equalsIgnoreCase("gif"))
+                    return true;
+            } else {
+                return false;
+            }
+            return false;
+        }
+
+        public String getDescription() {
+            return "GIF";
+        }
+
+        public String getString() {
+            return getDescription();
+        }
+    }
+
+    static class PngFilter extends FileFilter {
+
+        public boolean accept(File f) {
+            if (f.isDirectory()) {
+                return true;
+            }
+            String extension = getExtension(f);
+            if (extension != null) {
+                if (extension.equalsIgnoreCase("png"))
+                    return true;
+            } else {
+                return false;
+            }
+            return false;
+        }
+
+        public String getDescription() {
+            return "PNG";
+        }
+
+        public String getString() {
+            return getDescription();
+        }
+    }
+
+
+    /**
+     * returns the extension of a given file.
+     *
+     * @param f the file
+     * @return the file extension of <code>f</code>
+     */
+    public static String getExtension(File f) {
+        String ext = null;
+        String s = f.getName();
+        int i = s.lastIndexOf('.');
+
+        if (i > 0 && i < s.length() - 1) {
+            ext = s.substring(i + 1);
+        }
+        return ext;
+    }
+}
diff --git a/src/jloda/export/JPGExportType.java b/src/jloda/export/JPGExportType.java
new file mode 100644
index 0000000..b743346
--- /dev/null
+++ b/src/jloda/export/JPGExportType.java
@@ -0,0 +1,172 @@
+/**
+ * JPGExportType.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import jloda.util.Basic;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import javax.swing.filechooser.FileFilter;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.image.BufferedImage;
+import java.io.*;
+
+/**
+ * The export filetype for jpg images.
+ *
+ * @author Daniel Huson, Michael Schroeder
+ */
+public class JPGExportType extends FileFilter implements ExportGraphicType {
+
+    /**
+     * the mime type of this exportfile type
+     */
+    private final String mimeType = "image/jpeg";
+    /**
+     * the DataFlavor supported by this exportfile type
+     */
+    private final DataFlavor flavor;
+
+    public JPGExportType() {
+        flavor = new DataFlavor(mimeType + ";class=jloda.export.JPGExportType", "jpeg image");
+    }
+
+    public String getMimeType() {
+        return mimeType;
+    }
+
+    public DataFlavor getDataFlavor() {
+        return flavor;
+    }
+
+    /**
+     * <code>getData</code>: <i>currently not implemented since clipboard export of
+     * jpg images is not intended.</i>
+     *
+     * @param panel
+     * @return
+     */
+    public Object getData(JPanel panel) {
+        return null;
+    }
+
+    public static void stream(JPanel panel, ByteArrayOutputStream out) throws IOException {
+        (new JPGExportType()).stream(panel, null, false, out);
+    }
+
+    /**
+     * writes image to a stream. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @param out
+     * @throws IOException
+     */
+    public void stream(JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage, OutputStream out) throws IOException {
+        JPanel panel;
+        if (showWholeImage || imageScrollPane == null)
+            panel = imagePanel;
+        else
+            panel = ExportManager.makePanelFromScrollPane(imagePanel, imageScrollPane);
+        BufferedImage img = new BufferedImage(panel.getWidth(), panel.getHeight(), BufferedImage.TYPE_INT_RGB);
+        panel.paint(img.getGraphics());
+        ImageIO.write(img, "jpg", out);
+    }
+
+    /**
+     * writes image to file. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param file
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @throws IOException
+     */
+    public void writeToFile(File file, final JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage) throws IOException {
+        JPanel panel;
+        if (showWholeImage || imageScrollPane == null)
+            panel = imagePanel;
+        else
+            panel = ExportManager.makePanelFromScrollPane(imagePanel, imageScrollPane);
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        BufferedImage img = new BufferedImage(panel.getWidth(), panel.getHeight(), BufferedImage.TYPE_INT_RGB);
+        panel.paint(img.getGraphics());
+
+        ImageIO.write(img, "jpg", out);
+
+        FileOutputStream fos = new FileOutputStream(file);
+        fos.write(out.toByteArray());
+        fos.close();
+    }
+
+    /**
+     * writes the image as a jpg file.
+     *
+     * @param file  the file to write to.
+     * @param panel the panel which paints the image.
+     */
+    public static void writeToFile(File file, JPanel panel) throws IOException {
+        (new JPGExportType()).writeToFile(file, panel, null, false);
+    }
+
+    public boolean accept(File f) {
+        if (f.isDirectory()) {
+            return true;
+        }
+
+        String extension = Basic.getSuffix(f.getName());
+        if (extension != null) {
+            if (extension.equalsIgnoreCase("jpeg") ||
+                    extension.equalsIgnoreCase("jpg")) {
+                return true;
+            }
+        } else {
+            return false;
+        }
+        return false;
+    }
+
+    public String getDescription() {
+        return "JPEG (*.jpg, *.jpeg)";
+    }
+
+    public String toString() {
+        return getDescription();
+    }
+
+    /**
+     * gets the associated file filter and filename filter
+     *
+     * @return filename filter
+     */
+    public jloda.util.FileFilter getFileFilter() {
+        jloda.util.FileFilter filter = new jloda.util.FileFilter(getFileExtension());
+        filter.getFileExtensions().add(".jpeg");
+        return filter;
+    }
+
+    public String getFileExtension() {
+        return ".jpg";
+    }
+}
diff --git a/src/jloda/export/PDFExportType.java b/src/jloda/export/PDFExportType.java
new file mode 100644
index 0000000..9e5318e
--- /dev/null
+++ b/src/jloda/export/PDFExportType.java
@@ -0,0 +1,186 @@
+/**
+ * PDFExportType.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+
+import gnu.jpdf.PDFJob;
+import jloda.util.Basic;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.print.PageFormat;
+import java.awt.print.Paper;
+import java.io.*;
+
+
+/**
+ * The export filetype for pdf images.
+ *
+ * @author huson
+ * @version 2011
+ */
+public class PDFExportType extends SVGExportType implements ExportGraphicType {
+
+    /**
+     * the mime type of this exportfile type
+     */
+    private final String mimeType = "image/pdf";
+    /**
+     * the DataFlavor supported by this exportfile type
+     */
+    private final DataFlavor flavor;
+
+    public PDFExportType() {
+        flavor = new DataFlavor(mimeType + ";class=jloda.export.PDFExportType", "Portable Document Format");
+    }
+
+    public String getMimeType() {
+        return mimeType;
+    }
+
+    public DataFlavor getDataFlavor() {
+        return flavor;
+    }
+
+    public Object getData(JPanel panel) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try {
+            stream(panel, out);
+        } catch (IOException ex) {
+            Basic.caught(ex);
+        }
+        return new ByteArrayInputStream(out.toByteArray());
+    }
+
+    /**
+     * stream the image data to a given <code>ByteArrayOutputStream</code>.
+     *
+     * @param panel the panel which paints the image.
+     * @param out   the ByteArrayOutputStream.
+     */
+    public static void stream(JPanel panel, OutputStream out) throws IOException {
+
+        int width = panel.getWidth();
+        int height = panel.getHeight();
+
+        // Get the Graphics object for pdf writing
+        Graphics pdfGraphics;
+        PDFJob job = new PDFJob(out);
+
+        PageFormat pageFormat = new PageFormat();
+        Paper paper = new Paper();
+        paper.setSize(width, height);
+        pageFormat.setPaper(paper);
+
+        pdfGraphics = job.getGraphics(pageFormat);
+
+        panel.paint(pdfGraphics);
+
+        pdfGraphics.dispose();
+        job.end();
+        out.flush();
+    }
+
+
+    /**
+     * writes image to a stream. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @param out
+     * @throws java.io.IOException
+     */
+    public void stream(JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage, OutputStream out) throws IOException {
+        JPanel panel;
+        if (showWholeImage || imageScrollPane == null)
+            panel = imagePanel;
+        else {
+            // panel=(JPanel)((JViewport)imageScrollPane.getComponent(0)).getComponent(0) ;
+            panel = ExportManager.makePanelFromScrollPane(imagePanel, imageScrollPane);
+        }
+
+        stream(panel, out);
+    }
+
+    /**
+     * writes image to file. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param file
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @throws java.io.IOException
+     */
+    public void writeToFile(File file, final JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage) throws IOException {
+        OutputStream fos = new FileOutputStream(file);
+
+        stream(imagePanel, imageScrollPane, showWholeImage, fos);
+        fos.close();
+    }
+
+    /**
+     * write the image into an pdf file.
+     *
+     * @param file  the file to write to.
+     * @param panel the panel which paints the image.
+     */
+    public static void writeToFile(File file, JPanel panel) throws IOException {
+        (new PDFExportType()).writeToFile(file, panel, null, false);
+    }
+
+    public boolean accept(File f) {
+        if (f.isDirectory()) {
+            return true;
+        }
+        String extension = Basic.getSuffix(f.getName());
+        if (extension != null) {
+            if (extension.equalsIgnoreCase("pdf"))
+                return true;
+        } else {
+            return false;
+        }
+        return false;
+    }
+
+    public String getDescription() {
+        return "PDF (*.pdf)";
+    }
+
+    public String toString() {
+        return getDescription();
+    }
+
+    /**
+     * gets the associated file filter and filename filter
+     *
+     * @return filename filter
+     */
+    public jloda.util.FileFilter getFileFilter() {
+        return new jloda.util.FileFilter(getFileExtension());
+    }
+
+    public String getFileExtension() {
+        return ".pdf";
+    }
+}
diff --git a/src/jloda/export/PNGExportType.java b/src/jloda/export/PNGExportType.java
new file mode 100644
index 0000000..4f80528
--- /dev/null
+++ b/src/jloda/export/PNGExportType.java
@@ -0,0 +1,165 @@
+/**
+ * PNGExportType.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import jloda.util.Basic;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import javax.swing.filechooser.FileFilter;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.image.BufferedImage;
+import java.io.*;
+
+/**
+ * @author Daniel Huson, Michael Schroeder
+ */
+public class PNGExportType extends FileFilter implements ExportGraphicType {
+
+    /**
+     * the mime type of this exportfile type
+     */
+    private final String mimeType = "image/png";
+    /**
+     * the DataFlavor supported by this exportfile type
+     */
+    private final DataFlavor flavor;
+
+    public PNGExportType() {
+        flavor = new DataFlavor(mimeType + ";class=jloda.export.PNGExportType", "png image");
+    }
+
+    public String getMimeType() {
+        return mimeType;
+    }
+
+    public DataFlavor getDataFlavor() {
+        return flavor;
+    }
+
+    /**
+     * <code>getData</code>: <i>currently not implemented since clipboard export of
+     * png images is not intended.</i>
+     *
+     * @param panel
+     * @return
+     */
+    public Object getData(JPanel panel) {
+        return null;
+    }
+
+    public static void stream(JPanel panel, ByteArrayOutputStream out) throws IOException {
+        (new PNGExportType()).stream(panel, null, false, out);
+    }
+
+    /**
+     * writes image to a stream. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @param out
+     * @throws IOException
+     */
+    public void stream(JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage, OutputStream out) throws IOException {
+        JPanel panel;
+        if (showWholeImage || imageScrollPane == null)
+            panel = imagePanel;
+        else
+            panel = ExportManager.makePanelFromScrollPane(imagePanel, imageScrollPane);
+        BufferedImage img = new BufferedImage(panel.getWidth(), panel.getHeight(), BufferedImage.TYPE_INT_RGB);
+        panel.paint(img.getGraphics());
+        ImageIO.write(img, "png", out);
+    }
+
+    /**
+     * writes image to file. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param file
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @throws IOException
+     */
+    public void writeToFile(File file, final JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage) throws IOException {
+        JPanel panel;
+        if (showWholeImage || imageScrollPane == null)
+            panel = imagePanel;
+        else
+            panel = ExportManager.makePanelFromScrollPane(imagePanel, imageScrollPane);
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        BufferedImage img = new BufferedImage(panel.getWidth(), panel.getHeight(), BufferedImage.TYPE_INT_RGB);
+        panel.paint(img.getGraphics());
+
+        ImageIO.write(img, "png", out);
+
+        FileOutputStream fos = new FileOutputStream(file);
+        fos.write(out.toByteArray());
+        fos.close();
+    }
+
+    /**
+     * writes the image as a gif file.
+     *
+     * @param file  the file to write to.
+     * @param panel the panel which paints the image.
+     */
+    public static void writeToFile(File file, JPanel panel) throws IOException {
+        (new PNGExportType()).writeToFile(file, panel, null, false);
+    }
+
+    public boolean accept(File f) {
+        if (f.isDirectory()) {
+            return true;
+        }
+        String extension = Basic.getSuffix(f.getName());
+        if (extension != null) {
+            if (extension.equalsIgnoreCase("png"))
+                return true;
+        } else {
+            return false;
+        }
+        return false;
+    }
+
+    public String getDescription() {
+        return "PNG (*.png)";
+    }
+
+    public String toString() {
+        return getDescription();
+    }
+
+    /**
+     * gets the associated file filter and filename filter
+     *
+     * @return filename filter
+     */
+    public jloda.util.FileFilter getFileFilter() {
+        return new jloda.util.FileFilter(getFileExtension());
+    }
+
+    public String getFileExtension() {
+        return ".png";
+    }
+}
diff --git a/src/jloda/export/RenderedExportType.java b/src/jloda/export/RenderedExportType.java
new file mode 100644
index 0000000..5a46e99
--- /dev/null
+++ b/src/jloda/export/RenderedExportType.java
@@ -0,0 +1,163 @@
+/**
+ * RenderedExportType.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import jloda.util.Basic;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import javax.swing.filechooser.FileFilter;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.image.BufferedImage;
+import java.io.*;
+
+/**
+ * a pixel-based export type.
+ * Since the DataFlavor is DataFlavor.imageFlavor, the JVM will
+ * take care of the mapping to native clipboard types (e.g. WIN32: BMP, MAC OS: PICT).
+ *
+ * @author huson, schroeder
+ */
+public class RenderedExportType extends FileFilter implements ExportGraphicType {
+
+    /**
+     * the mime type of this exportfile type
+     */
+    private final String mimeType = DataFlavor.imageFlavor.getMimeType();
+    /**
+     * the DataFlavor supported by this exportfile type
+     */
+    private final DataFlavor flavor;
+
+    public RenderedExportType() {
+        flavor = DataFlavor.imageFlavor;
+    }
+
+    public String getMimeType() {
+        return mimeType;
+    }
+
+    public DataFlavor getDataFlavor() {
+        return flavor;
+    }
+
+    public Object getData(JPanel panel) {
+        /*
+        Image img = panel.createImage(panel.getWidth(), panel.getHeight());
+        Graphics g = img.getGraphics();
+
+        panel.paint(g);
+        g.dispose();
+        */
+        BufferedImage img = new BufferedImage(panel.getWidth(), panel.getHeight(), BufferedImage.TYPE_INT_RGB);
+        panel.paint(img.getGraphics());
+
+        return img;
+
+    }
+
+    public static void stream(JPanel panel, ByteArrayOutputStream out) throws IOException {
+        (new RenderedExportType()).stream(panel, null, false, out);
+    }
+
+    /**
+     * writes image to a stream. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @param out
+     * @throws IOException
+     */
+    public void stream(JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage, OutputStream out) throws IOException {
+        JPanel panel;
+        if (showWholeImage || imageScrollPane == null)
+            panel = imagePanel;
+        else
+            panel = ExportManager.makePanelFromScrollPane(imagePanel, imageScrollPane);
+        BufferedImage img = new BufferedImage(panel.getWidth(), panel.getHeight(), BufferedImage.TYPE_INT_RGB);
+        panel.paint(img.getGraphics());
+        ImageIO.write(img, "bmp", out);
+    }
+
+    public void writeToFile(File file, final JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage) throws IOException {
+        JPanel panel;
+        if (showWholeImage || imageScrollPane == null)
+            panel = imagePanel;
+        else
+            panel = ExportManager.makePanelFromScrollPane(imagePanel, imageScrollPane);
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        BufferedImage img = new BufferedImage(panel.getWidth(), panel.getHeight(), BufferedImage.TYPE_INT_RGB);
+        panel.paint(img.getGraphics());
+
+        ImageIO.write(img, "bmp", out);
+
+        FileOutputStream fos = new FileOutputStream(file);
+        fos.write(out.toByteArray());
+        fos.close();
+    }
+
+    /**
+     * writes the image in the bmp file format.
+     *
+     * @param file
+     * @param panel
+     */
+    public void writeToFile(File file, JPanel panel) throws IOException {
+        (new RenderedExportType()).writeToFile(file, panel, null, false);
+    }
+
+    public boolean accept(File f) {
+        if (f.isDirectory()) {
+            return true;
+        }
+        String extension = Basic.getSuffix(f.getName());
+        if (extension != null) {
+            if (extension.equalsIgnoreCase(".eps"))
+                return true;
+        } else {
+            return false;
+        }
+        return false;
+    }
+
+    public String getDescription() {
+        return "BMP (*.bmp)";
+    }
+
+    public String toString() {
+        return getDescription();
+    }
+
+    /**
+     * gets the associated file filter and filename filter
+     *
+     * @return filename filter
+     */
+    public jloda.util.FileFilter getFileFilter() {
+        return new jloda.util.FileFilter(getFileExtension());
+    }
+
+    public String getFileExtension() {
+        return ".bmp";
+    }
+}
diff --git a/src/jloda/export/SVGExportType.java b/src/jloda/export/SVGExportType.java
new file mode 100644
index 0000000..efedd6f
--- /dev/null
+++ b/src/jloda/export/SVGExportType.java
@@ -0,0 +1,180 @@
+/**
+ * SVGExportType.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+
+import jloda.util.Basic;
+import org.apache.batik.dom.GenericDOMImplementation;
+import org.apache.batik.svggen.SVGGraphics2D;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+
+import javax.swing.*;
+import javax.swing.filechooser.FileFilter;
+import java.awt.datatransfer.DataFlavor;
+import java.io.*;
+
+/**
+ * The export filetype for svg images.
+ *
+ * @author huson, schroeder, 2007
+ */
+public class SVGExportType extends FileFilter implements ExportGraphicType {
+
+    /**
+     * the mime type of this exportfile type
+     */
+    private final String mimeType = "image/svg+xml";
+    /**
+     * the DataFlavor supported by this exportfile type
+     */
+    private final DataFlavor flavor;
+
+    public SVGExportType() {
+        flavor = new DataFlavor(mimeType + ";class=jloda.export.SVGExportType", "Scalable Vector Graphic");
+    }
+
+    public String getMimeType() {
+        return mimeType;
+    }
+
+    public DataFlavor getDataFlavor() {
+        return flavor;
+    }
+
+    public Object getData(JPanel panel) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try {
+            stream(panel, out);
+        } catch (IOException ex) {
+            Basic.caught(ex);
+        }
+        return new ByteArrayInputStream(out.toByteArray());
+    }
+
+    /**
+     * stream the image data to a given <code>ByteArrayOutputStream</code>.
+     *
+     * @param panel the panel which paints the image.
+     * @param out   the ByteArrayOutputStream.
+     */
+    public static void stream(JPanel panel, ByteArrayOutputStream out) throws IOException {
+        (new SVGExportType()).stream(panel, null, false, out);
+    }
+
+    /**
+     * writes image to a stream. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @param out
+     * @throws IOException
+     */
+    public void stream(JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage, OutputStream out) throws IOException {
+        JPanel panel;
+        if (showWholeImage || imageScrollPane == null)
+            panel = imagePanel;
+        else
+            panel = ExportManager.makePanelFromScrollPane(imagePanel, imageScrollPane);
+        DOMImplementation dom = GenericDOMImplementation.getDOMImplementation();
+        Document doc = dom.createDocument(null, "svg", null);
+        SVGGraphics2D svgGenerator = new SVGGraphics2D(doc);
+
+        panel.paint(svgGenerator);
+
+        svgGenerator.stream(new OutputStreamWriter(out, "UTF-8"));
+        out.flush();
+        out.close();
+    }
+
+    /**
+     * writes image to file. If scrollPane given and showWholeImage=true, draws only visible portion
+     * of panel
+     *
+     * @param file
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param showWholeImage
+     * @throws IOException
+     */
+    public void writeToFile(File file, final JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage) throws IOException {
+        JPanel panel;
+        if (showWholeImage || imageScrollPane == null)
+            panel = imagePanel;
+        else
+            panel = ExportManager.makePanelFromScrollPane(imagePanel, imageScrollPane);
+
+        DOMImplementation dom = GenericDOMImplementation.getDOMImplementation();
+        Document doc = dom.createDocument(null, "svg", null);
+        SVGGraphics2D svgGenerator = new SVGGraphics2D(doc);
+        svgGenerator.setSVGCanvasSize(panel.getSize());
+        panel.paint(svgGenerator);
+        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));
+        svgGenerator.stream(bw);
+        bw.close();
+    }
+
+    /**
+     * write the image into an svg file.
+     *
+     * @param file  the file to write to.
+     * @param panel the panel which paints the image.
+     */
+    public static void writeToFile(File file, JPanel panel) throws IOException, FileNotFoundException {
+        (new SVGExportType()).writeToFile(file, panel, null, false);
+    }
+
+    public boolean accept(File f) {
+        if (f.isDirectory()) {
+            return true;
+        }
+        String extension = Basic.getSuffix(f.getName());
+        if (extension != null) {
+            if (extension.equalsIgnoreCase("svg"))
+                return true;
+        } else {
+            return false;
+        }
+        return false;
+    }
+
+    public String getDescription() {
+        return "SVG (*.svg)";
+    }
+
+    public String toString() {
+        return getDescription();
+    }
+
+    /**
+     * gets the associated file filter and filename filter
+     *
+     * @return filename filter
+     */
+    public jloda.util.FileFilter getFileFilter() {
+        return new jloda.util.FileFilter(getFileExtension());
+    }
+
+    public String getFileExtension() {
+        return ".svg";
+    }
+}
diff --git a/src/jloda/export/SVGStringExportType.java b/src/jloda/export/SVGStringExportType.java
new file mode 100644
index 0000000..16d0ef9
--- /dev/null
+++ b/src/jloda/export/SVGStringExportType.java
@@ -0,0 +1,124 @@
+/**
+ * SVGStringExportType.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import jloda.util.Basic;
+
+import javax.swing.*;
+import java.awt.datatransfer.DataFlavor;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * @author huson, schroeder
+ */
+public class SVGStringExportType implements ExportGraphicType {
+
+    private final String mimeType = "text/xml";
+    private final DataFlavor flavor;
+
+    public SVGStringExportType() {
+        flavor = new DataFlavor(mimeType, "SVG as plain xml");
+    }
+
+    public String getMimeType() {
+        return mimeType;
+    }
+
+    public DataFlavor getDataFlavor() {
+        return flavor;
+    }
+
+    public Object getData(JPanel panel) {
+        /*ByteArrayOutputStream out = new ByteArrayOutputStream();
+        SVGExport.export(gv,out);
+        try {
+            out.close();
+        } catch (IOException e) {
+            Basic.caught(e);
+        }
+        return new ByteArrayInputStream(out.toByteArray()); */
+        return null;
+    }
+
+    public void stream(JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage, OutputStream out) throws IOException {
+
+    }
+
+    public void writeToFile(File file, JPanel imagePanel, JScrollPane imageScrollPane, boolean showWholeImage) throws IOException {
+        writeToFile(file, imagePanel);
+    }
+
+    public static void writeToFile(File file, JPanel panel) {
+
+        /*ByteArrayOutputStream out = new ByteArrayOutputStream();
+        SVGExport.export(gv,out);
+        try {
+            out.close();
+            FileOutputStream fos = new FileOutputStream(file);
+            fos.write(out.toByteArray());
+            fos.flush();
+            fos.close();
+        } catch (Exception e) {
+            Basic.caught(e);
+        }  */
+
+    }
+
+    public boolean accept(File f) {
+        if (f.isDirectory()) {
+            return true;
+        }
+        String extension = Basic.getSuffix(f.getName());
+        if (extension != null) {
+            if (extension.equalsIgnoreCase("svg"))
+                return true;
+        } else {
+            return false;
+        }
+        return false;
+    }
+
+    public String getDescription() {
+        return "SVG (*.svg)";
+    }
+
+    public String toString() {
+        return getDescription();
+    }
+
+    /**
+     * gets the associated file filter and filename filter
+     *
+     * @return filename filter
+     */
+    public jloda.util.FileFilter getFileFilter() {
+        return new jloda.util.FileFilter(getFileExtension());
+    }
+
+    public boolean accept(File file, String s) {
+        return false;
+    }
+
+    public String getFileExtension() {
+        return ".svg";
+    }
+}
diff --git a/src/jloda/export/SaveImageDialog.java b/src/jloda/export/SaveImageDialog.java
new file mode 100644
index 0000000..2736818
--- /dev/null
+++ b/src/jloda/export/SaveImageDialog.java
@@ -0,0 +1,228 @@
+/**
+ * SaveImageDialog.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import jloda.gui.ChooseFileDialog;
+import jloda.util.Alert;
+import jloda.util.Basic;
+import jloda.util.ProgramProperties;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+/**
+ * A Dialog for saving the image in various graphic file formats.
+ *
+ * @author Daniel Huson, Michael Schroeder, 2005
+ */
+public class SaveImageDialog extends JDialog {
+    static public boolean useAWTDialog = false; // by default, use Swing file dialog to save file
+    static public final boolean allowEPS = true;
+
+    final JComboBox formatComboBox;
+    JRadioButton visibleRegionButton = null;
+    JRadioButton wholeImageButton = null;
+    final JCheckBox drawTextAsOutlinesCB;
+    final JFrame parent;
+
+    final ExportManager exportManager = ExportManager.getInstance();
+
+    final JPanel imagePanel;
+    final JScrollPane imageScrollPane;
+
+    final String fileBaseName;
+    final public static String GRAPHICSFORMAT = "GraphicsFormat";
+    final public static String GRAPHICSDIR = "GraphicsDir";
+
+
+    /**
+     * creates and displays a save image dialog and saves image, if desired. If performSave is true, saves image, other
+     * not, in which case a command string is returned via the getCommand method
+     *
+     * @param parent
+     * @param imagePanel
+     * @param imageScrollPane
+     * @param fileBaseName
+     */
+    public SaveImageDialog(JFrame parent, JPanel imagePanel, JScrollPane imageScrollPane, String fileBaseName) {
+        super(parent, "Export Image");
+        this.parent = parent;
+        this.imagePanel = imagePanel;
+        this.imageScrollPane = imageScrollPane;
+        this.fileBaseName = fileBaseName;
+
+        setModal(true);
+        setSize(new Dimension(260, 200));
+        setLocationRelativeTo(parent);
+
+        getContentPane().setLayout(new BorderLayout());
+
+        JPanel panel1 = new JPanel();
+        panel1.setLayout(new BorderLayout());
+        panel1.add(new JLabel("Format:"), BorderLayout.WEST);
+        panel1.setBorder(BorderFactory.createEtchedBorder());
+
+        formatComboBox = new JComboBox();
+
+        for (ExportGraphicType exportGraphicType : exportManager.getGraphicTypes()) {
+            if (true)
+                formatComboBox.addItem(exportGraphicType);
+        }
+        panel1.add(formatComboBox, BorderLayout.CENTER);
+
+        getContentPane().add(panel1, BorderLayout.NORTH);
+
+        JPanel panel2 = new JPanel();
+        panel2.setLayout(new BoxLayout(panel2, BoxLayout.Y_AXIS));
+
+        if (imageScrollPane != null) {
+            ButtonGroup group = new ButtonGroup();
+            visibleRegionButton = new JRadioButton("Save visible region");
+            group.add(visibleRegionButton);
+            panel2.add(visibleRegionButton);
+
+            wholeImageButton = new JRadioButton("Save whole image");
+            group.add(wholeImageButton);
+            panel2.add(wholeImageButton);
+        }
+
+        drawTextAsOutlinesCB = new JCheckBox("Convert Text to Graphics");
+        if (allowEPS)
+            panel2.add(drawTextAsOutlinesCB);
+        getContentPane().add(panel2, BorderLayout.CENTER);
+
+        JPanel panel3 = new JPanel();
+        panel3.setLayout(new BorderLayout());
+        panel3.setBorder(BorderFactory.createEtchedBorder());
+        panel3.add(new JButton(getCancelAction()), BorderLayout.WEST);
+        panel3.add(new JButton(getApplyAction()), BorderLayout.EAST);
+        getContentPane().add(panel3, BorderLayout.SOUTH);
+
+        formatComboBox.addItemListener(new ItemListener() {
+            public void itemStateChanged(ItemEvent e) {
+                Object item = e.getItem();
+                drawTextAsOutlinesCB.setEnabled(item instanceof EPSExportType);
+            }
+        });
+
+        String preSelectFormat = ProgramProperties.get(GRAPHICSFORMAT, (new EPSExportType()).getFileExtension());
+        for (int i = 0; i < formatComboBox.getItemCount(); i++) {
+            if (((ExportGraphicType) formatComboBox.getItemAt(i)).getFileExtension().equals(preSelectFormat)) {
+                formatComboBox.setSelectedIndex(i);
+                break;
+            }
+        }
+
+        boolean preSelectWholeImage = ProgramProperties.get("graphicsWholeImage", true);
+        if (preSelectWholeImage) {
+            if (wholeImageButton != null)
+                wholeImageButton.setSelected(true);
+        } else {
+            if (visibleRegionButton != null)
+                visibleRegionButton.setSelected(true);
+        }
+        drawTextAsOutlinesCB.setSelected(ProgramProperties.get("graphicsConvertText", true));
+
+        setVisible(true);
+    }
+
+    /**
+     * the cancel action
+     *
+     * @return cancel action
+     */
+    AbstractAction getCancelAction() {
+        AbstractAction action = new AbstractAction() {
+            public void actionPerformed(ActionEvent e) {
+                setVisible(false);
+                dispose();
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Cancel");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Cancel");
+        return action;
+    }
+
+    /**
+     * the apply action
+     *
+     * @return apply action
+     */
+    AbstractAction getApplyAction() {
+        AbstractAction action = new AbstractAction() {
+            public void actionPerformed(ActionEvent e) {
+                doSaveDialog(parent);
+                setVisible(false);
+                dispose();
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Apply");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Apply");
+        return action;
+    }
+
+    /**
+     * displays the file chooser and saves
+     *
+     * @param parent
+     */
+    private void doSaveDialog(JFrame parent) {
+        final ExportGraphicType graphicsType = (ExportGraphicType) formatComboBox.getSelectedItem();
+
+        FilenameFilter fileNameFilter = new FilenameFilter() {
+            public boolean accept(File dir, String name) {
+                return graphicsType.getFileFilter().accept(new File(dir, name));
+            }
+        };
+
+        File file = ChooseFileDialog.chooseFileToSave(parent, new File(fileBaseName), graphicsType.getFileFilter(), fileNameFilter, null, "Save Image", graphicsType.getFileExtension());
+
+        if (file == null)
+            return;
+
+        if (wholeImageButton != null)
+            ProgramProperties.put("graphicsWholeImage", wholeImageButton.isSelected());
+        ProgramProperties.put("graphicsConvertText", drawTextAsOutlinesCB.isSelected());
+        ProgramProperties.put(GRAPHICSDIR, file.getParentFile());
+        ProgramProperties.put(GRAPHICSFORMAT, graphicsType.getFileExtension());
+
+        try {
+            boolean textAsOutlines;
+            if (graphicsType instanceof EPSExportType) {
+                EPSExportType eps = (EPSExportType) graphicsType;
+                textAsOutlines = drawTextAsOutlinesCB.isSelected();
+                eps.setDrawTextAsOutlines(textAsOutlines);
+            }
+            boolean visibleOnly = wholeImageButton == null || !wholeImageButton.isSelected();
+            graphicsType.writeToFile(file, imagePanel, imageScrollPane, !visibleOnly);
+            System.err.println("Written to file: " + file);
+
+        } catch (IOException ex) {
+            Basic.caught(ex);
+            new Alert("Image NOT saved: " + ex);
+        }
+    }
+}
diff --git a/src/jloda/export/TransferableGraphic.java b/src/jloda/export/TransferableGraphic.java
new file mode 100644
index 0000000..10bb8f8
--- /dev/null
+++ b/src/jloda/export/TransferableGraphic.java
@@ -0,0 +1,140 @@
+/**
+ * TransferableGraphic.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.export;
+
+import jloda.util.Alert;
+
+import javax.swing.*;
+import java.awt.datatransfer.*;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Transferable for exporting graphics to the clipboard.
+ * To add a new export type, implement the <code>jloda.export.ExportGraphicType</code>
+ * interface and add it to the addCommonTypes or addCustomTypes() method.
+ *
+ * @author huson, schroeder
+ */
+public class TransferableGraphic implements ClipboardOwner, Transferable {
+
+    /**
+     * map supported <code>DataFlavor</code>s to
+     * <code>jloda.export.ExportGraphicType</code>s. *
+     */
+    private final Map types = new HashMap();
+
+    /**
+     * the JPanel doing the paint work
+     */
+    private final JPanel panel;
+
+    public TransferableGraphic(JPanel panel) {
+        this(panel, null);
+    }
+
+    public TransferableGraphic(JPanel panel, JScrollPane scrollPane) {
+        if (scrollPane != null)
+            this.panel = ExportManager.makePanelFromScrollPane(panel, scrollPane);
+        else
+            this.panel = panel;
+        addCommonTypes();
+        //addCustomTypes();
+    }
+
+    public void lostOwnership(Clipboard clipboard, Transferable contents) {
+    }
+
+
+    public DataFlavor[] getTransferDataFlavors() {
+        DataFlavor[] flavors = new DataFlavor[types.size()];
+        types.keySet().toArray(flavors);
+        return flavors;
+    }
+
+
+    public boolean isDataFlavorSupported(DataFlavor flavor) {
+        return types.containsKey(flavor);
+    }
+
+    /**
+     * get the transfer data from supported exportTypes
+     *
+     * @param dataFlavor the requested dataFlavor
+     * @return the data to be transferred to the clipboard
+     * @throws UnsupportedFlavorException
+     * @throws IOException
+     */
+    public Object getTransferData(DataFlavor dataFlavor) throws UnsupportedFlavorException, IOException {
+
+        ExportGraphicType type = (ExportGraphicType) types.get(dataFlavor);
+        if (type != null) {
+            return type.getData(panel);
+        } else {
+            throw new UnsupportedFlavorException(dataFlavor);
+        }
+    }
+
+    /**
+     * add types which don't need to be added to the native-mime mapping.
+     */
+    private void addCommonTypes() {
+
+        ExportGraphicType renderedType = new RenderedExportType();
+        types.put(renderedType.getDataFlavor(), renderedType);
+    }
+
+    /**
+     * add exportTypes which alter the mapping of native clipboard-types
+     * to mime types.
+     */
+    private void addCustomTypes() {
+
+        addType("Encapsulated PostScript", "image/x-eps",
+                "EPS graphic",
+                "jloda.export.EPSExportType");
+    }
+
+    /**
+     * add exportType to native-mime mapping.
+     *
+     * @param atom        name of the type in native clipboard
+     * @param mimeType    the mime type
+     * @param description human-readable name
+     * @param className   the corresponding java class
+     */
+    private void addType(String atom, String mimeType, String description, String className) {
+
+        try {
+            DataFlavor df = new DataFlavor(mimeType, description);
+            SystemFlavorMap map = (SystemFlavorMap) SystemFlavorMap.getDefaultFlavorMap();
+            map.addUnencodedNativeForFlavor(df, atom);
+
+            ClassLoader loader = Thread.currentThread().getContextClassLoader();
+            Class cls = loader == null ? Class.forName(className) : loader.loadClass(className);
+            ExportGraphicType type = (ExportGraphicType) cls.newInstance();
+            types.put(df, type);
+
+        } catch (Throwable x) {
+            new Alert("Unable to install flavor for mime type '" + mimeType + "'");
+        }
+    }
+}
diff --git a/src/jloda/export/gifEncode/DirectGif89Frame.java b/src/jloda/export/gifEncode/DirectGif89Frame.java
new file mode 100644
index 0000000..9232f2a
--- /dev/null
+++ b/src/jloda/export/gifEncode/DirectGif89Frame.java
@@ -0,0 +1,101 @@
+/**
+ * DirectGif89Frame.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+//******************************************************************************
+// DirectGif89Frame.java
+//******************************************************************************
+package jloda.export.gifEncode;
+
+import java.awt.*;
+import java.awt.image.PixelGrabber;
+import java.io.IOException;
+
+//==============================================================================
+
+/**
+ * Instances of this Gif89Frame subclass are constructed from RGB image info,
+ * either in the form of an Image object or a pixel array.
+ * <p/>
+ * There is an important restriction to note.  It is only permissible to add
+ * DirectGif89Frame objects to a Gif89Encoder constructed without an explicit
+ * color map.  The GIF color table will be automatically generated from pixel
+ * information.
+ *
+ * @author J. M. G. Elliott (tep at jmge.net)
+ * @version 0.90 beta (15-Jul-2000)
+ * @see Gif89Encoder
+ * @see Gif89Frame
+ * @see IndexGif89Frame
+ */
+public class DirectGif89Frame extends Gif89Frame {
+
+    private int[] argbPixels;
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Construct an DirectGif89Frame from a Java image.
+     *
+     * @param img A java.awt.Image object that supports pixel-grabbing.
+     * @throws IOException If the image is unencodable due to failure of pixel-grabbing.
+     */
+    public DirectGif89Frame(Image img) throws IOException {
+        PixelGrabber pg = new PixelGrabber(img, 0, 0, -1, -1, true);
+
+        String errmsg = null;
+        try {
+            if (!pg.grabPixels())
+                errmsg = "can't grab pixels from image";
+        } catch (InterruptedException e) {
+            errmsg = "interrupted grabbing pixels from image";
+        }
+
+        if (errmsg != null)
+            throw new IOException(errmsg + " (" + getClass().getName() + ")");
+
+        theWidth = pg.getWidth();
+        theHeight = pg.getHeight();
+        argbPixels = (int[]) pg.getPixels();
+        ciPixels = new byte[argbPixels.length];
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Construct an DirectGif89Frame from ARGB pixel data.
+     *
+     * @param width       Width of the bitmap.
+     * @param height      Height of the bitmap.
+     * @param argb_pixels Array containing at least width*height pixels in the format returned by
+     *                    java.awt.Color.getRGB().
+     */
+    public DirectGif89Frame(int width, int height, int argb_pixels[]) {
+        theWidth = width;
+        theHeight = height;
+        argbPixels = new int[theWidth * theHeight];
+        System.arraycopy(argb_pixels, 0, argbPixels, 0, argbPixels.length);
+        ciPixels = new byte[argbPixels.length];
+    }
+
+    //----------------------------------------------------------------------------
+
+    Object getPixelSource() {
+        return argbPixels;
+    }
+}
diff --git a/src/jloda/export/gifEncode/Gif89Encoder.java b/src/jloda/export/gifEncode/Gif89Encoder.java
new file mode 100644
index 0000000..61eed15
--- /dev/null
+++ b/src/jloda/export/gifEncode/Gif89Encoder.java
@@ -0,0 +1,732 @@
+/**
+ * Gif89Encoder.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+//******************************************************************************
+// Gif89Encoder.java
+//******************************************************************************
+package jloda.export.gifEncode;
+
+import java.awt.*;
+import java.io.*;
+import java.util.Vector;
+
+//==============================================================================
+
+/**
+ * This is the central class of a JDK 1.1 compatible GIF encoder that, AFAIK,
+ * supports more features of the extended GIF spec than any other Java open
+ * source encoder.  Some sections of the source are lifted or adapted from Jef
+ * Poskanzer's <cite>Acme GifEncoder</cite> (so please see the
+ * <a href="../readme.txt">readme</a> containing his notice), but much of it,
+ * including nearly all of the present class, is original code.  My main
+ * motivation for writing a new encoder was to support animated GIFs, but the
+ * package also adds support for embedded textual comments.
+ * <p/>
+ * There are still some limitations.  For instance, animations are limited to
+ * a single global color table.  But that is usually what you want anyway, so
+ * as to avoid irregularities on some displays.  (So this is not really a
+ * limitation, but a "disciplinary feature" :)  Another rather more serious
+ * restriction is that the total number of RGB colors in a given input-batch
+ * mustn't exceed 256.  Obviously, there is an opening here for someone who
+ * would like to add a color-reducing preprocessor.
+ * <p/>
+ * The encoder, though very usable in its present form, is at bottom only a
+ * partial implementation skewed toward my own particular needs.  Hence a
+ * couple of caveats are in order.  (1) During development it was in the back
+ * of my mind that an encoder object should be reusable - i.e., you should be
+ * able to make multiple calls to encode() on the same object, with or without
+ * intervening frame additions or changes to options.  But I haven't reviewed
+ * the code with such usage in mind, much less tested it, so it's likely I
+ * overlooked something.  (2) The encoder classes aren't thread safe, so use
+ * caution in a context where access is shared by multiple threads.  (Better
+ * yet, finish the library and re-release it :)
+ * <p/>
+ * There follow a couple of simple examples illustrating the most common way to
+ * use the encoder, i.e., to encode AWT Image objects created elsewhere in the
+ * program.  Use of some of the most popular format options is also shown,
+ * though you will want to peruse the API for additional features.
+ * <p/>
+ * <p/>
+ * <strong>Animated GIF Example</strong>
+ * <pre>
+ *  import net.jmge.gif.Gif89Encoder;
+ *  // ...
+ *  void writeAnimatedGIF(Image[] still_images,
+ *                        String annotation,
+ *                        boolean looped,
+ *                        double frames_per_second,
+ *                        OutputStream out) throws IOException
+ *  {
+ *    Gif89Encoder gifenc = new Gif89Encoder();
+ *    for (int i = 0; i < still_images.length; ++i)
+ *      gifenc.addFrame(still_images[i]);
+ *    gifenc.setComments(annotation);
+ *    gifenc.setLoopCount(looped ? 0 : 1);
+ *    gifenc.setUniformDelay((int) Math.round(100 / frames_per_second));
+ *    gifenc.encode(out);
+ *  }
+ *  </pre>
+ * <p/>
+ * <strong>Static GIF Example</strong>
+ * <pre>
+ *  import net.jmge.gif.Gif89Encoder;
+ *  // ...
+ *  void writeNormalGIF(Image img,
+ *                      String annotation,
+ *                      int transparent_index,  // pass -1 for none
+ *                      boolean interlaced,
+ *                      OutputStream out) throws IOException
+ *  {
+ *    Gif89Encoder gifenc = new Gif89Encoder(img);
+ *    gifenc.setComments(annotation);
+ *    gifenc.setTransparentIndex(transparent_index);
+ *    gifenc.getFrameAt(0).setInterlaced(interlaced);
+ *    gifenc.encode(out);
+ *  }
+ *  </pre>
+ *
+ * @author J. M. G. Elliott (tep at jmge.net)
+ * @version 0.90 beta (15-Jul-2000)
+ * @see Gif89Frame
+ * @see DirectGif89Frame
+ * @see IndexGif89Frame
+ */
+public class Gif89Encoder {
+
+    private Dimension dispDim = new Dimension(0, 0);
+    private final GifColorTable colorTable;
+    private int bgIndex = 0;
+    private int loopCount = 1;
+    private String theComments;
+    private final Vector vFrames = new Vector();
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Use this default constructor if you'll be adding multiple frames
+     * constructed from RGB data (i.e., AWT Image objects or ARGB-pixel arrays).
+     */
+    public Gif89Encoder() {
+        // empty color table puts us into "palette autodetect" mode
+        colorTable = new GifColorTable();
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Like the default except that it also adds a single frame, for conveniently
+     * encoding a static GIF from an image.
+     *
+     * @param static_image Any Image object that supports pixel-grabbing.
+     * @throws IOException See the addFrame() methods.
+     */
+    public Gif89Encoder(Image static_image) throws IOException {
+        this();
+        addFrame(static_image);
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * This constructor installs a user color table, overriding the detection of
+     * of a palette from ARBG pixels.
+     * <p/>
+     * Use of this constructor imposes a couple of restrictions:
+     * (1) Frame objects can't be of type DirectGif89Frame
+     * (2) Transparency, if desired, must be set explicitly.
+     *
+     * @param colors Array of color values; no more than 256 colors will be read, since that's
+     *               the limit for a GIF.
+     */
+    public Gif89Encoder(Color[] colors) {
+        colorTable = new GifColorTable(colors);
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Convenience constructor for encoding a static GIF from index-model data.
+     * Adds a single frame as specified.
+     *
+     * @param colors    Array of color values; no more than 256 colors will be read, since
+     *                  that's the limit for a GIF.
+     * @param width     Width of the GIF bitmap.
+     * @param height    Height of same.
+     * @param ci_pixels Array of color-index pixels no less than width * height in length.
+     * @throws IOException See the addFrame() methods.
+     */
+    public Gif89Encoder(Color[] colors, int width, int height, byte ci_pixels[])
+            throws IOException {
+        this(colors);
+        addFrame(width, height, ci_pixels);
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Get the number of frames that have been added so far.
+     *
+     * @return Number of frame items.
+     */
+    public int getFrameCount() {
+        return vFrames.size();
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Get a reference back to a Gif89Frame object by position.
+     *
+     * @param index Zero-based index of the frame in the sequence.
+     * @return Gif89Frame object at the specified position (or null if no such frame).
+     */
+    public Gif89Frame getFrameAt(int index) {
+        return isOk(index) ? (Gif89Frame) vFrames.elementAt(index) : null;
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Add a Gif89Frame frame to the end of the internal sequence.  Note that
+     * there are restrictions on the Gif89Frame type: if the encoder object was
+     * constructed with an explicit color table, an attempt to add a
+     * DirectGif89Frame will throw an exception.
+     *
+     * @param gf An externally constructed Gif89Frame.
+     * @throws IOException If Gif89Frame can't be accommodated.  This could happen if either (1) the
+     *                     aggregate cross-frame RGB color count exceeds 256, or (2) the Gif89Frame
+     *                     subclass is incompatible with the present encoder object.
+     */
+    public void addFrame(Gif89Frame gf) throws IOException {
+        accommodateFrame(gf);
+        vFrames.addElement(gf);
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Convenience version of addFrame() that takes a Java Image, internally
+     * constructing the requisite DirectGif89Frame.
+     *
+     * @param image Any Image object that supports pixel-grabbing.
+     * @throws IOException If either (1) pixel-grabbing fails, (2) the aggregate cross-frame RGB
+     *                     color count exceeds 256, or (3) this encoder object was constructed with
+     *                     an explicit color table.
+     */
+    public void addFrame(Image image) throws IOException {
+        addFrame(new DirectGif89Frame(image));
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * The index-model convenience version of addFrame().
+     *
+     * @param width     Width of the GIF bitmap.
+     * @param height    Height of same.
+     * @param ci_pixels Array of color-index pixels no less than width * height in length.
+     * @throws IOException Actually, in the present implementation, there aren't any unchecked
+     *                     exceptions that can be thrown when adding an IndexGif89Frame
+     *                     <i>per se</i>.  But I might add some pedantic check later, to justify the
+     *                     generality :)
+     */
+    public void addFrame(int width, int height, byte ci_pixels[])
+            throws IOException {
+        addFrame(new IndexGif89Frame(width, height, ci_pixels));
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Like addFrame() except that the frame is inserted at a specific point in
+     * the sequence rather than appended.
+     *
+     * @param index Zero-based index at which to insert frame.
+     * @param gf    An externally constructed Gif89Frame.
+     * @throws IOException If Gif89Frame can't be accommodated.  This could happen if either (1)
+     *                     the aggregate cross-frame RGB color count exceeds 256, or (2) the
+     *                     Gif89Frame subclass is incompatible with the present encoder object.
+     */
+    public void insertFrame(int index, Gif89Frame gf) throws IOException {
+        accommodateFrame(gf);
+        vFrames.insertElementAt(gf, index);
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Set the color table index for the transparent color, if any.
+     *
+     * @param index Index of the color that should be rendered as transparent, if any.
+     *              A value of -1 turns off transparency.  (Default: -1)
+     */
+    public void setTransparentIndex(int index) {
+        colorTable.setTransparent(index);
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Sets attributes of the multi-image display area, if applicable.
+     *
+     * @param dim        Width/height of display.  (Default: largest detected frame size)
+     * @param background Color table index of background color.  (Default: 0)
+     * @see Gif89Frame#setPosition
+     */
+    public void setLogicalDisplay(Dimension dim, int background) {
+        dispDim = new Dimension(dim);
+        bgIndex = background;
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Set animation looping parameter, if applicable.
+     *
+     * @param count Number of times to play sequence.  Special value of 0 specifies
+     *              indefinite looping.  (Default: 1)
+     */
+    public void setLoopCount(int count) {
+        loopCount = count;
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Specify some textual comments to be embedded in GIF.
+     *
+     * @param comments String containing ASCII comments.
+     */
+    public void setComments(String comments) {
+        theComments = comments;
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * A convenience method for setting the "animation speed".  It simply sets
+     * the delay parameter for each frame in the sequence to the supplied value.
+     * Since this is actually frame-level rather than animation-level data, take
+     * care to add your frames before calling this method.
+     *
+     * @param interval Interframe interval in centiseconds.
+     */
+    public void setUniformDelay(int interval) {
+        for (int i = 0; i < vFrames.size(); ++i)
+            ((Gif89Frame) vFrames.elementAt(i)).setDelay(interval);
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * After adding your frame(s) and setting your options, simply call this
+     * method to write the GIF to the passed stream.  Multiple calls are
+     * permissible if for some reason that is useful to your application.  (The
+     * method simply encodes the current state of the object with no thought
+     * to previous calls.)
+     *
+     * @param out The stream you want the GIF written to.
+     * @throws IOException If a write error is encountered.
+     */
+    public void encode(OutputStream out) throws IOException {
+        int nframes = getFrameCount();
+        boolean is_sequence = nframes > 1;
+
+        // N.B. must be called before writing screen descriptor
+        colorTable.closePixelProcessing();
+
+        // write GIF HEADER
+        Put.ascii("GIF89a", out);
+
+        // write global blocks
+        writeLogicalScreenDescriptor(out);
+        colorTable.encode(out);
+        if (is_sequence && loopCount != 1)
+            writeNetscapeExtension(out);
+        if (theComments != null && theComments.length() > 0)
+            writeCommentExtension(out);
+
+        // write out the control and rendering data for each frame
+        for (int i = 0; i < nframes; ++i)
+            ((Gif89Frame) vFrames.elementAt(i)).encode(out, is_sequence, colorTable.getDepth(), colorTable.getTransparent());
+
+        // write GIF TRAILER
+        out.write((int) ';');
+
+        out.flush();
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * A simple driver to test the installation and to demo usage.  Put the JAR
+     * on your classpath and run ala
+     * <blockquote>java net.jmge.gif.Gif89Encoder {filename}</blockquote>
+     * The filename must be either (1) a JPEG file with extension 'jpg', for
+     * conversion to a static GIF, or (2) a file containing a list of GIFs and/or
+     * JPEGs, one per line, to be combined into an animated GIF.  The output will
+     * appear in the current directory as 'gif89out.gif'.
+     * <p/>
+     * (N.B. This test program will abort if the input file(s) exceed(s) 256 total
+     * RGB colors, so in its present form it has no value as a generic JPEG to GIF
+     * converter.  Also, when multiple files are input, you need to be wary of the
+     * total color count, regardless of file type.)
+     *
+     * @param args Command-line arguments, only the first of which is used, as mentioned
+     *             above.
+     */
+    public static void main(String[] args) {
+        try {
+
+            Toolkit tk = Toolkit.getDefaultToolkit();
+            OutputStream out = new BufferedOutputStream(new FileOutputStream("gif89out.gif"));
+
+            if (args[0].toUpperCase().endsWith(".JPG"))
+                new Gif89Encoder(tk.getImage(args[0])).encode(out);
+            else {
+                BufferedReader in = new BufferedReader(new FileReader(args[0]));
+                Gif89Encoder ge = new Gif89Encoder();
+
+                String line;
+                while ((line = in.readLine()) != null)
+                    ge.addFrame(tk.getImage(line.trim()));
+                ge.setLoopCount(0);  // let's loop indefinitely
+                ge.encode(out);
+
+                in.close();
+            }
+            out.close();
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            System.exit(0);
+        } // must kill VM explicitly (Toolkit thread?)
+    }
+
+    //----------------------------------------------------------------------------
+
+    private void accommodateFrame(Gif89Frame gf) throws IOException {
+        dispDim.width = Math.max(dispDim.width, gf.getWidth());
+        dispDim.height = Math.max(dispDim.height, gf.getHeight());
+        colorTable.processPixels(gf);
+    }
+
+    //----------------------------------------------------------------------------
+
+    private void writeLogicalScreenDescriptor(OutputStream os) throws IOException {
+        Put.leShort(dispDim.width, os);
+        Put.leShort(dispDim.height, os);
+
+        // write 4 fields, packed into a byte  (bitfieldsize:value)
+        //   global color map present?         (1:1)
+        //   bits per primary color less 1     (3:7)
+        //   sorted color table?               (1:0)
+        //   bits per pixel less 1             (3:varies)
+        os.write(0xf0 | colorTable.getDepth() - 1);
+
+        // write background color index
+        os.write(bgIndex);
+
+        // Jef Poskanzer's notes on the next field, for our possible edification:
+        // Pixel aspect ratio - 1:1.
+        //Putbyte( (byte) 49, outs );
+        // Java's GIF reader currently has a bug, if the aspect ratio byte is
+        // not zero it throws an ImageFormatException.  It doesn't know that
+        // 49 means a 1:1 aspect ratio.  Well, whatever, zero works with all
+        // the other decoders I've tried so it probably doesn't hurt.
+
+        // OK, if it's good enough for Jef, it's definitely good enough for us:
+        os.write(0);
+    }
+
+    //----------------------------------------------------------------------------
+
+    private void writeNetscapeExtension(OutputStream os) throws IOException {
+        // n.b. most software seems to interpret the count as a repeat count
+        // (i.e., interations beyond 1) rather than as an iteration count
+        // (thus, to avoid repeating we have to omit the whole extension)
+
+        os.write((int) '!');           // GIF Extension Introducer
+        os.write(0xff);                // Application Extension Label
+
+        os.write(11);                  // application ID block size
+        Put.ascii("NETSCAPE2.0", os);  // application ID data
+
+        os.write(3);                   // data sub-block size
+        os.write(1);                   // a looping flag? dunno
+
+        // we finally write the relevent data
+        Put.leShort(loopCount > 1 ? loopCount - 1 : 0, os);
+
+        os.write(0);                   // block terminator
+    }
+
+    //----------------------------------------------------------------------------
+
+    private void writeCommentExtension(OutputStream os) throws IOException {
+        os.write((int) '!');     // GIF Extension Introducer
+        os.write(0xfe);          // Comment Extension Label
+
+        int remainder = theComments.length() % 255;
+        int nsubblocks_full = theComments.length() / 255;
+        int nsubblocks = nsubblocks_full + (remainder > 0 ? 1 : 0);
+        int ibyte = 0;
+        for (int isb = 0; isb < nsubblocks; ++isb) {
+            int size = isb < nsubblocks_full ? 255 : remainder;
+
+            os.write(size);
+            Put.ascii(theComments.substring(ibyte, ibyte + size), os);
+            ibyte += size;
+        }
+
+        os.write(0);    // block terminator
+    }
+
+    //----------------------------------------------------------------------------
+
+    private boolean isOk(int frame_index) {
+        return frame_index >= 0 && frame_index < vFrames.size();
+    }
+}
+
+//==============================================================================
+
+class GifColorTable {
+
+    // the palette of ARGB colors, packed as returned by Color.getRGB()
+    private final int[] theColors = new int[256];
+
+    // other basic attributes
+    private int colorDepth;
+    private int transparentIndex = -1;
+
+    // these fields track color-index info across frames
+    private int ciCount = 0; // count of distinct color indices
+    private ReverseColorMap ciLookup;    // cumulative rgb-to-ci lookup table
+
+    //----------------------------------------------------------------------------
+
+    GifColorTable() {
+        ciLookup = new ReverseColorMap();  // puts us into "auto-detect mode"
+    }
+
+    //----------------------------------------------------------------------------
+
+    GifColorTable(Color[] colors) {
+        int n2copy = Math.min(theColors.length, colors.length);
+        for (int i = 0; i < n2copy; ++i)
+            theColors[i] = colors[i].getRGB();
+    }
+
+    //----------------------------------------------------------------------------
+
+    int getDepth() {
+        return colorDepth;
+    }
+
+    //----------------------------------------------------------------------------
+
+    int getTransparent() {
+        return transparentIndex;
+    }
+
+    //----------------------------------------------------------------------------
+    // default: -1 (no transparency)
+
+    void setTransparent(int color_index) {
+        transparentIndex = color_index;
+    }
+
+    //----------------------------------------------------------------------------
+
+    void processPixels(Gif89Frame gf) throws IOException {
+        if (gf instanceof DirectGif89Frame)
+            filterPixels((DirectGif89Frame) gf);
+        else
+            trackPixelUsage((IndexGif89Frame) gf);
+    }
+
+    //----------------------------------------------------------------------------
+
+    void closePixelProcessing()  // must be called before encode()
+    {
+        colorDepth = computeColorDepth(ciCount);
+    }
+
+    //----------------------------------------------------------------------------
+
+    void encode(OutputStream os) throws IOException {
+        // size of palette written is the smallest power of 2 that can accomdate
+        // the number of RGB colors detected (or largest color index, in case of
+        // index pixels)
+        int palette_size = 1 << colorDepth;
+        for (int i = 0; i < palette_size; ++i) {
+            os.write(theColors[i] >> 16 & 0xff);
+            os.write(theColors[i] >> 8 & 0xff);
+            os.write(theColors[i] & 0xff);
+        }
+    }
+
+    //----------------------------------------------------------------------------
+    // This method accomplishes three things:
+    // (1) converts the passed rgb pixels to indexes into our rgb lookup table
+    // (2) fills the rgb table as new colors are encountered
+    // (3) looks for transparent pixels so as to set the transparent index
+    // The information is cumulative across multiple calls.
+    //
+    // (Note: some of the logic is borrowed from Jef Poskanzer's code.)
+    //----------------------------------------------------------------------------
+
+    private void filterPixels(DirectGif89Frame dgf) throws IOException {
+        if (ciLookup == null)
+            throw new IOException("RGB frames require palette autodetection");
+
+        int[] argb_pixels = (int[]) dgf.getPixelSource();
+        byte[] ci_pixels = dgf.getPixelSink();
+        int npixels = argb_pixels.length;
+        for (int i = 0; i < npixels; ++i) {
+            int argb = argb_pixels[i];
+
+            // handle transparency
+            if ((argb >>> 24) < 0x80)        // transparent pixel?
+                if (transparentIndex == -1)    // first transparent color encountered?
+                    transparentIndex = ciCount;  // record its index
+                else if (argb != theColors[transparentIndex]) // different pixel value?
+                {
+                    // collapse all transparent pixels into one color index
+                    ci_pixels[i] = (byte) transparentIndex;
+                    continue;  // CONTINUE - index already in table
+                }
+
+            // try to look up the index in our "reverse" color table
+            int color_index = ciLookup.getPaletteIndex(argb & 0xffffff);
+
+            if (color_index == -1)  // if it isn't in there yet
+            {
+                if (ciCount == 256)
+                    throw new IOException("can't encode as GIF (> 256 colors)");
+
+                // store color in our accumulating palette
+                theColors[ciCount] = argb;
+
+                // store index in reverse color table
+                ciLookup.put(argb & 0xffffff, ciCount);
+
+                // send color index to our output array
+                ci_pixels[i] = (byte) ciCount;
+
+                // increment count of distinct color indices
+                ++ciCount;
+            } else  // we've already snagged color into our palette
+                ci_pixels[i] = (byte) color_index;  // just send filtered pixel
+        }
+    }
+
+    //----------------------------------------------------------------------------
+
+    private void trackPixelUsage(IndexGif89Frame igf) throws IOException {
+        byte[] ci_pixels = (byte[]) igf.getPixelSource();
+        int npixels = ci_pixels.length;
+        for (byte ci_pixel : ci_pixels)
+            if (ci_pixel >= ciCount)
+                ciCount = ci_pixel + 1;
+    }
+
+    //----------------------------------------------------------------------------
+
+    private int computeColorDepth(int colorcount) {
+        // color depth = log-base-2 of maximum number of simultaneous colors, i.e.
+        // bits per color-index pixel
+        if (colorcount <= 2)
+            return 1;
+        if (colorcount <= 4)
+            return 2;
+        if (colorcount <= 16)
+            return 4;
+        return 8;
+    }
+}
+
+//==============================================================================
+// We're doing a very simple linear hashing thing here, which seems sufficient
+// for our needs.  I make no claims for this approach other than that it seems
+// an improvement over doing a brute linear search for each pixel on the one
+// hand, and creating a Java object for each pixel (if we were to use a Java
+// Hashtable) on the other.  Doubtless my little hash could be improved by
+// tuning the capacity (at the very least).  Suggestions are welcome.
+//==============================================================================
+
+class ReverseColorMap {
+
+    private static class ColorRecord {
+        final int rgb;
+        final int ipalette;
+
+        ColorRecord(int rgb, int ipalette) {
+            this.rgb = rgb;
+            this.ipalette = ipalette;
+        }
+    }
+
+    // I wouldn't really know what a good hashing capacity is, having missed out
+    // on data structures and algorithms class :)  Alls I know is, we've got a lot
+    // more space than we have time.  So let's try a sparse table with a maximum
+    // load of about 1/8 capacity.
+    private static final int HCAPACITY = 2053;  // a nice prime number
+
+    // our hash table proper
+    private final ColorRecord[] hTable = new ColorRecord[HCAPACITY];
+
+    //----------------------------------------------------------------------------
+    // Assert: rgb is not negative (which is the same as saying, be sure the
+    // alpha transparency byte - i.e., the high byte - has been masked out).
+    //----------------------------------------------------------------------------
+
+    int getPaletteIndex(int rgb) {
+        ColorRecord rec;
+
+        for (int itable = rgb % hTable.length;
+             (rec = hTable[itable]) != null && rec.rgb != rgb;
+             itable = ++itable % hTable.length
+                )
+            ;
+
+        if (rec != null)
+            return rec.ipalette;
+
+        return -1;
+    }
+
+    //----------------------------------------------------------------------------
+    // Assert: (1) same as above; (2) rgb key not already present
+    //----------------------------------------------------------------------------
+
+    void put(int rgb, int ipalette) {
+        int itable;
+
+        for (itable = rgb % hTable.length;
+             hTable[itable] != null;
+             itable = ++itable % hTable.length
+                )
+            ;
+
+        hTable[itable] = new ColorRecord(rgb, ipalette);
+    }
+}
diff --git a/src/jloda/export/gifEncode/Gif89Frame.java b/src/jloda/export/gifEncode/Gif89Frame.java
new file mode 100644
index 0000000..be998c3
--- /dev/null
+++ b/src/jloda/export/gifEncode/Gif89Frame.java
@@ -0,0 +1,603 @@
+/**
+ * Gif89Frame.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+//******************************************************************************
+// Gif89Frame.java
+//******************************************************************************
+package jloda.export.gifEncode;
+
+import java.awt.*;
+import java.io.IOException;
+import java.io.OutputStream;
+
+//==============================================================================
+
+/**
+ * First off, just to dispel any doubt, this class and its subclasses have
+ * nothing to do with GUI "frames" such as java.awt.Frame.  We merely use the
+ * term in its very common sense of a still picture in an animation sequence.
+ * It's hoped that the restricted context will prevent any confusion.
+ * <p/>
+ * An instance of this class is used in conjunction with a Gif89Encoder object
+ * to represent and encode a single static image and its associated "control"
+ * data.  A Gif89Frame doesn't know or care whether it is encoding one of the
+ * many animation frames in a GIF movie, or the single bitmap in a "normal"
+ * GIF. (FYI, this design mirrors the encoded GIF structure.)
+ * <p/>
+ * Since Gif89Frame is an abstract class we don't instantiate it directly, but
+ * instead create instances of its concrete subclasses, IndexGif89Frame and
+ * DirectGif89Frame.  From the API standpoint, these subclasses differ only
+ * in the sort of data their instances are constructed from.  Most folks will
+ * probably work with DirectGif89Frame, since it can be constructed from a
+ * java.awt.Image object, but the lower-level IndexGif89Frame class offers
+ * advantages in specialized circumstances.  (Of course, in routine situations
+ * you might not explicitly instantiate any frames at all, instead letting
+ * Gif89Encoder's convenience methods do the honors.)
+ * <p/>
+ * As far as the public API is concerned, objects in the Gif89Frame hierarchy
+ * interact with a Gif89Encoder only via the latter's methods for adding and
+ * querying frames.  (As a side note, you should know that while Gif89Encoder
+ * objects are permanently modified by the addition of Gif89Frames, the reverse
+ * is NOT true.  That is, even though the ultimate encoding of a Gif89Frame may
+ * be affected by the context its parent encoder object provides, it retains
+ * its original condition and can be reused in a different context.)
+ * <p/>
+ * The core pixel-encoding code in this class was essentially lifted from
+ * Jef Poskanzer's well-known <cite>Acme GifEncoder</cite>, so please see the
+ * <a href="../readme.txt">readme</a> containing his notice.
+ *
+ * @author J. M. G. Elliott (tep at jmge.net)
+ * @version 0.90 beta (15-Jul-2000)
+ * @see Gif89Encoder
+ * @see DirectGif89Frame
+ * @see IndexGif89Frame
+ */
+public abstract class Gif89Frame {
+
+    //// Public "Disposal Mode" constants ////
+
+    /**
+     * The animated GIF renderer shall decide how to dispose of this Gif89Frame's
+     * display area.
+     *
+     * @see Gif89Frame#setDisposalMode
+     */
+    public static final int DM_UNDEFINED = 0;
+
+    /**
+     * The animated GIF renderer shall take no display-disposal action.
+     *
+     * @see Gif89Frame#setDisposalMode
+     */
+    public static final int DM_LEAVE = 1;
+
+    /**
+     * The animated GIF renderer shall replace this Gif89Frame's area with the
+     * background color.
+     *
+     * @see Gif89Frame#setDisposalMode
+     */
+    public static final int DM_BGCOLOR = 2;
+
+    /**
+     * The animated GIF renderer shall replace this Gif89Frame's area with the
+     * previous frame's bitmap.
+     *
+     * @see Gif89Frame#setDisposalMode
+     */
+    public static final int DM_REVERT = 3;
+
+    //// Bitmap variables set in package subclass constructors ////
+    int theWidth = -1;
+    int theHeight = -1;
+    byte[] ciPixels;
+
+    //// GIF graphic frame control options ////
+    private Point thePosition = new Point(0, 0);
+    private boolean isInterlaced;
+    private int csecsDelay;
+    private int disposalCode = DM_LEAVE;
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Set the position of this frame within a larger animation display space.
+     *
+     * @param p Coordinates of the frame's upper left corner in the display space.
+     *          (Default: The logical display's origin [0, 0])
+     * @see Gif89Encoder#setLogicalDisplay
+     */
+    public void setPosition(Point p) {
+        thePosition = new Point(p);
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Set or erase the interlace flag.
+     *
+     * @param b true if you want interlacing.  (Default: false)
+     */
+    public void setInterlaced(boolean b) {
+        isInterlaced = b;
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Set the between-frame interval.
+     *
+     * @param interval Centiseconds to wait before displaying the subsequent frame.
+     *                 (Default: 0)
+     */
+    public void setDelay(int interval) {
+        csecsDelay = interval;
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Setting this option determines (in a cooperative GIF-viewer) what will be
+     * done with this frame's display area before the subsequent frame is
+     * displayed.  For instance, a setting of DM_BGCOLOR can be used for erasure
+     * when redrawing with displacement.
+     *
+     * @param code One of the four int constants of the Gif89Frame.DM_* series.
+     *             (Default: DM_LEAVE)
+     */
+    public void setDisposalMode(int code) {
+        disposalCode = code;
+    }
+
+    //----------------------------------------------------------------------------
+
+    Gif89Frame() {
+    }  // package-visible default constructor
+
+    //----------------------------------------------------------------------------
+
+    abstract Object getPixelSource();
+
+    //----------------------------------------------------------------------------
+
+    int getWidth() {
+        return theWidth;
+    }
+
+    //----------------------------------------------------------------------------
+
+    int getHeight() {
+        return theHeight;
+    }
+
+    //----------------------------------------------------------------------------
+
+    byte[] getPixelSink() {
+        return ciPixels;
+    }
+
+    //----------------------------------------------------------------------------
+
+    void encode(OutputStream os, boolean epluribus, int color_depth,
+                int transparent_index) throws IOException {
+        writeGraphicControlExtension(os, epluribus, transparent_index);
+        writeImageDescriptor(os);
+        (new GifPixelsEncoder(theWidth, theHeight, ciPixels, isInterlaced, color_depth)).encode(os);
+    }
+
+    //----------------------------------------------------------------------------
+
+    private void writeGraphicControlExtension(OutputStream os, boolean epluribus,
+                                              int itransparent) throws IOException {
+        int transflag = itransparent == -1 ? 0 : 1;
+        if (transflag == 1 || epluribus)   // using transparency or animating ?
+        {
+            os.write((int) '!');             // GIF Extension Introducer
+            os.write(0xf9);                  // Graphic Control Label
+            os.write(4);                     // subsequent data block size
+            os.write((disposalCode << 2) | transflag); // packed fields (1 byte)
+            Put.leShort(csecsDelay, os);  // delay field (2 bytes)
+            os.write(itransparent);          // transparent index field
+            os.write(0);                     // block terminator
+        }
+    }
+
+    //----------------------------------------------------------------------------
+
+    private void writeImageDescriptor(OutputStream os) throws IOException {
+        os.write((int) ',');                // Image Separator
+        Put.leShort(thePosition.x, os);
+        Put.leShort(thePosition.y, os);
+        Put.leShort(theWidth, os);
+        Put.leShort(theHeight, os);
+        os.write(isInterlaced ? 0x40 : 0);  // packed fields (1 byte)
+    }
+}
+
+//==============================================================================
+
+class GifPixelsEncoder {
+
+    private static final int EOF = -1;
+
+    private final int imgW;
+    private final int imgH;
+    private final byte[] pixAry;
+    private final boolean wantInterlaced;
+    private final int initCodeSize;
+
+    // raster data navigators
+    private int countDown;
+    private int xCur, yCur;
+    private int curPass;
+
+    //----------------------------------------------------------------------------
+
+    GifPixelsEncoder(int width, int height, byte[] pixels, boolean interlaced,
+                     int color_depth) {
+        imgW = width;
+        imgH = height;
+        pixAry = pixels;
+        wantInterlaced = interlaced;
+        initCodeSize = Math.max(2, color_depth);
+    }
+
+    //----------------------------------------------------------------------------
+
+    void encode(OutputStream os) throws IOException {
+        os.write(initCodeSize);         // write "initial code size" byte
+
+        countDown = imgW * imgH;        // reset navigation variables
+        xCur = yCur = curPass = 0;
+
+        compress(initCodeSize + 1, os); // compress and write the pixel data
+
+        os.write(0);                    // write block terminator
+    }
+
+    //****************************************************************************
+    // (J.E.) The logic of the next two methods is largely intact from
+    // Jef Poskanzer.  Some stylistic changes were made for consistency sake,
+    // plus the second method accesses the pixel value from a prefiltered linear
+    // array.  That's about it.
+    //****************************************************************************
+
+    //----------------------------------------------------------------------------
+    // Bump the 'xCur' and 'yCur' to point to the next pixel.
+    //----------------------------------------------------------------------------
+
+    private void bumpPosition() {
+        // Bump the current X position
+        ++xCur;
+
+        // If we are at the end of a scan line, set xCur back to the beginning
+        // If we are interlaced, bump the yCur to the appropriate spot,
+        // otherwise, just increment it.
+        if (xCur == imgW) {
+            xCur = 0;
+
+            if (!wantInterlaced)
+                ++yCur;
+            else
+                switch (curPass) {
+                    case 0:
+                        yCur += 8;
+                        if (yCur >= imgH) {
+                            ++curPass;
+                            yCur = 4;
+                        }
+                        break;
+                    case 1:
+                        yCur += 8;
+                        if (yCur >= imgH) {
+                            ++curPass;
+                            yCur = 2;
+                        }
+                        break;
+                    case 2:
+                        yCur += 4;
+                        if (yCur >= imgH) {
+                            ++curPass;
+                            yCur = 1;
+                        }
+                        break;
+                    case 3:
+                        yCur += 2;
+                        break;
+                }
+        }
+    }
+
+    //----------------------------------------------------------------------------
+    // Return the next pixel from the image
+    //----------------------------------------------------------------------------
+
+    private int nextPixel() {
+        if (countDown == 0)
+            return EOF;
+
+        --countDown;
+
+        byte pix = pixAry[yCur * imgW + xCur];
+
+        bumpPosition();
+
+        return pix & 0xff;
+    }
+
+    //****************************************************************************
+    // (J.E.) I didn't touch Jef Poskanzer's code from this point on.  (Well, OK,
+    // I changed the name of the sole outside method it accesses.)  I figure
+    // if I have no idea how something works, I shouldn't play with it :)
+    //
+    // Despite its unencapsulated structure, this section is actually highly
+    // self-contained.  The calling code merely calls compress(), and the present
+    // code calls nextPixel() in the caller.  That's the sum total of their
+    // communication.  I could have dumped it in a separate class with a callback
+    // via an interface, but it didn't seem worth messing with.
+    //****************************************************************************
+
+    // GIFCOMPR.C       - GIF Image compression routines
+    //
+    // Lempel-Ziv compression based on 'compress'.  GIF modifications by
+    // David Rowley (mgardi at watdcsu.waterloo.edu)
+
+    // General DEFINEs
+
+    static final int BITS = 12;
+
+    static final int HSIZE = 5003;                // 80% occupancy
+
+    // GIF Image compression - modified 'compress'
+    //
+    // Based on: compress.c - File compression ala IEEE Computer, June 1984.
+    //
+    // By Authors:  Spencer W. Thomas      (decvax!harpo!utah-cs!utah-gr!thomas)
+    //              Jim McKie              (decvax!mcvax!jim)
+    //              Steve Davies           (decvax!vax135!petsd!peora!srd)
+    //              Ken Turkowski          (decvax!decwrl!turtlevax!ken)
+    //              James A. Woods         (decvax!ihnp4!ames!jaw)
+    //              Joe Orost              (decvax!vax135!petsd!joe)
+
+    int n_bits;                                // number of bits/code
+    final int maxbits = BITS;                        // user settable max # bits/code
+    int maxcode;                        // maximum code, given n_bits
+    final int maxmaxcode = 1 << BITS; // should NEVER generate this code
+
+    final int MAXCODE(int n_bits) {
+        return (1 << n_bits) - 1;
+    }
+
+    final int[] htab = new int[HSIZE];
+    final int[] codetab = new int[HSIZE];
+
+    final int hsize = HSIZE;                // for dynamic table sizing
+
+    int free_ent = 0;                        // first unused entry
+
+    // block compression parameters -- after all codes are used up,
+    // and compression rate changes, start over.
+    boolean clear_flg = false;
+
+    // Algorithm:  use open addressing double hashing (no chaining) on the
+    // prefix code / next character combination.  We do a variant of Knuth's
+    // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
+    // secondary probe.  Here, the modular division first probe is gives way
+    // to a faster exclusive-or manipulation.  Also do block compression with
+    // an adaptive reset, whereby the code table is cleared when the compression
+    // ratio decreases, but after the table fills.  The variable-length output
+    // codes are re-sized at this point, and a special CLEAR code is generated
+    // for the decompressor.  Late addition:  construct the table according to
+    // file size for noticeable speed improvement on small files.  Please direct
+    // questions about this implementation to ames!jaw.
+
+    int g_init_bits;
+
+    int ClearCode;
+    int EOFCode;
+
+    void compress(int init_bits, OutputStream outs) throws IOException {
+        int fcode;
+        int i /* = 0 */;
+        int c;
+        int ent;
+        int disp;
+        int hsize_reg;
+        int hshift;
+
+        // Set up the globals:  g_init_bits - initial number of bits
+        g_init_bits = init_bits;
+
+        // Set up the necessary values
+        clear_flg = false;
+        n_bits = g_init_bits;
+        maxcode = MAXCODE(n_bits);
+
+        ClearCode = 1 << (init_bits - 1);
+        EOFCode = ClearCode + 1;
+        free_ent = ClearCode + 2;
+
+        char_init();
+
+        ent = nextPixel();
+
+        hshift = 0;
+        for (fcode = hsize; fcode < 65536; fcode *= 2)
+            ++hshift;
+        hshift = 8 - hshift;                        // set hash code range bound
+
+        hsize_reg = hsize;
+        cl_hash(hsize_reg);        // erase hash table
+
+        output(ClearCode, outs);
+
+        outer_loop:
+        while ((c = nextPixel()) != EOF) {
+            fcode = (c << maxbits) + ent;
+            i = (c << hshift) ^ ent;                // xor hashing
+
+            if (htab[i] == fcode) {
+                ent = codetab[i];
+                continue;
+            } else if (htab[i] >= 0)        // non-empty slot
+            {
+                disp = hsize_reg - i;        // secondary hash (after G. Knott)
+                if (i == 0)
+                    disp = 1;
+                do {
+                    if ((i -= disp) < 0)
+                        i += hsize_reg;
+
+                    if (htab[i] == fcode) {
+                        ent = codetab[i];
+                        continue outer_loop;
+                    }
+                } while (htab[i] >= 0);
+            }
+            output(ent, outs);
+            ent = c;
+            if (free_ent < maxmaxcode) {
+                codetab[i] = free_ent++;        // code -> hashtable
+                htab[i] = fcode;
+            } else
+                cl_block(outs);
+        }
+        // Put out the final code.
+        output(ent, outs);
+        output(EOFCode, outs);
+    }
+
+    // output
+    //
+    // Output the given code.
+    // Inputs:
+    //      code:   A n_bits-bit integer.  If == -1, then EOF.  This assumes
+    //              that n_bits =< wordsize - 1.
+    // Outputs:
+    //      Outputs code to the file.
+    // Assumptions:
+    //      Chars are 8 bits long.
+    // Algorithm:
+    //      Maintain a BITS character long buffer (so that 8 codes will
+    // fit in it exactly).  Use the VAX insv instruction to insert each
+    // code in turn.  When the buffer fills up empty it and start over.
+
+    int cur_accum = 0;
+    int cur_bits = 0;
+
+    final int[] masks = {0x0000, 0x0001, 0x0003, 0x0007, 0x000F,
+            0x001F, 0x003F, 0x007F, 0x00FF,
+            0x01FF, 0x03FF, 0x07FF, 0x0FFF,
+            0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF};
+
+    void output(int code, OutputStream outs) throws IOException {
+        cur_accum &= masks[cur_bits];
+
+        if (cur_bits > 0)
+            cur_accum |= (code << cur_bits);
+        else
+            cur_accum = code;
+
+        cur_bits += n_bits;
+
+        while (cur_bits >= 8) {
+            char_out((byte) (cur_accum & 0xff), outs);
+            cur_accum >>= 8;
+            cur_bits -= 8;
+        }
+
+        // If the next entry is going to be too big for the code size,
+        // then increase it, if possible.
+        if (free_ent > maxcode || clear_flg) {
+            if (clear_flg) {
+                maxcode = MAXCODE(n_bits = g_init_bits);
+                clear_flg = false;
+            } else {
+                ++n_bits;
+                if (n_bits == maxbits)
+                    maxcode = maxmaxcode;
+                else
+                    maxcode = MAXCODE(n_bits);
+            }
+        }
+
+        if (code == EOFCode) {
+            // At EOF, write the rest of the buffer.
+            while (cur_bits > 0) {
+                char_out((byte) (cur_accum & 0xff), outs);
+                cur_accum >>= 8;
+                cur_bits -= 8;
+            }
+
+            flush_char(outs);
+        }
+    }
+
+    // Clear out the hash table
+
+    // table erase for block compress
+
+    void cl_block(OutputStream outs) throws IOException {
+        cl_hash(hsize);
+        free_ent = ClearCode + 2;
+        clear_flg = true;
+
+        output(ClearCode, outs);
+    }
+
+    // reset code table
+
+    void cl_hash(int hsize) {
+        for (int i = 0; i < hsize; ++i)
+            htab[i] = -1;
+    }
+
+    // GIF Specific routines
+
+    // Number of characters so far in this 'packet'
+    int a_count;
+
+    // Set up the 'byte output' routine
+
+    void char_init() {
+        a_count = 0;
+    }
+
+    // Define the storage for the packet accumulator
+    final byte[] accum = new byte[256];
+
+    // Add a character to the end of the current packet, and if it is 254
+    // characters, flush the packet to disk.
+
+    void char_out(byte c, OutputStream outs) throws IOException {
+        accum[a_count++] = c;
+        if (a_count >= 254)
+            flush_char(outs);
+    }
+
+    // Flush the packet to disk, and reset the accumulator
+
+    void flush_char(OutputStream outs) throws IOException {
+        if (a_count > 0) {
+            outs.write(a_count);
+            outs.write(accum, 0, a_count);
+            a_count = 0;
+        }
+    }
+}
diff --git a/src/jloda/export/gifEncode/IndexGif89Frame.java b/src/jloda/export/gifEncode/IndexGif89Frame.java
new file mode 100644
index 0000000..1a61153
--- /dev/null
+++ b/src/jloda/export/gifEncode/IndexGif89Frame.java
@@ -0,0 +1,70 @@
+/**
+ * IndexGif89Frame.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+//******************************************************************************
+// IndexGif89Frame.java
+//******************************************************************************
+package jloda.export.gifEncode;
+
+//==============================================================================
+
+/**
+ * Instances of this Gif89Frame subclass are constructed from bitmaps in the
+ * form of color-index pixels, which accords with a GIF's native palettized
+ * color model.  The class is useful when complete control over a GIF's color
+ * palette is desired.  It is also much more efficient when one is using an
+ * algorithmic frame generator that isn't interested in RGB values (such
+ * as a cellular automaton).
+ * <p/>
+ * Objects of this class are normally added to a Gif89Encoder object that has
+ * been provided with an explicit color table at construction.  While you may
+ * also add them to "auto-map" encoders without an exception being thrown,
+ * there obviously must be at least one DirectGif89Frame object in the sequence
+ * so that a color table may be detected.
+ *
+ * @author J. M. G. Elliott (tep at jmge.net)
+ * @version 0.90 beta (15-Jul-2000)
+ * @see Gif89Encoder
+ * @see Gif89Frame
+ * @see DirectGif89Frame
+ */
+public class IndexGif89Frame extends Gif89Frame {
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Construct a IndexGif89Frame from color-index pixel data.
+     *
+     * @param width     Width of the bitmap.
+     * @param height    Height of the bitmap.
+     * @param ci_pixels Array containing at least width*height color-index pixels.
+     */
+    public IndexGif89Frame(int width, int height, byte ci_pixels[]) {
+        theWidth = width;
+        theHeight = height;
+        ciPixels = new byte[theWidth * theHeight];
+        System.arraycopy(ci_pixels, 0, ciPixels, 0, ciPixels.length);
+    }
+
+    //----------------------------------------------------------------------------
+
+    Object getPixelSource() {
+        return ciPixels;
+    }
+}
diff --git a/src/jloda/export/gifEncode/Put.java b/src/jloda/export/gifEncode/Put.java
new file mode 100644
index 0000000..9c3835d
--- /dev/null
+++ b/src/jloda/export/gifEncode/Put.java
@@ -0,0 +1,61 @@
+/**
+ * Put.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+//******************************************************************************
+// Put.java
+//******************************************************************************
+package jloda.export.gifEncode;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+//==============================================================================
+
+/**
+ * Just a couple of trivial output routines used by other classes in the
+ * package.  Normally this kind of stuff would be in a separate IO package, but
+ * I wanted the present package to be self-contained for ease of distribution
+ * and use by others.
+ */
+final class Put {
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Write just the low bytes of a String.  (This sucks, but the concept of an
+     * encoding seems inapplicable to a binary file ID string.  I would think
+     * flexibility is just what we don't want - but then again, maybe I'm slow.)
+     */
+    static void ascii(String s, OutputStream os) throws IOException {
+        byte[] bytes = new byte[s.length()];
+        for (int i = 0; i < bytes.length; ++i)
+            bytes[i] = (byte) s.charAt(i);  // discard the high byte
+        os.write(bytes);
+    }
+
+    //----------------------------------------------------------------------------
+
+    /**
+     * Write a 16-bit integer in little endian byte order.
+     */
+    static void leShort(int i16, OutputStream os) throws IOException {
+        os.write(i16 & 0xff);
+        os.write(i16 >> 8 & 0xff);
+    }
+}
diff --git a/src/jloda/graph/Dijkstra.java b/src/jloda/graph/Dijkstra.java
new file mode 100644
index 0000000..9707f76
--- /dev/null
+++ b/src/jloda/graph/Dijkstra.java
@@ -0,0 +1,130 @@
+/**
+ * Dijkstra.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graph;
+
+import jloda.util.Basic;
+import jloda.util.NotOwnerException;
+
+import java.util.*;
+
+/**
+ * Dijkstras algorithm for single source shortest path, non-negative edge lengths
+ *
+ * @author huson
+ *         Date: 11-Dec-2004
+ */
+public class Dijkstra {
+    /**
+     * compute single source shortest path from source to sink, non-negative edge weights
+     *
+     * @param graph  with edges labeled by Integers
+     * @param source
+     * @param sink
+     * @return shortest path from source to sink
+     */
+    public static List compute(final Graph graph, Node source, Node sink) throws Exception {
+        try {
+            NodeArray<Node> predecessor = new NodeArray<>(graph);
+            NodeIntegerArray dist = new NodeIntegerArray(graph);
+            SortedSet<Node> priorityQueue = newFullQueue(graph, dist);
+
+            // init:
+            for (Node v = graph.getFirstNode(); v != null; v = graph.getNextNode(v)) {
+                dist.set(v, 1000000);
+                predecessor.set(v, null);
+            }
+            dist.set(source, 0);
+
+            // main loop:
+            while (!priorityQueue.isEmpty()) {
+                int size = priorityQueue.size();
+                Node u = priorityQueue.first();
+                priorityQueue.remove(u);
+                if (priorityQueue.size() != size - 1)
+                    throw new RuntimeException("remove u=" + u + " failed: size=" + size);
+
+                Iterator out = graph.getOutEdges(u);
+                while (out.hasNext()) {
+                    Edge e = (Edge) out.next();
+                    int weight = (Integer) graph.getInfo(e);
+                    Node v = graph.getOpposite(u, e);
+                    if (dist.getValue(v) > dist.getValue(u) + weight) {
+                        // priorty of v changes, so must re-and to queue:
+                        priorityQueue.remove(v);
+                        dist.set(v, dist.getValue(u) + weight);
+                        priorityQueue.add(v);
+                        predecessor.set(v, u);
+                    }
+                }
+            }
+            System.err.println("done main loop");
+            List<Node> result = new LinkedList<>();
+            Node v = sink;
+            while (v != source) {
+                if (v == null)
+                    throw new Exception("No path from sink back to source");
+                System.err.println("v: " + v);
+                if (v != sink)
+                    result.add(0, v);
+                v = predecessor.get(v);
+            }
+            System.err.println("# Dijkstra: " + result.size());
+            return result;
+
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+            return null;
+        }
+    }
+
+    /**
+     * setups the priority queue
+     *
+     * @param graph
+     * @param dist
+     * @return full priority queue
+     * @
+     */
+    static public SortedSet<Node> newFullQueue(final Graph graph, final NodeIntegerArray dist) {
+        SortedSet<Node> queue = new TreeSet<>(new Comparator<Node>() {
+            public int compare(Node v1, Node v2) {
+                int weight1 = dist.getValue(v1);
+                int weight2 = dist.getValue(v2);
+                //System.out.println("weight1 " + weight1 + " weight2 " + weight2);
+                //System.out.println("graph.getId(v1) " + graph.getId(v1) + " graph.getId(v2) " + graph.getId(v2));
+                if (weight1 < weight2)
+                    return -1;
+                else if (weight1 > weight2)
+                    return 1;
+                else if (graph.getId(v1) < graph.getId(v2))
+                    return -1;
+                else if (graph.getId(v1) > graph.getId(v2))
+                    return 1;
+                else
+                    return 0;
+            }
+        });
+        for (Node v = graph.getFirstNode(); v != null; v = graph.getNextNode(v))
+            queue.add(v);
+        return queue;
+    }
+
+
+}
diff --git a/src/jloda/graph/DirectedCycleDetector.java b/src/jloda/graph/DirectedCycleDetector.java
new file mode 100644
index 0000000..33a6389
--- /dev/null
+++ b/src/jloda/graph/DirectedCycleDetector.java
@@ -0,0 +1,111 @@
+/*
+ *  Copyright (C) 2015 Daniel H. Huson
+ *
+ *  (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package jloda.graph;
+
+import java.util.Collection;
+import java.util.Stack;
+
+/**
+ * Detection of directed cycles
+ * See Sedewick and Wayne, Algorithms, 4th ed., 2011
+ * Created by huson on 8/21/16.
+ */
+public class DirectedCycleDetector {
+    private final Graph G;
+    private final NodeSet marked;
+    private final NodeArray<Edge> edgeTo;
+    private final NodeSet onStack;
+    private final Stack<Edge> cycle;
+
+    /**
+     * constructor
+     *
+     * @param G
+     */
+    public DirectedCycleDetector(Graph G) {
+        this.G = G;
+        onStack = new NodeSet(G);
+        edgeTo = new NodeArray<>(G);
+        marked = new NodeSet(G);
+        cycle = new Stack<>();
+    }
+
+    /**
+     * detects a cycle, if one exists
+     *
+     * @return true, if cycle detected
+     */
+    public boolean apply() {
+        onStack.clear();
+        edgeTo.clear();
+        marked.clear();
+        cycle.clear();
+
+        for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+            if (!marked.contains(v))
+                detectRec(G, v);
+        }
+        return hasCycle();
+    }
+
+    /**
+     * recursively does the work
+     *
+     * @param G
+     * @param v
+     */
+    private void detectRec(Graph G, Node v) {
+        onStack.add(v);
+        marked.add(v);
+
+        for (Edge e = v.getFirstOutEdge(); e != null; e = v.getNextOutEdge(e)) {
+            final Node w = e.getTarget();
+            if (this.hasCycle())
+                return;
+            else if (!marked.contains(w)) {
+                edgeTo.set(w, e);
+                detectRec(G, w);
+            } else if (onStack.contains(w)) {
+                cycle.push(e);
+                for (Node x = v; x != w; x = edgeTo.get(x).getSource())
+                    cycle.push(edgeTo.get(x));
+            }
+        }
+        onStack.remove(v);
+    }
+
+    /**
+     * does graph have a cycle?
+     *
+     * @return true, if has cycle
+     */
+    public boolean hasCycle() {
+        return cycle.size() > 0;
+    }
+
+    /**
+     * gets the cycle
+     *
+     * @return cycle
+     */
+    public Collection<Edge> cycle() {
+        return cycle;
+    }
+}
diff --git a/src/jloda/graph/Edge.java b/src/jloda/graph/Edge.java
new file mode 100644
index 0000000..730aee5
--- /dev/null
+++ b/src/jloda/graph/Edge.java
@@ -0,0 +1,433 @@
+/**
+ * Edge.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: Edge.java,v 1.17 2010-06-10 12:07:57 scornava Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+import jloda.util.NotOwnerException;
+
+/**
+ * Edge class used by Graph class
+ *
+ * @author Daniel Huson, 2003
+ */
+public class Edge extends NodeEdge implements Comparable {
+    /**
+     * insert before reference edge
+     */
+    final public static int AFTER = 1;
+    /**
+     * insert after reference edge
+     */
+    final public static int BEFORE = -1;
+
+    private Node source;   //Source vertex
+    private Node target;   //Target vertex
+
+    private Edge sNext;    //Next edge in list of edges incident with v
+    private Edge sPrev;    //Previous edge in list of edges incident with v
+
+    private Edge tNext;   //Next edge in list of edges incident with w
+    private Edge tPrev;   //Previous edge in list of edges incident with w
+
+    /**
+     * Construct a new edge from v to w.
+     *
+     * @param G
+     * @param v
+     * @param w
+     */
+    public Edge(Graph G, Node v, Node w) throws IllegalSelfEdgeException {
+        this(G, v, null, w, null, Edge.AFTER, Edge.AFTER, null);
+    }
+
+    /**
+     * construct a new edge from v to w and set its info object. The next and previous edges
+     * are set to Edge.AFTER.
+     *
+     * @param G
+     * @param v
+     * @param w
+     * @param obj
+     */
+    public Edge(Graph G, Node v, Node w, Object obj) throws IllegalSelfEdgeException {
+        this(G, v, null, w, null, Edge.AFTER, Edge.AFTER, obj);
+    }
+
+    /**
+     * constructs an edge from v to w, inserting it before or after the two given reference edges.
+     *
+     * @param graph graph
+     * @param v     source node
+     * @param e_v   reference edge at source node
+     * @param w     target node
+     * @param e_w   reference edge at target node
+     * @param dir_v place before or after source reference edge
+     * @param dir_w place before or after target reference edge
+     * @param obj   the info object
+     */
+    public Edge(final Graph graph, final Node v, final Edge e_v, final Node w, final Edge e_w, final int dir_v, final int dir_w, final Object obj) throws IllegalSelfEdgeException {
+        if (v == w)
+            throw new IllegalSelfEdgeException();
+        graph.registerNewEdge(v, e_v, w, e_w, dir_v, dir_w, obj, this);
+        getOwner().fireNewEdge(this);
+        getOwner().fireGraphHasChanged();
+    }
+
+    /**
+     * initialize this edge
+     *
+     * @param G     The graph
+     * @param id    The id of the edge
+     *              // [REMOVED] param prev0  The edge before this one in the list of all edges.
+     * @param v     Source vertex
+     * @param e_v
+     * @param dir_v
+     * @param w     Target Node
+     * @param e_w
+     * @param dir_w
+     * @param obj   If e_v is null, then edge is inserted immediately after the last node in the list of
+     *              edges incident with v (or before the first node if dir_v = BEFORE). Likewise for e_w.
+     */
+    void init(Graph G, int id, Node v, Edge e_v, int dir_v, Node w, Edge e_w, int dir_w, Object obj) throws NotOwnerException {
+
+        //ToDo: [DAVE] removed the prev0 parameter here, as this will cause a bug if prev0 is not the final edge.
+        super.init(G, G.lastEdge, null, id, obj);
+
+        source = v;
+        target = w;
+
+        if (dir_v == AFTER) {
+            if (e_v == null)
+                e_v = source.getLastAdjacentEdge();
+            if (e_v != null) {
+                Edge ee = e_v.getNextIncidentTo(source);
+                if (ee != null)
+                    ee.setPrev(source, this);
+                this.setPrev(source, e_v);
+                e_v.setNext(source, this);
+                this.setNext(source, ee);
+            } else {
+                this.setPrev(source, null);
+                this.setNext(source, null);
+            }
+            if (source.getFirstAdjacentEdge() == null)
+                source.setFirstAdjacentEdge(this);
+            if (source.getLastAdjacentEdge() == e_v)
+                source.setLastAdjacentEdge(this);
+        } else // dir_v==before
+        {
+            if (e_v == null)
+                e_v = source.getFirstAdjacentEdge();
+            if (e_v != null) {
+                Edge ee = e_v.getPrevIncidentTo(source);
+                if (ee != null)
+                    ee.setNext(source, this);
+                this.setNext(source, e_v);
+                e_v.setPrev(source, this);
+                this.setPrev(source, ee);
+            } else {
+                this.setPrev(source, null);
+                this.setNext(source, null);
+            }
+            if (source.getLastAdjacentEdge() == null)
+                source.setLastAdjacentEdge(this);
+            if (source.getFirstAdjacentEdge() == e_v)
+                source.setFirstAdjacentEdge(this);
+        }
+        if (dir_w == AFTER) {
+            if (e_w == null)
+                e_w = target.getLastAdjacentEdge();
+            if (e_w != null) {
+                Edge ee = e_w.getNextIncidentTo(target);
+                if (ee != null)
+                    ee.setPrev(target, this);
+                this.setPrev(target, e_w);
+                e_w.setNext(target, this);
+                this.setNext(target, ee);
+            } else {
+                this.setPrev(target, null);
+                this.setNext(target, null);
+            }
+            if (target.getFirstAdjacentEdge() == null)
+                target.setFirstAdjacentEdge(this);
+            if (target.getLastAdjacentEdge() == e_w)
+                target.setLastAdjacentEdge(this);
+        } else // dir==before
+        {
+            if (e_w == null)
+                e_w = target.getFirstAdjacentEdge();
+            if (e_w != null) {
+                Edge ee = e_w.getPrevIncidentTo(target);
+                if (ee != null)
+                    ee.setNext(target, this);
+                this.setNext(target, e_w);
+                e_w.setPrev(target, this);
+                this.setPrev(target, ee);
+            } else {
+                this.setPrev(target, null);
+                this.setNext(target, null);
+            }
+            if (target.getLastAdjacentEdge() == null)
+                target.setLastAdjacentEdge(this);
+            if (target.getFirstAdjacentEdge() == e_w)
+                target.setFirstAdjacentEdge(this);
+        }
+    }
+
+
+    /**
+     * set the next Edge of the current edge
+     *
+     * @param v Node
+     * @param f Edge is the next edge of e
+     */
+    void setNext(Node v, Edge f) throws NotOwnerException {
+        checkOwner(v);
+        if (f != null)
+            checkOwner(f);
+        if (source == v)
+            this.sNext = f;
+        else if (target == v)
+            this.tNext = f;
+    }
+
+    /**
+     * set the previous edge of the current edge
+     *
+     * @param v Node
+     * @param f Edge is the previous edge of e
+     */
+    void setPrev(Node v, Edge f) throws NotOwnerException {
+        checkOwner(v);
+        if (f != null)
+            checkOwner(f);
+        if (source == v)
+            this.sPrev = f;
+        else if (target == v)
+            this.tPrev = f;
+    }
+
+    /**
+     * Get the next edge incident to v
+     *
+     * @param v Node
+     * @return the next edge of e
+     */
+    public Edge getNextIncidentTo(Node v) throws NotOwnerException {
+        checkOwner(v);
+        if (source == v)
+            return getSNext();
+        else if (target == v)
+            return getTNext();
+        else
+            return null;
+    }
+
+    /**
+     * Get the previous edge incident to v
+     *
+     * @param v Node
+     * @return the previous edge
+     */
+    public Edge getPrevIncidentTo(Node v) throws NotOwnerException {
+        checkOwner(v);
+        if (source == v)
+            return getSPrev();
+        else if (target == v)
+            return getTPrev();
+        else
+            return null;
+    }
+
+    /**
+     * Gets the opposite Node
+     *
+     * @param v Node
+     * @return a Node the Opposite of the Node u
+     */
+    public Node getOpposite(Node v) throws NotOwnerException {
+        checkOwner(v);
+        if (v == source)
+            return target;
+        else if (v == target)
+            return source;
+        else
+            return null;
+    }
+
+    /**
+     * Produces a string representation
+     *
+     * @return string representation
+     */
+    public String toString() {
+        StringBuilder buf = new StringBuilder("[" + String.valueOf(getId()) + "] [");
+        if (getInfo() != null)
+            buf.append(getInfo().toString());
+        buf.append("]: ").append(source.getId()).append(" ").append(target.getId());
+        if (isHidden())
+            buf.append(" (hidden)");
+        return buf.toString();
+    }
+
+    /**
+     * remove this edge from the graph
+     */
+    public void deleteEdge() {
+        final Graph graph = getOwner();
+        graph.fireDeleteEdge(this);
+        graph.unregisterEdge(this);
+
+        if (source.getFirstAdjacentEdge() == this)
+            source.setFirstAdjacentEdge(getNextIncidentTo(source));
+        if (source.getLastAdjacentEdge() == this)
+            source.setLastAdjacentEdge(this.getPrevIncidentTo(source));
+        if (target.getFirstAdjacentEdge() == this)
+            target.setFirstAdjacentEdge(this.getNextIncidentTo(target));
+        if (target.getLastAdjacentEdge() == this)
+            target.setLastAdjacentEdge(this.getPrevIncidentTo(target));
+
+        if (prev != null)
+            prev.next = next;
+        if (next != null)
+            next.prev = prev;
+        if (sPrev != null)
+            sPrev.setNext(source, sNext);
+        if (sNext != null)
+            sNext.setPrev(source, sPrev);
+        if (tPrev != null)
+            tPrev.setNext(target, tNext);
+        if (tNext != null)
+            tNext.setPrev(target, tPrev);
+
+        setOwner(null);
+        info = null;
+        graph.fireGraphHasChanged();
+    }
+
+    /**
+     * get next edge in list of all edges
+     *
+     * @return next edge in list of all edges
+     */
+    public Edge getNext() {
+        Edge e = (Edge) next;
+        while (e != null && e.isHidden())
+            e = (Edge) e.next;
+        return e;
+    }
+
+    /**
+     * get previous edge in list of all edges
+     *
+     * @return previous edge in list of all edges
+     */
+    public Edge getPrev() {
+        Edge e = (Edge) prev;
+        while (e != null && e.isHidden())
+            e = (Edge) e.prev;
+        return e;
+    }
+
+
+    /**
+     * Get the source node of this edge
+     *
+     * @return source
+     */
+
+    public Node getSource() {
+        return source;
+    }
+
+    /**
+     * Get the target node of this edge
+     *
+     * @return target
+     */
+
+    public Node getTarget() {
+        return target;
+    }
+
+    /**
+     * compares with another edge of the same graph
+     *
+     * @param o
+     * @return -1, 1 or 0
+     */
+    public int compareTo(Object o) {
+        final Edge e = (Edge) o;
+        checkOwner(e);
+        if (this.getId() < e.getId())
+            return -1;
+        else if (this.getId() > e.getId())
+            return 1;
+        else
+            return 0;
+    }
+
+    /**
+     * reverses the orientation of this edge by swapping source and target
+     */
+    public void reverse() {
+        source.incrementInDegree();
+        source.decrementOutDegree();
+        target.decrementInDegree();
+        target.incrementOutDegree();
+
+        Node tmpNode = source;
+        source = target;
+        target = tmpNode;
+
+        Edge tmpEdge = sNext;
+        sNext = tNext;
+        tNext = tmpEdge;
+
+        tmpEdge = sPrev;
+        sPrev = tPrev;
+        tPrev = tmpEdge;
+
+        getOwner().fireGraphHasChanged();
+    }
+
+    Edge getSPrev() {
+        return sPrev;
+    }
+
+    Edge getSNext() {
+        return sNext;
+    }
+
+    Edge getTPrev() {
+        return tPrev;
+    }
+
+    Edge getTNext() {
+        return tNext;
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/EdgeArray.java b/src/jloda/graph/EdgeArray.java
new file mode 100644
index 0000000..54949d0
--- /dev/null
+++ b/src/jloda/graph/EdgeArray.java
@@ -0,0 +1,198 @@
+/**
+ * EdgeArray.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: EdgeArray.java,v 1.11 2005-12-05 13:25:44 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+
+/**
+ * Edge array
+ * Daniel Huson 2004
+ */
+
+public class EdgeArray<T> extends GraphBase implements EdgeAssociation<T> {
+    private T data[];
+    private boolean isClear = true;
+
+    /**
+     * Construct an edge array.
+     *
+     * @param g Graph
+     */
+    public EdgeArray(Graph g) {
+        setOwner(g);
+        data = (T[]) new Object[g.getMaxEdgeId() + 1];
+        g.registerEdgeAssociation(this);
+    }
+
+    /**
+     * Construct an edge array for the given graph and initialize all entries
+     * to obj.
+     *
+     * @param g   Graph
+     * @param obj Object
+     */
+    public EdgeArray(Graph g, T obj) {
+        this(g);
+        setAll(obj);
+        if (obj != null && isClear)
+            isClear = true;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param src EdgeArray
+     */
+    public EdgeArray(EdgeAssociation<T> src) {
+        setOwner(src.getOwner());
+        for (Edge e = getOwner().getFirstEdge(); e != null; e = e.getNext())
+            set(e, src.get(e));
+        isClear = src.isClear();
+    }
+
+    /**
+     * Get the entry for edge e.
+     *
+     * @param e Edge
+     * @return an object the entry for edge e
+     */
+    public T get(Edge e) {
+        checkOwner(e);
+        if (e.getId() < data.length)
+            return data[e.getId()];
+        else
+            return null;
+    }
+
+    /**
+     * Set the entry for edge e to obj.
+     *
+     * @param e   Edge
+     * @param obj Object
+     */
+    public void set(Edge e, T obj) {
+        checkOwner(e);
+        int id = e.getId();
+        if (id >= data.length) {
+            if (obj == null)
+                return; // nothing to do
+            grow(e.getId());
+        }
+        data[id] = obj;
+        if (obj != null && isClear)
+            isClear = true;
+    }
+
+    /**
+     * grows the array. Repeatedly doubles the size of the array until it contains index n
+     *
+     * @param n index to be included in array
+     */
+    private void grow(int n) {
+        int newSize = Math.max(1, 2 * data.length);
+        while (newSize <= n)
+            newSize *= 2;
+        if (newSize > data.length) {
+            T[] newData = (T[]) new Object[newSize];
+            for (Edge e = getOwner().getFirstEdge(); e != null; e = e.getNext()) {
+                int id = e.getId();
+                if (id < data.length)
+                    newData[id] = data[id];
+            }
+            data = newData;
+        }
+    }
+
+    /**
+     * Set the entry for all edges.
+     *
+     * @param obj Object
+     */
+    public void setAll(T obj) {
+        for (Edge e = getOwner().getFirstEdge(); e != null; e = e.getNext())
+            set(e, obj);
+        if (obj != null && isClear)
+            isClear = true;
+    }
+
+    /**
+     * Clear all entries.
+     */
+    public void clear() {
+        if (getOwner().getMaxEdgeId() < 0.5 * data.length)
+            data = (T[]) new Object[getOwner().getMaxEdgeId() + 1];
+        else
+            for (Edge e = getOwner().getFirstEdge(); e != null; e = e.getNext())
+                set(e, null);
+        isClear = true;
+    }
+
+    /**
+     * get the entry as an int
+     *
+     * @param e
+     * @return int value
+     */
+    public int getInt(Edge e) {
+        Object obj = get(e);
+        if (obj == null)
+            return 0;
+        else if (obj instanceof Double)
+            return (int) ((Double) obj).doubleValue();
+        else
+            return ((Integer) obj);
+
+    }
+
+    /**
+     * get the entry as a double
+     *
+     * @param e
+     * @return double value
+     */
+    public double getDouble(Edge e) {
+        Object obj = get(e);
+        if (obj == null)
+            return 0;
+        else if (obj instanceof Integer)
+            return ((Integer) obj);
+        else
+            return ((Double) obj);
+    }
+
+
+    /**
+     * is clean, that is, has never been set since last erase
+     *
+     * @return true, if erase
+     */
+    public boolean isClear() {
+        return isClear;
+    }
+
+
+}
+
+// EOF
diff --git a/src/jloda/graph/EdgeAssociation.java b/src/jloda/graph/EdgeAssociation.java
new file mode 100644
index 0000000..27b13ba
--- /dev/null
+++ b/src/jloda/graph/EdgeAssociation.java
@@ -0,0 +1,85 @@
+/**
+ * EdgeAssociation.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graph;
+
+/**
+ * Edge association
+ * daniel huson 2005
+ */
+public interface EdgeAssociation<T> {
+    /**
+     * Get the entry for edge e.
+     *
+     * @param e Edge
+     * @return an object the entry for edge e
+     */
+    T get(Edge e);
+
+    /**
+     * Set the entry for edge e to obj.
+     *
+     * @param e   Edge
+     * @param obj Object
+     */
+    void set(Edge e, T obj);
+
+    /**
+     * Set the entry for all edges.
+     *
+     * @param obj Object
+     */
+    void setAll(T obj);
+
+    /**
+     * Clear all entries.
+     */
+    void clear();
+
+    /**
+     * get the entry as an int
+     *
+     * @param e
+     * @return int value
+     */
+    int getInt(Edge e);
+
+    /**
+     * get the entry as a double
+     *
+     * @param e
+     * @return double value
+     */
+    double getDouble(Edge e);
+
+    /**
+     * returns a reference to the graph that owns this association
+     *
+     * @return owner
+     */
+    Graph getOwner();
+
+
+    /**
+     * is clean, that is, has never been set since last erase
+     *
+     * @return true, if erase
+     */
+    boolean isClear();
+}
diff --git a/src/jloda/graph/EdgeDoubleArray.java b/src/jloda/graph/EdgeDoubleArray.java
new file mode 100644
index 0000000..775ffae
--- /dev/null
+++ b/src/jloda/graph/EdgeDoubleArray.java
@@ -0,0 +1,106 @@
+/**
+ * EdgeDoubleArray.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: EdgeDoubleArray.java,v 1.7 2005-12-05 13:25:44 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+
+/**
+ * Edge double array
+ * Daniel Huson, 2003
+ */
+
+public class EdgeDoubleArray extends EdgeArray<Double> {
+    /**
+     * Construct an edge double array for the given graph.
+     *
+     * @param g Graph
+     */
+    public EdgeDoubleArray(Graph g) {
+        super(g);
+    }
+
+    /**
+     * Construct an edge double array for the given graph and initialize all
+     * entries to value.
+     *
+     * @param g   Graph
+     * @param val double
+     */
+    public EdgeDoubleArray(Graph g, double val) {
+        super(g, val);
+    }
+
+    /**
+     * copy constructor
+     *
+     * @param src
+     */
+    public EdgeDoubleArray(EdgeDoubleArray src) {
+        super(src);
+    }
+
+    /**
+     * copy constructor
+     *
+     * @param src
+     */
+    public EdgeDoubleArray(EdgeDoubleMap src) {
+        super(src);
+    }
+
+    /**
+     * Get the entry for edge e.
+     *
+     * @param e Edge
+     * @return a double value the entry for edge e
+     */
+    public double getValue(Edge e) {
+        if (super.get(e) == null)
+            return 0;
+        else
+            return super.get(e);
+    }
+
+    /**
+     * Set the entry for edge e to obj.
+     *
+     * @param e     Edge
+     * @param value double
+     */
+    public void set(Edge e, double value) {
+        super.set(e, value);
+    }
+
+    /**
+     * Set the entry for all edges.
+     *
+     * @param val double
+     */
+    public void setAll(double val) {
+        super.setAll(val);
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/EdgeDoubleMap.java b/src/jloda/graph/EdgeDoubleMap.java
new file mode 100644
index 0000000..f58a59c
--- /dev/null
+++ b/src/jloda/graph/EdgeDoubleMap.java
@@ -0,0 +1,106 @@
+/**
+ * EdgeDoubleMap.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: EdgeDoubleMap.java,v 1.2 2005-12-05 13:25:44 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+
+/**
+ * Edge double map
+ * Daniel Huson, 2003
+ */
+
+public class EdgeDoubleMap extends EdgeMap {
+    /**
+     * Construct an edge double map for the given graph.
+     *
+     * @param g Graph
+     */
+    public EdgeDoubleMap(Graph g) {
+        super(g);
+    }
+
+    /**
+     * Construct an edge double map for the given graph and initialize all
+     * entries to value.
+     *
+     * @param g   Graph
+     * @param val double
+     */
+    public EdgeDoubleMap(Graph g, double val) {
+        super(g, val);
+    }
+
+    /**
+     * copy constructor
+     *
+     * @param src
+     */
+    public EdgeDoubleMap(EdgeDoubleArray src) {
+        super(src);
+    }
+
+    /**
+     * copy constructor
+     *
+     * @param src
+     */
+    public EdgeDoubleMap(EdgeDoubleMap src) {
+        super(src);
+    }
+
+    /**
+     * Get the entry for edge e.
+     *
+     * @param e Edge
+     * @return a double value the entry for edge e
+     */
+    public double getValue(Edge e) {
+        if (super.get(e) == null)
+            return 0;
+        else
+            return (Double) super.get(e);
+    }
+
+    /**
+     * Set the entry for edge e to obj.
+     *
+     * @param e     Edge
+     * @param value double
+     */
+    public void set(Edge e, double value) {
+        super.set(e, value);
+    }
+
+    /**
+     * Set the entry for all edges.
+     *
+     * @param val double
+     */
+    public void setAll(double val) {
+        super.setAll(val);
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/EdgeIntegerArray.java b/src/jloda/graph/EdgeIntegerArray.java
new file mode 100644
index 0000000..4fde377
--- /dev/null
+++ b/src/jloda/graph/EdgeIntegerArray.java
@@ -0,0 +1,106 @@
+/**
+ * EdgeIntegerArray.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: EdgeIntegerArray.java,v 1.6 2005-12-05 13:25:44 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+
+/**
+ * Edge int array
+ * Daniel Huson, 2003
+ */
+
+public class EdgeIntegerArray extends EdgeArray<Integer> {
+    /**
+     * Construct an edge int array for the given graph and initialize all
+     * entries to value.
+     *
+     * @param g   Graph
+     * @param val int
+     */
+    public EdgeIntegerArray(Graph g, int val) {
+        super(g, val);
+    }
+
+    /**
+     * Construct an edge int array.
+     *
+     * @param g Graph
+     */
+    public EdgeIntegerArray(Graph g) {
+        super(g);
+    }
+
+    /**
+     * Construct an edge int map.
+     *
+     * @param src
+     */
+    public EdgeIntegerArray(EdgeIntegerMap src) {
+        super(src);
+    }
+
+    /**
+     * Construct an edge int map.
+     *
+     * @param src
+     */
+    public EdgeIntegerArray(EdgeIntegerArray src) {
+        super(src);
+    }
+
+    /**
+     * Get the entry for edge e.
+     *
+     * @param e Edge
+     * @return an integer value the entry for edge e
+     */
+    public int getValue(Edge e) {
+        if (super.get(e) == null)
+            return 0;
+        else
+            return super.get(e);
+    }
+
+    /**
+     * Set the entry for edge e to obj.
+     *
+     * @param e     Edge
+     * @param value int
+     */
+    public void set(Edge e, int value) {
+        super.set(e, value);
+    }
+
+    /**
+     * Set the entry for all edges.
+     *
+     * @param val int
+     */
+    public void setAll(int val) {
+        super.setAll(val);
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/EdgeIntegerMap.java b/src/jloda/graph/EdgeIntegerMap.java
new file mode 100644
index 0000000..e4ce65a
--- /dev/null
+++ b/src/jloda/graph/EdgeIntegerMap.java
@@ -0,0 +1,107 @@
+/**
+ * EdgeIntegerMap.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: EdgeIntegerMap.java,v 1.2 2005-12-05 13:25:44 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+
+/**
+ * Edge integer map
+ *
+ *  @author Daniel Huson, 2003
+ */
+
+public class EdgeIntegerMap extends EdgeMap<Integer> {
+    /**
+     * Construct an edge int map for the given graph and initialize all
+     * entries to value.
+     *
+     * @param g   Graph
+     * @param val int
+     */
+    public EdgeIntegerMap(Graph g, int val) {
+        super(g, val);
+    }
+
+    /**
+     * Construct an edge int map.
+     *
+     * @param g Graph
+     */
+    public EdgeIntegerMap(Graph g) {
+        super(g);
+    }
+
+    /**
+     * Construct an edge int map.
+     *
+     * @param src
+     */
+    public EdgeIntegerMap(EdgeIntegerMap src) {
+        super(src);
+    }
+
+    /**
+     * Construct an edge int map.
+     *
+     * @param src
+     */
+    public EdgeIntegerMap(EdgeIntegerArray src) {
+        super(src);
+    }
+
+    /**
+     * Get the entry for edge e.
+     *
+     * @param e Edge
+     * @return an integer value the entry for edge e
+     */
+    public int getValue(Edge e) {
+        if (super.get(e) == null)
+            return 0;
+        else
+            return super.get(e);
+    }
+
+    /**
+     * Set the entry for edge e to obj.
+     *
+     * @param e     Edge
+     * @param value int
+     */
+    public void set(Edge e, int value) {
+        super.set(e, value);
+    }
+
+    /**
+     * Set the entry for all edges.
+     *
+     * @param val int
+     */
+    public void setAll(int val) {
+        super.setAll(val);
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/EdgeMap.java b/src/jloda/graph/EdgeMap.java
new file mode 100644
index 0000000..af5ab52
--- /dev/null
+++ b/src/jloda/graph/EdgeMap.java
@@ -0,0 +1,168 @@
+/**
+ * EdgeMap.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: EdgeMap.java,v 1.2 2005-12-05 13:25:44 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Edge map
+ * @author Daniel Huson, 2003
+ */
+
+public class EdgeMap<T> extends GraphBase implements EdgeAssociation<T> {
+    private Map<Edge, T> data;
+    private boolean isClear;
+
+    /**
+     * Construct an edge map.
+     *
+     * @param g Graph
+     */
+    public EdgeMap(Graph g) {
+        setOwner(g);
+        g.registerEdgeAssociation(this);
+        data = new HashMap<>();
+        isClear = true;
+    }
+
+    /**
+     * Construct an edge map for the given graph and initialize all entries
+     * to obj.
+     *
+     * @param g   Graph
+     * @param obj Object
+     */
+    public EdgeMap(Graph g, T obj) {
+        this(g);
+        setAll(obj);
+        if (obj != null && isClear)
+            isClear = false;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param src EdgeMap
+     */
+    public EdgeMap(EdgeAssociation<T> src) {
+        Graph G = src.getOwner();
+        setOwner(G);
+        for (Edge e = getOwner().getFirstEdge(); e != null; e = e.getNext())
+            set(e, src.get(e));
+        isClear = src.isClear();
+    }
+
+    /**
+     * Get the entry for edge e.
+     *
+     * @param e Edge
+     * @return an object the entry for edge e
+     */
+    public T get(Edge e) {
+        checkOwner(e);
+        return data.get(e);
+    }
+
+    /**
+     * Set the entry for edge e to obj.
+     *
+     * @param e   Edge
+     * @param obj Object
+     */
+    public void set(Edge e, T obj) {
+        checkOwner(e);
+        data.put(e, obj);
+        if (obj != null && isClear)
+            isClear = false;
+    }
+
+    /**
+     * Set the entry for all edges.
+     *
+     * @param obj Object
+     */
+    public void setAll(T obj) {
+        for (Edge e = getOwner().getFirstEdge(); e != null; e = e.getNext())
+            data.put(e, obj);
+        if (obj != null && isClear)
+            isClear = false;
+
+    }
+
+    /**
+     * Clear all entries.
+     */
+    public void clear() {
+        for (Edge e = getOwner().getFirstEdge(); e != null; e = e.getNext())
+            data.remove(e);
+        isClear = true;
+    }
+
+    /**
+     * get the entry as an int
+     *
+     * @param e
+     * @return int value
+     */
+    public int getInt(Edge e) {
+        Object obj = get(e);
+        if (obj == null)
+            return 0;
+        else if (obj instanceof Double)
+            return (int) ((Double) obj).doubleValue();
+        else
+            return (Integer) obj;
+
+    }
+
+    /**
+     * get the entry as a double
+     *
+     * @param e
+     * @return double value
+     */
+    public double getDouble(Edge e) {
+        Object obj = get(e);
+        if (obj == null)
+            return 0;
+        else if (obj instanceof Integer)
+            return ((Integer) obj);
+        else
+            return ((Double) obj);
+    }
+
+    /**
+     * is clean, that is, has never been set since last erase
+     *
+     * @return true, if erase
+     */
+    public boolean isClear() {
+        return isClear;
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/EdgeSet.java b/src/jloda/graph/EdgeSet.java
new file mode 100644
index 0000000..b4b9729
--- /dev/null
+++ b/src/jloda/graph/EdgeSet.java
@@ -0,0 +1,340 @@
+/**
+ * EdgeSet.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * Edge set
+ * @author Daniel Huson, 2003
+ *
+ */
+package jloda.graph;
+
+import jloda.util.IteratorAdapter;
+
+import java.util.*;
+
+/**
+ * EdgeSet implements a set of edges contained in a given graph
+ */
+public class EdgeSet extends GraphBase implements Set<Edge> {
+    final BitSet bits;
+
+    /**
+     * Constructs a new empty EdgeSet for Graph G.
+     *
+     * @param graph Graph
+     */
+    public EdgeSet(Graph graph) {
+        setOwner(graph);
+        graph.registerEdgeSet(this);
+        bits = new BitSet();
+    }
+
+    /**
+     * Is edge v member?
+     *
+     * @param e Edge
+     * @return a boolean value
+     */
+    public boolean contains(Object e) {
+        return bits.get(getOwner().getId((Edge) e));
+    }
+
+    /**
+     * Insert edge e.
+     *
+     * @param e Edge
+     * @return true, if new
+     */
+    public boolean add(Edge e) {
+        if (contains(e))
+            return false;
+        else {
+            bits.set(getOwner().getId(e), true);
+            return true;
+        }
+    }
+
+    /**
+     * Delete edge v from set.
+     *
+     * @param e Edge
+     */
+    public boolean remove(Object e) {
+        if (contains(e)) {
+            bits.set(getOwner().getId((Edge) e), false);
+            return true;
+        } else
+            return false;
+
+    }
+
+    /**
+     * adds all edges in the given collection
+     *
+     * @param collection
+     * @return true, if some element is new
+     */
+    public boolean addAll(Collection collection) {
+        Iterator it = collection.iterator();
+
+        boolean result = false;
+        while (it.hasNext()) {
+            if (add((Edge) it.next()))
+                result = true;
+        }
+        return result;
+    }
+
+    /**
+     * returns true if all elements of collection are contained in this set
+     *
+     * @param collection
+     * @return all contained?
+     */
+    public boolean containsAll(Collection collection) {
+
+        for (Object aCollection : collection) {
+            if (!contains(aCollection))
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * removes all edges in the collection
+     *
+     * @param collection
+     * @return true, if something actually removed
+     */
+    public boolean removeAll(Collection collection) {
+        Iterator it = collection.iterator();
+
+        boolean result = false;
+        while (it.hasNext()) {
+            if (remove(it.next()))
+                result = true;
+        }
+        return result;
+    }
+
+    /**
+     * keep only those elements contained in the collection
+     *
+     * @param collection
+     * @return true, if set changes
+     */
+    public boolean retainAll(Collection collection) {
+        boolean changed = (collection.size() != size() || !containsAll(collection));
+        EdgeSet was = (EdgeSet) this.clone();
+
+        clear();
+        for (Object e : collection) {
+            if (e instanceof Edge) {
+                if (was.contains(e))
+                    add((Edge) e);
+            }
+        }
+        return changed;
+    }
+
+    /**
+     * Delete all edges from set.
+     */
+    public void clear() {
+        bits.clear();
+    }
+
+    /**
+     * is empty?
+     *
+     * @return true, if empty
+     */
+    public boolean isEmpty() {
+        return bits.isEmpty();
+    }
+
+    /**
+     * return all contained edges as edges
+     *
+     * @return contained edges
+     */
+    public Edge[] toArray() {
+        Edge[] result = new Edge[bits.cardinality()];
+        int i = 0;
+        Iterator<Edge> it = getOwner().edgeIterator();
+        while (it.hasNext()) {
+            Edge e = it.next();
+            if (contains(e))
+                result[i++] = e;
+        }
+        return result;
+    }
+
+
+    public <T> T[] toArray(T[] ts) {
+        int i = 0;
+        Iterator<Edge> it = getOwner().edgeIterator();
+        while (it.hasNext()) {
+            Edge e = it.next();
+            if (contains(e))
+                ts[i++] = (T) e;
+        }
+        return ts;
+    }
+
+    /**
+     * Puts all edges into set.
+     */
+    public void addAll() {
+        Iterator<Edge> it = getOwner().edgeIterator();
+        while (it.hasNext())
+            add(it.next());
+    }
+
+    /**
+     * Returns the size of the set.
+     *
+     * @return size
+     */
+    public int size() {
+        return bits.cardinality();
+    }
+
+    /**
+     * Returns an enumeration of the elements in the set.
+     *
+     * @return an enumeration of the elements in the set
+     */
+    public Iterator<Edge> iterator() {
+        return new IteratorAdapter<Edge>() {
+            private Edge e = getFirstElement();
+
+            protected Edge findNext() throws NoSuchElementException {
+                if (e != null) {
+                    final Edge result = e;
+                    e = getNextElement(e);
+                    return result;
+                } else {
+                    throw new NoSuchElementException("at end");
+                }
+            }
+        };
+    }
+
+    /**
+     * returns the edges in the given array, if they fit, or in a new array, otherwise
+     *
+     * @param edges
+     * @return edges in this set
+     */
+    public Edge[] toArray(Edge[] edges) {
+        return toArray();
+        /*
+        if (edges == null)
+            throw new NullPointerException();
+        if (bits.cardinality() > edges.length)
+            edges = (Edge[]) Array.newInstance((edges[0]).getClass(), bits.cardinality());
+
+        int i = 0;
+        Iterator it = getOwner().edgeIterator();
+        while (it.hasNext()) {
+            Edge v = it.next();
+            if (contains(v) == true)
+                edges[i++] = it.next();
+        }
+        return edges;
+        */
+    }
+
+    /**
+     * Returns the first element in the set.
+     *
+     * @return v Edge
+     */
+    public Edge getFirstElement() {
+        Edge e;
+        for (e = getOwner().getFirstEdge(); e != null; e = getOwner().getNextEdge(e))
+            if (contains(e))
+                break;
+        return e;
+    }
+
+    /**
+     * Gets the successor element in the set.
+     *
+     * @param v Edge
+     * @return a Edge the successor of edge v
+     */
+    public Edge getNextElement(Edge v) {
+        for (v = getOwner().getNextEdge(v); v != null; v = getOwner().getNextEdge(v))
+            if (contains(v))
+                break;
+        return v;
+    }
+
+    /**
+     * Gets the predecessor element in the set.
+     *
+     * @param v Edge
+     * @return a Edge the predecessor of edge v
+     */
+    public Edge getPrevElement(Edge v) {
+        for (v = getOwner().getPrevEdge(v); v != null; v = getOwner().getPrevEdge(v))
+            if (contains(v))
+                break;
+        return v;
+    }
+
+
+    /**
+     * Returns the last element in the set.
+     *
+     * @return the Edge the last element in the set
+     */
+    public Edge getLastElement() {
+        Edge v = null;
+        for (v = getOwner().getLastEdge(); v != null; v = getOwner().getPrevEdge(v))
+            if (contains(v))
+                break;
+        return v;
+    }
+
+    /**
+     * returns a clone of this set
+     *
+     * @return a clone
+     */
+    public Object clone() {
+        EdgeSet result = new EdgeSet(getOwner());
+        for (Edge edge : this) result.add(edge);
+        return result;
+    }
+
+    /**
+     * do the two sets have a non-empty intersection?
+     *
+     * @param aset
+     * @return true, if intersection is non-empty
+     */
+    public boolean intersects(EdgeSet aset) {
+        return bits.intersects(aset.bits);
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/FruchtermanReingoldLayout.java b/src/jloda/graph/FruchtermanReingoldLayout.java
new file mode 100644
index 0000000..ef73b8c
--- /dev/null
+++ b/src/jloda/graph/FruchtermanReingoldLayout.java
@@ -0,0 +1,212 @@
+/**
+ * FruchtermanReingoldLayout.java
+ *  Copyright (C) 2015 Mathieu Jacomy
+ * Original implementation in Gephi by Mathieu Jacomy
+ */
+package jloda.graph;
+
+import java.awt.geom.Point2D;
+import java.util.BitSet;
+import java.util.Stack;
+
+/**
+ * implements the Fruchterman-Reingold graph layout algorithm
+ * <p/>
+ * Original implementation in Gephi by Mathieu Jacomy
+ * adapted by Daniel Huson, 5.2013
+ */
+public class FruchtermanReingoldLayout {
+
+    private static final float SPEED_DIVISOR = 800;
+    private static final float AREA_MULTIPLICATOR = 10000;
+
+    //Properties
+    private float area;
+    private double gravity;
+    private double speed;
+
+    // data
+    private final Graph graph;
+    private final Node[] nodes;
+    private final int[][] edges;
+    private final float[][] coordinates;
+    private final float[][] forceDelta;
+
+    private final BitSet fixed;
+
+    /**
+     * constructor. Do not change graph after calling the constructor
+     *
+     * @param graph
+     * @param fixedNodes nodes not to be moved
+     */
+    public FruchtermanReingoldLayout(Graph graph, NodeSet fixedNodes) {
+        this.graph = graph;
+        nodes = graph.getNodes().toArray();
+        edges = new int[graph.getNumberOfEdges()][2];
+        coordinates = new float[nodes.length][2];
+        forceDelta = new float[nodes.length][2];
+        fixed = new BitSet();
+
+        initialize(fixedNodes);
+    }
+
+    /**
+     * initialize
+     */
+    private void initialize(NodeSet fixedNodes) {
+        NodeArray<Integer> node2id = new NodeArray<>(graph);
+        for (int v = 0; v < nodes.length; v++) {
+            node2id.set(nodes[v], v);
+            if (fixedNodes != null && fixedNodes.contains(nodes[v]))
+                fixed.set(v);
+        }
+        int eId = 0;
+        for (Edge e = graph.getFirstEdge(); e != null; e = e.getNext()) {
+            edges[eId][0] = node2id.get(e.getSource());
+            edges[eId][1] = node2id.get(e.getTarget());
+            eId++;
+        }
+
+        if (graph.getNumberOfNodes() > 0) {
+            NodeSet seen = new NodeSet(graph);
+            Stack<Node> stack = new Stack<>();
+            int count = 0;
+            for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+                if (!seen.contains(v)) {
+                    seen.add(v);
+                    stack.push(v);
+                    while (stack.size() > 0) {
+                        Node w = stack.pop();
+                        int id = node2id.get(w);
+                        coordinates[id][0] = (float) (100 * Math.sin(2 * Math.PI * count / nodes.length));
+                        coordinates[id][1] = (float) (100 * Math.cos(2 * Math.PI * count / nodes.length));
+                        count++;
+                        for (Edge e = w.getFirstAdjacentEdge(); e != null; e = w.getNextAdjacentEdge(e)) {
+                            Node u = e.getOpposite(w);
+                            if (!seen.contains(u)) {
+                                seen.add(u);
+                                stack.push(u);
+                            }
+                        }
+
+                    }
+                }
+            }
+        }
+
+        speed = 1;
+        area = 600;
+        gravity = 5;
+    }
+
+    /**
+     * apply the algorithm
+     *
+     * @param numberOfIterations
+     * @param result
+     */
+    public void apply(int numberOfIterations, NodeArray<Point2D> result) {
+
+        for (int i = 0; i < numberOfIterations; i++) {
+            speed = 100 * (1 - i / numberOfIterations); // linear cooling
+            iterate();
+        }
+
+        for (int v = 0; v < nodes.length; v++) {
+            result.set(nodes[v], new Point2D.Float(coordinates[v][0], coordinates[v][1]));
+
+        }
+    }
+
+    /**
+     * run one iteration of the algorithm
+     */
+    private void iterate() {
+
+        float maxDisplace = (float) (Math.sqrt(AREA_MULTIPLICATOR * area) / 10f);
+        float k = (float) Math.sqrt((AREA_MULTIPLICATOR * area) / (1f + nodes.length));
+
+        // repulsion
+        for (int v1 = 0; v1 < nodes.length; v1++) {
+            for (int v2 = 0; v2 < nodes.length; v2++) {
+                if (v1 != v2) {
+                    float xDist = coordinates[v1][0] - coordinates[v2][0];
+                    float yDist = coordinates[v1][1] - coordinates[v2][1];
+                    float dist = (float) Math.sqrt(xDist * xDist + yDist * yDist);
+                    if (dist > 0) {
+                        float repulsiveF = k * k / dist;
+                        forceDelta[v1][0] += xDist / dist * repulsiveF;
+                        forceDelta[v1][1] += yDist / dist * repulsiveF;
+                    }
+                }
+            }
+        }
+        // attraction
+        for (int[] edge : edges) {
+            int v1 = edge[0];
+            int v2 = edge[1];
+            float xDist = coordinates[v1][0] - coordinates[v2][0];
+            float yDist = coordinates[v1][1] - coordinates[v2][1];
+            float dist = (float) Math.sqrt(xDist * xDist + yDist * yDist);
+            if (dist > 0) {
+                float attractiveF = dist * dist / k;
+                forceDelta[v1][0] -= xDist / dist * attractiveF;
+                forceDelta[v1][1] -= yDist / dist * attractiveF;
+                forceDelta[v2][0] += xDist / dist * attractiveF;
+                forceDelta[v2][1] += yDist / dist * attractiveF;
+            }
+        }
+
+        // gravity
+        for (int v = 0; v < nodes.length; v++) {
+            float distSquared = (float) Math.sqrt(coordinates[v][0] * coordinates[v][0] + coordinates[v][1] * coordinates[v][1]);
+            float gravityF = 0.01f * k * (float) gravity * distSquared;
+            forceDelta[v][0] -= gravityF * coordinates[v][0] / distSquared;
+            forceDelta[v][1] -= gravityF * coordinates[v][1] / distSquared;
+        }
+
+        // speed
+        for (int v = 0; v < nodes.length; v++) {
+            forceDelta[v][0] *= speed / SPEED_DIVISOR;
+            forceDelta[v][1] *= speed / SPEED_DIVISOR;
+
+        }
+
+        // apply the forces:
+        for (int v = 0; v < nodes.length; v++) {
+            float xDist = forceDelta[v][0];
+            float yDist = forceDelta[v][1];
+            float dist = (float) Math.sqrt(xDist * xDist + yDist * yDist);
+            if (dist > 0 && !fixed.get(v)) {
+                float limitedDist = Math.min(maxDisplace * ((float) speed / SPEED_DIVISOR), dist);
+                coordinates[v][0] += xDist / dist * limitedDist;
+                coordinates[v][1] += yDist / dist * limitedDist;
+            }
+        }
+    }
+
+    public float getArea() {
+        return area;
+    }
+
+    public void setArea(float area) {
+        this.area = area;
+    }
+
+    public double getGravity() {
+        return gravity;
+    }
+
+    public void setGravity(double gravity) {
+        this.gravity = gravity;
+    }
+
+    public double getSpeed() {
+        return speed;
+    }
+
+    public void setSpeed(double speed) {
+        this.speed = speed;
+    }
+}
diff --git a/src/jloda/graph/Graph.java b/src/jloda/graph/Graph.java
new file mode 100644
index 0000000..547b828
--- /dev/null
+++ b/src/jloda/graph/Graph.java
@@ -0,0 +1,1735 @@
+/**
+ * Graph.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: Graph.java,v 1.51 2008-10-10 08:42:37 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+import jloda.util.Basic;
+import jloda.util.IteratorAdapter;
+import jloda.util.parse.NexusStreamParser;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.lang.ref.WeakReference;
+import java.util.*;
+
+/**
+ * A graph
+ * <p/>
+ * The nodes and edges are stored in several doubly-linked lists.
+ * The set of nodes in the graph is stored in a list
+ * The set of edges in the graph is stored in a list
+ * Around each node, the set of incident edges is stored in a list.
+ * Daniel Huson, 2002
+ * <p/>
+ */
+public class Graph extends GraphBase {
+    private Node firstNode;
+    private Node lastNode;
+    private int numberNodes;
+    private int numberOfNodesThatAreHidden;
+    private int idsNodes;
+
+    private Edge firstEdge;
+    protected Edge lastEdge;
+    private int numberEdges;
+    private int numberOfEdgesThatAreHidden;
+    private int idsEdges;
+
+    private boolean ignoreGraphHasChanged = false; // set this when we are deleting a whole graph
+
+    private final List<GraphUpdateListener> graphUpdateListeners = new LinkedList<>();  //List of listeners that are fired when the graph changes.
+    final EdgeSet specialEdges;
+
+    private final List<WeakReference<NodeSet>> nodeSets = new LinkedList<>();
+    // created node arrays are kept here. When an node is deleted, it's
+    // entry in all node arrays is set to null
+    private final List<WeakReference<NodeAssociation>> nodeAssociations = new LinkedList<>();
+
+    // created edge arrays are kept here. When an edge is deleted, it's
+    // entry in all edge arrays is set to null
+    private final List<WeakReference<EdgeAssociation>> edgeAssociations = new LinkedList<>();
+    // keep track of edge sets
+    private final List<WeakReference<EdgeSet>> edgeSets = new LinkedList<>();
+
+    /**
+     * Constructs a new empty graph.
+     */
+    public Graph() {
+        setOwner(this);
+        specialEdges = new EdgeSet(this);
+    }
+
+    /**
+     * Constructs a new node of the type used in the graph. This does not add the node to the graph
+     * structure
+     *
+     * @return Node a new node
+     */
+    public Node newNode() {
+        return newNode(null);
+    }
+
+    /**
+     * Constructs a new node and set its info to obj.  This does not add the node to the graph
+     * structure
+     *
+     * @param obj the info object
+     * @return Node a new node
+     */
+    public Node newNode(Object obj) {
+        return new Node(this, obj);
+    }
+
+    /**
+     * Adds a node to the graph. The information in the node is replaced with obj. The node
+     * is added to the end of the list of nodes.
+     *
+     * @param info the info object
+     * @param v    the new node
+     */
+    void registerNewNode(Object info, Node v) {
+        v.init(this, lastNode, null, ++idsNodes, info);
+        if (firstNode == null)
+            firstNode = v;
+        if (lastNode != null)
+            lastNode.next = v;
+        lastNode = v;
+        numberNodes++;
+    }
+
+    /**
+     * sets the hidden state of a node. Hidden nodes are not returned by node iterators
+     *
+     * @param v
+     * @param hide
+     * @return true, if hidden state changed
+     */
+    public boolean setHidden(Node v, boolean hide) {
+        if (hide) {
+            if (!v.isHidden()) {
+                v.setHidden(true);
+                numberOfNodesThatAreHidden++;
+                return true;
+            }
+        } else {
+            if (v.isHidden()) {
+                v.setHidden(false);
+                numberOfNodesThatAreHidden--;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * returns hidden state of a node
+     *
+     * @param v
+     * @return true, if hidden
+     */
+    public boolean isHidden(Node v) {
+        return v.isHidden();
+    }
+
+
+    /**
+     * sets the hidden state of a edge. Hidden edges are not returned by edge iterators
+     *
+     * @param e
+     * @param hide
+     * @return true, if hidden state changed
+     */
+    public boolean setHidden(Edge e, boolean hide) {
+        if (hide) {
+            if (!e.isHidden()) {
+                e.setHidden(true);
+                numberOfEdgesThatAreHidden++;
+                return true;
+            }
+        } else {
+            if (e.isHidden()) {
+                e.setHidden(false);
+                numberOfEdgesThatAreHidden--;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * returns hidden state of a edge
+     *
+     * @param e
+     * @return true, if hidden
+     */
+    public boolean isHidden(Edge e) {
+        return e.isHidden();
+    }
+
+    /**
+     * Constructs a new edge between nodes v and w. This edge is not added to the graph.
+     *
+     * @param v source node
+     * @param w target node
+     * @return a new edge between nodes v and w
+     */
+    public Edge newEdge(Node v, Node w) throws IllegalSelfEdgeException {
+        return new Edge(this, v, w);
+    }
+
+    /**
+     * Constructs a new edge between nodes v and w and sets its info to obj. This edge is not added to the graph.
+     *
+     * @param v   source node
+     * @param w   target node
+     * @param obj the info object
+     * @return a new edge between nodes v and w and sets its info to obj
+     */
+    public Edge newEdge(Node v, Node w, Object obj) throws IllegalSelfEdgeException {
+        return new Edge(this, v, w, obj);
+    }
+
+    /**
+     * Constructs a new edge between nodes v and w. The edge is inserted into the list of edges incident with
+     * v and the list of edges incident with w. The place it is inserted into these list for edges
+     * incident with v is determined by e_v and dir_v: if dir_v = Edge.AFTER then it is inserted after
+     * e_v in the list, otherwise it is inserted before e_v. Likewise for the list of edges incident with w.
+     * <p/>
+     * The info is set using the obj.
+     *
+     * @param v     source node
+     * @param e_v   reference edge incident to v
+     * @param w     target node
+     * @param e_w   reference edge incident to w
+     * @param dir_v before or after reference e_v
+     * @param dir_w before or after reference e_w
+     * @param obj   the info object
+     * @return a new edge
+     */
+    public Edge newEdge(Node v, Edge e_v, Node w, Edge e_w,
+                        int dir_v, int dir_w, Object obj) throws IllegalSelfEdgeException {
+        return new Edge(this, v, e_v, w, e_w, dir_v, dir_w, obj);
+    }
+
+    /**
+     * Adds a  edge to the graph. The edge is inserted into the list of edges incident with
+     * v and the list of edges incident with w. The place it is inserted into these list for edges
+     * incident with v is determined by e_v and dir_v: if dir_v = Edge.AFTER then it is inserted after
+     * e_v in the list, otherwise it is inserted before e_v. Likewise for the list of edges incident with w.
+     * <p/>
+     * The info is set using the obj.
+     *
+     * @param v     source
+     * @param e_v   reference source edge
+     * @param w     target
+     * @param e_w   reference target edge
+     * @param dir_v insert before/after source reference edge
+     * @param dir_w insert before/after target reference edge
+     * @param obj   info object
+     * @param e     the new edge
+     * @
+     */
+    void registerNewEdge(Node v, Edge e_v, Node w, Edge e_w, int dir_v, int dir_w, Object obj, Edge e) {
+        checkOwner(v);
+        checkOwner(w);
+        v.incrementOutDegree();
+        w.incrementInDegree();
+
+        e.init(this, ++idsEdges, v, e_v, dir_v, w, e_w, dir_w, obj);
+        if (firstEdge == null)
+            firstEdge = e;
+        if (lastEdge != null)
+            lastEdge.next = e;
+        lastEdge = e;
+        numberEdges++;
+    }
+
+    /**
+     * Removes edge e from the graph.
+     *
+     * @param e the edge
+     */
+    public void deleteEdge(Edge e) {
+        checkOwner(e);
+        // note: firstEdge and lastEdge are set in unregisterEdge
+        e.deleteEdge();
+    }
+
+    /**
+     * called from edge when being deleted
+     *
+     * @param e
+     */
+    void unregisterEdge(Edge e) {
+        checkOwner(e);
+        if (e.isHidden())
+            numberOfEdgesThatAreHidden--;
+        deleteEdgeFromArrays(e);
+        deleteEdgeFromSets(e);
+
+        getSource(e).decrementOutDegree();
+        getTarget(e).decrementInDegree();
+        if (firstEdge == e)
+            firstEdge = (Edge) e.next;
+        if (lastEdge == e)
+            lastEdge = (Edge) e.prev;
+        numberEdges--;
+        if (numberEdges == 0)
+            idsEdges = 0;
+    }
+
+    /**
+     * Removes node v from the graph.
+     *
+     * @param v the node
+     */
+    public void deleteNode(Node v) {
+        // note: firstNode and lastNode are set in unregisterNode
+        v.deleteNode();
+    }
+
+    /**
+     * called from node when being deleted
+     *
+     * @param v
+     */
+    void unregisterNode(Node v) {
+        checkOwner(v);
+        deleteNodeFromArrays(v);
+        deleteNodeFromSets(v);
+        if (v.isHidden())
+            numberOfNodesThatAreHidden--;
+
+        if (firstNode == v)
+            firstNode = (Node) v.next;
+        if (lastNode == v)
+            lastNode = (Node) v.prev;
+        numberNodes--;
+        if (numberNodes == 0)
+            idsNodes = 0;
+    }
+
+    /**
+     * Deletes all edges.
+     */
+    public void deleteAllEdges() {
+        while (firstEdge != null)
+            deleteEdge(firstEdge);
+    }
+
+    /**
+     * Deletes all nodes.
+     */
+    public void deleteAllNodes() {
+        ignoreGraphHasChanged = true;
+        while (firstNode != null) {
+            deleteNode(firstNode);
+        }
+        ignoreGraphHasChanged = false;
+    }
+
+    /**
+     * Clears the graph.
+     */
+    public void clear() {
+        deleteAllNodes();
+    }
+
+    /**
+     * Change the order of edges adjacent to a node.
+     *
+     * @param v        the node in question.
+     * @param newOrder the desired sequence of edges.
+     */
+    public void rearrangeAdjacentEdges(Node v, List<Edge> newOrder) {
+        checkOwner(v);
+        v.rearrangeAdjacentEdges(newOrder);
+    }
+
+    /**
+     * move the node to the front of the list of nodes
+     *
+     * @param v
+     */
+    public void moveToFront(Node v) {
+        if (v != null && v != firstNode) {
+            checkOwner(v);
+            if (v.prev != null)
+                v.prev.next = v.next;
+            if (v.next != null)
+                v.next.prev = v.prev;
+            v.prev = null;
+            Node w = firstNode;
+            firstNode = v;
+            v.next = w;
+            if (w != null)
+                w.prev = v;
+            fireGraphHasChanged();
+        }
+    }
+
+    /**
+     * move the node to the bacl of the list of nodes
+     *
+     * @param v
+     */
+    public void moveToBack(Node v) {
+        if (v != null && v != lastNode) {
+            checkOwner(v);
+            if (v.prev != null)
+                v.prev.next = v.next;
+            if (v.next != null)
+                v.next.prev = v.prev;
+            v.prev = null;
+            Node w = lastNode;
+            lastNode = v;
+            v.prev = w;
+            if (w != null)
+                w.next = v;
+            fireGraphHasChanged();
+        }
+    }
+
+    /**
+     * move the edge to the front of the list of edges
+     *
+     * @param e
+     */
+    public void moveToFront(Edge e) {
+        if (e != null && e != firstEdge) {
+            checkOwner(e);
+            if (e.prev != null)
+                e.prev.next = e.next;
+            if (e.next != null)
+                e.next.prev = e.prev;
+            e.prev = null;
+            Edge f = firstEdge;
+            firstEdge = e;
+            e.next = f;
+            if (f != null)
+                f.prev = e;
+            fireGraphHasChanged();
+        }
+    }
+
+    /**
+     * move the edge to the back of the list of edges
+     *
+     * @param e
+     */
+    public void moveToBack(Edge e) {
+        if (e != null && e != lastEdge) {
+            checkOwner(e);
+            if (e.prev != null)
+                e.prev.next = e.next;
+            if (e.next != null)
+                e.next.prev = e.prev;
+            e.prev = null;
+            Edge f = lastEdge;
+            lastEdge = f;
+            e.prev = f;
+            if (f != null)
+                f.next = e;
+            fireGraphHasChanged();
+        }
+    }
+
+    /**
+     * Returns the node opposite node v via edge e.
+     *
+     * @param v the node
+     * @param e the edge
+     * @return the opposite node
+     */
+    public Node getOpposite(Node v, Edge e) {
+        checkOwner(e);
+        return e.getOpposite(v);
+    }
+
+    /**
+     * Get the first adjacent edge to v.
+     *
+     * @param v the node
+     * @return the first adjacent edge
+     */
+    public Edge getFirstAdjacentEdge(Node v) {
+        checkOwner(v);
+        return v.getFirstAdjacentEdge();
+    }
+
+    /**
+     * Get the last adjacent edge to v.
+     *
+     * @param v the node
+     * @return the last adjacent edge
+     */
+    public Edge getLastAdjacentEdge(Node v) {
+        checkOwner(v);
+        return v.getLastAdjacentEdge();
+    }
+
+
+    /**
+     * Get the successor of e adjacent to v
+     *
+     * @param e the edge
+     * @param v the node
+     * @return the successor of edge adjacent to v
+     */
+    public Edge getNextAdjacentEdge(Edge e, Node v) {
+        checkOwner(v);
+        return v.getNextAdjacentEdge(e);
+    }
+
+    /**
+     * Get the predecessor of e adjacent to v
+     *
+     * @param e the edge
+     * @param v the node
+     * @return the predecessor of edge adjacent to v
+     */
+    public Edge getPrevAdjacentEdge(Edge e, Node v) {
+        checkOwner(v);
+        return v.getPrevAdjacentEdge(e);
+    }
+
+    /**
+     * Get the cyclic successor of e adjacent to v.
+     *
+     * @param e the edge
+     * @param v the node
+     * @return the cyclic successor of edge adjacent to v
+     */
+    public Edge getNextAdjacentEdgeCyclic(Edge e, Node v) {
+        checkOwner(v);
+        return v.getNextAdjacentEdgeCyclic(e);
+    }
+
+    /**
+     * Get the cyclic predecessor of e adjacent to v.
+     *
+     * @param e the edge
+     * @param v the node
+     * @return the cyclic predecessor of edge adjacent to v
+     */
+    public Edge getPrevAdjacentEdgeCyclic(Edge e, Node v) {
+        checkOwner(v);
+        return v.getPrevAdjacentEdgeCyclic(e);
+    }
+
+    /**
+     * Get the first edge in the graph.
+     *
+     * @return the first edge
+     */
+    public Edge getFirstEdge() {
+        if (firstEdge != null && firstEdge.isHidden()) {
+            return firstEdge.getNext();
+        }
+        return firstEdge;
+    }
+
+    /**
+     * Get the last edge in the graph.
+     *
+     * @return the last edge
+     */
+    public Edge getLastEdge() {
+        if (lastEdge != null && lastEdge.isHidden())
+            return lastEdge.getPrev();
+        return lastEdge;
+    }
+
+    /**
+     * Get the successor of edge e.
+     *
+     * @param e edge
+     * @return the successor edge
+     */
+    public Edge getNextEdge(Edge e) {
+        checkOwner(e);
+        return e.getNext();
+    }
+
+    /**
+     * Get the predecessor of edge e.
+     *
+     * @param e edge
+     * @return the predecessor edge
+     */
+    public Edge getPrevEdge(Edge e) {
+        checkOwner(e);
+        return e.getPrev();
+    }
+
+    /**
+     * Get an edge between the two nodes v and w, if it exists
+     *
+     * @param v source node
+     * @param w target node
+     * @return an edge between v and w
+     */
+    public Edge getCommonEdge(Node v, Node w) {
+        checkOwner(v);
+        return v.getCommonEdge(w);
+    }
+
+    /**
+     * Get the first node in the graph.
+     *
+     * @return the first node
+     */
+    public Node getFirstNode() {
+        if (firstNode != null && firstNode.isHidden())
+            return firstNode.getNext();
+        return firstNode;
+    }
+
+    /**
+     * Get the last node in the graph.
+     *
+     * @return the last node
+     */
+    public Node getLastNode() {
+        if (lastNode != null && lastNode.isHidden())
+            return lastNode.getPrev();
+        return lastNode;
+    }
+
+    /**
+     * Get the successor node of v
+     *
+     * @param v the node
+     * @return the successor node
+     */
+    public Node getNextNode(Node v) {
+        checkOwner(v);
+        return v.getNext();
+    }
+
+    /**
+     * Get the predecessor of v.
+     *
+     * @param v the node
+     * @return the predecessor node
+     */
+    public Node getPrevNode(Node v) {
+        checkOwner(v);
+        return v.getPrev();
+    }
+
+    /**
+     * Get the number of nodes.
+     *
+     * @return the number of nodes
+     */
+    public int getNumberOfNodes() {
+        return numberNodes - numberOfNodesThatAreHidden;
+    }
+
+    /**
+     * Get number of edges.
+     *
+     * @return the number of edges
+     */
+    public int getNumberOfEdges() {
+        return numberEdges - numberOfEdgesThatAreHidden;
+    }
+
+    /**
+     * Get the source node of e.
+     *
+     * @param e the edge
+     * @return the source of e
+     */
+    public Node getSource(Edge e) {
+        checkOwner(e);
+        return e.getSource();
+    }
+
+    /**
+     * Get the target node of e.
+     *
+     * @param e the edge
+     * @return the target of e
+     */
+    public Node getTarget(Edge e) {
+        checkOwner(e);
+        return e.getTarget();
+    }
+
+    /**
+     * Get the degree of node v.
+     *
+     * @return the degree
+     */
+    public int getDegree(Node v) {
+        checkOwner(v);
+        return v.getDegree();
+    }
+
+    /**
+     * Get the in-degree of node v.
+     *
+     * @return the in-degree
+     */
+    public int getInDegree(Node v) {
+        checkOwner(v);
+        return v.getInDegree();
+    }
+
+    /**
+     * Get the out-degree of node v.
+     *
+     * @return the out-degree
+     */
+    public int getOutDegree(Node v) {
+        checkOwner(v);
+        return v.getOutDegree();
+    }
+
+    /**
+     * Get an iterator over all edges
+     *
+     * @return edge iterator
+     */
+    public Iterator<Edge> edgeIterator() {
+        return new IteratorAdapter<Edge>() {
+            private Edge e = getFirstEdge();
+
+            protected Edge findNext() throws NoSuchElementException {
+                if (e != null) {
+                    final Edge result = e;
+                    e = getNextEdge(e);
+                    return result;
+                } else {
+                    throw new NoSuchElementException("at end");
+                }
+            }
+        };
+    }
+
+    /**
+     * Get an iterator over all edges, including hidden ones
+     *
+     * @return edge iterator
+     */
+    public Iterator<Edge> edgeIteratorIncludingHidden() {
+        return new IteratorAdapter<Edge>() {
+            private Edge e = firstEdge;
+
+            protected Edge findNext() throws NoSuchElementException {
+                if (e != null) {
+                    final Edge result = e;
+                    checkOwner(e);
+                    e = (Edge) result.next;
+                    return result;
+                } else {
+                    throw new NoSuchElementException("at end");
+                }
+            }
+        };
+    }
+
+    /**
+     * Get an iterator over all nodes
+     *
+     * @return node iterator
+     */
+    public Iterator<Node> nodeIterator() {
+        return new IteratorAdapter<Node>() {
+            private Node v = getFirstNode();
+
+            protected Node findNext() throws NoSuchElementException {
+                if (v != null) {
+                    final Node result = v;
+                    v = getNextNode(v);
+                    return result;
+                } else {
+                    throw new NoSuchElementException("at end");
+                }
+            }
+
+            public boolean hasNext() {
+                return v != null;
+            }
+        };
+    }
+
+    /**
+     * Get an iterator over all nodes
+     *
+     * @return node iterator
+     */
+    public Iterator<Node> nodeIteratorIncludingHidden() {
+        return new IteratorAdapter<Node>() {
+            private Node v = firstNode;
+
+            protected Node findNext() throws NoSuchElementException {
+                if (v != null) {
+                    final Node result = v;
+                    v = (Node) v.next;
+                    return result;
+                } else {
+                    throw new NoSuchElementException("at end");
+                }
+            }
+
+            public boolean hasNext() {
+                return v != null;
+            }
+        };
+    }
+
+    /**
+     * Get an iterator over all nodes adjacent to v.
+     *
+     * @param v node
+     * @return all nodes adjacent to v
+     */
+    public Iterator<Node> getAdjacentNodes(Node v) {
+        checkOwner(v);
+        return v.getAdjacentNodes();
+    }
+
+    /**
+     * Get an iterator over all edges adjacent to v.
+     *
+     * @param v node
+     * @return all edges adjacent to v
+     */
+    public Iterator<Edge> getAdjacentEdges(Node v) {
+        checkOwner(v);
+        return v.getAdjacentEdges();
+    }
+
+    /**
+     * Get an iterator over all all in-edges adjacent to v.
+     *
+     * @param v node
+     * @return all in-edges adjacent to v
+     */
+    public Iterator<Edge> getInEdges(Node v) {
+        checkOwner(v);
+        return v.getInEdges();
+    }
+
+    /**
+     * Get an iterator over all all out-edges adjacent to v.
+     *
+     * @param v node
+     * @return all out-edges adjacent to v
+     */
+    public Iterator<Edge> getOutEdges(Node v) {
+        checkOwner(v);
+        return v.getOutEdges();
+    }
+
+    /**
+     * Get the id of node v.
+     *
+     * @param v node
+     * @return the id
+     */
+    public int getId(Node v) {
+        checkOwner(v);
+        return v.getId();
+    }
+
+    /**
+     * Get the id of edge e.
+     *
+     * @param e edge
+     * @return the id
+     */
+    public int getId(Edge e) {
+        checkOwner(e);
+        return e.getId();
+    }
+
+    /**
+     * Get a string representation of the graph.
+     *
+     * @return the string
+     */
+    public String toString() {
+        StringBuilder buf = new StringBuilder("Graph:\n");
+        buf.append("Nodes: ").append(String.valueOf(getNumberOfNodes())).append("\n");
+
+        for (Node v = getFirstNode(); v != null; v = getNextNode(v))
+            buf.append(v.toString()).append("\n");
+        buf.append("Edges: ").append(String.valueOf(getNumberOfEdges())).append("\n");
+        for (Edge e = getFirstEdge(); e != null; e = getNextEdge(e))
+            buf.append(e.toString()).append("\n");
+
+        return buf.toString();
+    }
+
+    /**
+     * Get the info associated with node v.
+     *
+     * @param v node
+     * @return the info
+     */
+    public Object getInfo(Node v) {
+        checkOwner(v);
+        return v.getInfo();
+    }
+
+    /**
+     * set the info associated with node v.
+     *
+     * @param v   node
+     * @param obj the info object
+     */
+    public void setInfo(Node v, Object obj) {
+        checkOwner(v);
+        v.setInfo(obj);
+    }
+
+    /**
+     * Get the info associated with edge e.
+     *
+     * @param e edge
+     * @return the info
+     */
+    public Object getInfo(Edge e) {
+        checkOwner(e);
+        return e.getInfo();
+    }
+
+    /**
+     * set the info associated with edge e.
+     *
+     * @param e   edge
+     * @param obj the info object
+     */
+    public void setInfo(Edge e, Object obj) {
+        checkOwner(e);
+        e.setInfo(obj);
+    }
+
+    /**
+     * Get an edge directed from one given node to another, if it exists.
+     *
+     * @param v source node
+     * @param w target node
+     * @return edge from v tp w, if it exists, else null
+     */
+    public Edge findDirectedEdge(Node v, Node w) {
+        checkOwner(v);
+        return v.findDirectedEdge(w);
+    }
+
+    /**
+     * Adds a GraphUpdateListener
+     *
+     * @param graphUpdateListener the listener to be added
+     */
+    public void addGraphUpdateListener(GraphUpdateListener graphUpdateListener) {
+        graphUpdateListeners.add(graphUpdateListener);
+    }
+
+    /**
+     * Removes a GraphUpdateListener
+     *
+     * @param graphUpdateListener the listener to be removed
+     */
+    public void removeGraphUpdateListener
+    (GraphUpdateListener graphUpdateListener) {
+        graphUpdateListeners.remove(graphUpdateListener);
+    }
+
+    /* Fires the newNode event for all GraphUpdateListeners
+    *@param v the node
+    */
+
+    protected void fireNewNode(Node v) {
+        checkOwner(v);
+
+        for (GraphUpdateListener gul : graphUpdateListeners) {
+            gul.newNode(v);
+        }
+    }
+
+    /* Fires the deleteNode event for all GraphUpdateListeners
+    *@param v the node
+    */
+
+    protected void fireDeleteNode(Node v) {
+        checkOwner(v);
+
+        for (GraphUpdateListener gul : graphUpdateListeners) {
+            gul.deleteNode(v);
+        }
+    }
+
+    /* Fires the newEdge event for all GraphUpdateListeners
+    *@param e the edge
+    */
+
+    protected void fireNewEdge(Edge e) {
+        checkOwner(e);
+
+        for (GraphUpdateListener gul : graphUpdateListeners) {
+            gul.newEdge(e);
+        }
+    }
+
+    /* Fires the deleteEdge event for all GraphUpdateListeners
+    *@param e the edge
+    */
+
+    protected void fireDeleteEdge(Edge e) {
+        checkOwner(e);
+
+        for (GraphUpdateListener gul : graphUpdateListeners) {
+            gul.deleteEdge(e);
+        }
+    }
+
+    /* Fires the graphHasChanged event for all GraphUpdateListeners
+    */
+
+    protected void fireGraphHasChanged() {
+        if (!ignoreGraphHasChanged) {
+
+            for (GraphUpdateListener gul : graphUpdateListeners) {
+                gul.graphHasChanged();
+            }
+        }
+    }
+
+    /*
+     * Fires the graphWasRead event for all GraphUpdateListeners
+    */
+
+    protected void fireGraphRead(NodeSet nodes, EdgeSet edges) {
+        checkOwner(nodes);
+        checkOwner(edges);
+        GraphUpdateListener[] a = graphUpdateListeners.toArray(new GraphUpdateListener[graphUpdateListeners.size()]);
+        for (GraphUpdateListener gul : a) {
+            gul.graphWasRead(nodes, edges);
+        }
+    }
+
+    /**
+     * copies one graph onto another
+     *
+     * @param src the source graph
+     */
+    public void copy(Graph src) {
+        copy(src, null, null);
+    }
+
+    /**
+     * Copies one graph onto another. Maintains the ids of nodes and edges
+     *
+     * @param src             the source graph
+     * @param oldNode2newNode if not null, returns map: old node id onto new node id
+     * @param oldEdge2newEdge if not null, returns map: old edge id onto new edge id
+     */
+    public void copy(Graph src, NodeAssociation<Node> oldNode2newNode, EdgeAssociation<Edge> oldEdge2newEdge) {
+        clear();
+
+        if (oldNode2newNode == null)
+            oldNode2newNode = new NodeArray<>(src);
+        if (oldEdge2newEdge == null)
+            oldEdge2newEdge = new EdgeArray<>(src);
+
+        for (Node v = src.getFirstNode(); v != null; v = src.getNextNode(v)) {
+            Node w = newNode();
+            w.setId(v.getId());
+            setInfo(w, src.getInfo(v));
+            oldNode2newNode.set(v, w);
+        }
+        idsNodes = src.idsNodes;
+
+        for (Edge e = src.getFirstEdge(); e != null; e = src.getNextEdge(e)) {
+            Node p = oldNode2newNode.get(src.getSource(e));
+            Node q = oldNode2newNode.get(src.getTarget(e));
+            Edge f = null;
+            try {
+                f = newEdge(p, q);
+                f.setId(e.getId());
+            } catch (IllegalSelfEdgeException e1) {
+                Basic.caught(e1);
+            }
+            if (src.isSpecial(e))
+                setSpecial(f, true);
+            setInfo(f, src.getInfo(e));
+            oldEdge2newEdge.set(e, f);
+        }
+        idsEdges = src.idsEdges;
+
+        // change all adjacencies to reflect order in old graph:
+        for (Node v = src.getFirstNode(); v != null; v = src.getNextNode(v)) {
+            Node w = oldNode2newNode.get(v);
+            List<Edge> newOrder = new LinkedList<>();
+            for (Edge e = v.getFirstAdjacentEdge(); e != null; e = v.getNextAdjacentEdge(e)) {
+                newOrder.add(oldEdge2newEdge.get(e));
+            }
+            w.rearrangeAdjacentEdges(newOrder);
+        }
+    }
+
+    /**
+     * produces a clone of this graph
+     *
+     * @return a clone of this graph
+     */
+    public Object clone() {
+        Graph result = new Graph();
+        result.copy(this);
+        return result;
+    }
+
+    /**
+     * determines whether two nodes are adjacent
+     *
+     * @param a
+     * @param b
+     * @return true, if adjacent
+     */
+    public boolean areAdjacent(Node a, Node b) {
+        checkOwner(a);
+        return a.isAdjacent(b);
+    }
+
+    /**
+     * called from constructor of NodeAssociation to register with graph
+     *
+     * @param array
+     */
+    void registerNodeAssociation(NodeAssociation array) {
+        nodeAssociations.add(new WeakReference<>(array));
+    }
+
+    /**
+     * called from deleteNode to clean all array entries for the node
+     *
+     * @param v
+     */
+    void deleteNodeFromArrays(Node v) {
+        checkOwner(v);
+        List<WeakReference> toDelete = new LinkedList<>();
+        for (WeakReference<NodeAssociation> ref : nodeAssociations) {
+            NodeAssociation<?> as = ref.get();
+            if (as == null)
+                toDelete.add(ref); // reference is dead
+            else {
+                as.set(v, null);
+            }
+        }
+        for (WeakReference ref : toDelete) {
+            nodeAssociations.remove(ref);
+        }
+    }
+
+    /**
+     * called from constructor of NodeSet to register with graph
+     *
+     * @param set
+     */
+    void registerNodeSet(NodeSet set) {
+        nodeSets.add(new WeakReference<>(set));
+    }
+
+    /**
+     * called from deleteNode to clean all array entries for the node
+     *
+     * @param v
+     */
+    void deleteNodeFromSets(Node v) {
+        checkOwner(v);
+        List<WeakReference> toDelete = new LinkedList<>();
+        for (WeakReference<NodeSet> ref : nodeSets) {
+            NodeSet set = ref.get();
+            if (set == null)
+                toDelete.add(ref); // reference is dead
+            else {
+                set.remove(v);
+            }
+        }
+        for (WeakReference ref : toDelete) {
+            nodeSets.remove(ref);
+        }
+    }
+
+    /**
+     * called from constructor of EdgeAssociation to register with graph
+     *
+     * @param array
+     */
+    void registerEdgeAssociation(EdgeAssociation array) {
+        edgeAssociations.add(new WeakReference<>(array));
+    }
+
+    /**
+     * called from deleteEdge to clean all array entries for the edge
+     *
+     * @param edge
+     */
+    void deleteEdgeFromArrays(Edge edge) {
+        checkOwner(edge);
+        List<WeakReference> toDelete = new LinkedList<>();
+        for (WeakReference<EdgeAssociation> ref : edgeAssociations) {
+            EdgeAssociation<?> as = ref.get();
+            if (as == null)
+                toDelete.add(ref); // reference is dead
+            else {
+                as.set(edge, null);
+            }
+        }
+        for (WeakReference ref : toDelete) {
+            edgeAssociations.remove(ref);
+        }
+    }
+
+    /**
+     * called from constructor of EdgeSet to register with graph
+     *
+     * @param set
+     */
+    void registerEdgeSet(EdgeSet set) {
+        edgeSets.add(new WeakReference<>(set));
+    }
+
+    /**
+     * called from deleteEdge to clean all array entries for the edge
+     *
+     * @param v
+     */
+    void deleteEdgeFromSets(Edge v) {
+        checkOwner(v);
+        List<WeakReference> toDelete = new LinkedList<>();
+        for (WeakReference<EdgeSet> ref : edgeSets) {
+            EdgeSet set = ref.get();
+            if (set == null)
+                toDelete.add(ref); // reference is dead
+            else {
+                set.remove(v);
+            }
+        }
+        for (WeakReference ref : toDelete) {
+            edgeSets.remove(ref);
+        }
+    }
+
+    /**
+     * gets the number of connected components of the graph
+     *
+     * @return connected components
+     */
+    public int getNumberConnectedComponents() {
+        int result = 0;
+        NodeSet used = new NodeSet(this);
+
+        for (Node v = getFirstNode(); v != null; v = v.getNext()) {
+            if (!used.contains(v)) {
+                visitConnectedComponent(v, used);
+                result++;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * visit all nodes in a connected component
+     *
+     * @param v
+     * @param used
+     */
+    public void visitConnectedComponent(Node v, NodeSet used) {
+        used.add(v);
+        for (Edge f = getFirstAdjacentEdge(v); f != null; f = v.getNextAdjacentEdge(f)) {
+            Node w = f.getOpposite(v);
+            if (!used.contains(w))
+                visitConnectedComponent(w, used);
+        }
+    }
+
+    /**
+     * gets the current maximal node id
+     *
+     * @return max node id
+     */
+    int getMaxNodeId() {
+        return idsNodes;
+    }
+
+    /**
+     * gets the current maximal edge id
+     *
+     * @return max edge id
+     */
+    int getMaxEdgeId() {
+        return idsEdges;
+    }
+
+    /**
+     * writes a graph in jloda format
+     *
+     * @param w
+     * @throws IOException
+     */
+    public void write(Writer w) throws IOException {
+        Map<Integer, Integer> nodeId2Count = new HashMap<>();
+        Map<Integer, Integer> edgeId2Count = new HashMap<>();
+        write(w, nodeId2Count, edgeId2Count);
+    }
+
+    /**
+     * writes a graph in jloda format.  Node-id to node-number and edge-id to edge-number maps are set.
+     *
+     * @param w
+     * @param nodeId2Number after write, maps node-ids to numbers 1..numberOfNodes
+     * @param edgeId2Number after write, maps edge-ids to numbers 1..numberOfEdge
+     * @throws IOException
+     */
+    public void write(Writer w, Map<Integer, Integer> nodeId2Number, Map<Integer, Integer> edgeId2Number) throws IOException {
+        w.write("{GRAPH\n");
+        w.write("nnodes=" + getNumberOfNodes() + " nedges=" + getNumberOfEdges() + "\n");
+        w.write("node.labels\n");
+        int count = 0;
+        for (Node v = getFirstNode(); v != null; v = v.getNext()) {
+            nodeId2Number.put(v.getId(), ++count);
+            Object info = v.getInfo();
+            if (info != null) {
+                w.write("" + count + ":'" + info.toString() + "'");
+                if (!(info instanceof String))
+                    w.write(" [" + info.getClass().getName() + "]");
+                w.write("\n");
+            }
+        }
+        w.write("edges\n");
+        count = 0;
+        for (Edge e = getFirstEdge(); e != null; e = e.getNext()) {
+            edgeId2Number.put(e.getId(), ++count);
+            w.write("" + count + ":" + nodeId2Number.get(e.getSource().getId()) + " " +
+                    nodeId2Number.get(e.getTarget().getId()));
+            if (isSpecial(e))
+                w.write(" s");
+            w.write("\n");
+        }
+        w.write("edge.labels\n");
+        for (Edge e = getFirstEdge(); e != null; e = e.getNext()) {
+            Object info = e.getInfo();
+            if (info != null) {
+                w.write("" + edgeId2Number.get(e.getId()) + ":'" + info.toString() + "'");
+                if (!(info instanceof String))
+                    w.write(" [" + info.getClass().getName() + "]");
+                w.write("\n");
+            }
+        }
+        w.write("}\n");
+    }
+
+    /**
+     * reads a graph in jloda format
+     *
+     * @param r
+     * @throws IOException
+     */
+    public void read(Reader r) throws IOException {
+        read(r, new Num2NodeArray(), new Num2EdgeArray());
+    }
+
+    /**
+     * reads a graph in jloda format. Sets node-num to node map and edge-num edge map
+     *
+     * @param r
+     * @param num2node after read, contains mapping of numbers used in file to nodes created in Graph
+     * @param num2edge after read, contains mapping of numbers used in file to edges created in Graph
+     * @throws IOException
+     */
+    public void read(Reader r, Num2NodeArray num2node, Num2EdgeArray num2edge) throws IOException {
+        clear();
+
+        NexusStreamParser np = new NexusStreamParser(r);
+        np.matchRespectCase("{GRAPH\n");
+        np.matchRespectCase("nnodes=");
+        int nNodes = np.getInt(0, 10000000);
+
+        num2node.set(new Node[nNodes + 1]);
+        for (int i = 1; i <= nNodes; i++) {
+            num2node.put(i, newNode());
+        }
+
+        np.matchRespectCase("nedges=");
+        int nEdges = np.getInt(0, 10000000);
+        num2edge.set(new Edge[nEdges + 1]);
+
+        if (np.peekMatchRespectCase("node.labels"))
+            np.matchRespectCase("node.labels");
+
+        while (!np.peekMatchRespectCase("edges")) {
+            int vid = np.getInt(1, nNodes);
+            np.matchRespectCase(":");
+            num2node.get(vid).setInfo(np.getLabelRespectCase());
+        }
+
+        np.matchRespectCase("edges\n");
+        for (int i = 1; i <= nEdges; i++) {
+            int eid = np.getInt(1, nEdges);
+            np.matchRespectCase(":");
+            Node source = num2node.get(np.getInt(1, nNodes));
+            Node target = num2node.get(np.getInt(1, nNodes));
+            Edge e = newEdge(source, target);
+            num2edge.put(eid, e);
+            if (np.peekMatchIgnoreCase("s")) {
+                np.matchIgnoreCase("s");
+                setSpecial(e, true);
+            }
+        }
+
+        if (np.peekMatchRespectCase("edge.labels")) {
+            np.matchRespectCase("edge.labels\n");
+            while (!np.peekMatchRespectCase("}")) {
+                int eid = np.getInt(1, nEdges);
+                np.matchRespectCase(":");
+                num2edge.get(eid).setInfo(np.getLabelRespectCase());
+            }
+        }
+        np.matchRespectCase("}");
+    }
+
+    /**
+     * is this a special edge?
+     *
+     * @param e
+     * @return true, if marked as special
+     */
+    public boolean isSpecial(Edge e) {
+        return specialEdges.size() > 0 && specialEdges.contains(e);
+    }
+
+    /**
+     * mark as special or not
+     *
+     * @param e
+     * @param special
+     */
+    public void setSpecial(Edge e, boolean special) {
+        if (special && !specialEdges.contains(e))
+            specialEdges.add(e);
+        else if (!special && specialEdges.contains(e))
+            specialEdges.remove(e);
+    }
+
+    /**
+     * gets the number of special edges
+     *
+     * @return number of special edges
+     */
+    public int getNumberSpecialEdges() {
+        return specialEdges.size();
+    }
+
+    /**
+     * gets the set of special edges
+     *
+     * @return special edges
+     */
+    public EdgeSet getSpecialEdges() {
+        return specialEdges;
+    }
+
+    /**
+     * get the non-special-edge connected component containing v
+     *
+     * @param v
+     * @return component
+     */
+    public NodeSet getSpecialComponent(Node v) {
+        NodeSet nodes = new NodeSet(this);
+        List<Node> queue = new LinkedList<>();
+        queue.add(v);
+        nodes.add(v);
+        while (queue.size() > 0) {
+            v = queue.remove(0);
+            for (Edge e = v.getFirstAdjacentEdge(); e != null; e = v.getNextAdjacentEdge(e)) {
+                if (!isSpecial(e)) {
+                    Node w = e.getOpposite(v);
+                    if (!nodes.contains(w)) {
+                        queue.add(w);
+                        nodes.add(w);
+                    }
+                }
+            }
+        }
+        return nodes;
+    }
+
+    /**
+     * erase all data components
+     */
+    public void clearData() {
+        for (Node v = getFirstNode(); v != null; v = v.getNext()) {
+            v.setData(null);
+        }
+    }
+
+    /*public NodeSet computeSetOfLeaves(Node v) {
+        NodeSet sons = new NodeSet(this);
+        getLeavesRec(v,sons);
+        return sons;
+   }
+    public void getLeavesRec(Node v, NodeSet nodes) {
+        for (Edge f = v.getFirstOutEdge(); f != null; f = v.getNextOutEdge(f)) {
+           Node w = f.getTarget();
+           getLeavesRec(w,sons); 
+       }
+                  if (v.getOutDegree() == 0)
+           nodes.add(w);
+        return sons;
+   }*/
+
+    /**
+     * gets all nodes
+     *
+     * @return node set of nodes
+     */
+    public NodeSet getNodes() {
+        NodeSet nodes = new NodeSet(this);
+        for (Node v = getFirstNode(); v != null; v = getNextNode(v))
+            nodes.add(v);
+        return nodes;
+    }
+
+    /**
+     * gets all edges
+     *
+     * @return edge set of edges
+     */
+    public EdgeSet getEdges() {
+        EdgeSet edges = new EdgeSet(this);
+        for (Edge v = getFirstEdge(); v != null; v = getNextEdge(v))
+            edges.add(v);
+        return edges;
+    }
+
+
+    /**
+     * get the unhidden subset
+     *
+     * @param nodes
+     * @return
+     */
+    public NodeSet getUnhiddenSubset(Collection<Node> nodes) {
+        NodeSet unhidden = new NodeSet(this);
+        for (Node v : nodes) {
+            if (!v.isHidden())
+                unhidden.add(v);
+        }
+        return unhidden;
+    }
+
+    /**
+     * reorders nodes in graph. These nodes are put at the front of the list of nodes
+     *
+     * @param nodes
+     */
+    public void reorderNodes(List<Node> nodes) {
+        final List<Node> newOrder = new ArrayList<>(numberNodes + numberOfNodesThatAreHidden);
+        final Set<Node> toMove = new HashSet<>();
+        for (Node v : nodes) {
+            if (v.getOwner() != null) {
+                newOrder.add(v);
+                toMove.add(v);
+            }
+        }
+
+        if (toMove.size() > 0) {
+            for (Iterator<Node> it = nodeIteratorIncludingHidden(); it.hasNext(); ) {
+                Node v = it.next();
+                if (!toMove.contains(v))
+                    newOrder.add(v);
+            }
+
+            Node previousNode = null;
+            for (Node v : newOrder) {
+                if (previousNode == null) {
+                    firstNode = v;
+                    v.prev = null;
+                } else {
+                    previousNode.next = v;
+                    v.prev = previousNode;
+                }
+                previousNode = v;
+            }
+            if (previousNode != null) {
+                previousNode.next = null;
+            }
+            lastNode = previousNode;
+        }
+    }
+
+    /**
+     * write a graph in GML
+     *
+     * @param w
+     * @param comment
+     * @param directed
+     * @param label
+     * @param graphId
+     * @throws IOException
+     */
+    public void writeGML(Writer w, String comment, boolean directed, String label, int graphId) throws IOException {
+        writeGML(w, comment, directed, label, graphId, null, null);
+    }
+
+
+    /**
+     * write a graph in GML
+     *
+     * @param w
+     * @param comment
+     * @param directed
+     * @param label
+     * @param graphId
+     * @throws IOException
+     */
+    public void writeGML(Writer w, String comment, boolean directed, String label, int graphId, Map<String, NodeArray<?>> label2nodes, Map<String, EdgeArray<?>> label2edges) throws IOException {
+        w.write("graph [\n");
+        if (comment != null)
+            w.write("\tcomment \"" + comment + "\"\n");
+        w.write("\tdirected " + (directed ? 1 : 0) + "\n");
+        w.write("\tid " + graphId + "\n");
+        if (label != null)
+            w.write("\tlabel \"" + label + "\"\n");
+        boolean hasNodeLabels=(label2nodes != null && label2nodes.keySet().contains("label"));
+        for (Node v = getFirstNode(); v != null; v = getNextNode(v)) {
+            w.write("\tnode [\n");
+            w.write("\t\tid " + v.getId() + "\n");
+            if(label2nodes!=null) {
+                for (String aLabel : label2nodes.keySet()) {
+                    NodeArray<?> array = label2nodes.get(aLabel);
+                    if (array != null) {
+                        Object obj = array.get(v);
+                        w.write("\t\t" + aLabel + " \"" + (obj != null ? obj.toString() : "null") + "\"\n");
+                    }
+                }
+            }
+            if (!hasNodeLabels)
+                w.write("\t\tlabel \"" + (v.getInfo() != null ? v.getInfo().toString() : "null") + "\"\n");
+
+            w.write("\t]\n");
+        }
+        boolean hasEdgeLabels=(label2edges != null && label2edges.keySet().contains("label"));
+
+        for (Edge e = getFirstEdge(); e != null; e = getNextEdge(e)) {
+            w.write("\tedge [\n");
+            w.write("\t\tsource " + e.getSource().getId() + "\n");
+            w.write("\t\ttarget " + e.getTarget().getId() + "\n");
+
+            if(label2edges!=null) {
+                for (String aLabel : label2edges.keySet()) {
+                    EdgeArray<?> array = label2edges.get(aLabel);
+                    if (array != null) {
+                        Object obj = array.get(e);
+                        w.write("\t\t" + aLabel + " \"" + (obj != null ? obj.toString() : "null") + "\"\n");
+                    }
+                }
+            }
+            if (!hasEdgeLabels)
+                w.write("\t\tlabel \"" + (e.getInfo() != null ? e.getInfo().toString() : "null") + "\"\n");
+
+            w.write("\t]\n");
+        }
+        w.write("]\n");
+        w.flush();
+    }
+
+    /**
+     * read a graph in GML for that was previously saved using writeGML. This is not a general parser.
+     *
+     * @param r
+     */
+    public void readGML(Reader r) throws IOException {
+        final NexusStreamParser np = new NexusStreamParser(r);
+        np.setSquareBracketsSurroundComments(false);
+
+        clear();
+
+        np.matchIgnoreCase("graph [");
+        if (np.peekMatchIgnoreCase("comment")) {
+            np.matchIgnoreCase("comment");
+            System.err.println("Comment: " + getQuotedString(np));
+        }
+        np.matchIgnoreCase("directed");
+        System.err.println("directed: " + (np.getInt(0, 1) == 1));
+        np.matchIgnoreCase("id");
+        System.err.println("id: " + np.getInt());
+        if (np.peekMatchIgnoreCase("label")) {
+            np.matchIgnoreCase("label");
+            System.err.println("Label: " + getQuotedString(np));
+        }
+
+
+        Map<Integer, Node> id2node = new HashMap<>();
+        while (np.peekMatchIgnoreCase("node")) {
+            np.matchIgnoreCase("node [");
+            np.matchIgnoreCase("id");
+            int id = np.getInt();
+            Node v = newNode();
+            id2node.put(id, v);
+            if (np.peekMatchIgnoreCase("label")) {
+                np.matchIgnoreCase("label");
+                v.setInfo(getQuotedString(np));
+            }
+            np.matchIgnoreCase("]");
+        }
+        while (np.peekMatchIgnoreCase("edge")) {
+            np.matchIgnoreCase("edge [");
+            np.matchIgnoreCase("source");
+            int sourceId = np.getInt();
+            np.matchIgnoreCase("target");
+            int targetId = np.getInt();
+            if (!id2node.keySet().contains(sourceId))
+                throw new IOException("Undefined node id: " + sourceId);
+            if (!id2node.keySet().contains(targetId))
+                throw new IOException("Undefined node id: " + targetId);
+            Edge e = newEdge(id2node.get(sourceId), id2node.get(targetId));
+            if (np.peekMatchIgnoreCase("label")) {
+                np.matchIgnoreCase("label");
+                e.setInfo(getQuotedString(np));
+            }
+            np.matchIgnoreCase("]");
+        }
+        np.matchIgnoreCase("]");
+    }
+
+    public static String getQuotedString(NexusStreamParser np) throws IOException {
+        np.matchIgnoreCase("\"");
+        ArrayList<String> words = new ArrayList<>();
+        while (!np.peekMatchIgnoreCase("\""))
+            words.add(np.getWordRespectCase());
+        return Basic.toString(words, " ");
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/GraphBase.java b/src/jloda/graph/GraphBase.java
new file mode 100644
index 0000000..34fa40c
--- /dev/null
+++ b/src/jloda/graph/GraphBase.java
@@ -0,0 +1,74 @@
+/**
+ * GraphBase.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: GraphBase.java,v 1.6 2005-01-30 13:00:39 huson Exp $
+ *
+ * Base class for all graph related stuff.
+ *
+ * @author Daniel Huson
+ */
+package jloda.graph;
+
+import jloda.util.NotOwnerException;
+
+/**
+ * graph base class
+ * Daniel Huson, 2002
+ */
+public class GraphBase {
+    private Graph owner;
+
+    /**
+     * Sets the owner.
+     *
+     * @param G Graph
+     */
+    void setOwner(Graph G) {
+        owner = G;
+    }
+
+    /**
+     * Returns the owning graph.
+     *
+     * @return owner a Graph
+     */
+    public Graph getOwner() {
+        return owner;
+    }
+
+    /**
+     * If this and obj do not have the same graph, throw NotOwnerException
+     *
+     * @param obj GraphBase
+     */
+    public void checkOwner(GraphBase obj) {
+        if (obj == null)
+            throw new NotOwnerException("object is null");
+        if (obj.owner == null)
+            throw new NotOwnerException("object's owner is null");
+        if (owner == null)
+            throw new NotOwnerException("reference's owner is null");
+        if (owner != obj.owner) {
+            throw new NotOwnerException("wrong owner");
+        }
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/GraphUpdateAdapter.java b/src/jloda/graph/GraphUpdateAdapter.java
new file mode 100644
index 0000000..26f2547
--- /dev/null
+++ b/src/jloda/graph/GraphUpdateAdapter.java
@@ -0,0 +1,69 @@
+/**
+ * GraphUpdateAdapter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ *@version $Id: GraphUpdateAdapter.java,v 1.4 2005-01-07 14:23:05 huson Exp $
+ *
+ *@author Daniel Huson
+ * 11.02
+ */
+package jloda.graph;
+
+/** Extend this to get a GraphUpdateListener
+ * Daniel Huson, 2003
+ */
+public class GraphUpdateAdapter implements GraphUpdateListener {
+    /** A node has been created
+     *@param v the new node
+     */
+    public void newNode(Node v) {
+    }
+
+    /** A node is about to be deleted
+     *@param v the node that will be deleted
+     */
+    public void deleteNode(Node v) {
+    }
+
+    /** An edge has been created
+     *@param e the new edge
+     */
+    public void newEdge(Edge e) {
+    }
+
+    /** An edge is about to be deleted
+     *@param e the edge that will be deleted
+     */
+    public void deleteEdge(Edge e) {
+    }
+
+    /** The graph has changed.
+     * This method is called after one of the above specific methods has be
+     * called
+     */
+    public void graphHasChanged() {
+    }
+
+    /** (Partial) graph was read from Reader
+     *@param nodes the new nodes
+     *@param edges the new edges
+     */
+    public void graphWasRead(NodeSet nodes, EdgeSet edges) {
+    }
+}
diff --git a/src/jloda/graph/GraphUpdateListener.java b/src/jloda/graph/GraphUpdateListener.java
new file mode 100644
index 0000000..69e3317
--- /dev/null
+++ b/src/jloda/graph/GraphUpdateListener.java
@@ -0,0 +1,64 @@
+/**
+ * GraphUpdateListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ *@version $Id: GraphUpdateListener.java,v 1.4 2005-01-07 14:23:05 huson Exp $
+ *
+ *@author Daniel Huson
+ * 11.02
+ */
+package jloda.graph;
+
+/**
+ * graph update listener
+ * Daniel Huson, 2003
+ */
+public interface GraphUpdateListener {
+    /** A node has been created
+     *@param v the new node
+     */
+    void newNode(Node v);
+
+    /** A node is about to be deleted
+     *@param v the node that will be deleted
+     */
+    void deleteNode(Node v);
+
+    /** An edge has been created
+     *@param e the new edge
+     */
+    void newEdge(Edge e);
+
+    /** An edge is about to be deleted
+     *@param e the edge that will be deleted
+     */
+    void deleteEdge(Edge e);
+
+    /** The graph has changed.
+     * This method is called after one of the above specific methods has be
+     * called
+     */
+    void graphHasChanged();
+
+    /** (Partial) graph was read from Reader
+     *@param nodes the new nodes
+     *@param edges the new edges
+     */
+    void graphWasRead(NodeSet nodes, EdgeSet edges);
+}
diff --git a/src/jloda/graph/IllegalSelfEdgeException.java b/src/jloda/graph/IllegalSelfEdgeException.java
new file mode 100644
index 0000000..e7a10ed
--- /dev/null
+++ b/src/jloda/graph/IllegalSelfEdgeException.java
@@ -0,0 +1,27 @@
+/**
+ * IllegalSelfEdgeException.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graph;
+
+/**
+ * we don't support selfedges
+ * Daniel Huson
+ */
+public class IllegalSelfEdgeException extends RuntimeException {
+}
diff --git a/src/jloda/graph/MaxClique.java b/src/jloda/graph/MaxClique.java
new file mode 100644
index 0000000..9508e91
--- /dev/null
+++ b/src/jloda/graph/MaxClique.java
@@ -0,0 +1,335 @@
+/**
+ * MaxClique.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graph;
+
+
+import jloda.util.Basic;
+import jloda.util.NotOwnerException;
+
+import java.util.BitSet;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * slow exact max clique search and fast d-degree heuristic
+ *
+ * @author huson
+ *         Date: 10-Aug-2004
+ */
+public class MaxClique {
+    /**
+     * computes a maximum size clique
+     *
+     * @param graph
+     * @return max clique
+     */
+    public static NodeSet compute(Graph graph) {
+        int numNodes = graph.getNumberOfNodes();
+        Node[] id2node = new Node[numNodes];
+        NodeIntegerArray node2id = new NodeIntegerArray(graph);
+
+        int[][] matrix = convertGraphToMatrix(graph, id2node, node2id);
+
+        // compute max clique for matrix:
+        BitSet maxClique = compute(matrix);
+
+        return convertNodeSet(graph, maxClique, id2node);
+
+    }
+
+    /**
+     * computes the induced subgraph in which each node has degree >=d
+     *
+     * @param graph
+     * @return induced subgraph in which each node has degree >=d
+     */
+    public static NodeSet computeDSubgraph(Graph graph, int d) {
+        int numNodes = graph.getNumberOfNodes();
+        Node[] id2node = new Node[numNodes];
+        NodeIntegerArray node2id = new NodeIntegerArray(graph);
+
+        int[][] matrix = convertGraphToMatrix(graph, id2node, node2id);
+
+        BitSet clique = computeDSubgraph(matrix, d);
+
+        return convertNodeSet(graph, clique, id2node);
+    }
+
+
+    /**
+     * computes max clique from adjacency matrix
+     *
+     * @param matrix
+     * @return max clique
+     */
+    public static BitSet compute(int[][] matrix) {
+        BitSet maxClique = new BitSet();
+        int numNodes = matrix.length;
+        /*
+        * compute max clique for adjaceny matrix
+        */
+        for (int vi = 0; vi < numNodes; vi++) {
+            BitSet possible = getAllAdjacentNodes(matrix, vi);
+            BitSet clique = new BitSet();
+            clique.set(vi);
+            recurse(matrix, possible, clique, maxClique);
+        }
+        return maxClique;
+    }
+
+    /**
+     * gets the set of all nodes adjacent to vi
+     *
+     * @param matrix
+     * @param vi
+     * @return all adjacent nodes
+     */
+    private static BitSet getAllAdjacentNodes(int[][] matrix, int vi) {
+        BitSet adjNodes = new BitSet();
+        for (int wi = 0; wi < matrix.length; wi++)
+            if (matrix[vi][wi] != 0 || matrix[wi][vi] != 0)
+                adjNodes.set(wi);
+        return adjNodes;
+    }
+
+    /**
+     * recursively try to extend clique
+     *
+     * @param possible
+     * @param clique
+     * @param maxClique
+     */
+    private static void recurse(int[][] matrix, BitSet possible, BitSet clique, BitSet maxClique) {
+        if (clique.cardinality() > maxClique.cardinality()) {
+            maxClique.clear();
+            maxClique.or(clique);
+        }
+        // this is the bound step in branch and bound:
+        if (!checkBoundCondition(matrix, clique, possible, maxClique.cardinality()))
+            return;
+
+        for (int p = possible.nextSetBit(0); p >= 0; p = possible.nextSetBit(p + 1)) {
+            possible.set(p, false);
+
+            // FIRST, consider the case inwhich we include p in the clique:
+            {
+                boolean ok = true;
+
+                for (int q = clique.nextSetBit(0); ok && q >= 0; q = clique.nextSetBit(q + 1))
+                    if (matrix[p][q] == 0)
+                        ok = false;
+                if (ok) {
+                    clique.set(p);
+                    // remove
+                    BitSet impossible = new BitSet();
+                    for (int wi = possible.nextSetBit(0); wi >= 0; wi = possible.nextSetBit(wi + 1)) {
+                        if (matrix[p][wi] == 0)
+                            impossible.set(wi);
+                    }
+                    possible.andNot(impossible);
+
+                    recurse(matrix, possible, clique, maxClique);
+                    clique.set(p, false);
+
+                    possible.or(impossible);
+                }
+            }
+
+            // second compute clique not containing p:
+            {
+                recurse(matrix, possible, clique, maxClique);
+            }
+
+            possible.set(p, true);
+        }
+    }
+
+    /**
+     * checks whether the current clique has any chance of beating the global  bound
+     *
+     * @param matrix
+     * @param clique
+     * @param possible
+     * @param globalBound
+     * @return false, if current clique has no hope of beating the global bound
+     */
+    private static boolean checkBoundCondition(int[][] matrix, BitSet clique, BitSet possible, int globalBound) {
+        for (int i = clique.nextSetBit(0); i >= 0; i = clique.nextSetBit(i + 1)) {
+            int additional = 0;
+            for (int p = possible.nextSetBit(0); p >= 0; p = possible.nextSetBit(p + 1))
+                if (matrix[i][p] != 0)
+                    additional++;
+            if (additional + clique.cardinality() <= globalBound)
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * converts matrix-based node set  to graph-based node set
+     *
+     * @param graph
+     * @param nodes
+     * @param id2node
+     * @return graph-based nodes
+     */
+    private static NodeSet convertNodeSet(Graph graph, BitSet nodes, Node[] id2node) {
+        // convert clique to node set
+        NodeSet cliqueNS = new NodeSet(graph);
+        for (int vi = nodes.nextSetBit(0); vi >= 0; vi = nodes.nextSetBit(vi + 1))
+            cliqueNS.add(id2node[vi]);
+        return cliqueNS;
+    }
+
+    /**
+     * converts graph to adjacency matrix
+     *
+     * @param graph
+     * @param id2node
+     * @param node2id
+     * @return adjacency matrix
+     */
+    private static int[][] convertGraphToMatrix(Graph graph, Node[] id2node, NodeIntegerArray node2id) {
+        int[][] matrix = new int[graph.getNumberOfNodes()][graph.getNumberOfNodes()];
+        // convert graph to adjacency matrix:
+        try {
+            int id = 0;
+            for (Node v = graph.getFirstNode(); v != null; v = graph.getNextNode(v)) {
+                id2node[id] = v;
+                node2id.set(v, id++);
+            }
+
+            for (Node v = graph.getFirstNode(); v != null; v = graph.getNextNode(v)) {
+                for (Edge e = graph.getFirstAdjacentEdge(v); e != null;
+                     e = graph.getNextAdjacentEdge(e, v)) {
+                    int vi = node2id.getValue(v);
+                    int wi = node2id.getValue(graph.getOpposite(v, e));
+                    matrix[vi][wi] = matrix[wi][vi] = 1;
+                }
+            }
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+        return matrix;
+    }
+
+    /**
+     * computes the induced subgraph in which each node has degree >=d
+     *
+     * @param matrix
+     * @param d
+     * @return set of nodes such that all nodes habe degree >=d in induced subgraph
+     */
+    public static BitSet computeDSubgraph(int[][] matrix, int d) {
+        int num = matrix.length;
+        int[][] work = matrix.clone();
+
+        int[] deg = new int[num];
+        BitSet nodes = new BitSet();
+        for (int p = 0; p < num; p++)
+            nodes.set(p);
+
+        boolean changed = true;
+        while (changed) {
+            changed = false;
+            for (int p = nodes.nextSetBit(0); p >= 0; p = nodes.nextSetBit(p + 1)) {
+                int count = 0;
+                for (int q = nodes.nextSetBit(0); q >= 0; q = nodes.nextSetBit(q + 1))
+                    if (work[p][q] != 0)
+                        count++;
+                deg[p] = count;
+            }
+            for (int p = nodes.nextSetBit(0); p >= 0; p = nodes.nextSetBit(p + 1)) {
+                if (deg[p] < d) {
+                    nodes.set(p, false);
+                    changed = true;
+                }
+            }
+        }
+
+        // now find and return largest component:
+        BitSet seen = new BitSet();
+        BitSet maxComponent = new BitSet();
+        for (int p = nodes.nextSetBit(0); p >= 0; p = nodes.nextSetBit(p + 1)) {
+            if (!seen.get(p)) {
+                BitSet component = new BitSet();
+                visitComponent(matrix, p, nodes, component);
+                if (component.cardinality() > maxComponent.cardinality())
+                    maxComponent = component;
+            }
+        }
+        return maxComponent;
+    }
+
+    /**
+     * recursively visit all nodes reachable from p
+     *
+     * @param matrix
+     * @param p
+     * @param nodes
+     * @param component
+     */
+    private static void visitComponent(int[][] matrix, int p, BitSet nodes, BitSet component) {
+        if (!component.get(p)) {
+            component.set(p);
+            for (int q = nodes.nextSetBit(0); q >= 0; q = nodes.nextSetBit(q + 1)) {
+                if (matrix[p][q] != 0)
+                    visitComponent(matrix, q, nodes, component);
+            }
+        }
+    }
+
+    /**
+     * Given an adjacency matrix and a perfect elimination scheme on the nodes,
+     * returns the list of all maximal cliques
+     *
+     * @param matrix
+     * @param ordering perfect elimination scheme
+     * @return all maximal cliques
+     */
+    static public List computeAll(int[][] matrix, int[] ordering) throws Exception {
+        if (matrix.length != ordering.length)
+            throw new Exception("matrix.length=" + matrix.length + " != ordering.length=" +
+                    ordering.length);
+        List cliques = new LinkedList();
+
+        int previousReach = -1;
+        for (int i = 0; i < ordering.length; i++) {
+            int v = ordering[i];
+            int reach = i;
+            for (int j = i + 1; j < matrix.length; j++) {
+                int w = ordering[j];
+                if (matrix[v][w] != 0) // is successor of v in ordering
+                {
+                    if (j > reach)
+                        reach = j;
+                }
+            }
+            if (reach > previousReach) {
+                BitSet clique = new BitSet();
+                for (int k = i; k <= reach; k++)
+                    clique.set(ordering[k]);
+                cliques.add(clique);
+                previousReach = reach;
+            }
+        }
+        return cliques;
+    }
+}
diff --git a/src/jloda/graph/Node.java b/src/jloda/graph/Node.java
new file mode 100644
index 0000000..22e50de
--- /dev/null
+++ b/src/jloda/graph/Node.java
@@ -0,0 +1,639 @@
+/**
+ * Node.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: Node.java,v 1.20 2009-04-27 07:20:20 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+import jloda.util.IteratorAdapter;
+import jloda.util.NotOwnerException;
+
+import java.util.*;
+
+/**
+ * Node class used by Graph class
+ * Daniel Huson, 2003
+ */
+public class Node extends NodeEdge implements Comparable {
+    private Edge firstAdjacentEdge;
+    private Edge lastAdjacentEdge;
+    private int inDegree = 0;
+    private int outDegree = 0;
+    private Object data = null;
+
+    /**
+     * construct a new node for the given graph. The information in the node is replaced with obj. The node
+     * is added to the end of the list of nodes. Any NewNode listeners are fired.
+     *
+     * @param G
+     */
+    public Node(Graph G) {
+        super();
+        G.registerNewNode(null, this);
+        G.fireNewNode(this);
+        G.fireGraphHasChanged();
+    }
+
+    /**
+     * construct a new node for the given graph. The information in the node is replaced with obj. The node
+     * is added to the end of the list of nodes. Any NewNode listeners are fired.
+     *
+     * @param G
+     */
+    public Node(Graph G, Object info) {
+        super();
+        G.registerNewNode(info, this);
+        G.fireNewNode(this);
+        G.fireGraphHasChanged();
+    }
+
+    /**
+     * initalizes a new node in graph
+     *
+     * @param G    Graph
+     * @param prev NodeEdge
+     * @param next NodeEdge
+     * @param id   int
+     * @param info Object
+     */
+    void init(Graph G, Node prev, Node next, int id, Object info) {
+        super.init(G, prev, next, id, info);
+    }
+
+    /**
+     * Produces a string representation
+     *
+     * @return string representation
+     */
+    public String toString() {
+        StringBuilder buf = new StringBuilder("[" + String.valueOf(getId()) + "] [");
+        if (getInfo() != null)
+            buf.append(getInfo().toString());
+        buf.append("]:");
+        for (Edge e = getFirstAdjacentEdge(); e != null; e = getNextAdjacentEdge(e))
+            buf.append(" ").append(String.valueOf(e.getId()));
+        return buf.toString();
+    }
+
+    /**
+     * delete this node from graph. Any incident edges are deleted.
+     */
+    public void deleteNode() {
+        getOwner().fireDeleteNode(this);
+        getOwner().unregisterNode(this);
+        while (firstAdjacentEdge != null)
+            firstAdjacentEdge.deleteEdge();
+        if (prev != null)
+            prev.next = next;
+        if (next != null)
+            next.prev = prev;
+        Graph G = getOwner();
+        setOwner(null);
+        info = null;
+        data = null;
+        G.fireGraphHasChanged();
+    }
+
+    /**
+     * rearrange the adjacent edges
+     *
+     * @param newOrder
+     */
+    public void rearrangeAdjacentEdges(Collection<Edge> newOrder) {
+        Edge[] array = new Edge[newOrder.size()];
+        int i = 0;
+        for (Edge e : newOrder)
+            array[newOrder.size() - (++i)] = e;
+
+        for (i = 0; i < array.length; i++) {
+            Edge e = array[i];
+            checkOwner(e);
+            Edge pred = e.getPrevIncidentTo(this);
+            Edge succ = e.getNextIncidentTo(this);
+            if (pred != null) {
+                pred.setNext(this, succ);
+                if (succ != null) {
+                    succ.setPrev(this, pred);
+                } else {
+                    this.lastAdjacentEdge = pred;
+                }
+                e.setPrev(this, null);
+                this.firstAdjacentEdge.setPrev(this, e);
+                e.setNext(this, this.firstAdjacentEdge);
+                this.firstAdjacentEdge = e;
+            }
+        }
+        getOwner().fireGraphHasChanged();
+    }
+
+    /**
+     * reverse the order of the adjacent edges
+     */
+    public void reverseOrderAdjacentEdges() {
+        List<Edge> order = new LinkedList<>();
+        for (Edge e = getLastAdjacentEdge(); e != null; e = getPrevAdjacentEdge(e))
+            order.add(e);
+        rearrangeAdjacentEdges(order);
+    }
+
+    /**
+     * rotate the order of the adjacent edges
+     */
+    public void rotateOrderAdjacentEdges() {
+        List<Edge> order = new LinkedList<>();
+        for (Edge e = getFirstAdjacentEdge(); e != null; e = getNextAdjacentEdge(e))
+            order.add(e);
+        Edge e = order.remove(0);
+        order.add(e);
+        rearrangeAdjacentEdges(order);
+    }
+
+    /**
+     * get node on opposite end of edge
+     *
+     * @param e
+     * @return node
+     */
+    public Node getOpposite(Edge e) throws NotOwnerException {
+        checkOwner(e);
+        return e.getOpposite(this);
+    }
+
+    /**
+     * get first adjacent edge
+     *
+     * @return first adjacent edge
+     */
+    public Edge getFirstAdjacentEdge() {
+        return firstAdjacentEdge;
+    }
+
+    /**
+     * get last adjacent edge
+     *
+     * @return last adjacent edge
+     */
+    public Edge getLastAdjacentEdge() {
+        return lastAdjacentEdge;
+    }
+
+    /**
+     * get next adjacent edge
+     *
+     * @param e
+     * @return next adjacent edge
+     */
+    public Edge getNextAdjacentEdge(Edge e) {
+        checkOwner(e);
+        if (e.getSource() == this)
+            return e.getSNext();
+        else if (e.getTarget() == this)
+            return e.getTNext();
+        else
+            return null;
+    }
+
+    /**
+     * get previous adjacent edge
+     *
+     * @param e
+     * @return previous adjacent edge or null
+     */
+    public Edge getPrevAdjacentEdge(Edge e) {
+        checkOwner(e);
+        if (e.getSource() == this)
+            return e.getSPrev();
+        else if (e.getTarget() == this)
+            return e.getTPrev();
+        else
+            return null;
+    }
+
+    /**
+     * get next adjacent edge in cyclic ordering
+     *
+     * @param e
+     * @return next adjacent edge in cyclic ordering
+     */
+    public Edge getNextAdjacentEdgeCyclic(Edge e) {
+        checkOwner(e);
+        Edge f = getNextAdjacentEdge(e);
+        if (f != null)
+            return f;
+        else
+            return getFirstAdjacentEdge();
+    }
+
+    /**
+     * get previous adjacent edge in cyclic ordering
+     *
+     * @param e
+     * @return previous adjacent edge in cyclic ordering
+     */
+    public Edge getPrevAdjacentEdgeCyclic(Edge e) {
+        checkOwner(e);
+        Edge f = getPrevAdjacentEdge(e);
+        if (f != null)
+            return f;
+        else
+            return lastAdjacentEdge;
+    }
+
+    /**
+     * gets the first out edge
+     *
+     * @return first out edge or null
+     */
+    public Edge getFirstOutEdge() {
+        for (Edge e = getFirstAdjacentEdge(); e != null; e = getNextAdjacentEdge(e))
+            if (e.getSource() == this)
+                return e;
+        return null;
+    }
+
+    /**
+     * gets the next out edge
+     *
+     * @param e
+     * @return next out edge or null
+     */
+    public Edge getNextOutEdge(Edge e) {
+        for (e = getNextAdjacentEdge(e); e != null; e = getNextAdjacentEdge(e))
+            if (e.getSource() == this)
+                return e;
+        return null;
+    }
+
+    /**
+     * gets the last out edge
+     *
+     * @return last out edge or null
+     */
+    public Edge getLastOutEdge() {
+        for (Edge e = getLastAdjacentEdge(); e != null; e = getPrevAdjacentEdge(e))
+            if (e.getSource() == this)
+                return e;
+        return null;
+    }
+
+    /**
+     * gets the previous out edge
+     *
+     * @param e
+     * @return previous out edge or null
+     */
+    public Edge getPrevOutEdge(Edge e) {
+        for (e = getPrevAdjacentEdge(e); e != null; e = getPrevAdjacentEdge(e))
+            if (e.getSource() == this)
+                return e;
+        return null;
+    }
+
+    /**
+     * gets the first in edge
+     *
+     * @return first in edge or null
+     */
+    public Edge getFirstInEdge() {
+        for (Edge e = getFirstAdjacentEdge(); e != null; e = getNextAdjacentEdge(e))
+            if (e.getTarget() == this)
+                return e;
+        return null;
+    }
+
+    /**
+     * gets the next in edge
+     *
+     * @param e
+     * @return next in edge or null
+     */
+    public Edge getNextInEdge(Edge e) {
+        for (e = getNextAdjacentEdge(e); e != null; e = getNextAdjacentEdge(e))
+            if (e.getTarget() == this)
+                return e;
+        return null;
+    }
+
+    /**
+     * gets the last in edge
+     *
+     * @return last in edge or null
+     */
+    public Edge getLastInEdge() {
+        for (Edge e = getLastAdjacentEdge(); e != null; e = getPrevAdjacentEdge(e))
+            if (e.getTarget() == this)
+                return e;
+        return null;
+    }
+
+    /**
+     * gets the previous in edge
+     *
+     * @param e
+     * @return previous in edge or null
+     */
+    public Edge getPrevInEdge(Edge e) {
+        for (e = getPrevAdjacentEdge(e); e != null; e = getPrevAdjacentEdge(e))
+            if (e.getTarget() == this)
+                return e;
+        return null;
+    }
+
+    /**
+     * get common edge between this node and w, or null
+     *
+     * @param w
+     * @return common edge between this node and w, or null
+     */
+    public Edge getCommonEdge(Node w) throws NotOwnerException {
+        checkOwner(w);
+        for (Edge e = getFirstAdjacentEdge(); e != null; e = getNextAdjacentEdge(e)) {
+            if (getOpposite(e) == w)
+                return e;
+        }
+        return null;
+    }
+
+    /**
+     * get  edge from this node to w, or null
+     *
+     * @param w
+     * @return common edge from this node to w, or null
+     */
+    public Edge getEdgeTo(Node w) throws NotOwnerException {
+        checkOwner(w);
+        for (Edge e = getFirstOutEdge(); e != null; e = getNextOutEdge(e)) {
+            if (getOpposite(e) == w)
+                return e;
+        }
+        return null;
+    }
+
+    /**
+     * get  edge to this node from w, or null
+     *
+     * @param w
+     * @return common edge from this node to w, or null
+     */
+    public Edge getEdgeFrom(Node w) throws NotOwnerException {
+        checkOwner(w);
+        for (Edge e = getFirstInEdge(); e != null; e = getNextInEdge(e)) {
+            if (getOpposite(e) == w)
+                return e;
+        }
+        return null;
+    }
+
+    /**
+     * get next node in list of all node
+     *
+     * @return next node
+     */
+    public Node getNext() {
+        Node v = (Node) next;
+        while (v != null && v.isHidden())
+            v = (Node) v.next;
+        return v;
+    }
+
+    /**
+     * get previous node
+     *
+     * @return previous node
+     */
+    public Node getPrev() {
+        Node v = (Node) prev;
+        while (v != null && v.isHidden())
+            v = (Node) v.prev;
+        return v;
+    }
+
+    /**
+     * get the degree of this node
+     *
+     * @return degree of this node
+     */
+    public int getDegree() {
+        return inDegree + outDegree;
+    }
+
+    /**
+     * get the indegree of this node
+     *
+     * @return indegree of this node
+     */
+    public int getInDegree() {
+        return inDegree;
+    }
+
+    /**
+     * get the out degree of this node
+     *
+     * @return out degree of this node
+     */
+    public int getOutDegree() {
+        return outDegree;
+    }
+
+    /**
+     * get iterator over all adjacent nodes
+     *
+     * @return iterator over all adjacent nodes
+     */
+    public Iterator<Node> getAdjacentNodes() {
+        final Node v = this;
+        return new IteratorAdapter<Node>() {
+            private Edge e = v.getFirstAdjacentEdge();
+
+            protected Node findNext() throws NoSuchElementException {
+                if (e != null) {
+                    Node result;
+                    do {
+                        result = v.getOpposite(e);
+                        e = v.getNextAdjacentEdge(e);
+                    }
+                    while (result != null && result.isHidden());
+                    return result;
+                } else {
+                    throw new NoSuchElementException("at end");
+                }
+            }
+        };
+    }
+
+    /**
+     * get iterator over all adjacent edges
+     *
+     * @return iterator over all adjacent edges
+     */
+    public Iterator<Edge> getAdjacentEdges() {
+        final Node v = this;
+        return new IteratorAdapter<Edge>() {
+            private Edge e = v.getFirstAdjacentEdge();
+
+            protected Edge findNext() throws NoSuchElementException {
+                if (e != null) {
+                    final Edge result = e;
+                    e = v.getNextAdjacentEdge(e);
+                    return result;
+                } else {
+                    throw new NoSuchElementException("at end");
+                }
+            }
+        };
+    }
+
+    /**
+     * get iterator over all in edges
+     *
+     * @return iterator over all in edges
+     */
+    public Iterator<Edge> getInEdges() {
+        final Node v = this;
+        return new IteratorAdapter<Edge>() {
+            private Edge e = v.getFirstAdjacentEdge();
+
+            protected Edge findNext() throws NoSuchElementException {
+                while (e != null && e.getTarget() != v) {
+                    e = v.getNextAdjacentEdge(e);
+                }
+                if (e != null) {
+                    final Edge result = e;
+                    e = v.getNextAdjacentEdge(e);
+                    return result;
+                } else {
+                    throw new NoSuchElementException("at end");
+                }
+            }
+        };
+    }
+
+    /**
+     * get iterator over all out edges
+     *
+     * @return iterator over all out edges
+     */
+    public Iterator<Edge> getOutEdges() {
+        final Node v = this;
+        return new IteratorAdapter<Edge>() {
+            private Edge e = v.getFirstAdjacentEdge();
+
+            protected Edge findNext() throws NoSuchElementException {
+                while (e != null && e.getSource() != v) {
+                    e = v.getNextAdjacentEdge(e);
+                }
+                if (e != null) {
+                    final Edge result = e;
+                    e = v.getNextAdjacentEdge(e);
+                    return result;
+                } else {
+                    throw new NoSuchElementException("at end");
+                }
+            }
+        };
+    }
+
+    /**
+     * get a directed edge from this node to w, or null
+     *
+     * @param w
+     * @return directed edge from this node to w, or null
+     */
+    public Edge findDirectedEdge(Node w) throws NotOwnerException {
+        checkOwner(w);
+        for (Iterator<Edge> iterator = getOutEdges(); iterator.hasNext(); ) {
+            Edge e = iterator.next();
+            if (getOpposite(e) == w)
+                return e;
+        }
+        return null;
+    }
+
+    /**
+     * is this node adjacent to w
+     *
+     * @param w
+     * @return adjacent
+     */
+    public boolean isAdjacent(Node w) throws NotOwnerException {
+        checkOwner(w);
+        for (Iterator<Node> it = getAdjacentNodes(); it.hasNext(); ) {
+            if (it.next() == w)
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * compares with another node of the same graph
+     *
+     * @param o
+     * @return -1, 1 or 0
+     */
+    public int compareTo(Object o) {
+        final Node v = (Node) o;
+        checkOwner(v);
+        if (this.getId() < v.getId())
+            return -1;
+        else if (this.getId() > v.getId())
+            return 1;
+        else
+            return 0;
+    }
+
+    void setFirstAdjacentEdge(Edge e) {
+        firstAdjacentEdge = e;
+    }
+
+    void setLastAdjacentEdge(Edge e) {
+        lastAdjacentEdge = e;
+    }
+
+    void incrementInDegree() {
+        inDegree++;
+    }
+
+    void incrementOutDegree() {
+        outDegree++;
+    }
+
+    void decrementInDegree() {
+        inDegree--;
+    }
+
+    void decrementOutDegree() {
+        outDegree--;
+    }
+
+    public Object getData() {
+        return data;
+    }
+
+    public void setData(Object data) {
+        this.data = data;
+    }
+
+}
+
+// EOF
+
diff --git a/src/jloda/graph/NodeArray.java b/src/jloda/graph/NodeArray.java
new file mode 100644
index 0000000..91e3f28
--- /dev/null
+++ b/src/jloda/graph/NodeArray.java
@@ -0,0 +1,207 @@
+/**
+ * NodeArray.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: NodeArray.java,v 1.11 2005-12-05 13:25:44 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+
+/**
+ * Node array
+ * Daniel Huson, 2003
+ */
+
+public class NodeArray<T> extends GraphBase implements NodeAssociation<T> {
+    private T[] data;
+    private boolean isClear = true;
+
+    /**
+     * Construct a node array.
+     *
+     * @param g Graph
+     */
+    public NodeArray(Graph g) {
+        setOwner(g);
+        data = (T[]) new Object[g.getMaxNodeId() + 1];
+        g.registerNodeAssociation(this);
+    }
+
+    /**
+     * Construct a node array for the given graph and initialize all entries
+     * to obj.
+     *
+     * @param g   Graph
+     * @param obj Object
+     */
+    public NodeArray(Graph g, T obj) {
+        this(g);
+        setAll(obj);
+        isClear = false;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param src NodeArray
+     */
+    public NodeArray(NodeAssociation<T> src) {
+        this(src.getOwner());
+        for (Node v = getOwner().getFirstNode(); v != null; v = v.getNext())
+            set(v, src.get(v));
+        isClear = src.isClear();
+    }
+
+    /**
+     * Clear all entries.
+     */
+    public void clear() {
+        if (getOwner().getMaxNodeId() < 0.5 * data.length)
+            data = (T[]) new Object[getOwner().getMaxNodeId() + 1];
+        else
+            for (Node v = getOwner().getFirstNode(); v != null; v = v.getNext())
+                set(v, null);
+        isClear = true;
+    }
+
+    /**
+     * Get the entry for node v or the default object
+     *
+     * @param v Node
+     * @return an Object the entry for node v
+     */
+    public T get(Node v) {
+        checkOwner(v);
+        if (v.getId() < data.length)
+            return data[v.getId()];
+        else
+            return null;
+    }
+
+    /**
+     * Set the entry for node v to obj.
+     *
+     * @param v   Node
+     * @param obj Object
+     */
+    public void set(Node v, T obj) {
+        checkOwner(v);
+
+        if (v.getId() >= data.length) {
+            if (obj == null)
+                return;
+            grow(v.getId());
+        }
+        data[v.getId()] = obj;
+        if (obj != null && isClear)
+            isClear = false;
+    }
+
+    /**
+     * grows the array. Repeatedly doubles the size of the array until it contains index n
+     *
+     * @param n index to be included in array
+     */
+    private void grow(int n) {
+        int newSize = Math.max(1, 2 * data.length);
+        while (newSize <= n)
+            newSize *= 2;
+        if (newSize > data.length) {
+            T[] newData = (T[]) new Object[newSize];
+            for (Node v = getOwner().getFirstNode(); v != null; v = v.getNext())
+                if (v.getId() < data.length)
+                    newData[v.getId()] = data[v.getId()];
+            data = newData;
+        }
+    }
+
+    /**
+     * Set the entry for all nodes to obj.
+     *
+     * @param obj Object
+     */
+    public void setAll(T obj) {
+        for (Node v = getOwner().getFirstNode(); v != null; v = v.getNext())
+            set(v, obj);
+    }
+
+
+    /**
+     * get the entry as an int
+     *
+     * @param v
+     * @return int value
+     */
+    public int getInt(Node v) {
+        T obj = get(v);
+        if (obj == null)
+            return 0;
+        else if (obj instanceof Double)
+            return (int) ((Double) obj).doubleValue();
+        else
+            return ((Integer) obj);
+
+    }
+
+    /**
+     * get the entry as a double
+     *
+     * @param v
+     * @return double value
+     */
+    public double getDouble(Node v) {
+        T obj = get(v);
+        if (obj == null)
+            return 0;
+        else if (obj instanceof Integer)
+            return ((Integer) obj);
+        else
+            return ((Double) obj);
+    }
+
+    /**
+     * is array erase, that is, has nothing been set
+     *
+     * @return true, if erase
+     */
+    public boolean isClear() {
+        return isClear;
+    }
+
+    /**
+     * create a clone
+     *
+     * @return clone
+     */
+    public Object clone() {
+        Graph graph = getOwner();
+        NodeArray<T> result = new NodeArray<>(graph);
+        for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+            result.set(v, get(v));
+        }
+        return result;
+    }
+
+
+}
+
+// EOF
diff --git a/src/jloda/graph/NodeAssociation.java b/src/jloda/graph/NodeAssociation.java
new file mode 100644
index 0000000..dfd31c3
--- /dev/null
+++ b/src/jloda/graph/NodeAssociation.java
@@ -0,0 +1,84 @@
+/**
+ * NodeAssociation.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graph;
+
+/**
+ * Node assocation
+ * Daniel Huson, 2006
+ */
+public interface NodeAssociation<T> {
+    /**
+     * Clear all entries.
+     */
+    void clear();
+
+    /**
+     * Get the entry for node v or the default object
+     *
+     * @param v Node
+     * @return an Object the entry for node v
+     */
+    T get(Node v);
+
+    /**
+     * Set the entry for node v to obj.
+     *
+     * @param v   Node
+     * @param obj Object
+     */
+    void set(Node v, T obj);
+
+    /**
+     * Set the entry for all nodes to obj.
+     *
+     * @param obj Object
+     */
+    void setAll(T obj);
+
+    /**
+     * get the entry as an int
+     *
+     * @param v
+     * @return int value
+     */
+    int getInt(Node v);
+
+    /**
+     * get the entry as a double
+     *
+     * @param v
+     * @return double value
+     */
+    double getDouble(Node v);
+
+    /**
+     * returns a reference to the graph that owns this association
+     *
+     * @return owner
+     */
+    Graph getOwner();
+
+    /**
+     * is clean, that is, has never been set since last erase
+     *
+     * @return true, if erase
+     */
+    boolean isClear();
+}
diff --git a/src/jloda/graph/NodeData.java b/src/jloda/graph/NodeData.java
new file mode 100644
index 0000000..189b318
--- /dev/null
+++ b/src/jloda/graph/NodeData.java
@@ -0,0 +1,136 @@
+/**
+ * NodeData.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graph;
+
+import jloda.util.Basic;
+
+/**
+ * multi-sample data associated with a node
+ * Daniel Huson, 1.2013
+ */
+public class NodeData {
+    private int[] assigned;
+    private int[] summarized;
+    private int countAssigned;
+    private int maxAssigned;
+    private int countSummarized;
+    private int maxSummarized;
+
+    private double upPValue = -1; // p-value of left
+    private double downPValue = -1;   // p-value for right
+
+    /**
+     * constructor
+     *
+     * @param assigned
+     * @param summarized
+     */
+    public NodeData(int[] assigned, int[] summarized) {
+        setAssigned(assigned);
+        setSummarized(summarized);
+    }
+
+    public int[] getAssigned() {
+        return assigned;
+    }
+
+    public int getAssigned(int i) {
+        return assigned[i];
+    }
+
+    public void setAssigned(int[] assigned) {
+        this.assigned = assigned;
+        countAssigned = 0;
+        maxAssigned = 0;
+        if (assigned != null) {
+            for (int value : assigned) {
+                countAssigned += value;
+                maxAssigned = Math.max(maxAssigned, value);
+            }
+        }
+    }
+
+    public int[] getSummarized() {
+        return summarized;
+    }
+
+    public int getSummarized(int i) {
+        return summarized[i];
+    }
+
+    public void setSummarized(int[] summarized) {
+        this.summarized = summarized;
+        countSummarized = 0;
+        maxSummarized = 0;
+        if (summarized != null) {
+            for (int value : summarized) {
+                countSummarized += value;
+                maxSummarized = Math.max(maxSummarized, value);
+            }
+        }
+    }
+
+    public void addToSummarized(int i, int add) {
+        summarized[i] += add;
+        countSummarized += add;
+        if (summarized[i] > maxSummarized)
+            maxSummarized = summarized[i];
+    }
+
+    public int getCountAssigned() {
+        return countAssigned;
+    }
+
+    public int getMaxAssigned() {
+        return maxAssigned;
+    }
+
+    public int getCountSummarized() {
+        return countSummarized;
+    }
+
+    public int getMaxSummarized() {
+        return maxSummarized;
+    }
+
+    public double getUpPValue() {
+        return upPValue;
+    }
+
+    public void setUpPValue(double upPValue) {
+        this.upPValue = upPValue;
+    }
+
+    public double getDownPValue() {
+        return downPValue;
+    }
+
+    public void setDownPValue(double downPValue) {
+        this.downPValue = downPValue;
+    }
+
+    public String toString() {
+        return "assigned: " + Basic.toString(assigned, ",") + ", summarized: " + Basic.toString(summarized, ",");
+    }
+
+    public NodeData clone() {
+        return new NodeData(assigned, summarized);
+    }
+}
diff --git a/src/jloda/graph/NodeDoubleArray.java b/src/jloda/graph/NodeDoubleArray.java
new file mode 100644
index 0000000..448d4d6
--- /dev/null
+++ b/src/jloda/graph/NodeDoubleArray.java
@@ -0,0 +1,119 @@
+/**
+ * NodeDoubleArray.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: NodeDoubleArray.java,v 1.7 2007-10-23 13:10:53 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+
+/**
+ * Node array
+ * Daniel Huson, 2003
+ */
+
+public class NodeDoubleArray extends NodeArray<Double> {
+    /**
+     * Construct a node double array for the given graph and initialize all
+     * entries to value.
+     *
+     * @param g   Graph
+     * @param val double
+     */
+    public NodeDoubleArray(Graph g, double val) {
+        super(g, val);
+    }
+
+    /**
+     * Construct a node double array for the given graph.
+     *
+     * @param g Graph
+     */
+    public NodeDoubleArray(Graph g) {
+        super(g);
+    }
+
+    /**
+     * Construct a node double array.
+     *
+     * @param src NodeDoubleArray
+     */
+    public NodeDoubleArray(NodeDoubleArray src) {
+        super(src);
+    }
+
+    /**
+     * Construct a node double array.
+     *
+     * @param src NodeDoubleArray
+     */
+    public NodeDoubleArray(NodeDoubleMap src) {
+        super(src);
+    }
+
+
+    /**
+     * Get the entry for node v.
+     *
+     * @param v Node
+     * @return a double value the entry for node v
+     */
+    public double getValue(Node v) {
+        if (super.get(v) == null)
+            return 0;
+        else
+            return (Double) super.get(v);
+    }
+
+    /**
+     * Set the entry for node v to obj.
+     *
+     * @param v   Node
+     * @param val double
+     */
+    public void set(Node v, double val) {
+        super.set(v, val);
+    }
+
+
+    /**
+     * set the entry to the given int value
+     *
+     * @param v
+     * @param value
+     */
+    public void set(Node v, int value) {
+        set(v, (double) value);
+    }
+
+
+    /**
+     * Set the entry for all nodes to val.
+     *
+     * @param val double
+     */
+    public void setAll(double val) {
+        for (Node v = getOwner().getFirstNode(); v != null; v = v.getNext())
+            set(v, val);    }
+}
+
+// EOF
diff --git a/src/jloda/graph/NodeDoubleMap.java b/src/jloda/graph/NodeDoubleMap.java
new file mode 100644
index 0000000..39538f3
--- /dev/null
+++ b/src/jloda/graph/NodeDoubleMap.java
@@ -0,0 +1,106 @@
+/**
+ * NodeDoubleMap.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: NodeDoubleMap.java,v 1.2 2005-12-05 13:25:44 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+
+/**
+ * Node map
+ * Daniel Huson, 2003
+ */
+
+public class NodeDoubleMap extends NodeMap<Double> {
+    /**
+     * Construct a node double map for the given graph and initialize all
+     * entries to value.
+     *
+     * @param g   Graph
+     * @param val double
+     */
+    public NodeDoubleMap(Graph g, double val) {
+        super(g, val);
+    }
+
+    /**
+     * Construct a node double map for the given graph.
+     *
+     * @param g Graph
+     */
+    public NodeDoubleMap(Graph g) {
+        super(g);
+    }
+
+    /**
+     * Construct a node double map.
+     *
+     * @param src
+     */
+    public NodeDoubleMap(NodeDoubleArray src) {
+        super(src);
+    }
+
+    /**
+     * Construct a node double map.
+     *
+     * @param src
+     */
+    public NodeDoubleMap(NodeDoubleMap src) {
+        super(src);
+    }
+
+    /**
+     * Get the entry for node v.
+     *
+     * @param v Node
+     * @return a double value the entry for node v
+     */
+    public double getValue(Node v) {
+        if (super.get(v) == null)
+            return 0;
+        else
+            return super.get(v);
+    }
+
+    /**
+     * Set the entry for node v to obj.
+     *
+     * @param v   Node
+     * @param val double
+     */
+    public void set(Node v, double val) {
+        super.set(v, val);
+    }
+
+    /**
+     * Set the entry for all nodes to val.
+     *
+     * @param val double
+     */
+    public void setAll(double val) {
+        super.setAll(val);
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/NodeEdge.java b/src/jloda/graph/NodeEdge.java
new file mode 100644
index 0000000..3dc3289
--- /dev/null
+++ b/src/jloda/graph/NodeEdge.java
@@ -0,0 +1,132 @@
+/**
+ * NodeEdge.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id:
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+/**
+ * NodeEdge: util class for both Node and Edge
+ * Daniel Huson, 2003
+ */
+
+public class NodeEdge extends GraphBase {
+    private final static int HIDDEN_MASK = (1 << 31);
+    private final static int ID_MASK = ~HIDDEN_MASK;
+    Object info;
+    private int id;
+    NodeEdge prev;
+    NodeEdge next;
+
+    /**
+     * make an empty object
+     */
+    NodeEdge() {
+    }
+
+    /**
+     * initialize
+     *
+     * @param G    Graph
+     * @param prev NodeEdge
+     * @param next NodeEdge
+     * @param id   int
+     * @param info Object
+     */
+    void init(Graph G, NodeEdge prev, NodeEdge next, int id, Object info) {
+        setOwner(G);
+        this.prev = prev;
+        this.next = next;
+        setId(id);
+        if (info != null)
+            setInfo(info);
+    }
+
+    /**
+     * Get the associated info object
+     *
+     * @return info object
+     */
+    public Object getInfo() {
+        return info;
+    }
+
+    /**
+     * Set  the info   object
+     *
+     * @param info info object
+     */
+    public void setInfo(Object info) {
+        this.info = info;
+    }
+
+    /**
+     * Get the hash code of this object
+     *
+     * @return hash code
+     */
+    public int hashCode() {
+        return id;
+    }
+
+    /**
+     * Get the id
+     *
+     * @return id
+     */
+    public int getId() {
+        return id & ID_MASK;
+    }
+
+    /**
+     * sets the id
+     *
+     * @param id
+     */
+    void setId(int id) {
+        this.id = id & ID_MASK;
+    }
+
+    /**
+     * is this node hidden? If hidden, this node will not be considered when using an iteration
+     *
+     * @return hidden
+     */
+    public boolean isHidden() {
+        return (id & HIDDEN_MASK) == HIDDEN_MASK;
+    }
+
+    /**
+     * set the hidden state of this node
+     *
+     * @param hidden
+     */
+    void setHidden(boolean hidden) {
+        if (hidden)
+            id |= HIDDEN_MASK;
+        else
+            id &= (~HIDDEN_MASK);
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/NodeEdgeEnumeration.java b/src/jloda/graph/NodeEdgeEnumeration.java
new file mode 100644
index 0000000..80239b0
--- /dev/null
+++ b/src/jloda/graph/NodeEdgeEnumeration.java
@@ -0,0 +1,49 @@
+/**
+ * NodeEdgeEnumeration.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graph;
+
+/**
+ * @version $Id: NodeEdgeEnumeration.java,v 1.4 2005-01-07 14:23:05 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+
+
+/**
+ * NodeEdgeEnumeration implements a Enumeration for nodes and edges
+ * Daniel Huson, 2003
+ */
+
+class NodeEdgeItem {
+    /**Constructor of NodeEdgeItem
+     * @param ne0 NodeEdge
+     * @param next0 NodeEdgeItem
+     */
+    NodeEdgeItem(NodeEdge ne0, NodeEdgeItem next0) {
+        ne = ne0;
+        next = next0;
+    }
+
+    final NodeEdge ne;
+    final NodeEdgeItem next;
+}
+
+// EOF
diff --git a/src/jloda/graph/NodeIntegerArray.java b/src/jloda/graph/NodeIntegerArray.java
new file mode 100644
index 0000000..4dad681
--- /dev/null
+++ b/src/jloda/graph/NodeIntegerArray.java
@@ -0,0 +1,105 @@
+/**
+ * NodeIntegerArray.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: NodeIntegerArray.java,v 1.8 2007-11-05 16:59:44 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+
+/**
+ * Node array
+ * Daniel Huson, 2003
+ */
+
+public class NodeIntegerArray extends NodeArray<Integer> {
+    /**
+     * Construct a node int array for the given graph and initialize all
+     * entries to value.
+     *
+     * @param g            Graph
+     * @param initialValue int
+     */
+    public NodeIntegerArray(Graph g, int initialValue) {
+        super(g, initialValue);
+    }
+
+    /**
+     * Construct a node int array.
+     *
+     * @param g Graph
+     */
+    public NodeIntegerArray(Graph g) {
+        super(g);
+    }
+
+    /**
+     * Construct a node int map.
+     *
+     * @param src
+     */
+    public NodeIntegerArray(NodeIntegerArray src) {
+        super(src);
+    }
+
+    /**
+     * Construct a node int map.
+     *
+     * @param src
+     */
+    public NodeIntegerArray(NodeIntegerMap src) {
+        super(src);
+    }
+
+    /**
+     * Get the entry for node v.
+     *
+     * @param v Node
+     */
+    public int getValue(Node v) {
+        if (super.get(v) == null)
+            return 0;
+        else
+            return super.get(v);
+    }
+
+    /**
+     * set the entry for node v to obj.
+     *
+     * @param v   Node
+     * @param val int
+     */
+    public void set(Node v, int val) {
+        super.set(v, val);
+    }
+
+    /**
+     * Set the entry for all nodes to val.
+     *
+     * @param val int
+     */
+    public void setAll(int val) {
+        super.setAll(val);
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/NodeIntegerMap.java b/src/jloda/graph/NodeIntegerMap.java
new file mode 100644
index 0000000..a3bb608
--- /dev/null
+++ b/src/jloda/graph/NodeIntegerMap.java
@@ -0,0 +1,106 @@
+/**
+ * NodeIntegerMap.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: NodeIntegerMap.java,v 1.2 2005-12-05 13:25:44 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+
+/**
+ * Node map
+ * Daniel Huson, 2003
+ */
+
+public class NodeIntegerMap extends NodeMap<Integer> {
+    /**
+     * Construct a node int array for the given graph and initialize all
+     * entries to value.
+     *
+     * @param g   Graph
+     * @param val int
+     */
+    public NodeIntegerMap(Graph g, int val) {
+        super(g, val);
+    }
+
+    /**
+     * Construct a node int array.
+     *
+     * @param g Graph
+     */
+    public NodeIntegerMap(Graph g) {
+        super(g);
+    }
+
+    /**
+     * Construct a node int map.
+     *
+     * @param src
+     */
+    public NodeIntegerMap(NodeIntegerArray src) {
+        super(src);
+    }
+
+    /**
+     * Construct a node int map.
+     *
+     * @param src
+     */
+    public NodeIntegerMap(NodeIntegerMap src) {
+        super(src);
+    }
+
+
+    /**
+     * Get the entry for node v.
+     *
+     * @param v Node
+     */
+    public int getValue(Node v) {
+        if (super.get(v) == null)
+            return 0;
+        else
+            return super.get(v);
+    }
+
+    /**
+     * set the entry for node v to obj.
+     *
+     * @param v   Node
+     * @param val int
+     */
+    public void set(Node v, int val) {
+        super.set(v, val);
+    }
+
+    /**
+     * Set the entry for all nodes to val.
+     *
+     * @param val int
+     */
+    public void setAll(int val) {
+        super.setAll(val);
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/NodeMap.java b/src/jloda/graph/NodeMap.java
new file mode 100644
index 0000000..80d5bf7
--- /dev/null
+++ b/src/jloda/graph/NodeMap.java
@@ -0,0 +1,167 @@
+/**
+ * NodeMap.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: NodeMap.java,v 1.2 2005-12-05 13:25:45 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Node map
+ * Daniel Huson, 2003
+ */
+
+public class NodeMap<T> extends GraphBase implements NodeAssociation<T> {
+    private final Map<Node, T> data;
+    private boolean isClear;
+
+    /**
+     * Construct a node map.
+     *
+     * @param g Graph
+     */
+    public NodeMap(Graph g) {
+        setOwner(g);
+        data = new HashMap<>();
+        g.registerNodeAssociation(this);
+        isClear = true;
+    }
+
+
+    /**
+     * Construct a node map for the given graph and initialize all entries
+     * to obj.
+     *
+     * @param g   Graph
+     * @param obj Object
+     */
+    public NodeMap(Graph g, T obj) {
+        this(g);
+        setAll(obj);
+        if (obj != null && isClear)
+            isClear = false;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param src NodeAssociation
+     */
+    public NodeMap(NodeAssociation<T> src) {
+        this(src.getOwner());
+        for (Node v = getOwner().getFirstNode(); v != null; v = v.getNext())
+            set(v, src.get(v));
+        isClear = src.isClear();
+    }
+
+    /**
+     * Clear all entries.
+     */
+    public void clear() {
+        for (Node v = getOwner().getFirstNode(); v != null; v = v.getNext())
+            data.remove(v);
+        isClear = true;
+    }
+
+    /**
+     * Get the entry for node v or the default object
+     *
+     * @param v Node
+     * @return an Object the entry for node v
+     */
+    public T get(Node v) {
+        checkOwner(v);
+        return data.get(v);
+    }
+
+    /**
+     * Set the entry for node v to obj.
+     *
+     * @param v   Node
+     * @param obj Object
+     */
+    public void set(Node v, T obj) {
+        checkOwner(v);
+        data.put(v, obj);
+        if (obj != null && isClear)
+            isClear = false;
+    }
+
+    /**
+     * Set the entry for all nodes to obj.
+     *
+     * @param obj Object
+     */
+    public void setAll(T obj) {
+        for (Node v = getOwner().getFirstNode(); v != null; v = v.getNext())
+            data.put(v, obj);
+        if (obj != null && isClear)
+            isClear = false;
+    }
+
+    /**
+     * get the entry as an int
+     *
+     * @param v
+     * @return int value
+     */
+    public int getInt(Node v) {
+        Object obj = get(v);
+        if (obj == null)
+            return 0;
+        else if (obj instanceof Double)
+            return (int) ((Double) obj).doubleValue();
+        else
+            return (Integer) obj;
+
+    }
+
+    /**
+     * get the entry as a double
+     *
+     * @param v
+     * @return double value
+     */
+    public double getDouble(Node v) {
+        Object obj = get(v);
+        if (obj == null)
+            return 0;
+        else if (obj instanceof Integer)
+            return ((Integer) obj);
+        else
+            return ((Double) obj);
+    }
+
+    /**
+     * is clean, that is, has never been set since last erase
+     *
+     * @return true, if erase
+     */
+    public boolean isClear() {
+        return isClear;
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/NodeSet.java b/src/jloda/graph/NodeSet.java
new file mode 100644
index 0000000..eb4975d
--- /dev/null
+++ b/src/jloda/graph/NodeSet.java
@@ -0,0 +1,390 @@
+/**
+ * NodeSet.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: NodeSet.java,v 1.13 2007-07-10 13:21:52 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+package jloda.graph;
+
+import jloda.util.Basic;
+import jloda.util.IteratorAdapter;
+import jloda.util.NotOwnerException;
+
+import java.util.*;
+
+/**
+ * NodeSet implements a set of nodes contained in a given graph
+ * Daniel Huson, 2003
+ */
+public class NodeSet extends GraphBase implements Set<Node> {
+    final BitSet bits;
+
+    /**
+     * Constructs a new empty NodeSet for Graph G.
+     *
+     * @param graph Graph
+     */
+    public NodeSet(Graph graph) {
+        setOwner(graph);
+        graph.registerNodeSet(this);
+        bits = new BitSet();
+    }
+
+    /**
+     * Is node v member?
+     *
+     * @param v Node
+     * @return a boolean value
+     */
+    public boolean contains(Object v) {
+        boolean result = false;
+        try {
+            result = bits.get(getOwner().getId((Node) v));
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+        return result;
+    }
+
+    /**
+     * Insert node v.
+     *
+     * @param v Node
+     * @return true, if new
+     */
+    public boolean add(Node v) {
+        if (bits.get(getOwner().getId(v)))
+            return false;
+        else {
+            bits.set(getOwner().getId(v), true);
+            return true;
+        }
+    }
+
+    /**
+     * Delete node v from set.
+     *
+     * @param v Node
+     */
+    public boolean remove(Object v) {
+        if (bits.get(getOwner().getId((Node) v))) {
+            bits.set(getOwner().getId((Node) v), false);
+            return true;
+        } else
+            return false;
+
+    }
+
+    /**
+     * adds all nodes in the given collection
+     *
+     * @param collection
+     * @return true, if some element is new
+     */
+    public boolean addAll(final Collection<? extends Node> collection) {
+        Iterator it = collection.iterator();
+
+        boolean result = false;
+        while (it.hasNext()) {
+            if (add((Node) it.next()))
+                result = true;
+        }
+        return result;
+    }
+
+    /**
+     * returns true if all elements of collection are contained in this set
+     *
+     * @param collection
+     * @return all contained?
+     */
+    public boolean containsAll(final Collection<?> collection) {
+        Iterator it = collection.iterator();
+
+        while (it.hasNext()) {
+            if (!contains(it.next()))
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * equals
+     *
+     * @param obj
+     * @return true, if equal
+     */
+    public boolean equals(Object obj) {
+        if (obj instanceof Collection) {
+            Collection collection = (Collection) obj;
+            return size() == collection.size() && containsAll(collection);
+        } else
+            return false;
+    }
+
+    /**
+     * removes all nodes in the collection
+     *
+     * @param collection
+     * @return true, if something actually removed
+     */
+    public boolean removeAll(final Collection<?> collection) {
+        Iterator it = collection.iterator();
+
+        boolean result = false;
+        while (it.hasNext()) {
+            if (remove(it.next()))
+                result = true;
+        }
+        return result;
+    }
+
+    /**
+     * keep only those elements contained in the collection
+     *
+     * @param collection
+     * @return true, if set changes
+     */
+    public boolean retainAll(final Collection<?> collection) {
+        if (collection == null)
+            return false;
+        boolean changed = (collection.size() != size() || !containsAll(collection));
+        NodeSet was = (NodeSet) this.clone();
+
+        clear();
+        Iterator it = collection.iterator();
+        while (it.hasNext()) {
+            Object v = it.next();
+            if (v instanceof Node && was.contains(v))
+                add((Node) v);
+        }
+        return changed;
+    }
+
+    /**
+     * Delete all nodes from set.
+     */
+    public void clear() {
+        bits.clear();
+    }
+
+    /**
+     * is empty?
+     *
+     * @return true, if empty
+     */
+    public boolean isEmpty() {
+        return bits.isEmpty();
+    }
+
+    /**
+     * return all contained nodes as objects
+     *
+     * @return contained nodes
+     */
+    public Node[] toArray() {
+        Node[] result = new Node[bits.cardinality()];
+        int i = 0;
+        Iterator<Node> it = getOwner().nodeIterator();
+        while (it.hasNext()) {
+            Node v = it.next();
+            if (contains(v))
+                result[i++] = v;
+        }
+        return result;
+    }
+
+
+    /**
+     * Puts all nodes into set.
+     */
+    public void addAll() {
+        Iterator it = getOwner().nodeIterator();
+        while (it.hasNext())
+            add((Node) it.next());
+    }
+
+    /**
+     * Returns the size of the set.
+     *
+     * @return size
+     */
+    public int size() {
+        return bits.cardinality();
+    }
+
+    /**
+     * Returns an enumeration of the elements in the set.
+     *
+     * @return an enumeration of the elements in the set
+     */
+    public Iterator<Node> iterator() {
+        return new IteratorAdapter<Node>() {
+            private Node v = getFirstElement();
+
+            protected Node findNext() throws NoSuchElementException {
+                if (v != null) {
+                    final Node result = v;
+                    v = getNextElement(v);
+                    return result;
+                } else {
+                    throw new NoSuchElementException("at end");
+                }
+            }
+        };
+    }
+
+    /**
+     * returns the set as many objects as fit into the given array
+     *
+     * @param objects
+     * @return nodes in this set
+     */
+    public Node[] toArray(Node[] objects) {
+        if (objects == null)
+            throw new NullPointerException();
+        int i = 0;
+        for (Node node : this) {
+            if (i == objects.length)
+                break;
+            objects[i++] = node;
+        }
+        return objects;
+    }
+
+    /**
+     * todo: is this correct???
+     *
+     * @param objects
+     * @param <T>
+     * @return
+     */
+    public <T> T[] toArray(T[] objects) {
+        int i = 0;
+        for (Node node : this) {
+            if (i == objects.length)
+                break;
+            objects[i++] = (T) node;
+        }
+        return objects;
+    }
+
+    /**
+     * Returns the first element in the set.
+     *
+     * @return v Node
+     */
+    public Node getFirstElement() {
+        Node v;
+        for (v = getOwner().getFirstNode(); v != null; v = getOwner().getNextNode(v))
+            if (contains(v))
+                break;
+        return v;
+    }
+
+    /**
+     * Gets the successor element in the set.
+     *
+     * @param v Node
+     * @return a Node the successor of node v
+     */
+    public Node getNextElement(Node v) {
+        for (v = getOwner().getNextNode(v); v != null; v = getOwner().getNextNode(v))
+            if (contains(v))
+                break;
+        return v;
+    }
+
+    /**
+     * Gets the predecessor element in the set.
+     *
+     * @param v Node
+     * @return a Node the predecessor of node v
+     */
+    public Node getPrevElement(Node v) {
+        for (v = getOwner().getPrevNode(v); v != null; v = getOwner().getPrevNode(v))
+            if (contains(v))
+                break;
+        return v;
+    }
+
+
+    /**
+     * Returns the last element in the set.
+     *
+     * @return the Node the last element in the set
+     */
+    public Node getLastElement() {
+        Node v = null;
+        try {
+            for (v = getOwner().getLastNode(); v != null; v = getOwner().getPrevNode(v))
+                if (contains(v))
+                    break;
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+        return v;
+    }
+
+    /**
+     * returns a clone of this set
+     *
+     * @return a clone
+     */
+    public Object clone() {
+        NodeSet result = new NodeSet(getOwner());
+        for (Node v : this) result.add(v);
+        return result;
+    }
+
+    /**
+     * returns string rep
+     *
+     * @return string
+     */
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("[");
+        boolean first = true;
+        for (Object o : this) {
+            if (first)
+                first = false;
+            else
+                buf.append(", ");
+            buf.append(o);
+        }
+        buf.append("]");
+        return buf.toString();
+    }
+
+    /**
+     * do the two sets have a non-empty intersection?
+     *
+     * @param aset
+     * @return true, if intersection is non-empty
+     */
+    public boolean intersects(NodeSet aset) {
+        return bits.intersects(aset.bits);
+    }
+}
+
+// EOF
diff --git a/src/jloda/graph/Num2EdgeArray.java b/src/jloda/graph/Num2EdgeArray.java
new file mode 100644
index 0000000..2ba46ad
--- /dev/null
+++ b/src/jloda/graph/Num2EdgeArray.java
@@ -0,0 +1,109 @@
+/**
+ * Num2EdgeArray.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graph;
+
+/**
+ * A wrapper class for an array mapping numbers to edges
+ * Daniel Huson, 1.2007
+ */
+public class Num2EdgeArray {
+    Edge[] array = new Edge[0];
+
+    /**
+     * default constructor
+     */
+    public Num2EdgeArray() {
+        array = new Edge[0];
+    }
+
+    /**
+     * default constructor
+     *
+     * @param n number of edges (edges are  numbered 1..n+1)
+     */
+    public Num2EdgeArray(int n) {
+        array = new Edge[n + 1];
+    }
+
+    /**
+     * wrapper constructor
+     *
+     * @param array
+     */
+    public Num2EdgeArray(Edge[] array) {
+        this.array = array;
+    }
+
+    /**
+     * sets the wrapped array
+     *
+     * @param array
+     */
+    public void set(Edge[] array) {
+        this.array = array;
+    }
+
+    /**
+     * sets the i-th entry. Assumes the wrapped array has already been constructed or set
+     *
+     * @param i
+     * @param v
+     */
+    public void put(int i, Edge v) {
+        array[i] = v;
+    }
+
+    /**
+     * gets the -th entry
+     *
+     * @param i
+     * @return edge at position i of array
+     */
+    public Edge get(int i) {
+        return array[i];
+    }
+
+    /**
+     * gets the length of the array
+     *
+     * @return length
+     */
+    public int length() {
+        return array.length;
+    }
+
+    /**
+     * gets the wrapped array
+     *
+     * @return edge array
+     */
+    public Edge[] get() {
+        return array;
+    }
+
+    /**
+     * erase and resize  to hold (0,1,...,n)
+     *
+     * @param n
+     */
+    public void clear(int n) {
+        array = new Edge[n + 1];
+    }
+}
diff --git a/src/jloda/graph/Num2NodeArray.java b/src/jloda/graph/Num2NodeArray.java
new file mode 100644
index 0000000..b810c5e
--- /dev/null
+++ b/src/jloda/graph/Num2NodeArray.java
@@ -0,0 +1,110 @@
+/**
+ * Num2NodeArray.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graph;
+
+/**
+ * A wrapper class for an array mapping numbers to nodes
+ * Daniel Huson, 1.2007
+ */
+public class Num2NodeArray {
+    Node[] array = new Node[0];
+
+    /**
+     * default constructor
+     */
+    public Num2NodeArray() {
+        array = new Node[0];
+    }
+
+    /**
+     * default constructor
+     *
+     * @param n number of nodes (nodes are  numbered 1..n+1)
+     */
+    public Num2NodeArray(int n) {
+        array = new Node[n + 1];
+    }
+
+    /**
+     * wrapper constructor
+     *
+     * @param array
+     */
+    public Num2NodeArray(Node[] array) {
+        this.array = array;
+    }
+
+    /**
+     * sets the wrapped array
+     *
+     * @param array
+     */
+    public void set(Node[] array) {
+        this.array = array;
+    }
+
+    /**
+     * sets the i-th entry. Assumes the wrapped array has already been constructed or set
+     *
+     * @param i
+     * @param v
+     */
+    public void put(int i, Node v) {
+        array[i] = v;
+    }
+
+    /**
+     * gets the -th entry
+     *
+     * @param i
+     * @return node at position i of array
+     */
+    public Node get(int i) {
+        return array[i];
+    }
+
+    /**
+     * gets the length of the array
+     *
+     * @return length
+     */
+    public int length() {
+        return array.length;
+    }
+
+    /**
+     * gets the wrapped array
+     *
+     * @return node array
+     */
+    public Node[] get() {
+        return array;
+    }
+
+    /**
+     * erase and resize  to hold (0,1,...,n)
+     *
+     * @param n
+     */
+    public void clear(int n) {
+        array = new Node[n + 1];
+    }
+}
+
diff --git a/src/jloda/graphview/DefaultGraphDrawer.java b/src/jloda/graphview/DefaultGraphDrawer.java
new file mode 100644
index 0000000..96328fa
--- /dev/null
+++ b/src/jloda/graphview/DefaultGraphDrawer.java
@@ -0,0 +1,652 @@
+/**
+ * DefaultGraphDrawer.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.graph.*;
+import jloda.util.Basic;
+import jloda.util.Geometry;
+import jloda.util.NotOwnerException;
+import jloda.util.ProgramProperties;
+
+import java.awt.*;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+
+/**
+ * default graph drawer
+ * Daniel Huson, 12.2006
+ */
+public class DefaultGraphDrawer implements IGraphDrawer {
+    public static String DESCRIPTION = "Default graph drawer";
+
+    protected final GraphView graphView;
+    protected final Graph graph;
+    protected final Transform trans;
+
+    protected final NodeSet hitNodes;
+    protected final NodeSet hitNodeLabels;
+    protected final EdgeSet hitEdges;
+    protected final EdgeSet hitEdgeLabels;
+
+    private final LabelOverlapAvoider labelOverlapAvoider;
+
+    private NodeArray<Color> subtreeColors;
+
+    private INodeDrawer nodeDrawer;
+
+    private boolean radialLabels = false;
+
+    private Node foundNode = null;
+
+    private int auxilaryParameter = 0; // not used
+
+    /**
+     * constructor. Call only after graph and trans have been set for GraphView
+     *
+     * @param graphView
+     */
+    public DefaultGraphDrawer(GraphView graphView) {
+        this.graphView = graphView;
+        this.graph = graphView.getGraph();
+        trans = graphView.trans;
+        hitNodes = new NodeSet(graph);
+        hitNodeLabels = new NodeSet(graph);
+        hitEdges = new EdgeSet(graph);
+        hitEdgeLabels = new EdgeSet(graph);
+        labelOverlapAvoider = new LabelOverlapAvoider(graphView, 100);
+
+        setupGraphView(graphView);
+        nodeDrawer = new DefaultNodeDrawer(graphView);
+    }
+
+    /**
+     * setd up the graphview
+     *
+     * @param graphView
+     */
+    public void setupGraphView(GraphView graphView) {
+        graphView.setAllowInternalEdgePoints(false);
+        graphView.setMaintainEdgeLengths(true);
+        graphView.setAllowMoveNodes(true);
+        graphView.setAllowMoveInternalEdgePoints(false);
+        graphView.setKeepAspectRatio(true);
+        graphView.setAllowRotationArbitraryAngle(true);
+    }
+
+    /**
+     * paint the graph
+     *
+     * @param gc0
+     * @param rect
+     */
+    public void paint(Graphics gc0, Rectangle rect) {
+        final Graphics2D gc = (Graphics2D) gc0;
+        labelOverlapAvoider.resetHasNoOverlapToPreviouslyDrawnLabels();
+
+        Color tmpColor = gc0.getColor();
+        gc0.setColor(tmpColor);
+
+        gc.setFont(graphView.getFont());
+
+        BasicStroke stroke = new BasicStroke(1);
+        gc.setStroke(stroke);
+
+        MagnifierUtil magnifierUtil = new MagnifierUtil(graphView);
+
+        nodeDrawer.setup(graphView, gc);
+
+        try {
+// ensure that all nodes with labels have valid label rects
+            for (Node v = graph.getFirstNode(); v != null; v = graph.getNextNode(v)) {
+                final NodeView nv = graphView.getNV(v);
+                if (nv.getLabel() != null)
+                    nv.setLabelSize(Basic.getStringSize(gc, graphView.getLabel(v), graphView.getFont(v))); // ensure label rect is set
+            }
+
+            // edge label rects get computed during drawing!
+
+            if (graphView.getAutoLayoutLabels()) {
+                if (ProgramProperties.get("use-rtree-label-layouter", false)) {
+                    LabelLayoutRTree labelLayoutRTRee = new LabelLayoutRTree();
+                    labelLayoutRTRee.layout(graphView, gc);
+                } else {
+                    LabelLayouter layouter = new LabelLayouter(gc);
+                    layouter.initialLayout(trans, graphView);
+                    layouter.nonOverlappingLayout(trans, graphView);
+                }
+            }
+
+            // first we draw the unselected items:
+
+            // draw unselected edges
+            for (Edge e = graph.getFirstEdge(); e != null; e = graph.getNextEdge(e)) {
+                if (!graphView.selectedEdges.contains(e)) {
+                    final EdgeView ev = graphView.getEV(e);
+                    final Node v = graph.getSource(e);
+                    final Node w = graph.getTarget(e);
+                    final NodeView nv = graphView.getNV(v);
+                    final NodeView nw = graphView.getNV(w);
+
+                    Point2D nextToV = nw.getLocation();
+                    Point2D nextToW = nv.getLocation();
+
+                    if (nextToV == null || nextToW == null)
+                        continue;
+
+                    if (graphView.getInternalPoints(e) != null) {
+                        if (graphView.getInternalPoints(e).size() != 0) {
+                            nextToV = graphView.getInternalPoints(e).get(0);
+                            nextToW = graphView.getInternalPoints(e).get(
+                                    graphView.getInternalPoints(e).size() - 1);
+                        }
+                    }
+                    // if we are in magnifier mode and the edge does not contain any internal points,
+                    // add some
+                    magnifierUtil.addInternalPoints(e);
+
+                    boolean arcEdge = (ev.getShape() == EdgeView.ARC_LINE_EDGE || ev.getShape() == EdgeView.QUAD_EDGE);
+
+                    final Point pv = arcEdge ? trans.w2d(nv.getLocation()) : nv.computeConnectPoint(nextToV, trans);
+                    final Point pw = arcEdge ? trans.w2d(nw.getLocation()) : nw.computeConnectPoint(nextToW, trans);
+
+                    if (graphView.getEV(e).getLineWidth() != stroke.getLineWidth()) {
+                        stroke = new BasicStroke(graphView.getEV(e).getLineWidth());
+                        gc.setStroke(stroke);
+                    }
+
+                    if (graph.findDirectedEdge(w, v) != null)
+                        graphView.adjustBiEdge(pv, pw); // want parallel bi-edges
+
+                    ev.draw(gc, pv, pw, trans, false);
+
+                    if (ev.getLabel() != null && ev.getLabelVisible()) {
+                        ev.setLabelReferenceLocation(nextToV, nextToW, trans);
+                        //ev.setLabelSize(gc);
+                        ev.drawLabel(gc, trans, false);
+                    }
+                    magnifierUtil.removeAddedInternalPoints(e);
+                }
+            }
+
+            for (Node v = graph.getFirstNode(); v != null; v = graph.getNextNode(v)) {
+                if (!graphView.selectedNodes.contains(v)) {
+                    if (graphView.getNV(v).getLineWidth() != stroke.getLineWidth()) {
+                        stroke = new BasicStroke(graphView.getNV(v).getLineWidth());
+                        gc.setStroke(stroke);
+                    }
+                    nodeDrawer.draw(v, false);
+                }
+            }
+
+            for (Node v = graph.getFirstNode(); v != null; v = graph.getNextNode(v)) {
+                if (!graphView.selectedNodes.contains(v)) {
+                    final NodeView nv = graphView.getNV(v);
+                    if (graphView.getNV(v).getLineWidth() != stroke.getLineWidth()) {
+                        stroke = new BasicStroke(graphView.getNV(v).getLineWidth());
+                        gc.setStroke(stroke);
+                    }
+                    if (labelOverlapAvoider.hasNoOverlapToPreviouslyDrawnLabels(v, nv)) {
+                        nodeDrawer.drawLabel(v, false);
+                    }
+                }
+            }
+
+            //draw selected edges
+            for (Edge e = graphView.selectedEdges.getFirstElement(); e != null; e = graphView.selectedEdges.getNextElement(e)) {
+                final EdgeView ev = graphView.getEV(e);
+                final Node v = graph.getSource(e);
+                final NodeView nv = graphView.getNV(v);
+                final Node w = graph.getTarget(e);
+                final NodeView nw = graphView.getNV(w);
+
+                Point2D nextToV = nw.getLocation();
+                Point2D nextToW = nv.getLocation();
+
+                if (nextToV == null || nextToW == null)
+                    continue;
+
+                if (graphView.getInternalPoints(e) != null) {
+                    if (graphView.getInternalPoints(e).size() != 0) {
+                        nextToV = graphView.getInternalPoints(e).get(0);
+                        nextToW = graphView.getInternalPoints(e).get(graphView.getInternalPoints(e).size() - 1);
+                    }
+                }
+                // if we are in magnifier mode and the edge does not contain any internal points,
+                // add some
+                magnifierUtil.addInternalPoints(e);
+                final Point pv = nv.computeConnectPoint(nextToV, trans);
+                final Point pw = nw.computeConnectPoint(nextToW, trans);
+
+                if (graphView.getEV(e).getLineWidth() != stroke.getLineWidth()) {
+                    stroke = new BasicStroke(graphView.getEV(e).getLineWidth());
+                    gc.setStroke(stroke);
+                }
+
+                if (graph.findDirectedEdge(w, v) != null)
+                    graphView.adjustBiEdge(pv, pw); // want parallel bi-edges
+
+                ev.draw(gc, pv, pw, trans, true);
+
+                if (graphView.getLabel(e) != null && graphView.getLabelVisible(e)) {
+                    ev.setLabelReferenceLocation(nextToV, nextToW, trans);
+                    //ev.setLabelSize(gc);
+                    ev.drawLabel(gc, trans, true);
+                }
+                magnifierUtil.removeAddedInternalPoints(e);
+            }
+
+
+            // then we draw and highlight the selected nodes:
+            for (Node v = graphView.selectedNodes.getFirstElement(); v != null; v = graphView.selectedNodes.getNextElement(v)) {
+                final NodeView nv = graphView.getNV(v);
+                if (nv.getLineWidth() != stroke.getLineWidth()) {
+                    stroke = new BasicStroke(graphView.getNV(v).getLineWidth());
+                    gc.setStroke(stroke);
+                }
+                nodeDrawer.drawNodeAndLabel(v, true);
+            }
+
+            if (getFoundNode() != null) {
+                Node v = getFoundNode();
+                NodeView nv = graphView.getNV(v);
+                if (nv.getLabel() != null)
+                    nv.setLabelSize(Basic.getStringSize(gc, graphView.getLabel(v), graphView.getFont(v)));
+                Shape shape = nv.getLabelShape(trans);
+                gc.setColor(Color.YELLOW);
+                gc.fill(shape);
+                gc.setColor(ProgramProperties.SELECTION_COLOR_DARKER);
+                nodeDrawer.drawNodeAndLabel(v, false);
+            }
+        } catch (NotOwnerException ex) {
+
+        }
+    }
+
+    /**
+     * compute an embedding of the graph
+     *
+     * @return false, as this drawer cannot compute an embedding
+     */
+    public boolean computeEmbedding(boolean toScale) {
+        return false;
+    }
+
+    /**
+     * get all nodes hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return nodes hit
+     */
+    public NodeSet getHitNodes(int x, int y) {
+        hitNodes.clear();
+        for (Node v = graph.getLastNode(); v != null; v = graph.getPrevNode(v)) {
+            NodeView nv = graphView.getNV(v);
+            if (nv.getLocation() != null && nv.contains(trans, x, y))
+                hitNodes.add(v);
+        }
+        return hitNodes;
+    }
+
+    /**
+     * get all nodes hit by mouse at (x,y) with tolerance of d pixels
+     *
+     * @param x
+     * @param y
+     * @param d tolerance
+     * @return nodes hit
+     */
+    public NodeSet getHitNodes(int x, int y, int d) {
+        hitNodes.clear();
+
+        final Rectangle rect = new Rectangle(x - d, y - d, 2 * d, 2 * d);
+        for (Node v = graph.getLastNode(); v != null; v = graph.getPrevNode(v)) {
+            final NodeView nv = graphView.getNV(v);
+            if (nv.getLocation() != null && (rect.intersects(nv.getBox(trans)) || nv.contains(trans, x, y))) {
+                hitNodes.add(v);
+            }
+        }
+        return hitNodes;
+    }
+
+    /**
+     * get all node labels hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return node labels
+     */
+    public NodeSet getHitNodeLabels(int x, int y) {
+        hitNodeLabels.clear();
+        for (Node v = graph.getLastNode(); v != null; v = graph.getPrevNode(v)) {
+            final NodeView nv = graphView.getNV(v);
+            if (nv != null && nv.getLabel() != null && nv.getLocation() != null && nv.getLabelVisible()
+                    && (graphView.getSelected(v) || labelOverlapAvoider.isVisible(v))
+                    && (nv.getLabelShape(trans) != null && nv.getLabelShape(trans).contains(x, y)))
+                hitNodeLabels.add(v);
+        }
+        return hitNodeLabels;
+    }
+
+    /**
+     * get all nodes contained in rect
+     *
+     * @param rect
+     * @return nodes contained in rect
+     */
+    public NodeSet getHitNodes(Rectangle rect) {
+        hitNodes.clear();
+
+        for (Node v = graph.getLastNode(); v != null; v = graph.getPrevNode(v)) {
+            final NodeView nv = graphView.getNV(v);
+            if (nv.isEnabled() && nv.getLocation() != null) {
+                final Rectangle nodeRect = nv.getBox(trans);
+                if (nodeRect != null && rect.contains(nodeRect))
+                    hitNodes.add(v);
+            }
+        }
+        return hitNodes;
+    }
+
+    /**
+     * get all node labels contained in rect
+     *
+     * @param rect
+     * @return node labels contained in rect
+     */
+    public NodeSet getHitNodeLabels(Rectangle rect) {
+        hitNodeLabels.clear();
+        for (Node v = graph.getLastNode(); v != null; v = graph.getPrevNode(v)) {
+            final NodeView nv = graphView.getNV(v);
+            if (nv.getLabel() != null && nv.getLocation() != null
+                    && nv.getLabelVisible() &&
+                    (graphView.getSelected(v) || labelOverlapAvoider.isVisible(v))) {
+                final Rectangle labelRect = nv.getLabelRect(trans);
+                if (labelRect != null && rect.contains(labelRect))
+                    hitNodeLabels.add(v);
+            }
+        }
+        return hitNodeLabels;
+    }
+
+
+    /**
+     * get all edges hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return edges hits
+     */
+    public EdgeSet getHitEdges(int x, int y) {
+        hitEdges.clear();
+        final MagnifierUtil magnifierUtil = new MagnifierUtil(graphView);
+
+        for (Edge e = graph.getLastEdge(); e != null; e = graph.getPrevEdge(e)) {
+            final EdgeView ev = graphView.getEV(e);
+            if (ev.isEnabled() && ev.getColor() != null) {
+                final Node v = graph.getSource(e);
+                final Node w = graph.getTarget(e);
+                final NodeView nv = graphView.getNV(v);
+                final NodeView nw = graphView.getNV(w);
+                if (nv.getLocation() == null || nw.getLocation() == null)
+                    continue;
+
+                magnifierUtil.addInternalPoints(e);
+
+                final Point pv = nv.computeConnectPoint(nw.getLocation(), trans);
+                final Point pw = nw.computeConnectPoint(nv.getLocation(), trans);
+
+                if (graph.findDirectedEdge(graph.getTarget(e), graph.getSource(e)) != null)
+                    graphView.adjustBiEdge(pv, pw); // adjust for parallel edge
+
+                if (ev.hitEdge(pv, pw, trans, x, y, 3))
+                    hitEdges.add(e);
+                magnifierUtil.removeAddedInternalPoints(e);
+            }
+        }
+        return hitEdges;
+    }
+
+    /**
+     * get all edge labels hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return edge labels
+     */
+    public EdgeSet getHitEdgeLabels(int x, int y) {
+        hitEdgeLabels.clear();
+
+        for (Edge e = graph.getLastEdge(); e != null; e = graph.getPrevEdge(e)) {
+            EdgeView ev = graphView.getEV(e);
+            if (ev.isEnabled() && ev.getLabelVisible() && ev.getLabel() != null) {
+                Shape labelShape = ev.getLabelShape(trans);
+                if (labelShape != null &&
+                        labelShape.contains(x, y)) {
+                    hitEdgeLabels.add(e);
+                }
+            }
+        }
+        return hitEdgeLabels;
+    }
+
+    /**
+     * get all edges contained in rect
+     *
+     * @param rect
+     * @return edges contained in rect
+     */
+    public EdgeSet getHitEdges(Rectangle rect) {
+        hitEdges.clear();
+        for (Edge e = graph.getLastEdge(); e != null; e = graph.getPrevEdge(e)) {
+            if (graphView.getLocation(e.getSource()) != null && graphView.getLocation(e.getTarget()) != null &&
+                    rect.contains(trans.w2d(graphView.getLocation(e.getSource())))
+                    && rect.contains(trans.w2d(graphView.getLocation(e.getTarget()))))
+                hitEdges.add(e);
+        }
+        return hitEdges;
+    }
+
+    /**
+     * get all edge labels contained in rect
+     *
+     * @param rect
+     * @return edges contained in rect
+     */
+    public EdgeSet getHitEdgeLabels(Rectangle rect) {
+        hitEdgeLabels.clear();
+        for (Edge e = graph.getLastEdge(); e != null; e = graph.getPrevEdge(e)) {
+            EdgeView ev = graphView.getEV(e);
+            if (ev.getLabel() != null && ev.getLabelVisible() &&
+                    rect.contains(ev.getLabelRect(trans))) {
+                hitEdgeLabels.add(e);
+            }
+        }
+        return hitEdgeLabels;
+    }
+
+    /**
+     * get the set of flipped nodes
+     *
+     * @return flipped nodes
+     */
+    public NodeSet getFlipNodes() {
+        return null;
+    }
+
+    /**
+     * set the set of flipped nodes
+     */
+    public void setFlipNodes(NodeSet flipNodes) {
+    }
+
+    /**
+     * set the default label positions for nodes and edges
+     *
+     * @param resetAll if true, reset positions for user-placed labels, too
+     */
+    public void resetLabelPositions(boolean resetAll) {
+        for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+            NodeView nv = graphView.getNV(v);
+            if (nv.getLabelVisible() && nv.getLabel() != null && nv.getLabel().length() > 0
+                    && (resetAll || nv.getLabelLayout() != NodeView.USER)) {
+                if (v.getDegree() == 1) {
+                    final Edge e = v.getFirstAdjacentEdge();
+                    final Node w = e.getOpposite(v);
+                    final Point pV = trans.w2d(nv.getLocation());
+                    Point2D nextToV = graphView.getNV(w).getLocation();
+                    if (graphView.getInternalPoints(e) != null &&
+                            graphView.getInternalPoints(e).size() != 0) {
+                        if (v == e.getSource())
+                            nextToV = graphView.getInternalPoints(e).get(0);
+                        else
+                            nextToV = graphView.getInternalPoints(e).get(
+                                    graphView.getInternalPoints(e).size() - 1);
+                    }
+                    Point pW = trans.w2d(nextToV);
+
+                    double angle = Geometry.moduloTwoPI(Geometry.computeAngle(Geometry.diff(pW, pV)));
+                    if (angle > 1.75 * Math.PI)
+                        graphView.getNV(v).setLabelLayout(NodeView.WEST);
+                    else if (angle > 1.25 * Math.PI)
+                        graphView.getNV(v).setLabelLayout(NodeView.NORTH);
+                    else if (angle > 0.75 * Math.PI)
+                        graphView.getNV(v).setLabelLayout(NodeView.EAST);
+                    else if (angle > 0.25 * Math.PI)
+                        graphView.getNV(v).setLabelLayout(NodeView.SOUTH);
+                    else
+                        graphView.getNV(v).setLabelLayout(NodeView.WEST);
+                } else
+                    graphView.getNV(v).setLabelLayout(NodeView.NORTHEAST);
+            }
+        }
+        for (Edge e = graph.getFirstEdge(); e != null; e = e.getNext()) {
+            EdgeView ev = graphView.getEV(e);
+            if (resetAll || ev.getLabelLayout() != EdgeView.USER)
+                ev.setLabelLayout(EdgeView.CENTRAL);
+        }
+    }
+
+    /**
+     * gets the label overlap avoider
+     *
+     * @return label overlap avoider
+     */
+    public LabelOverlapAvoider getLabelOverlapAvoider() {
+        return labelOverlapAvoider;
+    }
+
+
+    /**
+     * to support bounding-box oriented drawers, report any node whose label has been interactively moved
+     *
+     * @param v
+     */
+    public void setNodeHasMovedLabel(Node v) {
+    }
+
+    /**
+     * to support bounding-box oriented drawers, report any edge whose label has been interactively moved
+     *
+     * @param e
+     */
+    public void setEdgesHasMovedLabel(Edge e) {
+    }
+
+    public void setEdgesHasMovedInternalPoints(Edge e) {
+    }
+
+
+    /**
+     * get the set of collapsed nodes
+     *
+     * @return collapsed nodes
+     */
+    public NodeSet getCollapsedNodes() {
+        return null;
+    }
+
+    /**
+     * set the set of collapsed nodes
+     */
+    public void setCollapsedNodes(NodeSet collapsedNodes) {
+    }
+
+    /**
+     * gets the bounding box of the graph in world coordinates
+     *
+     * @return bbox
+     */
+    public Rectangle2D getBBox() {
+        return graphView.getBBox();
+    }
+
+    /**
+     * rotate node labels to match edge directions?
+     *
+     * @param radialLabels
+     */
+    public void setRadialLabels(boolean radialLabels) {
+    }
+
+    /**
+     * node found by search, must be drawn  if !=null
+     *
+     * @return found node
+     */
+    public Node getFoundNode() {
+        return foundNode;
+    }
+
+    /**
+     * node found by search, must be drawn if !=null
+     *
+     * @param foundNode
+     */
+    public void setFoundNode(Node foundNode) {
+        this.foundNode = foundNode;
+    }
+
+    /**
+     * set the auxilary parameter
+     *
+     * @param parameter
+     */
+    public void setAuxilaryParameter(int parameter) {
+    }
+
+    /**
+     * get the auxilary parameter
+     *
+     * @return auxilary parameter
+     */
+    public int getAuxilaryParameter() {
+        return 0;
+    }
+
+    public INodeDrawer getNodeDrawer() {
+        return nodeDrawer;
+    }
+
+    public void setNodeDrawer(INodeDrawer nodeDrawer) {
+        this.nodeDrawer = nodeDrawer;
+    }
+}
diff --git a/src/jloda/graphview/DefaultNodeDrawer.java b/src/jloda/graphview/DefaultNodeDrawer.java
new file mode 100644
index 0000000..ac4f042
--- /dev/null
+++ b/src/jloda/graphview/DefaultNodeDrawer.java
@@ -0,0 +1,353 @@
+/**
+ * DefaultNodeDrawer.java
+ * Copyright (C) 2016 Daniel H. Huson
+ * <p>
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package jloda.graphview;
+
+import gnu.jpdf.PDFGraphics;
+import jloda.graph.Node;
+import jloda.util.Geometry;
+import jloda.util.ProgramProperties;
+
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.io.IOException;
+
+/**
+ * default node drawer
+ * Daniel Huson, 1.2013
+ */
+public class DefaultNodeDrawer implements INodeDrawer {
+    private GraphView graphView;
+    private Transform trans;
+    private Graphics2D gc;
+
+    /**
+     * constructor
+     *
+     * @param graphView
+     */
+    public DefaultNodeDrawer(GraphView graphView) {
+        this.graphView = graphView;
+        this.trans = graphView.trans;
+    }
+
+    /**
+     * setup data
+     *
+     * @param graphView
+     * @param gc
+     */
+    public void setup(GraphView graphView, Graphics2D gc) {
+        this.graphView = graphView;
+        this.trans = graphView.trans;
+        this.gc = gc;
+    }
+
+    /**
+     * draw the node
+     *
+     * @param selected
+     */
+    public void draw(Node v, boolean selected) {
+        NodeView nv = graphView.getNV(v);
+        if (selected)
+            hilite(nv);
+        draw(nv);
+    }
+
+    /**
+     * draw the label of the node
+     *
+     * @param selected
+     */
+    public void drawLabel(Node v, boolean selected) {
+        NodeView nv = graphView.getNV(v);
+        drawLabel(nv, graphView.getFont(), selected);
+    }
+
+    /**
+     * draw the node and the label
+     *
+     * @param hilited
+     */
+    public void drawNodeAndLabel(Node v, boolean hilited) {
+        NodeView nv = graphView.getNV(v);
+        draw(nv, hilited);
+        drawLabel(nv, graphView.getFont(), hilited);
+    }
+
+    /**
+     * Draw the node.
+     *
+     * @param hilited
+     */
+    private void draw(NodeView nv, boolean hilited) {
+        if (hilited)
+            hilite(nv);
+        draw(nv);
+    }
+
+    /**
+     * Draw the node.
+     */
+    private void draw(NodeView nv) {
+        if (nv.getLocation() == null)
+            return; // no location, don't draw
+        Point apt = trans.w2d(nv.getLocation());
+
+        int scaledWidth;
+        int scaledHeight;
+        if (nv.getFixedSize()) {
+            scaledWidth = nv.getWidth();
+            scaledHeight = nv.getHeight();
+        } else {
+            scaledWidth = NodeView.computeScaledWidth(trans, nv.getWidth());
+            scaledHeight = NodeView.computeScaledHeight(trans, nv.getHeight());
+        }
+
+        apt.x -= (scaledWidth >> 1);
+        apt.y -= (scaledHeight >> 1);
+
+        if (nv.getBorderColor() != null) {   // selected
+            if (nv.isEnabled())
+                gc.setColor(nv.getBorderColor());
+            else
+                gc.setColor(NodeView.DISABLED_COLOR);
+            if (nv.getShape() == NodeView.OVAL_NODE) {
+                gc.drawOval(apt.x - 2, apt.y - 2, scaledWidth + 4, scaledHeight + 4);
+                gc.drawOval(apt.x - 3, apt.y - 3, scaledWidth + 6, scaledHeight + 6);
+            } else if (nv.getShape() == NodeView.RECT_NODE) {
+                // default shape==GraphView.RECT_NODE
+                gc.drawRect(apt.x - 2, apt.y - 2, scaledWidth + 4, scaledHeight + 4);
+                gc.drawRect(apt.x - 3, apt.y - 3, scaledWidth + 6, scaledHeight + 6);
+            } else if (nv.getShape() == NodeView.TRIANGLE_NODE) {
+                Shape polygon = new Polygon(new int[]{apt.x - 2, apt.x + scaledWidth + 2, apt.x + scaledHeight / 2},
+                        new int[]{apt.y + scaledHeight + 2, apt.y + scaledHeight + 2, apt.y - 2}, 3);
+                gc.draw(polygon);
+            } else if (nv.getShape() == NodeView.DIAMOND_NODE) {
+                Shape polygon = new Polygon(new int[]{apt.x - 2, apt.x + scaledWidth / 2, apt.x + scaledWidth + 2, apt.x + scaledHeight / 2},
+                        new int[]{apt.y + scaledHeight / 2, apt.y + scaledHeight + 2, apt.y + scaledHeight / 2, apt.y - 2}, 4);
+                gc.draw(polygon);
+            }
+
+            // else draw nothing
+        }
+
+        if (nv.getBackgroundColor() != null) {
+            if (nv.isEnabled())
+                gc.setColor(nv.getBackgroundColor());
+            else
+                gc.setColor(Color.WHITE);
+            if (nv.getShape() == NodeView.OVAL_NODE)
+                gc.fillOval(apt.x, apt.y, scaledWidth, scaledHeight);
+            else if (nv.getShape() == NodeView.RECT_NODE)
+                gc.fillRect(apt.x, apt.y, scaledWidth, scaledHeight);
+            else if (nv.getShape() == NodeView.TRIANGLE_NODE) {
+                Shape polygon = new Polygon(new int[]{apt.x, apt.x + scaledWidth, apt.x + scaledHeight / 2},
+                        new int[]{apt.y + scaledHeight, apt.y + scaledHeight, apt.y}, 3);
+                gc.fill(polygon);
+            } else if (nv.getShape() == NodeView.DIAMOND_NODE) {
+                Shape polygon = new Polygon(new int[]{apt.x, apt.x + scaledWidth / 2, apt.x + scaledWidth, apt.x + scaledHeight / 2},
+                        new int[]{apt.y + scaledHeight / 2, apt.y + scaledHeight, apt.y + scaledHeight / 2, apt.y}, 4);
+                gc.fill(polygon);
+            }
+        }
+        if (nv.getColor() != null) {
+            if (nv.isEnabled())
+                gc.setColor(nv.getColor());
+            else
+                gc.setColor(NodeView.DISABLED_COLOR);
+            if (nv.getShape() == NodeView.OVAL_NODE)
+                gc.drawOval(apt.x, apt.y, scaledWidth, scaledHeight);
+            else if (nv.getShape() == NodeView.RECT_NODE)
+                gc.drawRect(apt.x, apt.y, scaledWidth, scaledHeight);
+            else if (nv.getShape() == NodeView.TRIANGLE_NODE) {
+                Shape polygon = new Polygon(new int[]{apt.x, apt.x + scaledWidth, apt.x + scaledHeight / 2},
+                        new int[]{apt.y + scaledHeight, apt.y + scaledHeight, apt.y}, 3);
+                gc.draw(polygon);
+            } else if (nv.getShape() == NodeView.DIAMOND_NODE) {
+                Shape polygon = new Polygon(new int[]{apt.x, apt.x + scaledWidth / 2, apt.x + scaledWidth, apt.x + scaledHeight / 2},
+                        new int[]{apt.y + scaledHeight / 2, apt.y + scaledHeight, apt.y + scaledHeight / 2, apt.y}, 4);
+                gc.draw(polygon);
+            }
+        }
+    }
+
+    /**
+     * Highlights the node.
+     */
+    private void hilite(NodeView nv) {
+        if (nv.getLocation() == null)
+            return;
+        int scaledWidth;
+        int scaledHeight;
+        if (nv.getShape() == NodeView.NONE_NODE) {
+            scaledWidth = scaledHeight = 2;
+        } else {
+            if (nv.getFixedSize()) {
+                scaledWidth = nv.getWidth();
+                scaledHeight = nv.getHeight();
+            } else {
+                scaledWidth = NodeView.computeScaledWidth(trans, nv.getWidth());
+                scaledHeight = NodeView.computeScaledHeight(trans, nv.getHeight());
+            }
+        }
+
+        Point apt = trans.w2d(nv.getLocation());
+        apt.x -= (scaledWidth >> 1);
+        apt.y -= (scaledHeight >> 1);
+
+        Shape shape = new Rectangle(apt.x - 2, apt.y - 2, scaledWidth + 4, scaledHeight + 4);
+        gc.setColor(ProgramProperties.SELECTION_COLOR);
+        gc.fill(shape);
+        gc.setColor(ProgramProperties.SELECTION_COLOR_DARKER);
+        final Stroke oldStroke = gc.getStroke();
+        gc.setStroke(NodeView.NORMAL_STROKE);
+        gc.draw(shape);
+        gc.setStroke(oldStroke);
+    }
+
+
+    /**
+     * Highlights the node label
+     *
+     * @param defaultFont font to use if node has no font set
+     */
+    public void hiliteLabel(NodeView nv, Font defaultFont) {
+        if (nv.getLocation() == null)
+            return;
+
+        if (nv.getLabelColor() != null && nv.getLabel() != null && nv.isLabelVisible() && nv.getLabel().length() > 0) {
+            if (nv.getFont() != null)
+                gc.setFont(nv.getFont());
+            else if (defaultFont != null)
+                gc.setFont(defaultFont);
+            Shape shape = (nv.getLabelAngle() == 0 ? nv.getLabelRect(trans) : nv.getLabelShape(trans));
+            gc.setColor(ProgramProperties.SELECTION_COLOR);
+            gc.fill(shape);
+            gc.setColor(ProgramProperties.SELECTION_COLOR_DARKER);
+            final Stroke oldStroke = gc.getStroke();
+            gc.setStroke(ViewBase.NORMAL_STROKE);
+            gc.draw(shape);
+            gc.setStroke(oldStroke);
+        }
+    }
+
+    /**
+     * Draws the node's label and the image, if set
+     */
+    private void drawLabel(NodeView nv, Font defaultFont, boolean hilited) {
+        if (nv.getLocation() == null)
+            return;
+
+        if (nv.getLabelColor() != null && nv.getLabel() != null && nv.getLabel().length() > 0) {
+            if (hilited)
+                hiliteLabel(nv, defaultFont);
+            else {
+                //labelShape = null;
+                //gc.setColor(Color.WHITE);
+                //gc.fill(getLabelRect(trans));
+                if (nv.getLabelBackgroundColor() != null && nv.isLabelVisible() && nv.isEnabled()) {
+                    gc.setColor(nv.getLabelBackgroundColor());
+                    gc.fill(nv.getLabelShape(trans));
+                }
+            }
+
+            if (nv.getFont() != null)
+                gc.setFont(nv.getFont());
+            else if (defaultFont != null)
+                gc.setFont(defaultFont);
+
+            if (nv.isEnabled())
+                gc.setColor(nv.getLabelColor());
+            else
+                gc.setColor(NodeView.DISABLED_COLOR);
+
+            Point2D apt = nv.getLabelPosition(trans);
+
+            if (apt != null) {
+                if (nv.isLabelVisible()) {
+                    if (nv.getLabelAngle() == 0) {
+                        gc.drawString(nv.getLabel(), (int) apt.getX(), (int) apt.getY());
+                    } else {
+                        float labelAngle = nv.getLabelAngle() + 0.00001f; // to ensure that labels all get same orientation in
+
+                        Dimension labelSize = nv.getLabelSize();
+                        if (gc instanceof PDFGraphics) {
+                            if (labelAngle >= 0.5 * Math.PI && labelAngle <= 1.5 * Math.PI) {
+                                apt = Geometry.translateByAngle(apt, labelAngle, labelSize.getWidth());
+                                ((PDFGraphics) gc).drawString(nv.getLabel(), (float) (apt.getX()), (float) (apt.getY()), (float) (labelAngle - Math.PI));
+                            } else {
+                                ((PDFGraphics) gc).drawString(nv.getLabel(), (float) (apt.getX()), (float) (apt.getY()), labelAngle);
+                            }
+                        } else {
+                            // save current transform:
+                            AffineTransform saveTransform = gc.getTransform();
+                            // a vertical phylogram view
+
+                            /*
+                            AffineTransform localTransform =  gc.getTransform();
+                            // rotate label to desired angle
+                            if (labelAngle >= 0.5 * Math.PI && labelAngle <= 1.5 * Math.PI) {
+                                double d = getLabelSize().getWidth();
+                                apt = Geometry.translateByAngle(apt, labelAngle, d);
+                                localTransform.rotate(Geometry.moduloTwoPI(labelAngle - Math.PI), apt.getX(), apt.getY());
+                            } else
+                                localTransform.rotate(labelAngle, apt.getX(), apt.getY());
+                           gc.setTransform(localTransform);
+                            */
+                            // todo: this doesn't work well as the angles aren't drawn correctly
+
+                            // rotate label to desired angle
+                            if (labelAngle >= 0.5 * Math.PI && labelAngle <= 1.5 * Math.PI) {
+                                apt = Geometry.translateByAngle(apt, labelAngle, nv.getLabelSize().getWidth());
+                                gc.rotate(Geometry.moduloTwoPI(labelAngle - Math.PI), apt.getX(), apt.getY());
+                            } else {
+                                gc.rotate(labelAngle, apt.getX(), apt.getY());
+                            }
+                            gc.drawString(nv.getLabel(), (int) apt.getX(), (int) apt.getY());
+                            gc.setTransform(saveTransform);
+                        }
+                    }
+                }
+            }
+        }
+        // draw the image:
+        if (nv.getImage() != null && nv.getImage().isVisible()) {
+            nv.getImage().draw(nv, trans, gc, hilited);
+        }
+
+        if (NodeView.descriptionWriter != null && nv.getLabelVisible() && nv.getLabel() != null
+                && nv.getLabel().length() > 0) {
+            Rectangle bounds;
+            if (nv.getLabelAngle() == 0) {
+                bounds = nv.getLabelRect(trans).getBounds();
+            } else {
+                bounds = nv.getLabelShape(trans).getBounds();
+            }
+            try {
+                NodeView.descriptionWriter.write(String.format("%s; x=%d y=%d w=%d h=%d\n", nv.getLabel(),
+                        bounds.x, bounds.y, bounds.width, bounds.height));
+            } catch (IOException e) {
+                // silently ignore
+            }
+        }
+    }
+}
diff --git a/src/jloda/graphview/EdgeActionAdapter.java b/src/jloda/graphview/EdgeActionAdapter.java
new file mode 100644
index 0000000..7ffd38f
--- /dev/null
+++ b/src/jloda/graphview/EdgeActionAdapter.java
@@ -0,0 +1,108 @@
+/**
+ * EdgeActionAdapter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.graph.Edge;
+import jloda.graph.EdgeSet;
+
+//import jloda.util.*;
+
+/**
+ * This provides a class that implements the EdgeAction interface, but does
+ * nothing.
+ */
+public class EdgeActionAdapter implements EdgeActionListener {
+    /**
+     * Called when creating a new edge.
+     *
+     * @param e Edge
+     */
+    public void doNew(Edge e) {
+    }
+
+    /**
+     * Called when deleting a new edge.
+     *
+     * @param e Edge
+     */
+    public void doDelete(Edge e) {
+    }
+
+    /**
+     * Called when edges are clicked on.
+     *
+     * @param edges  EdgeSet
+     * @param clicks int
+     */
+    public void doClick(EdgeSet edges, int clicks) {
+    }
+
+    /**
+     * Called when edges are pressed.
+     *
+     * @param edges EdgeSet
+     */
+    public void doPress(EdgeSet edges) {
+    }
+
+    /**
+     * Called when edges are released.
+     *
+     * @param edges EdgeSet
+     */
+    public void doRelease(EdgeSet edges) {
+    }
+
+    /**
+     * Called when edges are selected.
+     *
+     * @param edges EdgeSet
+     */
+    public void doSelect(EdgeSet edges) {
+    }
+
+    /**
+     * Called when edges are de-selected.
+     *
+     * @param edges EdgeSet
+     */
+    public void doDeselect(EdgeSet edges) {
+    }
+
+    /**
+     * Called when edge labels are clicked on.
+     *
+     * @param edges  EdgeSet
+     * @param clicks int
+     */
+    public void doClickLabel(EdgeSet edges, int clicks) {
+    }
+
+    /**
+     * Called when edge labels were moved
+     *
+     * @param edges EdgeSet
+     */
+    public void doLabelMoved(EdgeSet edges) {
+    }
+
+}
+
+// EOF
diff --git a/src/jloda/graphview/EdgeActionListener.java b/src/jloda/graphview/EdgeActionListener.java
new file mode 100644
index 0000000..4869cda
--- /dev/null
+++ b/src/jloda/graphview/EdgeActionListener.java
@@ -0,0 +1,108 @@
+/**
+ * EdgeActionListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+/**
+ * @version $Id: EdgeActionListener.java,v 1.5 2007-01-05 17:38:14 huson Exp $
+ *
+ * Actions performed during interaction.
+ *
+ * @author Daniel Huson
+ * 6.2001
+ */
+
+import jloda.graph.Edge;
+import jloda.graph.EdgeSet;
+
+//import jloda.util.*;
+
+/**
+ * Interface for actions performed on edges during GraphView interaction.
+ */
+public interface EdgeActionListener {
+    /**
+     * Called when creating a new edge.
+     *
+     * @param e Edge
+     */
+    void doNew(Edge e);
+
+    /**
+     * Called when deleting a new edge.
+     *
+     * @param e Edge
+     */
+    void doDelete(Edge e);
+
+    /**
+     * Called when edges are clicked on.
+     *
+     * @param edges  EdgeSet
+     * @param clicks int
+     */
+    void doClick(EdgeSet edges, int clicks);
+
+    /**
+     * Called when edge labels are clicked on.
+     *
+     * @param edges  EdgeSet
+     * @param clicks int
+     */
+    void doClickLabel(EdgeSet edges, int clicks);
+
+
+    /**
+     * Called when edges are pressed.
+     *
+     * @param edges EdgeSet
+     */
+    void doPress(EdgeSet edges);
+
+    /**
+     * Called when edges are released.
+     *
+     * @param edges EdgeSet
+     */
+    void doRelease(EdgeSet edges);
+
+    /**
+     * Called when edges are selected.
+     *
+     * @param edges EdgeSet
+     */
+    void doSelect(EdgeSet edges);
+
+    /**
+     * Called when edges are de-selected.
+     *
+     * @param edges EdgeSet
+     */
+    void doDeselect(EdgeSet edges);
+
+    /**
+     * Called when edge labels were moved
+     *
+     * @param edges EdgeSet
+     */
+    void doLabelMoved(EdgeSet edges);
+
+}
+
+// EOF
diff --git a/src/jloda/graphview/EdgeView.java b/src/jloda/graphview/EdgeView.java
new file mode 100644
index 0000000..64997b6
--- /dev/null
+++ b/src/jloda/graphview/EdgeView.java
@@ -0,0 +1,1141 @@
+/**
+ * EdgeView.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * Edge visualization
+ *
+ * @version $Id: EdgeView.java,v 1.61 2010-05-18 15:42:26 huson Exp $
+ *
+ * @author Daniel Huson
+ */
+package jloda.graphview;
+
+import gnu.jpdf.PDFGraphics;
+import jloda.util.Basic;
+import jloda.util.Geometry;
+import jloda.util.ProgramProperties;
+import jloda.util.parse.NexusStreamParser;
+
+import java.awt.*;
+import java.awt.geom.*;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.Writer;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * Edge visualization
+ */
+final public class EdgeView extends ViewBase implements Cloneable { //, IEdgeView {
+    private Font font;
+    private byte shape = POLY_EDGE;
+    private byte direction = DIRECTED;
+    private java.util.List<Point2D> internalPoints = null;
+    private Point labelReferencePoint = null; // label reference point in device coordinates
+
+    /**
+     * polygonal edge
+     */
+    public final static byte POLY_EDGE = 1;
+    /**
+     * arc+line edge
+     */
+    public final static byte ARC_LINE_EDGE = 2;
+    /**
+     * quadratic polynomial edge. Must contain precisely one interior control point.
+     */
+    public final static byte QUAD_EDGE = 3;
+    /**
+     * cubic polynomial edge. Must contain precisely two interior control points.
+     */
+    public final static byte CUBIC_EDGE = 4;
+    /**
+     * straight edge
+     */
+    public final static byte STRAIGHT_EDGE = 5;
+    /**
+     * rounded edge
+     */
+    public final static byte ROUNDED_EDGE = 6;
+    public static int ROUNDED_EDGE_INCREMENT = 50;
+
+    /**
+     * Edge type.
+     */
+    public static final byte UNDIRECTED = 1;
+    public static final byte DIRECTED = 2;
+    public static final byte BIDIRECTED = 3;
+    public static final byte RDIRECTED = 4;
+
+    /**
+     * Construct an edge view.
+     */
+    public EdgeView() {
+        labelLayout = CENTRAL;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param src EdgeView
+     */
+    public EdgeView(EdgeView src) {
+        this();
+        copy(src);
+    }
+
+    /**
+     * copies the given src edge view
+     *
+     * @param src
+     */
+    public void copy(EdgeView src) {
+        super.copy(src);
+        linewidth = src.linewidth;
+        shape = src.shape;
+        direction = src.direction;
+        if (src.internalPoints == null)
+            internalPoints = null;
+        else {
+            internalPoints = new LinkedList<>();
+            for (Point2D apt : src.internalPoints) {
+                internalPoints.add((Point2D) apt.clone());
+            }
+        }
+        labelLayout = src.getLabelLayout();
+    }
+
+    /**
+     * Gets the direction.
+     *
+     * @return direction int
+     */
+    public int getDirection() {
+        return direction;
+    }
+
+
+    /**
+     * Sets the edge shape.
+     *
+     * @param a int
+     */
+    public void setShape(byte a) {
+        shape = a;
+    }
+
+    /**
+     * gets the edge shape
+     *
+     * @return edge shape
+     */
+    public byte getShape() {
+        return shape;
+    }
+
+    /**
+     * Sets the edge direction.
+     *
+     * @param a int
+     */
+    public void setDirection(byte a) {
+        direction = a;
+    }
+
+
+    /**
+     * gets the font
+     *
+     * @return font used for drawing label, or null, if default is to be used
+     */
+    public Font getFont() {
+        return font;
+    }
+
+    /**
+     * sets the font
+     *
+     * @param font
+     */
+    public void setFont(Font font) {
+        this.font = font;
+    }
+
+    /**
+     * Draw the edge given device coordinates.
+     *
+     * @param gc Graphics
+     * @param vp Point in device coordinates
+     * @param wp Point in device coordiantes
+     */
+    public void draw2(Graphics2D gc, Point vp, Point wp, Transform trans, boolean hilited) {
+        if (fgColor != null) {
+            if (hilited) {// draw edge highlighting first
+                if (fgColor.equals(ProgramProperties.SELECTION_COLOR))
+                    gc.setColor(Color.ORANGE);
+                else
+                    gc.setColor(ProgramProperties.SELECTION_COLOR);
+                if (shape == STRAIGHT_EDGE || getInternalPoints() == null || getInternalPoints().size() == 0) {
+                    gc.drawLine(vp.x - 1, vp.y - 1, wp.x - 1, wp.y - 1);
+                    gc.drawLine(vp.x - 1, vp.y + 1, wp.x - 1, wp.y + 1);
+                    gc.drawLine(vp.x + 1, vp.y - 1, wp.x + 1, wp.y - 1);
+                    gc.drawLine(vp.x + 1, vp.y + 1, wp.x + 1, wp.y + 1);
+
+                } else // some internal points are given
+                {
+                    Point prev = vp;
+                    for (Point2D aptWorld : getInternalPoints()) {
+                        final Point apt = trans.w2d(aptWorld);
+// if(shape==GraphView.poly_edge)
+                        gc.drawLine(prev.x - 1, prev.y - 1, apt.x - 1, apt.y - 1);
+                        gc.drawLine(prev.x - 1, prev.y + 1, apt.x - 1, apt.y + 1);
+                        gc.drawLine(prev.x + 1, prev.y - 1, apt.x + 1, apt.y - 1);
+                        gc.drawLine(prev.x + 1, prev.y + 1, apt.x + 1, apt.y + 1);
+
+                        prev = apt;
+                    }
+                    gc.drawLine(prev.x - 1, prev.y - 1, wp.x - 1, wp.y - 1);
+                    gc.drawLine(prev.x - 1, prev.y + 1, wp.x - 1, wp.y + 1);
+                    gc.drawLine(prev.x + 1, prev.y - 1, wp.x + 1, wp.y - 1);
+                    gc.drawLine(prev.x + 1, prev.y + 1, wp.x + 1, wp.y + 1);
+                }
+            }
+
+            // now draw un-highlighted edge:
+            if (enabled)
+                gc.setColor(fgColor);
+            else
+                gc.setColor(DISABLED_COLOR);
+
+            Point vp1 = null;
+            final Point wp1;
+
+            if (shape == STRAIGHT_EDGE || getInternalPoints() == null || getInternalPoints().size() == 0) {
+                vp1 = wp;
+                wp1 = vp;
+
+                gc.drawLine(vp.x, vp.y, wp.x, wp.y);
+            } else // some internal points are given
+            {
+                Point prev = vp;
+                for (Point2D point2D : getInternalPoints()) {
+                    final Point apt = trans.w2d(point2D);
+                    if (vp1 == null)
+                        vp1 = apt;
+
+                    gc.drawLine(prev.x, prev.y, apt.x, apt.y);
+                    prev = apt;
+                }
+                wp1 = prev;
+                gc.drawLine(prev.x, prev.y, wp.x, wp.y);
+            }
+
+            if (direction == DIRECTED ||
+                    direction == BIDIRECTED)
+                drawArrowHead(gc, wp1, wp);
+            else if (direction == RDIRECTED)
+                drawArrowHead(gc, vp1, vp);
+        }
+    }
+
+    /**
+     * Draw the edge given device coordinates.
+     *
+     * @param gc Graphics
+     * @param vp Point in device coordinates
+     * @param wp Point in device coordinates
+     */
+    public void draw(Graphics2D gc, Point vp, Point wp, Transform trans, boolean hilited) {
+        if (hilited) {// draw edge highlighting first
+            gc.setStroke(HEAVY_STROKE);
+
+            if (fgColor != null && fgColor.equals(ProgramProperties.SELECTION_COLOR))
+                gc.setColor(Color.ORANGE);
+            else
+                gc.setColor(ProgramProperties.SELECTION_COLOR);
+            if (shape == STRAIGHT_EDGE || getInternalPoints() == null || getInternalPoints().size() == 0) {
+                gc.drawLine(vp.x - 1, vp.y - 1, wp.x - 1, wp.y - 1);
+                gc.drawLine(vp.x - 1, vp.y + 1, wp.x - 1, wp.y + 1);
+                gc.drawLine(vp.x + 1, vp.y - 1, wp.x + 1, wp.y - 1);
+                gc.drawLine(vp.x + 1, vp.y + 1, wp.x + 1, wp.y + 1);
+            } else if (shape == ROUNDED_EDGE && getInternalPoints().size() == 1) {
+                final Point vp1 = trans.w2d(getInternalPoints().get(0));
+                final int dist = (int) trans.w2d(ROUNDED_EDGE_INCREMENT, 0).getX() - (int) trans.w2d(0, 0).getX();
+                final Point center = new Point(dist < wp.x - vp.x ? vp.x + dist : wp.x, wp.y);
+
+                gc.draw(new QuadCurve2D.Double(vp.x - 1, vp.y - 1, vp1.x - 1, vp1.y - 1, center.x - 1, center.y - 1));
+                gc.draw(new QuadCurve2D.Double(vp.x - 1, vp.y + 1, vp1.x - 1, vp1.y + 1, center.x - 1, center.y + 1));
+                gc.draw(new QuadCurve2D.Double(vp.x + 1, vp.y - 1, vp1.x + 1, vp1.y - 1, center.x + 1, center.y - 1));
+                gc.draw(new QuadCurve2D.Double(vp.x + 1, vp.y + 1, vp1.x + 1, vp1.y + 1, center.x + 1, center.y + 1));
+
+                gc.drawLine(center.x - 1, center.y - 1, wp.x - 1, wp.y - 1);
+                gc.drawLine(center.x - 1, center.y + 1, wp.x - 1, wp.y + 1);
+                gc.drawLine(center.x + 1, center.y - 1, wp.x + 1, wp.y - 1);
+                gc.drawLine(center.x + 1, center.y + 1, wp.x + 1, wp.y + 1);
+
+            } else if (shape == QUAD_EDGE && getInternalPoints().size() == 1) {
+                Point aPt = trans.w2d(getInternalPoints().get(0));
+                gc.draw(new QuadCurve2D.Double(vp.x - 1, vp.y - 1, aPt.x - 1, aPt.y - 1, wp.x - 1, wp.y - 1));
+                gc.draw(new QuadCurve2D.Double(vp.x - 1, vp.y + 1, aPt.x - 1, aPt.y + 1, wp.x - 1, wp.y + 1));
+                gc.draw(new QuadCurve2D.Double(vp.x + 1, vp.y - 1, aPt.x + 1, aPt.y - 1, wp.x + 1, wp.y - 1));
+                gc.draw(new QuadCurve2D.Double(vp.x + 1, vp.y + 1, aPt.x + 1, aPt.y + 1, wp.x + 1, wp.y + 1));
+                final Stroke stroke = gc.getStroke();
+                gc.setStroke(NORMAL_STROKE);
+                gc.drawRect(aPt.x - 2, aPt.y - 2, 4, 4);
+                gc.setStroke(stroke);
+            } else if (shape == CUBIC_EDGE && getInternalPoints().size() == 2) {
+                Point aPt = trans.w2d(getInternalPoints().get(0));
+                Point bPt = trans.w2d(getInternalPoints().get(1));
+                gc.draw(new CubicCurve2D.Double(vp.x - 1, vp.y - 1, aPt.x - 1, aPt.y - 1, bPt.x - 1, bPt.y - 1, wp.x - 1, wp.y - 1));
+                gc.draw(new CubicCurve2D.Double(vp.x - 1, vp.y + 1, aPt.x - 1, aPt.y + 1, bPt.x - 1, bPt.y + 1, wp.x - 1, wp.y + 1));
+                gc.draw(new CubicCurve2D.Double(vp.x + 1, vp.y - 1, aPt.x + 1, aPt.y - 1, bPt.x + 1, bPt.y - 1, wp.x + 1, wp.y - 1));
+                gc.draw(new CubicCurve2D.Double(vp.x + 1, vp.y + 1, aPt.x + 1, aPt.y + 1, bPt.x + 1, bPt.y + 1, wp.x + 1, wp.y + 1));
+                final Stroke stroke = gc.getStroke();
+                gc.setStroke(NORMAL_STROKE);
+                gc.drawRect(aPt.x - 2, aPt.y - 2, 4, 4);
+                gc.drawRect(bPt.x - 2, bPt.y - 2, 4, 4);
+                gc.setStroke(stroke);
+            } else if (shape == ARC_LINE_EDGE && getInternalPoints().size() == 2) {
+                // node vp is start of arc, first internal point is center of circle, second is end of arc, second is joined to wp by straight line
+                Iterator it = getInternalPoints().iterator();
+                Point center = trans.w2d((Point2D) it.next());
+                Point arcStart = (Point) vp.clone();
+                Point arcEnd = trans.w2d((Point2D) it.next());
+                Point lineStart = (Point) arcEnd.clone();
+                // flip along h-axis:
+                arcStart.y = center.y - (arcStart.y - center.y);
+                arcEnd.y = center.y - (arcEnd.y - center.y);
+
+                double dist = arcStart.distance(center);
+                Point2D diffV = Geometry.diff(arcStart, center);
+                double angleV = Geometry.computeAngle(diffV);
+                Point2D diffEnd = Geometry.diff(arcEnd, center);
+                double angleEnd = Geometry.computeAngle(diffEnd);
+
+                double minAngle = angleV;
+                double maxAngle = angleEnd;
+                if (minAngle > maxAngle) {
+                    double tmp = minAngle;
+                    minAngle = maxAngle;
+                    maxAngle = tmp;
+                }
+                if (maxAngle - minAngle > Math.PI) {
+                    double tmp = minAngle + 2 * Math.PI;
+                    minAngle = maxAngle;
+                    maxAngle = tmp;
+                }
+                double extent = maxAngle - minAngle;
+
+                dist += 1;
+                Arc2D arc = new Arc2D.Double(center.getX() - dist, center.getY() - dist, 2 * dist, 2 * dist, Geometry.rad2deg(minAngle), Geometry.rad2deg(extent), Arc2D.OPEN);
+                gc.draw(arc);
+                dist -= 2;
+                arc = new Arc2D.Double(center.getX() - dist, center.getY() - dist, 2 * dist, 2 * dist, Geometry.rad2deg(minAngle), Geometry.rad2deg(extent), Arc2D.OPEN);
+                gc.draw(arc);
+                gc.drawLine(lineStart.x - 1, lineStart.y - 1, wp.x - 1, wp.y - 1);
+                gc.drawLine(lineStart.x - 1, lineStart.y + 1, wp.x - 1, wp.y + 1);
+                gc.drawLine(lineStart.x + 1, lineStart.y - 1, wp.x + 1, wp.y - 1);
+                gc.drawLine(lineStart.x + 1, lineStart.y + 1, wp.x + 1, wp.y + 1);
+            } else // some internal points are given
+            {
+                Point prev = vp;
+                for (Point2D point2D : getInternalPoints()) {
+                    final Point aPt = trans.w2d(point2D);
+                    gc.drawLine(prev.x - 1, prev.y - 1, aPt.x - 1, aPt.y - 1);
+                    gc.drawLine(prev.x - 1, prev.y + 1, aPt.x - 1, aPt.y + 1);
+                    gc.drawLine(prev.x + 1, prev.y - 1, aPt.x + 1, aPt.y - 1);
+                    gc.drawLine(prev.x + 1, prev.y + 1, aPt.x + 1, aPt.y + 1);
+                    gc.drawRect(aPt.x - 2, aPt.y - 2, 4, 4);
+                    prev = aPt;
+                }
+                gc.drawLine(prev.x - 1, prev.y - 1, wp.x - 1, wp.y - 1);
+                gc.drawLine(prev.x - 1, prev.y + 1, wp.x - 1, wp.y + 1);
+                gc.drawLine(prev.x + 1, prev.y - 1, wp.x + 1, wp.y - 1);
+                gc.drawLine(prev.x + 1, prev.y + 1, wp.x + 1, wp.y + 1);
+            }
+        }
+
+        if (fgColor != null) {
+            // now draw un-highlighted edge:
+            if (enabled)
+                gc.setColor(fgColor);
+            else
+                gc.setColor(DISABLED_COLOR);
+
+            Point vp1 = null;
+            final Point wp1;
+
+            if (shape == STRAIGHT_EDGE || getInternalPoints() == null || getInternalPoints().size() == 0) {
+                vp1 = wp;
+                wp1 = vp;
+                gc.drawLine(vp.x, vp.y, wp.x, wp.y);
+            } else if (shape == ROUNDED_EDGE && getInternalPoints().size() == 1) {
+                vp1 = wp1 = trans.w2d(getInternalPoints().get(0));
+                final int dist = (int) trans.w2d(ROUNDED_EDGE_INCREMENT, 0).getX() - (int) trans.w2d(0, 0).getX();
+                final Point center = new Point(dist < wp.x - vp.x ? vp.x + dist : wp.x, wp.y);
+                gc.draw(new QuadCurve2D.Double(vp.x, vp.y, vp1.x, vp1.y, center.x, center.y));
+                gc.drawLine(center.x, center.y, wp.x, wp.y);
+            } else if (shape == QUAD_EDGE && getInternalPoints().size() == 1) {
+                Point aPt = trans.w2d(getInternalPoints().get(0));
+                vp1 = wp1 = aPt;
+                gc.draw(new QuadCurve2D.Double(vp.x, vp.y, aPt.x, aPt.y, wp.x, wp.y));
+            } else if (shape == CUBIC_EDGE && getInternalPoints().size() == 2) {
+                Point aPt = trans.w2d(getInternalPoints().get(0));
+                Point bPt = trans.w2d(getInternalPoints().get(1));
+                vp1 = aPt;
+                wp1 = bPt;
+                gc.draw(new CubicCurve2D.Double(vp.x, vp.y, aPt.x, aPt.y, bPt.x, bPt.y, wp.x, wp.y));
+            } else if (shape == ARC_LINE_EDGE && getInternalPoints().size() == 2) {
+                // node vp is start of arc, first internal point is center of circle, second is end of arc, second is joined to wp by straight line
+                Iterator it = getInternalPoints().iterator();
+                Point center = trans.w2d((Point2D) it.next());
+                Point arcStart = (Point) vp.clone();
+                Point arcEnd = trans.w2d((Point2D) it.next());
+                Point lineStart = (Point) arcEnd.clone();
+                // flip along h-axis:
+                arcStart.y = center.y - (arcStart.y - center.y);
+                arcEnd.y = center.y - (arcEnd.y - center.y);
+
+                vp1 = wp1 = arcEnd;
+
+                double dist = arcStart.distance(center);
+                Point2D diffV = Geometry.diff(arcStart, center);
+                double angleV = Geometry.computeAngle(diffV);
+                Point2D diffEnd = Geometry.diff(arcEnd, center);
+                double angleEnd = Geometry.computeAngle(diffEnd);
+
+                double minAngle = angleV;
+                double maxAngle = angleEnd;
+                if (minAngle > maxAngle) {
+                    double tmp = minAngle;
+                    minAngle = maxAngle;
+                    maxAngle = tmp;
+                }
+                if (maxAngle - minAngle > Math.PI) {
+                    double tmp = minAngle + 2 * Math.PI;
+                    minAngle = maxAngle;
+                    maxAngle = tmp;
+                }
+                double extent = maxAngle - minAngle;
+
+                Arc2D arc = new Arc2D.Double(center.getX() - dist, center.getY() - dist, 2 * dist, 2 * dist, Geometry.rad2deg(minAngle), Geometry.rad2deg(extent), Arc2D.OPEN);
+                gc.draw(arc);
+                gc.drawLine(lineStart.x, lineStart.y, wp.x, wp.y);
+            } else  // some internal points are given
+            {
+                Point prev = vp;
+                for (Point2D point2D : getInternalPoints()) {
+                    final Point apt = trans.w2d(point2D);
+                    if (vp1 == null)
+                        vp1 = apt;
+
+                    gc.drawLine(prev.x, prev.y, apt.x, apt.y);
+                    prev = apt;
+                }
+                wp1 = prev;
+                gc.drawLine(prev.x, prev.y, wp.x, wp.y);
+            }
+
+            if (direction == DIRECTED ||
+                    direction == BIDIRECTED)
+                drawArrowHead(gc, wp1, wp);
+            else if (direction == RDIRECTED)
+                drawArrowHead(gc, vp1, vp);
+        }
+    }
+
+
+    /**
+     * Does given point (x,y) hit the edge, and if so, after which point?
+     * Returns -1, if edge not hit
+     *
+     * @param vp
+     * @param wp
+     * @param trans
+     * @param x
+     * @param y
+     * @param i
+     * @return the rank of the point preceding the point where the edge was hit
+     */
+    public int hitEdgeRank(Point vp, Point wp, Transform trans, int x, int y, int i) {
+        if (shape == STRAIGHT_EDGE || getInternalPoints() == null || getInternalPoints().size() == 0) {
+            if (Geometry.hitSegment(vp, wp, x, y, i))
+                return 0;
+        } else // some internal points are given
+        {
+            int rank = 0;
+            Point prev = vp;
+            for (Point2D point2D : getInternalPoints()) {
+                Point apt = trans.w2d(point2D);
+                if (Geometry.hitSegment(prev, apt, x, y, i))
+                    return rank;
+                prev = apt;
+                rank++;
+            }
+            if (Geometry.hitSegment(prev, wp, x, y, i))
+                return rank;
+        }
+        return -1;
+    }
+
+    /**
+     * Does given point (x,y) hit the edge?
+     *
+     * @param vp    source node location in device coordinates
+     * @param wp    target node location in device coordinates
+     * @param trans
+     * @param x     mouse x
+     * @param y     mouse y
+     * @param i     tolerance in pixels
+     * @return true, if point (x,y) lies on edge
+     */
+    public boolean hitEdge(Point vp, Point wp, Transform trans, int x, int y, int i) {
+        if (shape == STRAIGHT_EDGE || getInternalPoints() == null || getInternalPoints().size() == 0) {
+            if (Geometry.hitSegment(vp, wp, x, y, i))
+                return true;
+        } else if (shape == ROUNDED_EDGE && getInternalPoints().size() == 1) {
+            final Point vp1 = trans.w2d(getInternalPoints().get(0));
+            final int dist = (int) trans.w2d(ROUNDED_EDGE_INCREMENT, 0).getX() - (int) trans.w2d(0, 0).getX();
+            final Point center = new Point(dist < wp.x - vp.x ? vp.x + dist : wp.x, wp.y);
+            return (new QuadCurve2D.Double(vp.x, vp.y, vp1.x, vp1.y, center.x, center.y)).contains(x, y) ||
+                    (new Line2D.Double(center.x, center.y, wp.x, wp.y)).contains(x, y);
+        } else if (shape == QUAD_EDGE && getInternalPoints().size() == 1) {
+            Point aPt = trans.w2d(getInternalPoints().get(0));
+            return (new QuadCurve2D.Double(vp.x, vp.y, aPt.x, aPt.y, wp.x, wp.y)).contains(x, y);
+        } else if (shape == CUBIC_EDGE && getInternalPoints().size() == 2) {
+            Point aPt = trans.w2d(getInternalPoints().get(0));
+            Point bPt = trans.w2d(getInternalPoints().get(1));
+            return new CubicCurve2D.Double(vp.x, vp.y, aPt.x, aPt.y, bPt.x, bPt.y, wp.x, wp.y).contains(x, y);
+        } else if (shape == ARC_LINE_EDGE && getInternalPoints().size() == 2) {
+            // node vp is start of arc, first internal point is center of circle, second is end of arc, second is joined to wp by straight line
+            Iterator it = getInternalPoints().iterator();
+            Point center = trans.w2d((Point2D) it.next());
+            Point arcStart = (Point) vp.clone();
+            Point arcEnd = trans.w2d((Point2D) it.next());
+            Point lineStart = (Point) arcEnd.clone();
+            // flip along h-axis:
+            arcStart.y = center.y - (arcStart.y - center.y);
+            arcEnd.y = center.y - (arcEnd.y - center.y);
+
+            double dist = arcStart.distance(center);
+            Point2D diffV = Geometry.diff(arcStart, center);
+            double angleV = Geometry.computeAngle(diffV);
+            Point2D diffEnd = Geometry.diff(arcEnd, center);
+            double angleEnd = Geometry.computeAngle(diffEnd);
+
+            double minAngle = angleV;
+            double maxAngle = angleEnd;
+            if (minAngle > maxAngle) {
+                double tmp = minAngle;
+                minAngle = maxAngle;
+                maxAngle = tmp;
+            }
+            if (maxAngle - minAngle > Math.PI) {
+                double tmp = minAngle + 2 * Math.PI;
+                minAngle = maxAngle;
+                maxAngle = tmp;
+            }
+            double extent = maxAngle - minAngle;
+
+            Arc2D arc = new Arc2D.Double(center.getX() - dist, center.getY() - dist, 2 * dist, 2 * dist, Geometry.rad2deg(minAngle), Geometry.rad2deg(extent), Arc2D.OPEN);
+            if (arc.contains(x, y))
+                return true;
+
+            if (Geometry.hitSegment(lineStart, wp, x, y, i))
+                return true;
+        } else // some internal points are given
+        {
+            Point prev = vp;
+            for (Point2D point2D : getInternalPoints()) {
+                Point apt = trans.w2d(point2D);
+                if (Geometry.hitSegment(prev, apt, x, y, i))
+                    return true;
+                prev = apt;
+            }
+            if (Geometry.hitSegment(prev, wp, x, y, i))
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * moves the first internal point located at p1 to position p2
+     *
+     * @param trans
+     * @param p1
+     * @param p2
+     */
+    public void moveInternalPoint(Transform trans, Point p1, Point p2) {
+        if (shape != STRAIGHT_EDGE && getInternalPoints() != null) {
+            for (Point2D point2D : getInternalPoints()) {
+                Point apt = trans.w2d(point2D);
+                if (apt.distance(p1) <= 300) // todo: why do we check this?
+                {
+                    point2D.setLocation(trans.d2w(p2));
+                    return;
+                }
+            }
+        }
+    }
+
+
+    /**
+     * returns the list of internal points
+     *
+     * @return list of internal points or null
+     */
+    public java.util.List<Point2D> getInternalPoints() {
+        return internalPoints;
+    }
+
+    /**
+     * sets the list of internal points
+     *
+     * @param internalPoints a list of internal points from source to target
+     */
+    public void setInternalPoints(List<Point2D> internalPoints) {
+        this.internalPoints = internalPoints;
+    }
+
+
+    /**
+     * Draw an arrow head.
+     *
+     * @param gc Graphics
+     * @param vp Point
+     * @param wp Point
+     */
+    // Used to be private. Changed it to public in order to access from Function.java
+    public static void drawArrowHead(Graphics gc, Point vp, Point wp) {
+        final int arrowLength = 5;
+        final double arrowAngle = 2.2;
+        double alpha = Geometry.computeAngle(new Point(wp.x - vp.x, wp.y - vp.y));
+        Point a = new Point(arrowLength, 0);
+        a = Geometry.rotate(a, alpha + arrowAngle);
+        a.translate(wp.x, wp.y);
+        Point b = new Point(arrowLength, 0);
+        b = Geometry.rotate(b, alpha - arrowAngle);
+        b.translate(wp.x, wp.y);
+        gc.drawLine(a.x, a.y, wp.x, wp.y);
+        gc.drawLine(wp.x, wp.y, b.x, b.y);
+    }
+
+    /**
+     * Draw the edge label at the position given in device coordinates.
+     *
+     * @param gc Graphics
+     */
+    public void drawLabel(Graphics2D gc, Transform trans) {
+        if (this.isLabelVisible() && labelColor != null && label != null && enabled) {
+            if (labelBackgroundColor != null) {
+                gc.setColor(labelBackgroundColor);
+                gc.fill(getLabelShape(trans));
+            }
+
+            if (enabled)
+                gc.setColor(labelColor);
+            else
+                gc.setColor(DISABLED_COLOR);
+
+            if (getFont() != null)
+                gc.setFont(getFont());
+
+            Point aPt = getLabelPosition(trans);
+
+            gc.drawString(label, aPt.x, aPt.y);
+
+            setLabelSize(gc);
+        }
+    }
+
+
+    /**
+     * Draw the edge label at the position given in device coordinates.
+     *
+     * @param gc Graphics
+     */
+    public void drawLabel(Graphics2D gc, Transform trans, boolean hilited) {
+        if (this.isLabelVisible() && labelColor != null && label != null) {
+
+            if (getFont() != null)
+                gc.setFont(getFont());
+
+            setLabelSize(gc);
+
+            if (hilited) {
+                hiliteLabel(gc, trans);
+            }
+
+            Point2D apt = getLabelPosition(trans);
+            if (enabled && labelBackgroundColor != null) {
+                gc.setColor(labelBackgroundColor);
+                gc.fill(getLabelShape(trans));
+            }
+            if (enabled)
+                gc.setColor(labelColor);
+            else
+                gc.setColor(DISABLED_COLOR);
+
+            if (labelAngle == 0) {
+                gc.drawString(label, (int) apt.getX(), (int) apt.getY());
+            } else if (gc instanceof PDFGraphics) {
+                if (labelAngle >= 0.5 * Math.PI && labelAngle <= 1.5 * Math.PI) {
+                    double d = getLabelSize().getWidth();
+                    apt = Geometry.translateByAngle(apt, labelAngle, d);
+                    ((PDFGraphics) gc).drawString(label, (float) apt.getX(), (float) apt.getY(), (float) (labelAngle - Math.PI));
+                } else {
+                    ((PDFGraphics) gc).drawString(label, (float) apt.getX(), (float) apt.getY(), labelAngle);
+                }
+            } else {
+                // save current transform:
+                AffineTransform saveTransform = gc.getTransform();
+
+                // rotate label to desired angle
+                if (labelAngle >= 0.5 * Math.PI && labelAngle <= 1.5 * Math.PI) {
+                    double d = getLabelSize().getWidth();
+                    apt = Geometry.translateByAngle(apt, labelAngle, d);
+                    gc.rotate(Geometry.moduloTwoPI(labelAngle - Math.PI), apt.getX(), apt.getY());
+                } else
+                    gc.rotate(labelAngle, apt.getX(), apt.getY());
+
+                gc.drawString(label, (int) apt.getX(), (int) apt.getY());
+                gc.setTransform(saveTransform);
+            }
+        }
+    }
+
+    /**
+     * hilites the label
+     *
+     * @param gc
+     * @param trans
+     */
+    public void hiliteLabel(Graphics2D gc, Transform trans) {
+        if (this.isLabelVisible() && labelColor != null && label != null) {
+            if (enabled)
+                gc.setColor(labelColor);
+            else
+                gc.setColor(DISABLED_COLOR);
+
+            if (getFont() != null)
+                gc.setFont(getFont());
+
+            setLabelSize(gc);
+
+            gc.setColor(ProgramProperties.SELECTION_COLOR);
+            Shape shape = getLabelShape(trans);
+            gc.fill(shape);
+            gc.setColor(ProgramProperties.SELECTION_COLOR_DARKER);
+            final Stroke oldStroke = gc.getStroke();
+            gc.setStroke(NORMAL_STROKE);
+            gc.draw(shape);
+            gc.setStroke(oldStroke);
+        }
+    }
+
+    /**
+     * Sets the label reference point from the device coordinates of the two edge end nodes
+     *
+     * @param vp Point
+     * @param wp Point
+     */
+    public void setLabelReferencePosition(Point vp, Point wp, Transform trans) {
+        if (shape != STRAIGHT_EDGE && getInternalPoints() != null && getInternalPoints().size() != 0) {
+            Point bestPt = null;
+            double bestDist = -1;
+            for (Point2D point2D : getInternalPoints()) {
+                Point apt = trans.w2d(point2D);
+                double dist = apt.distance(vp) + apt.distance(wp);
+                if (dist > bestDist) {
+                    bestDist = dist;
+                    bestPt = apt;
+                }
+            }
+            labelReferencePoint = bestPt;
+        } else {
+            int x = (int) (0.5 * (vp.x + wp.x));
+            int y = (int) (0.5 * (vp.y + wp.y));
+            labelReferencePoint = new Point(x, y);
+        }
+    }
+
+    /**
+     * Sets the label reference point from the world coordinates of the two edge end nodes
+     *
+     * @param vp Point2D
+     * @param wp Point2D
+     */
+    public void setLabelReferenceLocation(Point2D vp, Point2D wp, Transform trans) {
+        setLabelReferencePosition(trans.w2d(vp), trans.w2d(wp), trans);
+    }
+
+    /**
+     * Gets the label position, computed from the given reference point, in device  coordinates
+     *
+     * @param trans Transform
+     * @return location
+     */
+    public Point getLabelPosition(Transform trans) {
+        if (labelReferencePoint == null || labelSize == null || label == null)
+            return null;
+
+        Point apt = new Point(labelReferencePoint);
+        Dimension size = getLabelSize();
+        switch (labelLayout) {
+            case RADIAL:
+            case USER:
+                apt.x += dxLabel;
+                apt.y += dyLabel;
+                break;
+            case CENTRAL:
+                apt.x -= size.width / 2;
+                apt.y += size.height / 2;
+                break;
+            case NORTH:
+                apt.x -= size.width / 2;
+                apt.y -= 3;
+                break;
+            case NORTHEAST:
+                apt.x += 3;
+                apt.y -= 3;
+                break;
+            case EAST:
+                apt.x += 3;
+                apt.y += size.height / 2;
+                break;
+            case SOUTHEAST:
+                apt.x -= 3;
+                apt.y += 3 + size.height;
+                break;
+            case SOUTH:
+                apt.x -= size.width / 2;
+                apt.y += 3 + size.height;
+                break;
+            case SOUTHWEST:
+                apt.x -= 3 + size.width;
+                apt.y += 3 + size.height;
+                break;
+            case WEST:
+                apt.x -= 3 + size.width;
+                apt.y += size.height / 2;
+                break;
+            case NORTHWEST:
+                apt.x -= 3 + size.width;
+                apt.y -= 3;
+                break;
+        }
+        return apt;
+    }
+
+    /**
+     * Sets the location of the edge label in world coordinates
+     *
+     * @param x     double
+     * @param y     double
+     * @param trans Transform
+     */
+    public void setLabelPosition(int x, int y, Transform trans) {
+        if (labelLayout != USER && labelLayout != LAYOUT)
+            setLabelLayout(USER);
+        Point apt = trans.w2d(x, y);
+        if (labelReferencePoint == null)
+            throw new RuntimeException("setLabelPosition(): labelReferencePoint not set");
+        dxLabel = apt.x - labelReferencePoint.x;
+        dyLabel = apt.y - labelReferencePoint.y;
+    }
+
+    /**
+     * gets the relative position of the label in device coordinates
+     *
+     * @return relative position
+     */
+    public Point getLabelPositionRelative(Transform trans) {
+        if (labelLayout == USER)
+            return new Point(dxLabel, dyLabel);
+        else {
+            if (labelReferencePoint == null)
+                throw new RuntimeException("getLabelPositionRelative(): labelReferencePoint not set");
+            Point loc = getLabelPosition(trans);
+            return new Point(loc.x - labelReferencePoint.x, loc.y - labelReferencePoint.y);
+        }
+    }
+
+    /**
+     * gets the label reference point in device coordinates
+     *
+     * @return label reference
+     */
+    public Point getLabelReferencePoint() {
+        return labelReferencePoint;
+    }
+
+    /**
+     * sets the edge label reference point in world coordinates
+     *
+     * @param labelReferencePoint
+     */
+    public void setLabelReferencePoint(Point labelReferencePoint) {
+        this.labelReferencePoint = labelReferencePoint;
+    }
+
+    /**
+     * sets the edge label reference point in world coordinates
+     *
+     * @param x
+     * @param y
+     */
+    public void setLabelReferencePoint(int x, int y) {
+        this.labelReferencePoint = new Point(x, y);
+    }
+
+    /**
+     * gets the rectangle of the label
+     *
+     * @param trans
+     * @return bounding box of label
+     */
+    public Rectangle getLabelRect(Transform trans) {
+        Point apt = getLabelPosition(trans);
+
+        if (apt != null && labelSize != null)
+            return new Rectangle(apt.x, apt.y - labelSize.height, labelSize.width, labelSize.height);
+        else
+            return null;
+    }
+
+    /**
+     * gets the bounding box of the label in device coordinates as a shape (rectangle or polygon)
+     *
+     * @param trans
+     * @return bounding box
+     */
+    public Shape getLabelShape(Transform trans) {
+        if (labelSize != null) {
+            Point2D apt = getLabelPosition(trans);
+            if (apt != null) {
+                if (labelAngle == 0) {
+                    return new Rectangle((int) apt.getX(), (int) apt.getY() - labelSize.height + 1, labelSize.width, labelSize.height);
+                } else {
+                    AffineTransform localTransform = new AffineTransform();
+                    // rotate label to desired angle
+                    if (labelAngle >= 0.5 * Math.PI && labelAngle <= 1.5 * Math.PI) {
+                        double d = getLabelSize().getWidth();
+                        apt = Geometry.translateByAngle(apt, labelAngle, d);
+                        localTransform.rotate(Geometry.moduloTwoPI(labelAngle - Math.PI), apt.getX(), apt.getY());
+                    } else
+                        localTransform.rotate(labelAngle, apt.getX(), apt.getY());
+                    double[] pts = new double[]{apt.getX(), apt.getY(),
+                            apt.getX() + labelSize.width, apt.getY(),
+                            apt.getX() + labelSize.width, apt.getY() - labelSize.height,
+                            apt.getX(), apt.getY() - labelSize.height};
+                    localTransform.transform(pts, 0, pts, 0, 4);
+                    return new Polygon(new int[]{(int) pts[0], (int) pts[2], (int) pts[4], (int) pts[6]}, new int[]{(int) pts[1], (int) pts[3],
+                            (int) pts[5], (int) pts[7]}, 4);
+                }
+            }
+        }
+        return null;
+    }
+
+
+    /**
+     * writes this edge view. Include internal points
+     *
+     * @param w
+     * @param previousEV if not null, only write those fields that differ from the values in previousNV
+     */
+    public void write(Writer w, EdgeView previousEV) throws IOException {
+        w.write(toString(previousEV, true, true));
+        w.write("\n");
+    }
+
+    /**
+     * gets a string representation of this node view, including internal points
+     *
+     * @return string representation
+     */
+    public String toString() {
+        return toString(null, true, true);
+    }
+
+    /**
+     * gets a string representation of this node view
+     *
+     * @param withInternalPoints show internal points as well?
+     * @return string representation
+     */
+    public String toString(boolean withInternalPoints) {
+        return toString(null, withInternalPoints, true);
+    }
+
+    /**
+     * gets a string representation of this edge view
+     *
+     * @param previousEV if not null, only write those fields that differ from the values in previousNV
+     * @return string representation
+     */
+    public String toString(EdgeView previousEV, boolean withInternalPoints, boolean withLabel) {
+
+        StringBuilder buf = new StringBuilder();
+
+        if (fgColor != null && (previousEV == null || previousEV.fgColor == null || !fgColor.equals(previousEV.fgColor)))
+            buf.append(" fg=").append(Basic.toString3Int(fgColor));
+
+        if (previousEV == null || linewidth != previousEV.linewidth)
+            buf.append(" w=").append(linewidth);
+        if (previousEV == null || shape != previousEV.shape)
+            buf.append(" sh=").append(shape);
+        if (labelReferencePoint != null && (previousEV == null || previousEV.labelReferencePoint == null || !labelReferencePoint.equals(previousEV.labelReferencePoint))) {
+            buf.append(" rx=").append((float) labelReferencePoint.getX());
+            buf.append(" ry=").append((float) labelReferencePoint.getY());
+        }
+        if (withInternalPoints && internalPoints != null && internalPoints.size() > 0) {
+            buf.append(" ip= <");
+            for (Point2D apt : internalPoints) {
+                buf.append(" ").append((float) apt.getX()).append(" ").append((float) apt.getY());
+            }
+            buf.append(">");
+        }
+        buf.append(" dr=").append(direction);
+
+        if (labelColor != null && (previousEV == null || previousEV.labelColor == null || !labelColor.equals(previousEV.labelColor)))
+            buf.append(" lc=").append(Basic.toString3Int(labelColor));
+
+        if (previousEV == null || ((previousEV.labelBackgroundColor == null) != (labelBackgroundColor == null))
+                || (previousEV.labelBackgroundColor != null && labelBackgroundColor != null && !previousEV.labelBackgroundColor.equals(labelBackgroundColor)))
+            buf.append(" lk=").append(Basic.toString3Int(labelBackgroundColor));
+
+        if (font != null && (previousEV == null || previousEV.font == null || !font.equals(previousEV.font)))
+            buf.append(" ft='").append(Basic.getCode(font)).append("'");
+        if (previousEV == null || dxLabel != previousEV.dxLabel)
+            buf.append(" lx=").append(dxLabel);
+        if (previousEV == null || dyLabel != previousEV.dyLabel)
+            buf.append(" ly=").append(dyLabel);
+        if (previousEV == null || labelLayout != previousEV.labelLayout)
+            buf.append(" ll=").append(labelLayout);
+        if (previousEV == null || labelVisible != previousEV.labelVisible)
+            buf.append(" lv=").append(labelVisible ? 1 : 0);
+        if (labelAngle != 0)
+            buf.append(" la=").append(labelAngle);
+
+        if (withLabel && label != null && label.length() > 0)
+            buf.append(" lb='").append(label).append("'");
+
+        buf.append(";");
+        return buf.toString();
+    }
+
+    /**
+     * read edge format from a string
+     *
+     * @param src
+     * @throws IOException
+     */
+    public void read(String src) throws IOException {
+        NexusStreamParser np = new NexusStreamParser(new StringReader(src));
+        java.util.List<String> tokens = np.getTokensRespectCase(null, ";");
+        read(np, tokens, this);
+    }
+
+    /**
+     * read edge format from a string
+     *
+     * @param src
+     * @throws IOException
+     */
+    public void read(String src, EdgeView prevEV) throws IOException {
+        NexusStreamParser np = new NexusStreamParser(new StringReader(src));
+        java.util.List<String> tokens = np.getTokensRespectCase(null, ";");
+        read(np, tokens, prevEV != null ? prevEV : this);
+    }
+
+    /**
+     * reads a edge view from a line
+     *
+     * @param tokens
+     * @param prevEV this must be !=null, for example can be set to graphView.defaultEdgeView
+     */
+    public void read(NexusStreamParser np, java.util.List<String> tokens, EdgeView prevEV) throws IOException {
+        if (prevEV == null)
+            throw new IOException("prevEV=null");
+
+        fgColor = np.findIgnoreCase(tokens, "fg=", prevEV.fgColor);
+        linewidth = (byte) np.findIgnoreCase(tokens, "w=", prevEV.linewidth);
+        shape = (byte) np.findIgnoreCase(tokens, "sh=", prevEV.shape);
+
+        int x = (int) np.findIgnoreCase(tokens, "rx=",
+                prevEV.labelReferencePoint != null ? (float) prevEV.labelReferencePoint.getX() : 0);
+        int y = (int) np.findIgnoreCase(tokens, "ry=",
+                prevEV.labelReferencePoint != null ? (float) prevEV.labelReferencePoint.getY() : 0);
+        setLabelReferencePoint(new Point(x, y));
+
+        String internalPointsStr = np.findIgnoreCase(tokens, "ip=", "<", ">", "");
+        if (internalPointsStr != null && internalPointsStr.length() > 0) {
+            try {
+                StringTokenizer st = new StringTokenizer(internalPointsStr);
+
+                List<Point2D> list = new LinkedList<>();
+                while (st.hasMoreTokens()) {
+                    double ix = Double.parseDouble(st.nextToken());
+                    double iy = Double.parseDouble(st.nextToken());
+                    list.add(new Point2D.Double(ix, iy));
+
+                }
+                setInternalPoints(list);
+            } catch (Exception ex) {
+                throw new IOException("line " + np.lineno() + ": error parsing internal points: " + internalPointsStr
+                        + ": " + ex);
+            }
+        }
+
+        direction = (byte) np.findIgnoreCase(tokens, "dr=", prevEV.direction);
+
+        labelColor = np.findIgnoreCase(tokens, "lc=", prevEV.labelColor);
+        labelBackgroundColor = np.findIgnoreCase(tokens, "lk=", prevEV.labelBackgroundColor);
+
+        String fontName = np.findIgnoreCase(tokens, "ft=", null, "");
+        if (fontName != null && fontName.length() > 0)
+            font = Font.decode(fontName);
+        else
+            font = GraphView.defaultEdgeView.getFont(); // will use default font
+        dxLabel = (int) np.findIgnoreCase(tokens, "lx=", prevEV.dxLabel);
+        dyLabel = (int) np.findIgnoreCase(tokens, "ly=", prevEV.dyLabel);
+        setLabelAngle(np.findIgnoreCase(tokens, "la=", 0));
+        labelLayout = (byte) np.findIgnoreCase(tokens, "ll=", prevEV.labelLayout);
+        labelVisible = (np.findIgnoreCase(tokens, "lv=", prevEV.labelVisible ? 1 : 0) != 0);
+        label = (np.findIgnoreCase(tokens, "lb=", null, ""));
+        if (label != null && label.length() == 0)
+            label = null;
+        setLabel(label);
+        if (label == null)
+            labelReferencePoint = null; // don't need this, will remake it later, if necessary
+        if (tokens.size() > 0) {
+            if (tokens.size() == 2 && tokens.get(0).equals("0") && tokens.get(1).equals("0") && linewidth == (byte) 255)  // this is the w=255 0 0 bug
+            {
+                linewidth = 1;
+            } else
+                throw new IOException("Unexpected tokens: " + tokens);
+        }
+    }
+}
+
+// EOF
diff --git a/src/jloda/graphview/GraphEditor.java b/src/jloda/graphview/GraphEditor.java
new file mode 100644
index 0000000..3b47fb2
--- /dev/null
+++ b/src/jloda/graphview/GraphEditor.java
@@ -0,0 +1,545 @@
+/**
+ * GraphEditor.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+/**
+ * @version $Id: GraphEditor.java,v 1.10 2006-01-19 12:00:33 huson Exp $
+ *
+ * Graph editor class.
+ *
+ * @author Daniel Huson
+ */
+
+import jloda.graph.Edge;
+import jloda.graph.Graph;
+import jloda.graph.Node;
+import jloda.util.Basic;
+import jloda.util.NotOwnerException;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+import java.awt.print.PrinterJob;
+
+/**
+ * A graph editor.
+ */
+public class GraphEditor extends GraphView {
+    private MenuBar MB; // the menu bar.
+    private GraphEditorActionListener actionListener; // handles menus
+    private GraphEditorWindowListener windowListener; // handles window events
+
+    static int numberEditors = 0;
+    static boolean exitOnLastClose = false;
+
+    /**
+     * Constructs a GraphEditor.
+     *
+     * @param G graph to be viewed
+     */
+    public GraphEditor(Graph G) {
+        super(G, 400, 400);
+        init(G, "GraphEditor", 400, 400);
+    }
+
+    /**
+     * Constructs a GraphEditor.
+     *
+     * @param G     graph to be viewed
+     * @param title title of frame
+     */
+    public GraphEditor(Graph G, String title) {
+        super(G, 400, 400);
+        init(G, title, 400, 400);
+    }
+
+    /**
+     * Constructs a GraphEditor.
+     *
+     * @param G     graph to be viewed
+     * @param title title of frame
+     * @param w     width of frame
+     * @param h     height of frame
+     */
+    public GraphEditor(Graph G, String title, int w, int h) {
+        super(G, w, h);
+        init(G, title, w, h);
+    }
+
+    /**
+     * Sets the associated Frame.
+     *
+     * @param frame the Frame
+     */
+    public void setFrame(JFrame frame) {
+        super.setFrame(frame);
+        frame.addWindowListener(windowListener);
+    }
+
+    /**
+     * Does initialization.
+     *
+     * @param G     Graph
+     * @param title String
+     * @param w     int the width of frame
+     * @param h     int the height of frame
+     */
+    private void init(Graph G, String title, int w, int h) {
+        frame = new JFrame(title);
+        frame.setSize(getSize());
+        frame.add(this);
+        MB = new MenuBar();
+
+        actionListener = new GraphEditorActionListener(this);
+        windowListener = new GraphEditorWindowListener(this);
+        frame.addWindowListener(windowListener);
+
+        addFileMenu();
+        addEditMenu();
+        addFormatMenu();
+        addLayoutMenu();
+
+        frame.setMenuBar(MB);
+    }
+
+    /* Setup the File menu. */
+
+    void addFileMenu() {
+        Menu fileMenu = new Menu("File", true);
+        fileMenu.add("New");
+        fileMenu.addSeparator();
+        fileMenu.add("Open...");
+        fileMenu.add("Save");
+        fileMenu.add("Save As...");
+        fileMenu.addSeparator();
+        fileMenu.add("Print...");
+        fileMenu.addSeparator();
+        fileMenu.add("Close");
+        fileMenu.addSeparator();
+        fileMenu.add("Quit");
+
+        fileMenu.addActionListener(actionListener);
+
+        MB.add(fileMenu);
+    }
+
+    /* Setup the Edit menu. */
+
+    void addEditMenu() {
+        Menu editMenu = new Menu("Edit", true);
+        editMenu.add("Cut");
+        editMenu.add("Copy");
+        editMenu.add("Paste");
+        editMenu.addSeparator();
+        editMenu.add("Delete");
+        editMenu.addSeparator();
+        editMenu.add("Select All");
+        editMenu.add("Select All Nodes");
+        editMenu.add("Select All Edges");
+        editMenu.addSeparator();
+        editMenu.add("Horizontal Flip");
+        editMenu.add("Vertical Flip");
+
+        editMenu.addActionListener(actionListener);
+
+        MB.add(editMenu);
+    }
+
+    /* Setup the Format menu. */
+
+    void addFormatMenu() {
+        Menu formatMenu = new Menu("Format", true);
+
+        Menu linewidthMenu = new Menu("Line Width", true);
+        linewidthMenu.add("0pt");
+        linewidthMenu.add("1pt");
+        linewidthMenu.add("2pt");
+        linewidthMenu.add("4pt");
+        linewidthMenu.add("8pt");
+        linewidthMenu.add("10pt");
+        linewidthMenu.addActionListener(actionListener);
+
+        formatMenu.add(linewidthMenu);
+
+        Menu lineColorMenu = new Menu("Line Color", true);
+        lineColorMenu.add("black");
+        lineColorMenu.add("blue");
+        lineColorMenu.add("cyan");
+        lineColorMenu.add("darkGray");
+        lineColorMenu.add("gray");
+        lineColorMenu.add("green");
+        lineColorMenu.add("lightGray");
+        lineColorMenu.add("magenta");
+        lineColorMenu.add("orange");
+        lineColorMenu.add("pink");
+        lineColorMenu.add("red");
+        lineColorMenu.add("white");
+        lineColorMenu.add("yellow");
+        lineColorMenu.addSeparator();
+        lineColorMenu.add("none");
+        lineColorMenu.addActionListener(new ColorActionListener(this, "line"));
+        formatMenu.add(lineColorMenu);
+
+        Menu fillColorMenu = new Menu("Fill Color", true);
+        fillColorMenu.add("black");
+        fillColorMenu.add("blue");
+        fillColorMenu.add("cyan");
+        fillColorMenu.add("darkGray");
+        fillColorMenu.add("gray");
+        fillColorMenu.add("green");
+        fillColorMenu.add("lightGray");
+        fillColorMenu.add("magenta");
+        fillColorMenu.add("orange");
+        fillColorMenu.add("pink");
+        fillColorMenu.add("red");
+        fillColorMenu.add("white");
+        fillColorMenu.add("yellow");
+        fillColorMenu.addSeparator();
+        fillColorMenu.add("none");
+        fillColorMenu.addActionListener(new ColorActionListener(this, "fill"));
+        formatMenu.add(fillColorMenu);
+
+        Menu labelColorMenu = new Menu("Label Color", true);
+        labelColorMenu.add("black");
+        labelColorMenu.add("blue");
+        labelColorMenu.add("cyan");
+        labelColorMenu.add("darkGray");
+        labelColorMenu.add("gray");
+        labelColorMenu.add("green");
+        labelColorMenu.add("lightGray");
+        labelColorMenu.add("magenta");
+        labelColorMenu.add("orange");
+        labelColorMenu.add("pink");
+        labelColorMenu.add("red");
+        labelColorMenu.add("white");
+        labelColorMenu.add("yellow");
+        labelColorMenu.addSeparator();
+        labelColorMenu.add("none");
+        labelColorMenu.addActionListener(new ColorActionListener(this, "label"));
+        formatMenu.add(labelColorMenu);
+
+        formatMenu.addSeparator();
+        formatMenu.add("Style");
+        formatMenu.add("Size");
+
+        formatMenu.addSeparator();
+        formatMenu.add("Label");
+
+        formatMenu.addActionListener(actionListener);
+
+        MB.add(formatMenu);
+    }
+
+    /* Setup the Layout menu. */
+
+    void addLayoutMenu() {
+        Menu layoutMenu = new Menu("Layout", true);
+        layoutMenu.add("Zoom to Graph");
+        layoutMenu.addSeparator();
+        layoutMenu.add("Zoom In");
+        layoutMenu.add("Zoom Out");
+        layoutMenu.addSeparator();
+        layoutMenu.add("Spring Embedding");
+
+        layoutMenu.addActionListener(actionListener);
+
+        MB.add(layoutMenu);
+    }
+
+
+    /**
+     * This closes the editor.
+     */
+    public void close() {
+        frame.dispose();
+        frame = null;
+    }
+
+    /**
+     * Set whether program should exit when last open GraphEditor
+     * is closed
+     *
+     * @param yes true, if exit is desired
+     */
+    static public void setExitOnLastClose(boolean yes) {
+        exitOnLastClose = yes;
+    }
+
+    /**
+     * Will program exit when last open GraphEditor is closed
+     *
+     * @return true, if program will exit
+     */
+    static public boolean getExitOnLastClose() {
+        return exitOnLastClose;
+    }
+
+    /**
+     * How many GraphEditors are open?
+     *
+     * @return number of open GraphEditors
+     */
+    static public int getNumberEditors() {
+        return numberEditors;
+    }
+}
+
+
+class GraphEditorActionListener implements ActionListener {
+    private GraphEditor GE;
+
+    GraphEditorActionListener(GraphEditor GE) {
+        this.GE = GE;
+    }
+
+    /**
+     * This takes care of menu events
+     *
+     * @param ev ActionEvent
+     */
+    public void actionPerformed(ActionEvent ev) {
+        // Basic.message("Menu action: "+ev);
+
+        if (ev.getActionCommand().equals("New")) {
+
+            // this shouldn't work! once we leave the scope of
+                // ed, you'ed expect ed to go away, but it doesn't...
+                GraphEditor ed = new GraphEditor(GE.getGraph(), "New", 400, 400);
+                ed.getFrame().setResizable(true);
+                ed.getFrame().setVisible(true);
+        } else if (ev.getActionCommand().equals("Print...")) {
+            PrinterJob job = PrinterJob.getPrinterJob();
+            // Specify the Printable is an instance of SimplePrint
+            job.setPrintable(GE);
+            // Put up the dialog box
+            if (job.printDialog()) {
+                // Print the job if the user didn't cancel printing
+                try {
+                    job.print();
+                } catch (Exception ex) {
+                    Basic.caught(ex);
+                }
+            }
+        } else if (ev.getActionCommand().equals("Select All")) {
+            GE.selectAllNodes(true);
+            GE.selectAllEdges(true);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("Horizontal Flip")) {
+            GE.horizontalFlipSelected();
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("Vertical Flip")) {
+            GE.verticalFlipSelected();
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("Close")) {
+            GE.close();
+            GE = null;
+        } else if (ev.getActionCommand().equals("Quit")) {
+            // need to replace this by code that closes all windows
+            System.exit(0);
+        } else if (ev.getActionCommand().equals("Delete")) {
+            GE.delSelectedEdges();
+            GE.delSelectedNodes();
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("Select All")) {
+            GE.selectAllNodes(true);
+            GE.selectAllEdges(true);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("Select All Nodes")) {
+            GE.selectAllNodes(true);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("Select All Edges")) {
+            GE.selectAllEdges(true);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("Zoom to Graph")) {
+            GE.fitGraphToWindow();
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("Zoom In")) {
+            double s = 1.5;
+            GE.trans.composeScale(1.0 / s, 1.0 / s);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("Zoom Out")) {
+            double s = 1.5;
+            GE.trans.composeScale(s, s);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("Spring Embedding")) {
+            GE.computeSpringEmbedding(100, true);
+            GE.fitGraphToWindow();
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("0pt")) {
+            GE.setLineWidthSelected(0);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("1pt")) {
+            GE.setLineWidthSelected(1);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("2pt")) {
+            GE.setLineWidthSelected(2);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("4pt")) {
+            GE.setLineWidthSelected(4);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("8pt")) {
+            GE.setLineWidthSelected(8);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("10pt")) {
+            GE.setLineWidthSelected(10);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("Label")) {
+            String label = JOptionPane.showInputDialog("Enter label");
+            if (label != null) {
+                try {
+                    Graph graph = GE.getGraph();
+
+                    for (Node v = graph.getFirstNode(); v != null; v = graph.getNextNode(v)) {
+                        if (GE.getSelected(v))
+                            GE.setLabel(v, label);
+                    }
+                    for (Edge e = graph.getFirstEdge(); e != null; e = graph.getNextEdge(e)) {
+                        if (GE.getSelected(e))
+                            GE.setLabel(e, label);
+                    }
+                } catch (NotOwnerException ex) {
+                    Basic.caught(ex);
+                }
+                GE.repaint();
+            }
+        } else
+            System.err.println("Not implemented: " + ev);
+    }
+}
+
+class ColorActionListener implements ActionListener {
+    private final GraphEditor GE;
+    private final String kind;
+
+    /**
+     * constructor of ColorActionListener
+     *
+     * @param GE   GraphEditor
+     * @param kind String
+     */
+    ColorActionListener(GraphEditor GE, String kind) {
+        this.GE = GE;
+        this.kind = kind;
+    }
+
+    /**
+     * This takes care of menu events
+     *
+     * @param ev ActionEvent
+     */
+    public void actionPerformed(ActionEvent ev) {
+        if (ev.getActionCommand().equals("black")) {
+            GE.setColorSelected(Color.black, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("blue")) {
+            GE.setColorSelected(Color.blue, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("cyan")) {
+            GE.setColorSelected(Color.cyan, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("darkGray")) {
+            GE.setColorSelected(Color.darkGray, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("gray")) {
+            GE.setColorSelected(Color.gray, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("green")) {
+            GE.setColorSelected(Color.green, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("lightGray")) {
+            GE.setColorSelected(Color.lightGray, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("magenta")) {
+            GE.setColorSelected(Color.magenta, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("orange")) {
+            GE.setColorSelected(Color.orange, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("pink")) {
+            GE.setColorSelected(Color.pink, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("red")) {
+            GE.setColorSelected(Color.red, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("white")) {
+            GE.setColorSelected(Color.white, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("yellow")) {
+            GE.setColorSelected(Color.yellow, kind);
+            GE.repaint();
+        } else if (ev.getActionCommand().equals("none")) {
+            GE.setColorSelected(null, kind);
+            GE.repaint();
+        } else
+            System.err.println("Not implemented: " + ev);
+    }
+}
+
+class GraphEditorWindowListener implements WindowListener {
+    final GraphEditor GE;
+
+    /**
+     * the constructor of GraphEditorWindowListener
+     *
+     * @param GE GraphEditor
+     */
+    GraphEditorWindowListener(GraphEditor GE) {
+        this.GE = GE;
+    }
+
+    public void windowActivated(WindowEvent e) {
+        // Basic.message("activiated: "+e);
+    }
+
+    public void windowClosed(WindowEvent e) {
+        // Basic.message("closed: "+e);
+        GraphEditor.numberEditors--;
+        if (GraphEditor.getExitOnLastClose()
+                && GraphEditor.getNumberEditors() == 0)
+            System.exit(0);
+    }
+
+    public void windowClosing(WindowEvent e) {
+        // ask whether graph should be saved here!
+        // Basic.message("closing: "+e);
+        GE.getFrame().dispose();
+        GE.setFrame(null);
+    }
+
+    public void windowDeactivated(WindowEvent e) {
+        // Basic.message("deactiviated: "+e);
+    }
+
+    public void windowDeiconified(WindowEvent e) {
+    }
+
+    public void windowIconified(WindowEvent e) {
+    }
+
+    public void windowOpened(WindowEvent e) {
+        GraphEditor.numberEditors++;
+        // Basic.message("opened: "+e);
+    }
+}
+
+// EOF
diff --git a/src/jloda/graphview/GraphView.java b/src/jloda/graphview/GraphView.java
new file mode 100644
index 0000000..cf57eae
--- /dev/null
+++ b/src/jloda/graphview/GraphView.java
@@ -0,0 +1,3847 @@
+/**
+ * GraphView.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: GraphView.java,v 1.189 2010-06-08 08:55:32 huson Exp $
+ *
+ * Graph view class.
+ *
+ * @author Daniel Huson
+ */
+package jloda.graphview;
+
+import jloda.export.ExportManager;
+import jloda.graph.*;
+import jloda.util.*;
+import jloda.util.parse.NexusStreamParser;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.print.PageFormat;
+import java.awt.print.Printable;
+import java.awt.print.PrinterException;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.*;
+import java.util.List;
+
+/**
+ * This class implements the visualization of a graph as a Canvas.
+ */
+public class GraphView extends JPanel implements Printable, Scrollable, INodeEdgeFormatable {
+
+    private final NodeArray<NodeView> nodeViews;
+    private final EdgeArray<EdgeView> edgeViews;
+
+    private final java.util.List<NodeActionListener> nodeActionListeners = new LinkedList<>();
+    private final java.util.List<EdgeActionListener> edgeActionListeners = new LinkedList<>();
+    private final java.util.List<PanelActionListener> panelActionListeners = new LinkedList<>();
+
+    private IGraphViewListener graphViewListener = null;
+    private IPopupListener popupListener = null;
+
+    public final static NodeView defaultNodeView = new NodeView(); // must be static for SplitsTree!
+    public final static EdgeView defaultEdgeView = new EdgeView(); // must be static for SplitsTree!
+
+    public final NodeSet selectedNodes;
+    public final EdgeSet selectedEdges;
+
+    private boolean allowEdit = true;
+    private boolean allowNewNodeDoubleClick = true;
+    private boolean maintainEdgeLengths = false; // in inaction, maintain lengths
+    private boolean allowMoveNodes = true;
+    private boolean allowMoveInternalEdgePoints = true;
+
+    private boolean allowRubberbandNodes = true; // allow rubberbanding of nodes?
+    private boolean allowRubberbandEdges = true;
+
+    private boolean allowInternalEdgePoints = false; // allow user to interactively insert and move internal edge points
+
+    private boolean allowEditNodeLabelsOnDoubleClick = false;
+    private boolean allowEditEdgeLabelsOnDoubleClick = false;
+
+    private boolean allowRotation = true;
+    private boolean allowRotationArbitraryAngle = true;
+
+    private final JScrollPane scrollPane;
+
+    private boolean repaintOnGraphHasChanged = true;
+
+    private Node foundNode;
+
+    // do away with this:
+    protected Color canvasColor = Color.lightGray;
+
+    private boolean drawScaleBar = false;
+    static protected final Font scaleBarFont = Font.decode("Helvetica-ITALIC-10");
+
+    private boolean autoLayoutLabels = false; // attempt to do smart layout of labels?
+
+    private final Graph G;
+
+    private Font font = Font.decode("Helvetica-PLAIN-11");
+    protected final Font poweredByFont = Font.decode("Helvetica-ITALIC-9");
+
+    public final Transform trans;
+    private String POWEREDBY = null;
+
+    public boolean inPrint = false; // are we printing?
+
+    final static public double XMIN_SCALE = 0.00000001;
+    final static public double YMIN_SCALE = 0.00000001;
+    final static public double XMAX_SCALE = 100000000;
+    final static public double YMAX_SCALE = 100000000;
+
+    private IGraphDrawer graphDrawer;
+
+    protected JFrame frame;
+
+    protected boolean locked = false; // are critical user actions currently locked?
+
+    /**
+     * Construct a GraphView for a graph G.
+     *
+     * @param G graph
+     */
+    public GraphView(Graph G) {
+        this(G, 400, 400, Color.white);
+    }
+
+    /**
+     * Construct a GraphView for a graph G.
+     *
+     * @param G graph
+     * @param w width of canvas
+     * @param h height of canvas
+     */
+    public GraphView(Graph G, int w, int h) {
+        this(G, w, h, Color.white);
+    }
+
+    /**
+     * Construct a GraphView for a graph G.
+     *
+     * @param G   graph
+     * @param w   width of canvas
+     * @param h   height of canvas
+     * @param col color of canvas
+     */
+    public GraphView(Graph G, int w, int h, Color col) {
+        super();
+        this.G = G;
+
+        nodeViews = new NodeArray<>(G);
+        edgeViews = new EdgeArray<>(G);
+
+        defaultNodeView.setFont(ProgramProperties.get(ProgramProperties.DEFAULT_FONT, defaultNodeView.getFont()));
+        defaultEdgeView.setFont(ProgramProperties.get(ProgramProperties.DEFAULT_FONT, defaultEdgeView.getFont()));
+
+        selectedNodes = new NodeSet(G);
+        selectedEdges = new EdgeSet(G);
+
+        setGraphArrays();
+
+        setBackground(col);
+
+        setSize(w, h);
+        setPreferredSize(new Dimension(w, h));
+        scrollPane = new JScrollPane(this);
+        scrollPane.setWheelScrollingEnabled(true);
+        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
+        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
+        scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
+        scrollPane.setAutoscrolls(true);
+
+        JButton zoomButton = new JButton();
+        zoomButton.setAction(new AbstractAction() {
+            public void actionPerformed(ActionEvent e) {
+                fitGraphToWindow();
+            }
+        });
+        scrollPane.setCorner(JScrollPane.LOWER_RIGHT_CORNER, zoomButton);
+
+        trans = new Transform(this);
+        trans.addChangeListener(new ITransformChangeListener() {
+            public void hasChanged(Transform trans) {
+                recomputeMargins();
+            }
+        });
+
+        getScrollPane().addComponentListener(new ComponentAdapter() {
+            public void componentResized(ComponentEvent event) {
+                    //centerGraph();
+            }
+        });
+
+        G.addGraphUpdateListener(new GraphUpdateAdapter() {
+            public void graphHasChanged() {
+                if (isRepaintOnGraphHasChanged())
+                    GraphView.this.repaint();
+            }
+
+            public void deleteNode(Node v) {
+                if (selectedNodes.contains(v))
+                    selectedNodes.remove(v);
+            }
+
+            public void deleteEdge(Edge e) {
+                if (selectedEdges.contains(e))
+                    selectedEdges.remove(e);
+            }
+        });
+
+        // set the default graph dendroscope
+        this.graphDrawer = new DefaultGraphDrawer(this);
+
+        // process mouse, key and component events
+        setGraphViewListener(new GraphViewListener(this));
+
+        resetCursor();
+
+        /* NodeViews and EdgeViews are set when they are first referenced */
+    }
+
+    public Font getScaleBarFont() {
+        return scaleBarFont;
+    }
+
+    /**
+     * set the graphViewListener. Remove the old one
+     *
+     * @param graphViewListener
+     */
+    public void setGraphViewListener(IGraphViewListener graphViewListener) {
+        if (this.graphViewListener != null) {
+            removeMouseListener(this.graphViewListener);
+            removeMouseMotionListener(this.graphViewListener);
+            removeKeyListener(this.graphViewListener);
+            removeComponentListener(this.graphViewListener);
+            removeMouseWheelListener(this.graphViewListener);
+        }
+        this.graphViewListener = graphViewListener;
+        addMouseListener(graphViewListener);
+        addMouseMotionListener(graphViewListener);
+        addKeyListener(graphViewListener);
+        addComponentListener(graphViewListener);
+        addMouseWheelListener(graphViewListener);
+    }
+
+    /**
+     * gets the current graph view listener
+     *
+     * @return graph view listener
+     */
+    public IGraphViewListener getGraphViewListener() {
+        return graphViewListener;
+    }
+
+    /**
+     * Sets graph-related arrays
+     */
+    public void setGraphArrays() {
+    }
+
+    /**
+     * Set the status of interactive editing of graph.
+     *
+     * @param ok allow editing
+     */
+    public void setAllowEdit(boolean ok) {
+        allowEdit = ok;
+    }
+
+    /**
+     * Get status of interactive editing of graph.
+     *
+     * @return editing allowed
+     */
+    public boolean getAllowEdit() {
+        return allowEdit;
+    }
+
+    /**
+     * allowed to interactive move nodes?
+     *
+     * @return move nodes
+     */
+    public boolean getAllowMoveNodes() {
+        return allowMoveNodes;
+    }
+
+    /**
+     * allow or disallow interactive moving of nodes
+     *
+     * @param allowMoveNodes
+     */
+    public void setAllowMoveNodes(boolean allowMoveNodes) {
+        this.allowMoveNodes = allowMoveNodes;
+    }
+
+    /**
+     * allow to move internal edge points
+     *
+     * @return true, if allowed
+     */
+    public boolean isAllowMoveInternalEdgePoints() {
+        return allowMoveInternalEdgePoints;
+    }
+
+    /**
+     * allow to move internal edge points
+     *
+     * @param allowMoveInternalEdgePoints
+     */
+    public void setAllowMoveInternalEdgePoints(boolean allowMoveInternalEdgePoints) {
+        this.allowMoveInternalEdgePoints = allowMoveInternalEdgePoints;
+    }
+
+    /**
+     * Set the status of interactive creation of new nodes
+     *
+     * @param ok allow double click creation of new node
+     */
+    public void setAllowNewNodeDoubleClick(boolean ok) {
+        allowNewNodeDoubleClick = ok;
+    }
+
+    /**
+     * Gets the  status of interactive creation of new nodes
+     *
+     * @return allow double click creation of new node
+     */
+    public boolean getAllowNewNodeDoubleClick() {
+        return allowNewNodeDoubleClick;
+    }
+
+
+    /**
+     * Set the status of maintaining edge lengths in interaction.
+     *
+     * @param ok maintain edge lengths
+     */
+    public void setMaintainEdgeLengths(boolean ok) {
+        maintainEdgeLengths = ok;
+    }
+
+    /**
+     * Get status of maintaining edge lengths in interaction.
+     *
+     * @return editing allowed
+     */
+    public boolean getMaintainEdgeLengths() {
+        return maintainEdgeLengths;
+    }
+
+    /**
+     * is a scale bar being displayed?
+     *
+     * @return display scalebar
+     */
+    public boolean isDrawScaleBar() {
+        return drawScaleBar;
+    }
+
+    /**
+     * determine whether to display a scale bar
+     *
+     * @param drawScaleBar
+     */
+    public void setDrawScaleBar(boolean drawScaleBar) {
+        this.drawScaleBar = drawScaleBar;
+    }
+
+    /**
+     * lock aspect ratio
+     *
+     * @return true, if aspect ratio is locked
+     */
+    public boolean isKeepAspectRatio() {
+        return trans.getLockXYScale();
+    }
+
+    /**
+     * set lock aspect ratio
+     *
+     * @param keepAspectRatio
+     */
+    public void setKeepAspectRatio(boolean keepAspectRatio) {
+        trans.setLockXYScale(keepAspectRatio);
+    }
+
+    /**
+     * is rotation by arbitrary angle allowed?
+     *
+     * @return true, if rotation by arbitary angle is allowed
+     */
+    public boolean isAllowRotationArbitraryAngle() {
+        return allowRotationArbitraryAngle;
+    }
+
+    /**
+     * is rotation by arbitary angle allowed
+     *
+     * @param allowRotationArbitraryAngle
+     */
+    public void setAllowRotationArbitraryAngle(boolean allowRotationArbitraryAngle) {
+        this.allowRotationArbitraryAngle = allowRotationArbitraryAngle;
+    }
+
+    public boolean isAllowRotation() {
+        return allowRotation;
+    }
+
+    public void setAllowRotation(boolean allowRotation) {
+        this.allowRotation = allowRotation;
+    }
+
+    /**
+     * Set the default node label.
+     *
+     * @param a the default value
+     */
+    public void setDefaultNodeLabel(String a) {
+        defaultNodeView.setLabel(a);
+    }
+
+    /**
+     * sets the default node font
+     *
+     * @param font
+     */
+    public void setDefaultNodeFont(Font font) {
+        defaultNodeView.setFont(font);
+    }
+
+    /**
+     * Set the default node location in world coordinates.
+     *
+     * @param p the default value
+     */
+    public void setDefaultNodeLocation(Point2D p) {
+        defaultNodeView.setLocation(p);
+    }
+
+    /**
+     * Set the default node location in world coordinates.
+     *
+     * @param x the default x coordinate
+     * @param y the default y coordinate
+     */
+    public void setDefaultNodeLocation(double x, double y) {
+        defaultNodeView.setLocation(new Point2D.Double(x, y));
+    }
+
+    /**
+     * Set the default node color.
+     *
+     * @param a the default value
+     */
+    public void setDefaultNodeColor(Color a) {
+        defaultNodeView.setColor(a);
+    }
+
+    /**
+     * Set the default node background color.
+     *
+     * @param a the default value
+     */
+    public void setDefaultNodeBackgroundColor(Color a) {
+        defaultNodeView.setBackgroundColor(a);
+    }
+
+    /**
+     * Set the default node label color.
+     *
+     * @param a the default value
+     */
+    public void setDefaultNodeLabelColor(Color a) {
+        defaultNodeView.setLabelColor(a);
+    }
+
+    /**
+     * Set the default node width.
+     *
+     * @param a the default value
+     */
+    public void setDefaultNodeWidth(int a) {
+        defaultNodeView.setWidth(a);
+    }
+
+    /**
+     * get the default node width.
+     *
+     * @return the default value
+     */
+    public int getDefaultNodeWidth() {
+        return defaultNodeView.getWidth();
+    }
+
+    /**
+     * Set the default node height.
+     *
+     * @param a the default value
+     */
+    public void setDefaultNodeHeight(int a) {
+        defaultNodeView.setHeight(a);
+    }
+
+    /**
+     * Set the default node line width.
+     *
+     * @param a the default value
+     */
+    public void setDefaultNodeLineWidth(int a) {
+        defaultNodeView.setLineWidth((byte) a);
+    }
+
+    /**
+     * Set the default node shape.
+     *
+     * @param a the default value
+     */
+    public void setDefaultNodeShape(byte a) {
+        defaultNodeView.setShape(a);
+    }
+
+    /**
+     * get the default node shape
+     *
+     * @return node shape
+     */
+    public byte getDefaultNodeShape() {
+        return defaultNodeView.getShape();
+    }
+
+    /**
+     * Set the default edge label.
+     *
+     * @param a the default value
+     */
+    public void setDefaultEdgeLabel(String a) {
+        defaultEdgeView.setLabel(a);
+    }
+
+    public void setDefaultEdgeFont(Font font) {
+        defaultEdgeView.setFont(font);
+    }
+
+    /**
+     * Set the default edge color.
+     *
+     * @param a the default value
+     */
+    public void setDefaultEdgeColor(Color a) {
+        defaultEdgeView.setColor(a);
+    }
+
+    /**
+     * Set the default edge label color.
+     *
+     * @param a the default value
+     */
+    public void setDefaultEdgeLabelColor(Color a) {
+        defaultEdgeView.setLabelColor(a);
+    }
+
+    /**
+     * Set the default edge line width.
+     *
+     * @param a the default value
+     */
+    public void setDefaultEdgeLineWidth(int a) {
+        defaultEdgeView.setLineWidth((byte) a);
+    }
+
+    /**
+     * Set the default edge shape.
+     *
+     * @param a the default value
+     */
+    public void setDefaultEdgeShape(byte a) {
+        defaultEdgeView.setShape(a);
+    }
+
+    /**
+     * get the default edge shape
+     *
+     * @return edge shape
+     */
+    public byte getDefaultEdgeShape() {
+        return defaultEdgeView.getShape();
+    }
+
+    /**
+     * Set the default edge direction.
+     *
+     * @param a the default value
+     */
+    public void setDefaultEdgeDirection(byte a) {
+        defaultEdgeView.setDirection(a);
+    }
+
+    /**
+     * Sets the default node label position.
+     *
+     * @param a the position (CENTAL_POS, NORTHWEST_POS,...)
+     */
+    public void setDefaultNodeLabelLayout(byte a) {
+        defaultNodeView.setLabelLayout(a);
+    }
+
+    /**
+     * Gets the node label.
+     *
+     * @param v the node
+     * @return node label
+     */
+    public String getLabel(Node v) {
+        return getNV(v).getLabel();
+    }
+
+    /**
+     * Gets the selection state of a node.
+     *
+     * @param v the node
+     * @return selection state
+     */
+    public boolean getSelected(Node v) {
+        return selectedNodes.contains(v);
+    }
+
+
+    /**
+     * Gets the node location.
+     *
+     * @param v the node
+     * @return location
+     */
+    public Point2D getLocation(Node v) {
+        return getNV(v).getLocation();
+    }
+
+    /**
+     * Gets the node foreground color.
+     *
+     * @param v the node
+     * @return foreground color
+     */
+    public Color getColor(Node v) {
+        return getNV(v).getColor();
+    }
+
+    /**
+     * Gets the node background color.
+     *
+     * @param v the node
+     * @return background color
+     */
+    public Color getBackgroundColor(Node v) {
+        return getNV(v).getBackgroundColor();
+    }
+
+    /**
+     * Gets the node border color.
+     *
+     * @param v the node
+     * @return background color
+     */
+    public Color getBorderColor(Node v) {
+        return getNV(v).getBorderColor();
+    }
+
+    /**
+     * Gets the node label color.
+     *
+     * @param v the node
+     * @return label color
+     */
+    public Color getLabelColor(Node v) {
+        return getNV(v).getLabelColor();
+    }
+
+    /**
+     * Gets the node
+     *
+     * @param v the node
+     * @return node width
+     */
+    public int getWidth(Node v) {
+        return getNV(v).getWidth();
+    }
+
+    /**
+     * Gets the node height.
+     *
+     * @param v the node
+     * @return node height
+     */
+    public int getHeight(Node v) {
+        return getNV(v).getHeight();
+    }
+
+    /**
+     * Gets the node line width.
+     *
+     * @param v the node
+     * @return line width
+     */
+    public int getLineWidth(Node v) {
+        return getNV(v).getLineWidth();
+    }
+
+    /**
+     * Gets the node bounding box in device coordinates
+     *
+     * @param v the node
+     * @return device bounding box
+     */
+    public Rectangle getBox(Node v) {
+        return getNV(v).getBox(trans);
+    }
+
+    /**
+     * Gets the node label bounding box in device coordinates
+     *
+     * @param v the node
+     * @return device bounding box
+     */
+    public Rectangle getLabelRect(Node v) {
+        return getNV(v).getLabelRect(trans);
+    }
+
+    /**
+     * Gets the edge label bounding box in device coordinates
+     *
+     * @param e the node
+     * @return device bounding box
+     */
+    public Rectangle getLabelRect(Edge e) {
+        return getEV(e).getLabelRect(trans);
+    }
+
+    /**
+     * gets the list of internal points associated with an edge, or null
+     *
+     * @param e
+     * @return list of internal points, or null
+     * @
+     */
+    public java.util.List<Point2D> getInternalPoints(Edge e) {
+        return getEV(e).getInternalPoints();
+    }
+
+    /**
+     * sets the list of internal points associated with an edge, or null
+     *
+     * @param e
+     * @param list
+     * @
+     */
+    public void setInternalPoints(Edge e, java.util.List<Point2D> list) {
+        getEV(e).setInternalPoints(list);
+    }
+
+    /**
+     * Sets the label of a node.
+     *
+     * @param v the node
+     * @param a the label
+     */
+    public void setLabel(Node v, String a) {
+        getNV(v).setLabel(a);
+    }
+
+    /**
+     * Sets the node label position for a node
+     *
+     * @param v the node
+     * @param a a position (e.g., GraphView.CENTRAL_POS, GraphView.NORTH_POS...)
+     */
+    public void setLabelLayout(Node v, byte a) {
+        getNV(v).setLabelLayout(a);
+    }
+
+    /**
+     * gets the label layout mode of the node
+     *
+     * @param v
+     * @return layout mode
+     */
+    public byte getLabelLayout(Node v) {
+        return getNV(v).getLabelLayout();
+    }
+
+    /**
+     * Sets the edge label position for a edge
+     *
+     * @param e the edge
+     * @param a a position (e.g., GraphView.CENTRAL_POS, GraphView.NORTH_POS...)
+     */
+    public void setLabelLayout(Edge e, byte a) {
+        getEV(e).setLabelLayout(a);
+    }
+
+    /**
+     * gets the label layout mode of the edge
+     *
+     * @param e
+     * @return layout mode
+     */
+    public byte getLabelLayout(Edge e) {
+        return getEV(e).getLabelLayout();
+    }
+
+    /**
+     * Is the label visible ?
+     *
+     * @param v
+     * @return visibility of the label
+     */
+    public boolean isLabelVisible(Node v) {
+        try {
+            return getNV(v).isLabelVisible();
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+            return false;
+        }
+    }
+
+    /**
+     * Is the label visible ?
+     *
+     * @param e the concerned edge
+     * @return visibility of the label
+     */
+    public boolean isLabelVisible(Edge e) {
+        try {
+            return getEV(e).isLabelVisible();
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+            return false;
+        }
+    }
+
+    /**
+     * Set label visibility
+     *
+     * @param e            the  edge
+     * @param labelVisible visibility of the label
+     */
+    public void setLabelVisible(Edge e, boolean labelVisible) {
+        try {
+            getEV(e).setLabelVisible(labelVisible);
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * is label visible?
+     *
+     * @return true, if  visible
+     */
+
+    public boolean getLabelVisible(Edge e) {
+        return getEV(e).getLabelVisible();
+    }
+
+    /**
+     * Set label visibility
+     *
+     * @param v            the node
+     * @param labelVisible visibility of the label
+     */
+    public void setLabelVisible(Node v, boolean labelVisible) {
+        getNV(v).setLabelVisible(labelVisible);
+    }
+
+    /**
+     * is label visible?
+     *
+     * @return true, if  visible
+     */
+    public boolean getLabelVisible(Node v) {
+        return getNV(v).getLabelVisible();
+    }
+
+
+    /**
+     * Set the node label position for all nodes
+     *
+     * @param a a position (e.g., GraphView.CENTRAL_POS, GraphView.NORTH_POS...)
+     */
+    public void setLabelLayoutAllNodes(byte a) {
+        try {
+            Graph graph = this.getGraph();
+            for (Node v = graph.getFirstNode(); v != null; v = graph.getNextNode(v)) {
+                setLabelLayout(v, a);
+            }
+        } catch (Exception ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * Sets the selection state.
+     *
+     * @param v      the node
+     * @param select the selection state
+     */
+    public void setSelected(Node v, boolean select) {
+        if (select) {
+            if (!selectedNodes.contains(v)) {
+                selectedNodes.add(v);
+                NodeSet aset = new NodeSet(G);
+                aset.add(v);
+                fireDoSelect(aset);
+            }
+        } else {
+            if (selectedNodes.contains(v)) {
+                NodeSet aset = new NodeSet(G);
+                aset.add(v);
+                selectedNodes.remove(v);
+                fireDoDeselect(aset);
+            }
+        }
+    }
+
+    /**
+     * Sets the selection state.
+     *
+     * @param nodes the nodes
+     * @param a     the selection state
+     */
+    public void setSelected(NodeSet nodes, boolean a) {
+        if (a) {
+            if (!selectedNodes.containsAll(nodes)) {
+                selectedNodes.addAll(nodes);
+                fireDoSelect(nodes);
+            }
+        } else {
+            if (selectedNodes.intersects(nodes)) {
+                selectedNodes.removeAll(nodes);
+                fireDoDeselect(nodes);
+            }
+        }
+    }
+
+    /**
+     * Sets the location.
+     *
+     * @param v the node
+     * @param p the location
+     */
+    public void setLocation(Node v, Point2D p) {
+        getNV(v).setLocation(p);
+    }
+
+    /**
+     * Sets the location.
+     *
+     * @param v the node
+     * @param x the x-coordinate of the location
+     * @param y the y-coordinate of the location
+     */
+    public void setLocation(Node v, double x, double y) {
+        getNV(v).setLocation(new Point2D.Double(x, y));
+    }
+
+    /**
+     * set the relative label location for a node
+     *
+     * @param v  the node
+     * @param pt the offset in device coordinates
+     */
+    public void setLabelPositionRelative(Node v, Point pt) {
+        getNV(v).setLabelPositionRelative(pt.x, pt.y);
+    }
+
+    /**
+     * set the relative label location for an edge
+     *
+     * @param e        edge
+     * @param location in device coordinates
+     */
+    public void setLabelPositionRelative(Edge e, Point location) {
+        getEV(e).setLabelPositionRelative(location);
+    }
+
+    /**
+     * Sets the foreground color.
+     *
+     * @param v the node
+     * @param a the color
+     */
+    public void setColor(Node v, Color a) {
+        getNV(v).setColor(a);
+    }
+
+    /**
+     * Sets the background color.
+     *
+     * @param v the node
+     * @param a the color
+     */
+    public void setBackgroundColor(Node v, Color a) {
+        getNV(v).setBackgroundColor(a);
+    }
+
+    /**
+     * Sets the border color.
+     *
+     * @param v the node
+     * @param a the color
+     */
+    public void setBorderColor(Node v, Color a) throws
+            NotOwnerException {
+        getNV(v).setBorderColor(a);
+    }
+
+    /**
+     * Sets the label color.
+     *
+     * @param v the node
+     * @param a the color
+     */
+    public void setLabelColor(Node v, Color a) {
+        if (getNV(v).getLabelVisible())
+            getNV(v).setLabelColor(a);
+    }
+
+    /**
+     * Sets the label background color.
+     *
+     * @param v the node
+     * @param a the color
+     */
+    public void setLabelBackgroundColor(Node v, Color a) {
+        if (getNV(v).getLabelVisible())
+            getNV(v).setLabelBackgroundColor(a);
+    }
+
+    /**
+     * gets the labels background color
+     *
+     * @param v
+     * @return color
+     */
+    public Color getLabelBackgroundColor(Node v) {
+        return getNV(v).getLabelBackgroundColor();
+    }
+
+    /**
+     * Sets the width.
+     *
+     * @param v the node
+     * @param a the width
+     */
+    public void setWidth(Node v, int a) {
+        getNV(v).setWidth(a);
+    }
+
+    /**
+     * Sets the height.
+     *
+     * @param v the node
+     * @param a the height
+     */
+    public void setHeight(Node v, int a) {
+        getNV(v).setHeight(a);
+    }
+
+    /**
+     * Sets the line width.
+     *
+     * @param v the node
+     * @param a the line width
+     */
+    public void setLineWidth(Node v, int a) {
+        getNV(v).setLineWidth((byte) a);
+    }
+
+    /**
+     * Sets the line width for all selected nodes and edges
+     *
+     * @param a the line width
+     */
+    public void setLineWidthSelected(int a) {
+        setLineWidthSelectedNodes((byte) a);
+        setLineWidthSelectedEdges((byte) a);
+    }
+
+    /**
+     * Sets the line width for all selected nodes
+     *
+     * @param a the line width
+     */
+    public void setLineWidthSelectedNodes(byte a) {
+        try {
+            for (Node v = selectedNodes.getFirstElement(); v != null;
+                 v = selectedNodes.getNextElement(v)) {
+                setLineWidth(v, a);
+            }
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * Sets the line width for all selected edges
+     *
+     * @param a the line width
+     */
+    public void setLineWidthSelectedEdges(byte a) {
+        try {
+            for (Edge e = selectedEdges.getFirstElement(); e != null;
+                 e = selectedEdges.getNextElement(e)) {
+                setLineWidth(e, a);
+            }
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+
+    /**
+     * Sets the shape.
+     *
+     * @param v the node
+     * @param a the shape
+     */
+    public void setShape(Node v, byte a) {
+        getNV(v).setShape(a);
+    }
+
+    /**
+     * Gets the shape.
+     *
+     * @param v the node
+     * @return the shape
+     */
+
+    public byte getShape(Node v) {
+        return getNV(v).getShape();
+    }
+
+    /**
+     * Gets the edge label.
+     *
+     * @param e the edge
+     * @return the label
+     */
+    public String getLabel(Edge e) {
+        return getEV(e).getLabel();
+    }
+
+    /**
+     * Gets the selection state.
+     *
+     * @param e the edge
+     * @return the selection state
+     */
+    public boolean getSelected(Edge e) {
+        return selectedEdges.contains(e);
+    }
+
+
+    /**
+     * Gets the edge foreground color.
+     *
+     * @param e the edge
+     * @return the color
+     */
+    public Color getColor(Edge e) {
+        return getEV(e).getColor();
+    }
+
+    /**
+     * Gets the edge label color.
+     *
+     * @param e the edge
+     * @return the color
+     */
+    public Color getLabelColor(Edge e) {
+        return getEV(e).getLabelColor();
+    }
+
+    /**
+     * Sets the label background color.
+     *
+     * @param e the edge
+     * @param a the color
+     */
+    public void setLabelBackgroundColor(Edge e, Color a) {
+        if (getEV(e).isLabelVisible())
+            getEV(e).setLabelBackgroundColor(a);
+    }
+
+    /**
+     * gets the labels background color
+     *
+     * @param e
+     * @return color
+     */
+    public Color getLabelBackgroundColor(Edge e) {
+        return getEV(e).getLabelBackgroundColor();
+    }
+
+
+    /**
+     * Gets the edge line width.
+     *
+     * @param e the edge
+     * @return the line width
+     */
+    public int getLineWidth(Edge e) {
+        return getEV(e).getLineWidth();
+    }
+
+    /**
+     * Gets the edges direction.
+     *
+     * @param e the edge
+     * @return the direction
+     */
+    public int getDirection(Edge e) {
+        return getEV(e).getDirection();
+    }
+
+    /**
+     * Sets the label.
+     *
+     * @param e the edge
+     * @param a the label
+     */
+    public void setLabel(Edge e, String a) {
+        getEV(e).setLabel(a);
+    }
+
+    /**
+     * Sets the selection state of an edge.
+     *
+     * @param e the edge
+     * @param a the selection state
+     */
+    public void setSelected(Edge e, boolean a) {
+        if (a) {
+            if (!selectedEdges.contains(e)) {
+                selectedEdges.add(e);
+                EdgeSet aset = new EdgeSet(G);
+                aset.add(e);
+                fireDoSelect(aset);
+            }
+        } else {
+            if (selectedEdges.contains(e)) {
+                EdgeSet aset = new EdgeSet(G);
+                aset.add(e);
+                fireDoDeselect(aset);
+                selectedEdges.remove(e);
+            }
+        }
+    }
+
+    /**
+     * Sets the selection state.
+     *
+     * @param edges the edges
+     * @param a     the selection state
+     */
+    public void setSelected(EdgeSet edges, boolean a) {
+        if (a) {
+            if (!selectedEdges.containsAll(edges)) {
+                selectedEdges.addAll(edges);
+                fireDoSelect(edges);
+            }
+        } else {
+            if (selectedEdges.intersects(edges)) {
+                selectedEdges.removeAll(edges);
+                fireDoDeselect(edges);
+            }
+        }
+    }
+
+
+    /**
+     * Sets the foreground color.
+     *
+     * @param e the edge
+     * @param a the color
+     */
+    public void setColor(Edge e, Color a) {
+        getEV(e).setColor(a);
+    }
+
+    /**
+     * set the color of selected nodes and edges
+     *
+     * @param a    color
+     * @param kind "line", "fill" or "label"
+     */
+    public boolean setColorSelected(Color a, String kind) {
+        boolean changed = false;
+        switch (kind) {
+            case "line":
+                if (setColorSelectedNodes(a))
+                    changed = true;
+                if (setColorSelectedEdges(a)) changed = true;
+                break;
+            case "fill":
+                if (setBackgroundColorSelectedNodes(a)) changed = true;
+                break;
+            case "label":
+                if (setLabelColorSelectedNodes(a)) changed = true;
+                if (setLabelColorSelectedEdges(a)) changed = true;
+                break;
+        }
+        return changed;
+    }
+
+    /**
+     * Sets the color for all selected nodes and edges
+     *
+     * @param a the color
+     */
+    public boolean setColorSelectedEdges(Color a) {
+        boolean changed = false;
+        for (Edge e = selectedEdges.getFirstElement(); e != null;
+             e = selectedEdges.getNextElement(e)) {
+            if (getColor(e) == null || !getColor(e).equals(a)) {
+                changed = true;
+                setColor(e, a);
+            }
+        }
+        return changed;
+    }
+
+    /**
+     * Sets the color for all selected nodes and edges
+     *
+     * @param a the color
+     */
+    public boolean setLabelColorSelectedEdges(Color a) {
+        boolean changed = false;
+        for (Edge e = selectedEdges.getFirstElement(); e != null;
+             e = selectedEdges.getNextElement(e)) {
+            if (getLabelVisible(e) && (getLabelColor(e) == null || !getLabelColor(e).equals(a))) {
+                setLabelColor(e, a);
+                changed = true;
+            }
+        }
+        return changed;
+    }
+
+
+    /**
+     * Sets the color for all selected nodes
+     *
+     * @param a the color
+     */
+    public boolean setColorSelectedNodes(Color a) {
+        boolean changed = false;
+        for (Node v = selectedNodes.getFirstElement(); v != null;
+             v = selectedNodes.getNextElement(v)) {
+            if (getColor(v) == null || !getColor(v).equals(a)) {
+                changed = true;
+                setColor(v, a);
+            }
+        }
+        return changed;
+    }
+
+    /**
+     * Sets the label color for all selected nodes
+     *
+     * @param a the color
+     */
+    public boolean setLabelColorSelectedNodes(Color a) {
+        boolean changed = false;
+        for (Node v = selectedNodes.getFirstElement(); v != null;
+             v = selectedNodes.getNextElement(v)) {
+            if (getLabelVisible(v) && (getLabelColor(v) == null || !getLabelColor(v).equals(a))) {
+                changed = true;
+                setLabelColor(v, a);
+            }
+        }
+        return changed;
+    }
+
+    /**
+     * Sets the background color for all selected nodes
+     *
+     * @param a the color
+     */
+    public boolean setBackgroundColorSelectedNodes(Color a) {
+        boolean changed = false;
+        for (Node v = selectedNodes.getFirstElement(); v != null;
+             v = selectedNodes.getNextElement(v)) {
+            if (getBackgroundColor(v) == null || !getBackgroundColor(v).equals(a)) {
+                changed = true;
+                setBackgroundColor(v, a);
+            }
+        }
+        return changed;
+    }
+
+    /**
+     * Sets the label color.
+     *
+     * @param e the edge
+     * @param a the color
+     */
+    public void setLabelColor(Edge e, Color a) {
+        if (getEV(e).isLabelVisible())
+            getEV(e).setLabelColor(a);
+    }
+
+    /**
+     * Sets the line width.
+     *
+     * @param e the edge
+     * @param a the line width
+     */
+    public void setLineWidth(Edge e, int a) {
+        getEV(e).setLineWidth((byte) Math.min(a, Byte.MAX_VALUE));
+    }
+
+    /**
+     * Sets the shape.
+     *
+     * @param e the edge
+     * @param a the shape
+     */
+    public void setShape(Edge e, byte a) {
+        getEV(e).setShape(a);
+    }
+
+    /**
+     * gets the shape.
+     *
+     * @param e the edge
+     * @return a the shape
+     */
+    public byte getShape(Edge e) {
+        return getEV(e).getShape();
+    }
+
+    /**
+     * Sets the edges direction.
+     *
+     * @param e the edge
+     * @param a the direction
+     */
+    public void setDirection(Edge e, byte a) {
+        getEV(e).setDirection(a);
+    }
+
+    public Font getFont(Node v) {
+        if (getNV(v).getFont() != null)
+            return getNV(v).getFont();
+        else
+            return getFont();
+    }
+
+    public void setFont(Node v, Font font) {
+        getNV(v).setFont(font);
+    }
+
+    public Font getFont(Edge e) {
+        if (getEV(e).getFont() != null)
+            return getEV(e).getFont();
+        else
+            return getFont();
+    }
+
+    public void setFont(Edge e, Font font) {
+        getEV(e).setFont(font);
+    }
+
+
+    /**
+     * Sets the font for all selected nodes and edges
+     *
+     * @param font
+     */
+    public void setFontSelected(Font font) {
+        setFontSelectedNodes(font);
+        setFontSelectedEdges(font);
+    }
+
+
+    /**
+     * Sets the font for all selected edges
+     *
+     * @param font
+     */
+    public void setFontSelectedEdges(Font font) {
+        for (Edge e = selectedEdges.getFirstElement(); e != null;
+             e = selectedEdges.getNextElement(e)) {
+            setFont(e, font);
+        }
+    }
+
+
+    /**
+     * Sets the font for all selected edges. Only sets those parts that are not null or -1
+     *
+     * @param family
+     * @param bold
+     * @param italics
+     * @param size
+     * @return changed?
+     */
+    public boolean setFontSelectedEdges(String family, int bold, int italics, int size) {
+        boolean changed = false;
+        for (Edge e : getSelectedEdges()) {
+            String familyE = getFont(e).getFamily();
+            int styleE = getFont(e).getStyle();
+            int sizeE = getFont(e).getSize();
+            int style = 0;
+            if (bold == 1 || (bold == -1 && (styleE == Font.BOLD || styleE == Font.BOLD + Font.ITALIC)))
+                style += Font.BOLD;
+            if (italics == 1 || (italics == -1 && (styleE == Font.ITALIC || styleE == Font.BOLD + Font.ITALIC)))
+                style += Font.ITALIC;
+
+            Font font = new Font(family != null ? family : familyE, style != -1 ? style : styleE, size != -1 ? size : sizeE);
+            if (getFont(e) == null || !getFont(e).equals(font)) {
+                changed = true;
+                setFont(e, font);
+            }
+        }
+        return changed;
+    }
+
+
+    /**
+     * Sets the font for all selected nodes. Only sets those parts that are not null or -1
+     *
+     * @param family
+     * @param bold
+     * @param italics
+     * @param size
+     * @return changed?
+     */
+    public boolean setFontSelectedNodes(String family, int bold, int italics, int size) {
+        boolean changed = false;
+        for (Node v : getSelectedNodes()) {
+            String familyE = getFont(v).getFamily();
+            int styleE = getFont(v).getStyle();
+            int sizeE = getFont(v).getSize();
+            int style = 0;
+            if (bold == 1 || (bold == -1 && (styleE == Font.BOLD || styleE == Font.BOLD + Font.ITALIC)))
+                style += Font.BOLD;
+            if (italics == 1 || (italics == -1 && (styleE == Font.ITALIC || styleE == Font.BOLD + Font.ITALIC)))
+                style += Font.ITALIC;
+
+            Font font = new Font(family != null ? family : familyE, style != -1 ? style : styleE, size != -1 ? size : sizeE);
+            if (getFont(v) == null || !getFont(v).equals(font)) {
+                changed = true;
+                setFont(v, font);
+            }
+        }
+        return changed;
+    }
+
+
+    /**
+     * Sets the font for all selected nodes
+     *
+     * @param font
+     */
+    public void setFontSelectedNodes(Font font) {
+        try {
+            for (Node v = selectedNodes.getFirstElement(); v != null;
+                 v = selectedNodes.getNextElement(v)) {
+                setFont(v, font);
+            }
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * sets the size of all selected nodes
+     *
+     * @param width
+     * @param height
+     */
+    public void setSizeSelectedNodes(byte width, byte height) {
+        try {
+            for (Node v = selectedNodes.getFirstElement(); v != null;
+                 v = selectedNodes.getNextElement(v)) {
+                setWidth(v, width);
+                setHeight(v, height);
+            }
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+
+    /**
+     * Returns the NodeView corresponding to v.
+     *
+     * @param v the node
+     * @return the NodeView
+     */
+    public NodeView getNV(Node v) {
+        if (nodeViews.get(v) == null) {
+            NodeView nv = new NodeView(defaultNodeView);
+            nodeViews.set(v, nv);
+            if (nv.getLocation() == null)
+                nv.setLocation(trans.getRandomVisibleLocation());
+        }
+        return nodeViews.get(v);
+    }
+
+    /**
+     * Returns the EdgeView corresponding to e.
+     *
+     * @param e the edge
+     * @return the EdgeView
+     */
+    public EdgeView getEV(Edge e) {
+        if (edgeViews.get(e) == null) {
+            EdgeView ev = new EdgeView(defaultEdgeView);
+            edgeViews.set(e, ev);
+        }
+        return edgeViews.get(e);
+    }
+
+    /**
+     * Returns the graph.
+     *
+     * @return the graph
+     */
+    public Graph getGraph() {
+        return G;
+    }
+
+
+    /**
+     * Select all nodes.
+     *
+     * @param select value to set selection states to
+     * @return true, if selection state of at least one node changed
+     */
+    public boolean selectAllNodes(boolean select) {
+        if (!select) {
+            if (selectedNodes.size() > 0) {
+                NodeSet oldSelected = (NodeSet) selectedNodes.clone();
+                selectedNodes.clear();
+                fireDoDeselect(oldSelected);
+                return true;
+            }
+        } else {
+            if (selectedNodes.size() < getGraph().getNumberOfNodes()) {
+                selectedNodes.addAll();
+                fireDoSelect(selectedNodes);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Select all inner nodes.
+     *
+     * @param select value to set selection states to
+     * @return true, if selection state of at least one node changed
+     */
+    public boolean selectAllInnerNodes(boolean select) {
+        NodeSet changed = new NodeSet(getGraph());
+
+        for (Node v = getGraph().getFirstNode(); v != null; v = v.getNext()) {
+            if (v.getDegree() > 1 && getSelected(v) != select) {
+                if (select)
+                    selectedNodes.add(v);
+                else
+                    selectedNodes.remove(v);
+                changed.add(v);
+            }
+        }
+        if (select)
+            fireDoSelect(changed);
+        else
+            fireDoSelect(changed);
+        return changed.size() > 0;
+    }
+
+    /**
+     * Select all leaf nodes.
+     *
+     * @param select value to set selection states to
+     * @return true, if selection state of at least one node changed
+     */
+    public boolean selectAllLeafNodes(boolean select) {
+        NodeSet changed = new NodeSet(getGraph());
+
+        for (Node v = getGraph().getFirstNode(); v != null; v = v.getNext()) {
+            if (v.getDegree() == 1 && getSelected(v) != select) {
+                if (select)
+                    selectedNodes.add(v);
+                else
+                    selectedNodes.remove(v);
+                changed.add(v);
+            }
+        }
+        if (select)
+            fireDoSelect(changed);
+        else
+            fireDoSelect(changed);
+        return changed.size() > 0;
+    }
+
+    /**
+     * Select all edges.
+     *
+     * @param select value to set selection states to
+     * @return true, if selection state of at least one edge changed
+     */
+    public boolean selectAllEdges(boolean select) {
+        if (!select) {
+            if (selectedEdges.size() > 0) {
+                EdgeSet oldSelected = (EdgeSet) selectedEdges.clone();
+                selectedEdges.clear();
+                fireDoDeselect(oldSelected);
+                return true;
+            }
+        } else {
+            if (selectedEdges.size() < getGraph().getNumberOfEdges()) {
+                selectedEdges.addAll();
+                fireDoSelect(selectedEdges);
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+    /**
+     * inverts the selection state of all nodes and edges
+     */
+    public void invertSelection() {
+        invertNodeSelection();
+        invertEdgeSelection();
+    }
+
+    /**
+     * inverts the selection state of all nodes
+     */
+    public void invertNodeSelection() {
+        NodeSet oldSelected = (NodeSet) selectedNodes.clone();
+        for (Node a = G.getFirstNode(); a != null; a = G.getNextNode(a)) {
+            if (selectedNodes.contains(a))
+                selectedNodes.remove(a);
+
+            else
+                selectedNodes.add(a);
+        }
+        fireDoDeselect(oldSelected);
+        fireDoSelect(selectedNodes);
+    }
+
+    /**
+     * inverts the selection state of all edges
+     */
+    public void invertEdgeSelection() {
+        EdgeSet oldSelected = (EdgeSet) selectedEdges.clone();
+        for (Edge a = G.getFirstEdge(); a != null; a = G.getNextEdge(a)) {
+            if (selectedEdges.contains(a))
+                selectedEdges.remove(a);
+
+            else
+                selectedEdges.add(a);
+        }
+        fireDoDeselect(oldSelected);
+        fireDoSelect(selectedEdges);
+    }
+
+    /**
+     * delete all selected nodes.
+     */
+    public void delSelectedNodes() {
+        Graph G = getGraph();
+        // synchronized (G)
+        {
+
+            for (Node v : selectedNodes) {
+                fireDoDelete(v);
+                G.deleteNode(v);
+            }
+        }
+    }
+
+    /**
+     * delete all selected edges.
+     */
+    public void delSelectedEdges() {
+        Graph G = getGraph();
+        for (Edge e : selectedEdges) {
+            fireDoDelete(e);
+            G.deleteEdge(e);
+        }
+    }
+
+    /**
+     * Creates a new node and informs the listeners of it. These in turn may
+     * cancel the node creation.
+     *
+     * @return the new node or null if creation was cancelled.
+     */
+    public Node newNode() {
+        Node v = G.newNode();
+        fireDoNew(v);
+        if (v.getOwner() != null) {
+            return v;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Creates a new edge and informs the listeners of it. These in turn may
+     * cancel the edge creation.
+     *
+     * @param v the source vertex.
+     * @param w the target vertex.
+     * @return the new edge or null if creation was cancelled..
+     * @ if the source or target is not ours.
+     */
+    public Edge newEdge(Node v, Node w) {
+        if (w == null) {
+            w = G.newNode();
+            fireDoNew(v, w);
+        }
+        if (w != null && w.getOwner() != null) {
+            Edge e = G.newEdge(v, w);
+            if (e != null) {
+                fireDoNew(e);
+                if (e.getOwner() != null) {
+                    return e;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Horizonally flips all selected nodes.
+     */
+    public void horizontalFlipSelected() {
+        Graph G = getGraph();
+        double minx = 10000000;
+        double maxx = -10000000;
+
+        try {
+            for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                if (getSelected(v)) {
+                    Point2D p = getLocation(v);
+                    if (p.getX() < minx)
+                        minx = p.getX();
+                    if (p.getX() > maxx)
+                        maxx = p.getX();
+                }
+            }
+            double pivot = (maxx + minx);
+            for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                if (getSelected(v)) {
+                    Point2D p = getLocation(v);
+                    p.setLocation(pivot - p.getX(), p.getY());
+                }
+            }
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * Vertically flips all selected nodes.
+     */
+    public void verticalFlipSelected() {
+        Graph G = getGraph();
+        double miny = 10000000;
+        double maxy = -10000000;
+
+        try {
+            for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                if (getSelected(v)) {
+                    Point2D p = getLocation(v);
+                    if (p.getY() < miny)
+                        miny = p.getY();
+                    if (p.getY() > maxy)
+                        maxy = p.getY();
+                }
+            }
+            double pivot = (maxy + miny);
+            for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                if (getSelected(v)) {
+                    Point2D p = getLocation(v);
+                    p.setLocation(p.getX(), pivot - p.getY());
+                }
+            }
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * Computes a spring embedding of the graph
+     *
+     * @param iterations       the number of iterations used
+     * @param startFromCurrent use current node positions
+     */
+    public void computeSpringEmbedding(int iterations, boolean startFromCurrent) {
+        try {
+            final Graph G = getGraph();
+
+            final double width = getSize().width;
+            final double height = getSize().height;
+
+            if (G.getNumberOfNodes() < 2)
+                return;
+
+            // Initial positions are on a circle:
+            final NodeDoubleArray xPos = new NodeDoubleArray(G);
+            final NodeDoubleArray yPos = new NodeDoubleArray(G);
+
+            int i = 0;
+            for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                if (startFromCurrent) {
+                    Point2D p = getLocation(v);
+                    xPos.set(v, p.getX());
+                    yPos.set(v, p.getY());
+                } else {
+                    xPos.set(v, 1000 * Math.sin(6.24 * i / G.getNumberOfNodes()));
+                    yPos.set(v, 1000 * Math.cos(6.24 * i / G.getNumberOfNodes()));
+                    i++;
+                }
+            }
+
+            // run iterations of spring embedding:
+            double log2 = Math.log(2);
+            for (int count = 1; count < iterations; count++) {
+                final double k = Math.sqrt(width * height / G.getNumberOfNodes()) / 2;
+                final double l2 = 25 * log2 * Math.log(1 + count);
+                final double tx = width / l2;
+                final double ty = height / l2;
+
+                final NodeDoubleArray xDispl = new NodeDoubleArray(G);
+                final NodeDoubleArray yDispl = new NodeDoubleArray(G);
+
+                // repulsive forces
+
+                for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                    double xv = xPos.getValue(v);
+                    double yv = yPos.getValue(v);
+
+                    for (Node u = G.getFirstNode(); u != null; u = G.getNextNode(u)) {
+                        if (u == v)
+                            continue;
+                        double xDist = xv - xPos.getValue(u);
+                        double yDist = yv - yPos.getValue(u);
+                        double dist = xDist * xDist + yDist * yDist;
+                        if (dist < 1e-3)
+                            dist = 1e-3;
+                        double repulse = k * k / dist;
+                        xDispl.set(v, xDispl.getValue(v) + repulse * xDist);
+                        yDispl.set(v, yDispl.getValue(v) + repulse * yDist);
+                    }
+
+                    for (Edge e = G.getFirstEdge(); e != null; e = G.getNextEdge(e)) {
+                        final Node a = G.getSource(e);
+                        final Node b = G.getTarget(e);
+                        if (a == v || b == v)
+                            continue;
+                        double xDist = xv - (xPos.getValue(a) + xPos.getValue(b)) / 2;
+                        double yDist = yv - (yPos.getValue(a) + yPos.getValue(b)) / 2;
+                        double dist = xDist * xDist + yDist * yDist;
+                        if (dist < 1e-3)
+                            dist = 1e-3;
+                        double repulse = k * k / dist;
+                        xDispl.set(v, xDispl.getValue(v) + repulse * xDist);
+                        yDispl.set(v, yDispl.getValue(v) + repulse * yDist);
+                    }
+                }
+
+                // attractive forces
+
+                for (Edge e = G.getFirstEdge(); e != null; e = G.getNextEdge(e)) {
+                    final Node u = G.getSource(e);
+                    final Node v = G.getTarget(e);
+
+                    double xDist = xPos.getValue(v) - xPos.getValue(u);
+                    double yDist = yPos.getValue(v) - yPos.getValue(u);
+
+                    double dist = Math.sqrt(xDist * xDist + yDist * yDist);
+
+                    dist /= ((G.getDegree(u) + G.getDegree(v)) / 16.0);
+
+                    xDispl.set(v, xDispl.getValue(v) - xDist * dist / k);
+                    yDispl.set(v, yDispl.getValue(v) - yDist * dist / k);
+                    xDispl.set(u, xDispl.getValue(u) + xDist * dist / k);
+                    yDispl.set(u, yDispl.getValue(u) + yDist * dist / k);
+                }
+
+                // preventions
+
+                for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                    double xd = xDispl.getValue(v);
+                    double yd = yDispl.getValue(v);
+
+                    final double dist = Math.sqrt(xd * xd + yd * yd);
+
+                    xd = tx * xd / dist;
+                    yd = ty * yd / dist;
+
+                    xPos.set(v, xPos.getValue(v) + xd);
+                    yPos.set(v, yPos.getValue(v) + yd);
+                }
+            }
+
+            // set node positions
+            for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                setLocation(v, xPos.getValue(v), yPos.getValue(v));
+            }
+        } catch (Exception ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * Paint.
+     *
+     * @param gc0 the Graphics
+     */
+    public void paint(Graphics gc0) {
+        boolean inDrawOnScreen = (!ExportManager.inWriteToFileOrGetData() && !inPrint);
+
+        Graphics2D gc = (Graphics2D) gc0;
+
+        Rectangle totalRect;
+        Rectangle frameRect;
+        frameRect = new Rectangle(getScrollPane().getHorizontalScrollBar().getValue(),
+                getScrollPane().getVerticalScrollBar().getValue(),
+                getScrollPane().getHorizontalScrollBar().getVisibleAmount(),
+                getScrollPane().getVerticalScrollBar().getVisibleAmount());
+
+        if (inDrawOnScreen)
+            totalRect = frameRect;
+        else
+            totalRect = trans.getPreferredRect();
+
+        /*
+        gc.setColor(Color.GREEN);
+        gc.draw(getBBox());
+        */
+
+        if (canvasColor != null) {
+            gc.setColor(inDrawOnScreen ? canvasColor : Color.WHITE);
+            if (!inPrint)
+                gc.fill(totalRect);
+        }
+
+        if (inDrawOnScreen && trans.getMagnifier().isActive())
+            trans.getMagnifier().draw(gc);
+
+        /*
+        Point2D c = trans.getDeviceRotationCenter();
+        Rectangle2D r = new Rectangle2D.Double(c.getX()-3,c.getY()-3,6,6);
+        gc.setColor(Color.red);
+        gc.draw(r);
+         gc.fill(r);
+        */
+
+        gc.setColor(Color.BLACK);
+
+        // gc.draw(trans.w2d(getBBox()));
+
+        if (graphDrawer != null)
+            graphDrawer.paint(gc, inDrawOnScreen ? totalRect : null);
+
+        if (getFoundNode() != null && (getFoundNode().getOwner() == null || !getSelected(getFoundNode()))) {
+            setFoundNode(null);
+        }
+
+        if (getFoundNode() != null) {
+            Node v = getFoundNode();
+            NodeView nv = getNV(v);
+            boolean selected = getSelected(v);
+            if (selected) {
+                if (nv.getLabel() != null)
+                    nv.setLabelSize(Basic.getStringSize(gc, getLabel(v), getFont(v)));
+                Color saveColor = nv.getLabelBackgroundColor();
+                nv.setLabelBackgroundColor(Color.YELLOW);
+                nv.drawLabel(gc, trans, getFont(), true);
+                nv.setLabelBackgroundColor(saveColor);
+            }
+        }
+        drawScaleBar(gc, inPrint ? totalRect : frameRect);
+        drawPoweredBy(gc, inPrint ? totalRect : frameRect);
+    }
+
+    /**
+     * draws a scale bar
+     */
+    protected void drawScaleBar(Graphics2D gc, Rectangle rect) {
+
+        if (isDrawScaleBar() && getGraph().getNumberOfNodes() > 0) {
+            gc.setColor(Color.gray);
+            gc.setStroke(new BasicStroke(1));
+
+
+            double d = 0.00001;
+            for (; d < 1000000; d *= 10) {
+                Point2D start = trans.w2d(new Point2D.Double(0, 0));
+                Point2D finish = trans.w2d(new Point2D.Double(10 * d, 0));
+                if (Math.abs(start.getX() - finish.getX()) > 150)
+                    break;
+            }
+
+            int x = rect.x + 20;
+            int y = rect.y + 20;
+
+            Point2D start = trans.w2d(new Point2D.Double(0, 0));
+            Point2D finish = trans.w2d(new Point2D.Double(d, 0));
+            finish = new Point2D.Double(start.distance(finish) + x, y);
+            start = new Point2D.Double(x, y);
+
+            gc.drawLine((int) start.getX(), (int) start.getY(), (int) finish.getX(), (int) finish.getY());
+
+            Font oldFont = gc.getFont();
+            gc.setFont(scaleBarFont);
+
+            gc.drawLine((int) start.getX(), (int) start.getY() - 3, (int) start.getX(), (int) start.getY() + 3);
+            gc.drawLine((int) finish.getX(), (int) finish.getY() - 3, (int) finish.getX(), (int) finish.getY() + 3);
+
+            gc.drawString("" + d, (int) finish.getX() + 2, (int) finish.getY() + 6);
+
+            gc.setFont(oldFont);
+        }
+    }
+
+    /**
+     * draws the powered by logo
+     *
+     * @param gc
+     */
+    protected void drawPoweredBy(Graphics2D gc, Rectangle rect) {
+        if (POWEREDBY != null && POWEREDBY.length() > 2) {
+            gc.setColor(Color.gray);
+            gc.setStroke(new BasicStroke(1));
+            gc.setFont(poweredByFont);
+            int width = (int) Basic.getStringSize(gc, POWEREDBY, gc.getFont()).getWidth();
+            int x = rect.x + rect.width - width - 2;
+            int y = rect.y + rect.height - 2;
+            gc.drawString(POWEREDBY, x, y);
+        }
+    }
+
+    /**
+     * Fit graph to canvas.
+     */
+    public void fitGraphToWindow() {
+        Dimension size = scrollPane.getSize();
+        if (getGraph().getNumberOfNodes() > 0)
+            trans.fitToSize(new Dimension(size.width - 200, size.height - 200));
+        else
+            trans.fitToSize(new Dimension(0, 0));
+        centerGraph();
+    }
+
+    /**
+     * centers the graph
+     */
+    public void centerGraph() {
+        JScrollBar hScrollBar = getScrollPane().getHorizontalScrollBar();
+        hScrollBar.setValue((hScrollBar.getMaximum() - hScrollBar.getModel().getExtent() + hScrollBar.getMinimum()) / 2);
+        JScrollBar vScrollBar = getScrollPane().getVerticalScrollBar();
+        vScrollBar.setValue((vScrollBar.getMaximum() - vScrollBar.getModel().getExtent() + vScrollBar.getMinimum()) / 2);
+/*
+        JScrollBar hScrollBar = getScrollPane().getHorizontalScrollBar();
+        int x = trans.getLeftMargin() - 100;
+        hScrollBar.setValue(x);
+        JScrollBar vScrollBar = getScrollPane().getVerticalScrollBar();
+        int y = trans.getTopMargin() - 100;
+        vScrollBar.setValue(y);
+        getScrollPane().getViewport().setViewPosition(new Point(x, y));
+        */
+        //revalidate();
+    }
+
+    /**
+     * reset the transform margins after a resize or center graph operation.
+     * This automatically sets the margins to half of the width or height of the pane
+     */
+    public void recomputeMargins() {
+        Dimension size = scrollPane.getSize();
+        if (size.width == 0 || size.height == 0) {
+            scrollPane.setSize(getSize());
+            size = getSize();
+        }
+
+        trans.setLeftMargin(size.width / 2);
+        trans.setRightMargin(size.width / 2);
+        trans.setTopMargin(size.height / 2);
+        trans.setBottomMargin(size.height / 2);
+
+        Dimension ps = trans.getPreferredSize();
+        if (Math.abs(size.width - ps.width) > 3 || Math.abs(size.height - ps.height) > 3) {
+            setPreferredSize(ps);
+            getScrollPane().getViewport().setViewSize(ps);
+        }
+        //revalidate();
+    }
+
+    /**
+     * We want bidirectional edges to be drawn parallel, not on top of each
+     * other and this method does the necessary coordinate adjustments.
+     *
+     * @param pv source position in device coordinates
+     * @param pw target position in device coordinates
+     */
+    public void adjustBiEdge(Point pv, Point pw) {
+        Point2D p = new Point2D.Double(pw.getX() - pv.getX(), pw.getY() - pv.getY());
+        if (p.getX() == 0 && p.getY() == 0)
+            return;
+        double alpha = Geometry.computeAngle(p) + 1.57079632679489661923; // PI/2
+        p = Geometry.translateByAngle(pv, alpha, 3);
+        pv.setLocation(p.getX(), p.getY());
+        p = Geometry.translateByAngle(pw, alpha, 3);
+        pw.setLocation(p.getX(), p.getY());
+    }
+
+    /**
+     * We want multiedges to be drawn parallel, not on top of each
+     * other.
+     * This method does the necessary coordinate adjustments.
+     *
+     * @param i  the rank 0..n-1 of the edge
+     * @param n  the number of parallel edges
+     * @param pv source position in device coordinates
+     * @param pw target position in device coordinates
+     */
+    public void adjustMultiEdge(int i, int n, Point pv, Point pw) {
+        Point2D p = new Point2D.Double(pw.getX() - pv.getX(), pw.getY() - pv.getY());
+        if (p.getX() == 0 && p.getY() == 0)
+            return;
+        double offset = 2.0 * (i - 0.5 * (n - 1));
+        double alpha = Geometry.computeAngle(p) + 1.57079632679489661923; // PI/2
+        p = Geometry.translateByAngle(pv, alpha, offset);
+        pv.setLocation(p.getX(), p.getY());
+        p = Geometry.translateByAngle(pw, alpha, offset);
+        pw.setLocation(p.getX(), p.getY());
+    }
+
+
+    /**
+     * Update.
+     *
+     * @param gc the graphics context.
+     */
+    public void update(Graphics gc) {
+        paint(gc);
+    }
+
+    /**
+     * Print the graph associated with this viewer.
+     *
+     * @param gc0        the graphics context.
+     * @param format     page format
+     * @param pagenumber page index
+     */
+    public int print(Graphics gc0, PageFormat format, int pagenumber) throws PrinterException {
+        if (pagenumber == 0) {
+            Graphics2D gc = ((Graphics2D) gc0);
+            gc.setFont(getFont());
+
+            Dimension dim = trans.getPreferredSize();
+
+            int image_w = dim.width;
+            int image_h = dim.height;
+
+            double paper_x = format.getImageableX() + 1;
+            double paper_y = format.getImageableY() + 1;
+            double paper_w = format.getImageableWidth() - 2;
+            double paper_h = format.getImageableHeight() - 2;
+
+            double scale_x = paper_w / image_w;
+            double scale_y = paper_h / image_h;
+            double scale = (scale_x <= scale_y) ? scale_x : scale_y;
+
+            double shift_x = paper_x + (paper_w - scale * image_w) / 2.0;
+            double shift_y = paper_y + (paper_h - scale * image_h) / 2.0;
+
+            gc.translate(shift_x, shift_y);
+            gc.scale(scale, scale);
+
+            gc.setStroke(new BasicStroke(1.0f));
+            gc.setColor(Color.BLACK);
+
+            boolean save = getAutoLayoutLabels();
+            setAutoLayoutLabels(false);
+
+            inPrint = true;
+            paint(gc);
+            inPrint = false;
+
+            setAutoLayoutLabels(save);
+
+            return Printable.PAGE_EXISTS;
+        } else
+            return Printable.NO_SUCH_PAGE;
+    }
+
+
+    /**
+     * gets the canvas color
+     *
+     * @return canvas color
+     */
+    public Color getCanvasColor() {
+        return canvasColor;
+    }
+
+    /**
+     * sets the canvas color
+     *
+     * @param canvasColor the new color
+     */
+
+    public void setCanvasColor(Color canvasColor) {
+        this.canvasColor = canvasColor;
+    }
+
+    /**
+     * Add a NodeActionListener
+     *
+     * @param nal the NodeActionListener to be added
+     */
+    public void addNodeActionListener(NodeActionListener nal) {
+        nodeActionListeners.add(nal);
+    }
+
+    /**
+     * Remove a NodeActionListener
+     *
+     * @param nal the NodeActionListener to be removed
+     */
+    public void removeNodeActionListener(NodeActionListener nal) {
+        nodeActionListeners.remove(nal);
+    }
+
+    public void removeAllNodeActionListeners() {
+        nodeActionListeners.clear();
+    }
+
+    /* Fire doNew */
+    public void fireDoNew(Node v) {
+        for (NodeActionListener lis : nodeActionListeners) {
+            if (v.getOwner() == null) break; // has been deleted
+            lis.doNew(v);
+        }
+    }
+
+    /**
+     * Fire doNew
+     *
+     * @param v the source node of the new edge leading to the new node
+     * @param w the new node
+     */
+    public void fireDoNew(Node v, Node w) {
+
+        for (NodeActionListener lis : nodeActionListeners) {
+            if (v.getOwner() == null) break; // has been deleted
+            lis.doNew(v, w);
+        }
+    }
+
+
+    /**
+     * Fire doDelete
+     *
+     * @param v Node
+     */
+    public void fireDoDelete(Node v) {
+
+        for (NodeActionListener lis : nodeActionListeners) {
+            if (v.getOwner() == null) break; // has been deleted
+            lis.doDelete(v);
+        }
+    }
+
+    /**
+     * Fire doClick
+     *
+     * @param nodes  NodeSet
+     * @param clicks int
+     */
+    public void fireDoClick(NodeSet nodes, int clicks) {
+
+        for (NodeActionListener lis : nodeActionListeners) {
+            lis.doClick(nodes, clicks);
+        }
+    }
+
+    /**
+     * Fire doClickLabels
+     *
+     * @param nodes  NodeSet
+     * @param clicks int
+     */
+    public void fireDoClickLabel(NodeSet nodes, int clicks) {
+
+        for (NodeActionListener lis : nodeActionListeners) {
+            lis.doClickLabel(nodes, clicks);
+        }
+    }
+
+    public void fireDoClickPanel(int x, int y) {
+    }
+
+    /**
+     * Fire doPress
+     *
+     * @param nodes NodeSet
+     */
+    public void fireDoPress(NodeSet nodes) {
+        for (NodeActionListener lis : nodeActionListeners) {
+            lis.doPress(nodes);
+        }
+    }
+
+    /**
+     * Fire doRelease
+     *
+     * @param nodes NodeSet
+     */
+    public void fireDoRelease(NodeSet nodes) {
+
+        for (NodeActionListener lis : nodeActionListeners) {
+            lis.doRelease(nodes);
+        }
+    }
+
+    /**
+     * Fire doSelect
+     *
+     * @param nodes NodeSet
+     */
+    public void fireDoSelect(NodeSet nodes) {
+        for (NodeActionListener lis : nodeActionListeners) {
+            lis.doSelect(nodes);
+        }
+    }
+
+    /**
+     * Fire doDeselect
+     *
+     * @param nodes NodeSet
+     */
+    public void fireDoDeselect(NodeSet nodes) {
+
+        for (NodeActionListener lis : nodeActionListeners) {
+            lis.doDeselect(nodes);
+        }
+    }
+
+    /**
+     * fire this whenever nodes have been moved
+     */
+    public void fireDoNodesMoved() {
+
+        for (NodeActionListener lis : nodeActionListeners) {
+            lis.doNodesMoved();
+        }
+    }
+
+    /**
+     * fire this whenever node labels have been moved
+     */
+    public void fireDoNodeLabelsMoved(NodeSet nodes) {
+
+        for (NodeActionListener lis : nodeActionListeners) {
+            lis.doMoveLabel(nodes);
+        }
+    }
+
+    /**
+     * fire this whenever edge labels have been moved
+     */
+    public void fireDoEdgeLabelsMoved(EdgeSet edges) {
+
+        for (EdgeActionListener lis : edgeActionListeners) {
+            lis.doLabelMoved(edges);
+        }
+    }
+
+    /**
+     * Add a EdgeActionListener
+     *
+     * @param eal the EdgeActionListener to be added
+     */
+    public void addEdgeActionListener(EdgeActionListener eal) {
+        edgeActionListeners.add(eal);
+    }
+
+    /**
+     * Remove a EdgeActionListener
+     *
+     * @param eal the EdgeActionListener to be removed
+     */
+    public void removeEdgeActionListener(EdgeActionListener eal) {
+        edgeActionListeners.remove(eal);
+    }
+
+    public void removeAllEdgeActionListeners() {
+        edgeActionListeners.clear();
+    }
+
+    /**
+     * Fire doNew
+     *
+     * @param e Edge
+     */
+    public void fireDoNew(Edge e) {
+
+        for (EdgeActionListener edgeActionListener : edgeActionListeners) {
+            if (e == null || e.getOwner() == null) break; // has been deleted
+            edgeActionListener.doNew(e);
+        }
+    }
+
+    /**
+     * Fire doDelete
+     *
+     * @param e Edge
+     */
+    public void fireDoDelete(Edge e) {
+
+        for (EdgeActionListener edgeActionListener : edgeActionListeners) {
+            if (e == null || e.getOwner() == null) break; // has been deleted
+            edgeActionListener.doDelete(e);
+        }
+    }
+
+    /**
+     * Fire doClick
+     *
+     * @param edges  EdgeSet
+     * @param clicks int
+     */
+    public void fireDoClick(EdgeSet edges, int clicks) {
+
+        for (EdgeActionListener lis : edgeActionListeners) {
+            lis.doClick(edges, clicks);
+        }
+    }
+
+    /**
+     * Fire doClickLabel
+     *
+     * @param edges  EdgeSet
+     * @param clicks int
+     */
+    public void fireDoClickLabel(EdgeSet edges, int clicks) {
+
+        for (EdgeActionListener lis : edgeActionListeners) {
+            lis.doClickLabel(edges, clicks);
+        }
+    }
+
+    /**
+     * Fire doPress
+     *
+     * @param edges EdgeSet
+     */
+    public void fireDoPress(EdgeSet edges) {
+
+        for (EdgeActionListener lis : edgeActionListeners) {
+            lis.doPress(edges);
+        }
+    }
+
+    /**
+     * Fire doRelease
+     *
+     * @param edges EdgeSet
+     */
+    public void fireDoRelease(EdgeSet edges) {
+
+        for (EdgeActionListener lis : edgeActionListeners) {
+            lis.doRelease(edges);
+        }
+    }
+
+    /**
+     * Fire doSelect
+     *
+     * @param edges EdgeSet
+     */
+    public void fireDoSelect(EdgeSet edges) {
+
+        for (EdgeActionListener lis : edgeActionListeners) {
+            lis.doSelect(edges);
+        }
+    }
+
+    /**
+     * Fire doDeselect
+     *
+     * @param edges EdgeSet
+     */
+    public void fireDoDeselect(EdgeSet edges) {
+
+        for (EdgeActionListener lis : edgeActionListeners) {
+            lis.doDeselect(edges);
+        }
+    }
+
+
+    /**
+     * Add a panel action listener
+     *
+     * @param nal the NodeActionListener to be added
+     */
+    public void addPanelActionListener(PanelActionListener nal) {
+        panelActionListeners.add(nal);
+    }
+
+    /**
+     * Remove a PanelActionListener
+     *
+     * @param nal the PanelActionListener to be removed
+     */
+    public void removePanelActionListener(PanelActionListener nal) {
+        panelActionListeners.remove(nal);
+    }
+
+
+    public void firePanelClicked(MouseEvent ev) {
+        for (PanelActionListener pal : panelActionListeners) {
+            pal.doMouseClicked(ev);
+        }
+    }
+
+    /**
+     * Gets the set of all selected nodes
+     *
+     * @return selected nodes
+     */
+    public NodeSet getSelectedNodes() {
+        return selectedNodes;
+    }
+
+    /**
+     * gets an iterator over all selected nodes, if any selected, otherwise over all nodes
+     *
+     * @return iterator
+     */
+    public Iterator<Node> getSelectedOrAllNodesIterator() {
+        if (selectedNodes.size() > 0)
+            return selectedNodes.iterator();
+        else return getGraph().nodeIterator();
+    }
+
+    /**
+     * Gets the set of all selected edges
+     *
+     * @return selected edges
+     */
+    public EdgeSet getSelectedEdges() {
+        return selectedEdges;
+    }
+
+    /**
+     * gets an iterator over all selected edges, if any selected, otherwise over all edges
+     *
+     * @return iterator
+     */
+    public Iterator getSelectedOrAllEdgesIterator() {
+        if (selectedEdges.size() > 0)
+            return selectedEdges.iterator();
+        else
+            return getGraph().edgeIterator();
+    }
+
+    /**
+     * Gets the number of selected edges
+     *
+     * @return the number of selected edges
+     */
+    public int getNumberSelectedEdges() {
+        return selectedEdges.size();
+    }
+
+    /**
+     * Gets the number of selected nodes
+     *
+     * @return the number of selected nodes
+     */
+    public int getNumberSelectedNodes() {
+        return selectedNodes.size();
+    }
+
+    /**
+     * is user allowed to rubberband nodes?
+     *
+     * @return allowed to rubberband select nodes?
+     */
+    public boolean isAllowRubberbandNodes() {
+        return allowRubberbandNodes;
+    }
+
+    /**
+     * set user is allowed to rubberband select nodes
+     *
+     * @param allowRubberbandNodes
+     */
+    public void setAllowRubberbandNodes(boolean allowRubberbandNodes) {
+        this.allowRubberbandNodes = allowRubberbandNodes;
+    }
+
+    /**
+     * is user allowed to rubberband selecte edges?
+     *
+     * @return allowed to rubberband select edges?
+     */
+    public boolean isAllowRubberbandEdges() {
+        return allowRubberbandEdges;
+    }
+
+    /**
+     * allow interactive insertion and moving of edge internal points?
+     *
+     * @return true, if user is allowed to add internal points
+     */
+    public boolean isAllowInternalEdgePoints() {
+        return allowInternalEdgePoints;
+    }
+
+    /**
+     * set internal edge points insertion mode.
+     *
+     * @param allowInternalEdgePoints
+     */
+    public void setAllowInternalEdgePoints(boolean allowInternalEdgePoints) {
+        this.allowInternalEdgePoints = allowInternalEdgePoints;
+    }
+
+    /**
+     * set user is allowed to rubberband select edges
+     *
+     * @param allowRubberbandEdges
+     */
+    public void setAllowRubberbandEdges(boolean allowRubberbandEdges) {
+        this.allowRubberbandEdges = allowRubberbandEdges;
+    }
+
+    /**
+     * allow edit edge label on double click on edge label?
+     *
+     * @return allow edit
+     */
+    public boolean isAllowEditEdgeLabelsOnDoubleClick() {
+        return allowEditEdgeLabelsOnDoubleClick;
+    }
+
+    /**
+     * allow edit edge label on double click on edge label?
+     *
+     * @param allowEditEdgeLabelsOnDoubleClick
+     */
+    public void setAllowEditEdgeLabelsOnDoubleClick(boolean allowEditEdgeLabelsOnDoubleClick) {
+        this.allowEditEdgeLabelsOnDoubleClick = allowEditEdgeLabelsOnDoubleClick;
+    }
+
+    /**
+     * allow edit edge label on double click on edge label?
+     *
+     * @return allow edge
+     */
+    public boolean isAllowEditNodeLabelsOnDoubleClick() {
+        return allowEditNodeLabelsOnDoubleClick;
+    }
+
+    /**
+     * allow undo node label on double click on node?
+     *
+     * @param allowEditNodeLabelsOnDoubleClick
+     */
+    public void setAllowEditNodeLabelsOnDoubleClick(boolean allowEditNodeLabelsOnDoubleClick) {
+        this.allowEditNodeLabelsOnDoubleClick = allowEditNodeLabelsOnDoubleClick;
+    }
+
+    /**
+     * gets the bounding box of this graph in world coordinates
+     *
+     * @return bounding box
+     */
+    public Rectangle2D getBBox() {
+        double xmin = Double.MIN_VALUE, xmax = Double.MIN_VALUE, ymin = Double.MAX_VALUE, ymax = Double.MAX_VALUE;
+        boolean first = true;
+        try {
+            for (Node v = getGraph().getFirstNode(); v != null; v = getGraph().getNextNode(v)) {
+                if (getLocation(v) == null)
+                    continue;
+                double x = getLocation(v).getX();
+                double y = getLocation(v).getY();
+                if (first) {
+                    xmin = xmax = x;
+                    ymin = ymax = y;
+                    first = false;
+                } else {
+                    if (x < xmin) xmin = x;
+                    if (x > xmax) xmax = x;
+                    if (y < ymin) ymin = y;
+                    if (y > ymax) ymax = y;
+                }
+            }
+            for (Edge e = getGraph().getFirstEdge(); e != null; e = e.getNext()) {
+                List<Point2D> internalPoints = getInternalPoints(e);
+                if (internalPoints != null) {
+                    for (Point2D apt : internalPoints) {
+                        double x = apt.getX();
+                        double y = apt.getY();
+                        if (first) {
+                            xmin = xmax = x;
+                            ymin = ymax = y;
+                            first = false;
+                        } else {
+                            if (x < xmin) xmin = x;
+                            if (x > xmax) xmax = x;
+                            if (y < ymin) ymin = y;
+                            if (y > ymax) ymax = y;
+                        }
+                    }
+                }
+            }
+
+        } catch (NotOwnerException ex) {
+            //Basic.caught(ex);
+        }
+        Rectangle2D rect = new Rectangle2D.Double(xmin, ymin, xmax - xmin, ymax - ymin);
+
+        // add in the world shapes, too
+        //  for (Iterator it = glyphs.keySet().iterator(); it.hasNext();) {
+        // WorldShape ws = (WorldShape) it.next();  @todo update
+        //rect.add(ws.getShape().getBounds2D());
+        //  }
+
+        if (rect.getX() == Double.MIN_VALUE) // hasn't been set
+            rect.setRect(0, 0, 100, 100);
+        if (rect.getWidth() == 0 || rect.getHeight() == 0) {
+            double m = Math.max(rect.getWidth(), rect.getHeight());
+            if (m == 0)
+                m = 100;
+            rect.setRect(rect.getX(), rect.getY(), m, m);
+        }
+        return rect;
+    }
+
+
+    /**
+     * get use label layouter?
+     *
+     * @return use layouter
+     */
+    public boolean getAutoLayoutLabels() {
+        return autoLayoutLabels;
+    }
+
+    /**
+     * set use label layouter
+     *
+     * @param autoLayoutLabels
+     */
+    public void setAutoLayoutLabels(boolean autoLayoutLabels) {
+        this.autoLayoutLabels = autoLayoutLabels;
+    }
+
+    /**
+     * gets the current font
+     *
+     * @return font
+     */
+    public Font getFont() {
+        return font;
+    }
+
+    /**
+     * sets the font
+     *
+     * @param font
+     */
+    public void setFont(Font font) {
+        this.font = font;
+    }
+
+
+    /**
+     * set  draw nodes at fixed size
+     *
+     * @param fixedNodeSize
+     */
+    public void setFixedNodeSize(boolean fixedNodeSize) {
+        for (Node v = getGraph().getFirstNode(); v != null; v = v.getNext()) {
+            getNV(v).setFixedSize(fixedNodeSize);
+        }
+        defaultNodeView.setFixedSize(fixedNodeSize);
+    }
+
+    /**
+     * remove all internal points contained in an edge
+     */
+    public void removeAllInternalPoints() {
+        for (Edge e = getGraph().getFirstEdge(); e != null; e = e.getNext())
+            setInternalPoints(e, null);
+    }
+
+    /**
+     * remove all internal points contained in an edge
+     */
+    public void removeAllLocations() {
+        for (Node v = getGraph().getFirstNode(); v != null; v = v.getNext())
+            setLocation(v, null);
+    }
+
+    /**
+     * sets the dendroscope used to draw the graph and to handle mouse interactions
+     *
+     * @param graphDrawer
+     */
+    public void setGraphDrawer(IGraphDrawer graphDrawer) {
+        this.graphDrawer = graphDrawer;
+    }
+
+    /**
+     * gets the dendroscope used to draw the graph and to handle mouse interactions
+     *
+     * @return current graph dendroscope
+     */
+    public IGraphDrawer getGraphDrawer() {
+        return graphDrawer;
+    }
+
+    /**
+     * Returns the preferred size of the viewport for a view component.
+     *
+     * @return The preferredSize of a JViewport whose view is this Scrollable.
+     * @see javax.swing.JViewport#getPreferredSize
+     */
+    public Dimension getPreferredScrollableViewportSize() {
+        return getPreferredSize();
+    }
+
+    /**
+     * @param visibleRect The view area visible within the viewport
+     * @param orientation Either SwingConstants.VERTICAL or SwingConstants.HORIZONTAL.
+     * @param direction   Less than zero to scroll up/left, greater than zero for down/right.
+     * @return The "block" increment for scrolling in the specified direction.
+     * This value should always be positive.
+     * @see javax.swing.JScrollBar#setBlockIncrement
+     */
+    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
+        return 200;
+    }
+
+    /**
+     * Return true if a viewport should always force the height of this
+     * Scrollable to match the height of the viewport.  For example a
+     * columnar text view that flowed text in left to right columns
+     * could effectively disable vertical scrolling by returning
+     * true here.
+     * <p/>
+     * Scrolling containers, like JViewport, will use this method each
+     * time they are validated.
+     *
+     * @return True if a viewport should force the Scrollables height to match its own.
+     */
+    public boolean getScrollableTracksViewportHeight() {
+        return false;
+    }
+
+    /**
+     * Return true if a viewport should always force the width of this
+     * <code>Scrollable</code> to match the width of the viewport.
+     * For example a normal
+     * text view that supported line wrapping would return true here, since it
+     * would be undesirable for wrapped lines to disappear beyond the right
+     * edge of the viewport.  Note that returning true for a Scrollable
+     * whose ancestor is a JScrollPane effectively disables horizontal
+     * scrolling.
+     * <p/>
+     * Scrolling containers, like JViewport, will use this method each
+     * time they are validated.
+     *
+     * @return True if a viewport should force the Scrollables width to match its own.
+     */
+    public boolean getScrollableTracksViewportWidth() {
+        return false;
+    }
+
+    /**
+     * Components that display logical rows or columns should compute
+     * the scroll increment that will completely expose one new row
+     * or column, depending on the value of orientation.  Ideally,
+     * components should handle a partially exposed row or column by
+     * returning the distance required to completely expose the item.
+     * <p/>
+     * Scrolling containers, like JScrollPane, will use this method
+     * each time the user requests a unit scroll.
+     *
+     * @param visibleRect The view area visible within the viewport
+     * @param orientation Either SwingConstants.VERTICAL or SwingConstants.HORIZONTAL.
+     * @param direction   Less than zero to scroll up/left, greater than zero for down/right.
+     * @return The "unit" increment for scrolling in the specified direction.
+     * This value should always be positive.
+     * @see javax.swing.JScrollBar#setUnitIncrement
+     */
+    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
+        return 10;
+    }
+
+    /**
+     * gets the scrollpane associated with this viewer
+     *
+     * @return scroll pane
+     */
+    public JScrollPane getScrollPane() {
+        return scrollPane;
+    }
+
+    /**
+     * get the current popup listener
+     *
+     * @return graph popup listener
+     */
+    public IPopupListener getPopupListener() {
+        return popupListener;
+    }
+
+    /**
+     * sets the popup listener
+     *
+     * @param popupListener
+     */
+    public void setPopupListener(IPopupListener popupListener) {
+        this.popupListener = popupListener;
+    }
+
+    /**
+     * fire the node popup menu
+     *
+     * @param me
+     * @param nodes
+     */
+    public void fireNodePopup(MouseEvent me, NodeSet nodes) {
+        if (popupListener != null) popupListener.doNodePopup(me, nodes);
+    }
+
+    /**
+     * fire the node label popup menu
+     *
+     * @param me
+     * @param nodes
+     */
+    public void fireNodeLabelPopup(MouseEvent me, NodeSet nodes) {
+        if (popupListener != null) popupListener.doNodeLabelPopup(me, nodes);
+    }
+
+    /**
+     * fire the edge popup menu
+     *
+     * @param me
+     * @param edges
+     */
+    public void fireEdgePopup(MouseEvent me, EdgeSet edges) {
+        if (popupListener != null) popupListener.doEdgePopup(me, edges);
+    }
+
+    /**
+     * fire the edge label popup menu
+     *
+     * @param me
+     * @param edges
+     */
+    public void fireEdgeLabelPopup(MouseEvent me, EdgeSet edges) {
+        if (popupListener != null) popupListener.doEdgeLabelPopup(me, edges);
+    }
+
+    /**
+     * fire the panel popup (when nothing was hit)
+     *
+     * @param me
+     */
+    public void firePanelPopup(MouseEvent me) {
+        if (popupListener != null) popupListener.doPanelPopup(me);
+    }
+
+    private NodeSet origNodeSelection = null;
+
+    /**
+     * replace current selection of nodes by given one. Do not fire any
+     * node deselection events
+     *
+     * @param nodes
+     */
+    public void pushNodeSelection(NodeSet nodes) {
+        if (origNodeSelection != null)
+            throw new RuntimeException("pushNodeSelection(): stack full");
+        origNodeSelection = new NodeSet(getGraph());
+        origNodeSelection.addAll(selectedNodes);
+        selectedNodes.clear();
+        for (Node v = nodes.getFirstElement(); v != null; v = nodes.getNextElement(v))
+            setSelected(v, true);
+    }
+
+    /**
+     * restores node selection to original one. Doesn't fire any node selection events
+     */
+    public void popNodeSelection() {
+        if (origNodeSelection == null)
+            throw new RuntimeException("popNodeSelection(): stack empty");
+        selectedNodes.clear();
+        for (Node v = origNodeSelection.getFirstElement(); v != null; v = origNodeSelection.getNextElement(v))
+            setSelected(v, true);
+
+        origNodeSelection = null;
+
+    }
+
+    /**
+     * currently locked for critical user input?
+     *
+     * @return true, if locked
+     */
+    public boolean isLocked() {
+        return locked;
+    }
+
+    /**
+     * sets the cursor
+     *
+     * @param cursor
+     */
+    public void setCursor(Cursor cursor) {
+        getScrollPane().setCursor(cursor);
+        getScrollPane().getHorizontalScrollBar().setCursor(Cursor.getDefaultCursor());
+        getScrollPane().getVerticalScrollBar().setCursor(Cursor.getDefaultCursor());
+    }
+
+    /**
+     * gets the cursor
+     *
+     * @return cursor
+     */
+    public Cursor getCursor() {
+        return getScrollPane().getCursor();
+    }
+
+    /**
+     * reset cursor to open hand cursor
+     */
+    public void resetCursor() {
+        if (isLocked())
+            setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+        else
+            //setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+            setCursor(Cursors.getOpenHand());
+    }
+
+    /**
+     * flip node label layout horizontally, vertically, or both
+     *
+     * @param hflip
+     * @param vflip
+     */
+    public void flipNodeLabels(boolean hflip, boolean vflip) {
+        for (Node v = getGraph().getFirstNode(); v != null; v = v.getNext()) {
+            switch (getLabelLayout(v)) {
+                case NodeView.EAST:
+                    if (hflip) setLabelLayout(v, NodeView.WEST);
+                    break;
+                case NodeView.WEST:
+                    if (hflip) setLabelLayout(v, NodeView.EAST);
+                    break;
+                case NodeView.NORTH:
+                    if (vflip) setLabelLayout(v, NodeView.SOUTH);
+                    break;
+                case NodeView.NORTHEAST:
+                    if (hflip && vflip)
+                        setLabelLayout(v, NodeView.SOUTHWEST);
+                    else if (hflip)
+                        setLabelLayout(v, NodeView.NORTHWEST);
+                    else if (vflip) setLabelLayout(v, NodeView.SOUTHEAST);
+                    break;
+                case NodeView.NORTHWEST:
+                    if (hflip && vflip)
+                        setLabelLayout(v, NodeView.SOUTHEAST);
+                    else if (hflip)
+                        setLabelLayout(v, NodeView.NORTHEAST);
+                    else if (vflip) setLabelLayout(v, NodeView.SOUTHWEST);
+                    break;
+                case NodeView.SOUTH:
+                    if (vflip) setLabelLayout(v, NodeView.NORTH);
+                    break;
+
+                case NodeView.SOUTHEAST:
+                    if (hflip && vflip)
+                        setLabelLayout(v, NodeView.NORTHWEST);
+                    else if (hflip)
+                        setLabelLayout(v, NodeView.SOUTHWEST);
+                    else if (vflip) setLabelLayout(v, NodeView.NORTHEAST);
+                    break;
+                case NodeView.SOUTHWEST:
+                    if (hflip && vflip)
+                        setLabelLayout(v, NodeView.NORTHEAST);
+                    else if (hflip)
+                        setLabelLayout(v, NodeView.SOUTHEAST);
+                    else if (vflip) setLabelLayout(v, NodeView.NORTHWEST);
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+
+    /**
+     * writes the graphview
+     *
+     * @param w
+     * @throws IOException
+     */
+    public void write(Writer w) throws IOException {
+        Graph graph = getGraph();
+        Map<Integer, Integer> nodeId2Number = new HashMap<>();
+        Map<Integer, Integer> edgeId2Number = new HashMap<>();
+
+        int count = 0;
+        for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+            nodeId2Number.put(v.getId(), ++count);
+        }
+        count = 0;
+        for (Edge e = graph.getFirstEdge(); e != null; e = e.getNext()) {
+            edgeId2Number.put(e.getId(), ++count);
+        }
+        write(w, nodeId2Number, edgeId2Number);
+    }
+
+    /**
+     * writes the graphview
+     *
+     * @param w
+     * @param nodeId2Number the node-id to number mapping established by Graph.write
+     * @param edgeId2Number the edge-id to number mapping established by Graph.write
+     * @throws IOException
+     */
+    public void write(Writer w, Map nodeId2Number, Map edgeId2Number) throws IOException {
+        Graph graph = getGraph();
+        w.write("{GRAPHVIEW\n");
+        w.write("nnodes=" + graph.getNumberOfNodes() + " nedges=" + graph.getNumberOfEdges() + "\n");
+        w.write("nodes\n");
+        NodeView prevNV = null;
+        for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+            w.write(nodeId2Number.get(v.getId()) + ":");
+            getNV(v).write(w, prevNV);
+            prevNV = getNV(v);
+        }
+        w.write("edges\n");
+        EdgeView prevEV = null;
+        for (Edge e = graph.getFirstEdge(); e != null; e = e.getNext()) {
+            w.write((edgeId2Number.get(e.getId())) + ":");
+            getEV(e).write(w, prevEV);
+            prevEV = getEV(e);
+        }
+        w.write("}\n");
+    }
+
+
+    /**
+     * read graph and graphview.
+     *
+     * @param r
+     * @throws IOException
+     */
+    public void read(Reader r) throws IOException {
+        final Graph graph = getGraph();
+
+        Num2NodeArray num2node = new Num2NodeArray(graph.getNumberOfNodes() + 1);
+        Num2EdgeArray num2edge = new Num2EdgeArray(graph.getNumberOfEdges() + 1);
+
+        int count = 0;
+        for (Node v = graph.getFirstNode(); v != null; v = v.getNext())
+            num2node.put(++count, v);
+
+
+        count = 0;
+        for (Edge e = graph.getFirstEdge(); e != null; e = e.getNext())
+            num2edge.put(++count, e);
+
+        read(r, num2node, num2edge);
+    }
+
+    /**
+     * read graph and graphview.
+     *
+     * @param r
+     * @param num2node the num2node map computed by Graph.read
+     * @param num2edge the num2edge map computed by Graph.read
+     * @throws IOException
+     */
+    public void read(Reader r, Num2NodeArray num2node, Num2EdgeArray num2edge) throws IOException {
+        final Graph graph = getGraph();
+
+        NexusStreamParser np = new NexusStreamParser(r);
+        np.matchRespectCase("{GRAPHVIEW\n");
+        np.matchRespectCase("nnodes = " + graph.getNumberOfNodes() + " nedges = " + graph.getNumberOfEdges());
+
+        np.matchRespectCase("nodes");
+        NodeView prevNV = defaultNodeView;
+        while (!np.peekMatchRespectCase("edges")) {
+            int vid = np.getInt(1, graph.getNumberOfNodes());
+            Node v = num2node.get(vid);
+            NodeView nv = getNV(v);
+            nv.read(np, np.getTokensRespectCase(":", ";"), prevNV);
+            prevNV = nv;
+        }
+
+        np.matchRespectCase("edges");
+        EdgeView prevEV = defaultEdgeView;
+        while (!np.peekMatchRespectCase("}")) {
+            int eid = np.getInt(1, graph.getNumberOfEdges());
+            Edge e = num2edge.get(eid);
+            EdgeView ev = getEV(e);
+            ev.read(np, np.getTokensRespectCase(":", ";"), prevEV);
+            prevEV = ev;
+        }
+        np.matchRespectCase("}");
+    }
+
+    /**
+     * rotateAbout labels of all selected nodes and edges
+     *
+     * @param percent
+     */
+    public void rotateLabels(NodeSet nodes, EdgeSet edges, int percent) {
+        float angle = (float) (Math.PI / 50.0 * percent);
+        if (nodes != null)
+            for (Node v : nodes) {
+                NodeView nv = getNV(v);
+                if (nv.getLabelVisible() && nv.getLabel() != null && nv.getLabel().length() > 0) {
+                    nv.setLabelAngle(nv.getLabelAngle() + angle);
+                    nv.setLabelLayout(NodeView.USER);
+                }
+            }
+        if (edges != null)
+            for (Edge e : edges) {
+                EdgeView ev = getEV(e);
+                if (ev.getLabelVisible() && ev.getLabel() != null && ev.getLabel().length() > 0) {
+                    ev.setLabelAngle(ev.getLabelAngle() + angle);
+                    ev.setLabelLayout(EdgeView.USER);
+                }
+            }
+    }
+
+
+    /**
+     * node found by search, must be drawn  if !=null
+     *
+     * @return found node
+     */
+    public Node getFoundNode() {
+        return foundNode;
+    }
+
+    /**
+     * node found by search, must be drawn if !=null
+     *
+     * @param foundNode
+     */
+    public void setFoundNode(Node foundNode) {
+        this.foundNode = foundNode;
+    }
+
+    /**
+     * automatically repaint on graph change?
+     *
+     * @return true, if automatically repainted
+     */
+    public boolean isRepaintOnGraphHasChanged() {
+        return repaintOnGraphHasChanged;
+    }
+
+    /**
+     * automatically repaint on graph change?
+     *
+     * @param repaintOnGraphHasChanged
+     */
+    public void setRepaintOnGraphHasChanged(boolean repaintOnGraphHasChanged) {
+        this.repaintOnGraphHasChanged = repaintOnGraphHasChanged;
+    }
+
+    /**
+     * set the default label positions
+     *
+     * @param resetAll reset all labels, including user modified ones
+     */
+    public void resetLabelPositions(boolean resetAll) {
+        if (getGraphDrawer() != null)
+            getGraphDrawer().resetLabelPositions(resetAll);
+    }
+
+    public void reset() {
+        //nodeViews = new NodeArray<NodeView>(G);
+        //edgeViews = new EdgeArray<EdgeView>(G);
+
+        /*trans = new Transform(this);
+        trans.addChangeListener(new TransformChangedListener() {
+            public void hasChanged(Transform trans) {
+                recomputeMargins();
+            }
+        });*/
+        trans.reset();
+    }
+
+    /**
+     * select the given edges and all nodes and edges below
+     *
+     * @param edges
+     */
+    public void selectAllBelow(EdgeSet edges) {
+        NodeSet seen = new NodeSet(getGraph());
+        Stack<Node> stack = new Stack<>();
+        for (Edge e = edges.getFirstElement(); e != null; e = edges.getNextElement(e)) {
+            setSelected(e, true);
+            Node v = e.getTarget();
+            stack.push(v);
+            seen.add(v);
+            while (stack.size() > 0) {
+                v = stack.pop();
+                setSelected(v, true);
+                for (Edge f = v.getFirstOutEdge(); f != null; f = v.getNextOutEdge(f)) {
+                    setSelected(f, true);
+                    Node w = f.getTarget();
+                    if (!seen.contains(w)) {
+                        stack.add(w);
+                        seen.add(w);
+                    }
+                }
+            }
+        }
+    }
+
+    public Color getColorSelectedNodes() {
+        Color color = null;
+        for (Node v : getSelectedNodes()) {
+            if (color == null)
+                color = getColor(v);
+            else if (!color.equals(getColor(v)))
+                return null;
+        }
+        return color;
+    }
+
+    public Color getBackgroundColorSelectedNodes() {
+        Color color = null;
+        for (Node v : getSelectedNodes()) {
+            if (color == null)
+                color = getBackgroundColor(v);
+            else if (!color.equals(getBackgroundColor(v)))
+                return null;
+        }
+        return color;
+    }
+
+    public Color getBorderColorSelectedNodes() {
+        Color color = null;
+        for (Node v : getSelectedNodes()) {
+            if (color == null)
+                color = getBorderColor(v);
+            else if (!color.equals(getBorderColor(v)))
+                return null;
+        }
+        return color;
+    }
+
+    public Color getLabelColorSelectedNodes() {
+        Color color = null;
+        for (Node v : getSelectedNodes()) {
+            if (color == null)
+                color = getLabelColor(v);
+            else if (!color.equals(getLabelColor(v)))
+                return null;
+        }
+        return color;
+    }
+
+    public Color getLabelBackgroundColorSelectedNodes() {
+        Color color = null;
+        for (Node v : getSelectedNodes()) {
+            if (color == null)
+                color = getLabelBackgroundColor(v);
+            else if (!color.equals(getLabelBackgroundColor(v)))
+                return null;
+        }
+        return color;
+    }
+
+
+    public int getWidthSelectedNodes() {
+        int width = -1;
+        for (Node v : getSelectedNodes()) {
+            if (width == -1)
+                width = getWidth(v);
+            else if (width != getWidth(v))
+                return -1;
+        }
+        return width;
+    }
+
+    public int getHeightSelectedNodes() {
+        int height = -1;
+        for (Node v : getSelectedNodes()) {
+            if (height == -1)
+                height = getHeight(v);
+            else if (height != getHeight(v))
+                return -1;
+        }
+        return height;
+    }
+
+    public int getLineWidthSelectedNodes() {
+        int linewidth = -1;
+        for (Node v : getSelectedNodes()) {
+            if (linewidth == -1)
+                linewidth = getLineWidth(v);
+            else if (linewidth != getLineWidth(v))
+                return -1;
+        }
+        return linewidth;
+    }
+
+    public void setBorderColorSelectedNodes(Color a) {
+        for (Node v : getSelectedNodes()) {
+            setBorderColor(v, a);
+        }
+    }
+
+    public boolean setLabelBackgroundColorSelectedNodes(Color a) {
+        boolean changed = false;
+        for (Node v : getSelectedNodes()) {
+            if (isLabelVisible(v) && getLabelBackgroundColor(v) == null || !getLabelBackgroundColor(v).equals(a)) {
+                changed = true;
+                setLabelBackgroundColor(v, a);
+            }
+        }
+        return changed;
+    }
+
+
+    public void setWidthSelectedNodes(byte a) {
+        for (Node v : getSelectedNodes()) {
+            setWidth(v, a);
+        }
+    }
+
+    public void setHeightSelectedNodes(byte a) {
+        for (Node v : getSelectedNodes()) {
+            setHeight(v, a);
+        }
+    }
+
+    public void setShapeSelectedNodes(byte a) {
+        for (Node v : getSelectedNodes()) {
+            setShape(v, a);
+        }
+    }
+
+    public byte getShapeSelectedNodes() {
+        byte value = 0;
+        for (Node v : getSelectedNodes()) {
+            if (value == 0)
+                value = getShape(v);
+            else if (value != getShape(v))
+                return 0;
+        }
+        return value;
+    }
+
+    public Font getFontSelected() {
+        Font font = null;
+        for (Node v : getSelectedNodes()) {
+            if (font == null)
+                font = getFont(v);
+            else if (getFont(v) != null && !font.equals(getFont(v)))
+                return null;
+        }
+        for (Edge e : getSelectedEdges()) {
+            if (font == null)
+                font = getFont(e);
+            else if (getFont(e) != null && !font.equals(getFont(e)))
+                return null;
+        }
+        return font;
+    }
+
+
+    public Color getColorSelectedEdges() {
+        Color color = null;
+        for (Edge e : getSelectedEdges()) {
+            if (color == null)
+                color = getColor(e);
+            else if (!color.equals(getColor(e)))
+                return null;
+        }
+        return color;
+    }
+
+    public Color getLabelColorSelectedEdges() {
+        Color color = null;
+        for (Edge e : getSelectedEdges()) {
+            if (color == null)
+                color = getLabelColor(e);
+            else if (!color.equals(getLabelColor(e)))
+                return null;
+        }
+        return color;
+    }
+
+    public Color getLabelBackgroundColorSelectedEdges() {
+        Color color = null;
+        for (Edge e : getSelectedEdges()) {
+            if (color == null)
+                color = getLabelBackgroundColor(e);
+            else if (!color.equals(getLabelBackgroundColor(e)))
+                return null;
+        }
+        return color;
+    }
+
+    public boolean setLabelBackgroundColorSelectedEdges(Color a) {
+        boolean changed = false;
+        for (Edge e : getSelectedEdges()) {
+            if (getLabelVisible(e) && (getLabelBackgroundColor(e) == null || !getLabelBackgroundColor(e).equals(a))) {
+                changed = true;
+                setLabelBackgroundColor(e, a);
+            }
+        }
+        return changed;
+    }
+
+    public int getLineWidthSelectedEdges() {
+        int value = -1;
+        for (Edge e : getSelectedEdges()) {
+            if (value == -1)
+                value = getLineWidth(e);
+            else if (value != getLineWidth(e))
+                return -1;
+        }
+        return value;
+    }
+
+    public int getDirectionSelectedEdges() {
+        int value = -1;
+        for (Edge e : getSelectedEdges()) {
+            if (value == -1)
+                value = getDirection(e);
+            else if (value != getDirection(e))
+                return -1;
+        }
+        return value;
+    }
+
+    public void setShapeSelectedEdges(byte a) {
+        for (Edge e : getSelectedEdges()) {
+            setShape(e, a);
+        }
+    }
+
+    public byte getShapeSelectedEdges() {
+        byte value = 0;
+        for (Edge e : getSelectedEdges()) {
+            if (value == 0)
+                value = getShape(e);
+            else if (value != getShape(e))
+                return 0;
+        }
+        return value;
+    }
+
+    public void setDirectionSelectedEdges(byte a) {
+        for (Edge e : getSelectedEdges()) {
+            setDirection(e, a);
+        }
+    }
+
+    public Font getFontSelectedEdges() {
+        Font font = null;
+        for (Edge e : getSelectedEdges()) {
+            if (font == null)
+                font = getFont(e);
+            else if (getFont(e) != null && !font.equals(getFont(e)))
+                return null;
+        }
+        return font;
+    }
+
+    public boolean hasSelectedNodes() {
+        return getSelectedNodes().size() > 0;
+    }
+
+    public boolean hasSelectedEdges() {
+        return getSelectedEdges().size() > 0;
+    }
+
+    public void setLabelVisibleSelectedNodes(boolean visible) {
+        for (Node v : getSelectedNodes())
+            setLabelVisible(v, visible);
+    }
+
+    public boolean hasLabelVisibleSelectedNodes() {
+        for (Node v : getSelectedNodes())
+            if (getLabelVisible(v) && getLabel(v) != null)
+                return true;
+        return false;
+    }
+
+    public void setLabelVisibleSelectedEdges(boolean visible) {
+        for (Edge e : getSelectedEdges())
+            setLabelVisible(e, visible);
+    }
+
+    public boolean hasLabelVisibleSelectedEdges() {
+        for (Edge e : getSelectedEdges())
+            if (getLabelVisible(e) && getLabel(e) != null)
+                return true;
+        return false;
+    }
+
+    public boolean getLockXYScale() {
+        return trans.getLockXYScale();
+    }
+
+    public void rotateLabelsSelectedNodes(int percent) {
+        float angle = (float) (Math.PI / 50.0 * percent);
+        for (Node v : getSelectedNodes()) {
+            NodeView nv = getNV(v);
+            if (nv.getLabelVisible() && nv.getLabel() != null && nv.getLabel().length() > 0) {
+                nv.setLabelAngle(nv.getLabelAngle() + angle);
+                nv.setLabelLayout(NodeView.USER);
+            }
+        }
+
+    }
+
+    public void rotateLabelsSelectedEdges(int percent) {
+        float angle = (float) (Math.PI / 50.0 * percent);
+        for (Edge e : getSelectedEdges()) {
+            EdgeView nv = getEV(e);
+            if (nv.getLabelVisible() && nv.getLabel() != null && nv.getLabel().length() > 0) {
+                nv.setLabelAngle(nv.getLabelAngle() + angle);
+                nv.setLabelLayout(EdgeView.USER);
+            }
+        }
+    }
+
+    public JPanel getPanel() {
+        return this;
+    }
+
+    public void setRandomColorsSelectedNodes(boolean foreground, boolean background, boolean labelforeground, boolean labelbackgrond) {
+        Random rand = new Random();
+        for (Node v : getSelectedNodes()) {
+            Color color = new Color(rand.nextInt(256), rand.nextInt(256), rand.nextInt(256));
+            if (foreground)
+                setColor(v, color);
+            if (background)
+                setBackgroundColor(v, color);
+            if (isLabelVisible(v)) {
+                if (labelforeground)
+                    setLabelColor(v, color);
+                if (labelbackgrond)
+                    setLabelBackgroundColor(v, color);
+            }
+        }
+    }
+
+    public void setRandomColorsSelectedEdges(boolean foreground, boolean labelforeground, boolean labelbackgrond) {
+        Random rand = new Random();
+        for (Edge e : getSelectedEdges()) {
+            Color color = new Color(rand.nextInt(256), rand.nextInt(256), rand.nextInt(256));
+            if (foreground)
+                setColor(e, color);
+            if (isLabelVisible(e)) {
+                if (labelforeground)
+                    setLabelColor(e, color);
+                if (labelbackgrond)
+                    setLabelBackgroundColor(e, color);
+            }
+        }
+    }
+
+    /**
+     * set the tool tip text to the label of the given node
+     *
+     * @param v
+     */
+    public void setToolTipText(Node v) {
+        setToolTipText(getLabel(v));
+    }
+
+    public String getPOWEREDBY() {
+        return POWEREDBY;
+    }
+
+    public void setPOWEREDBY(String POWEREDBY) {
+        this.POWEREDBY = POWEREDBY;
+    }
+
+    /**
+     * selects all connected components containing any of the given nodes
+     *
+     * @param nodes
+     */
+    public void selectConnectedComponents(NodeSet nodes) {
+        final NodeSet nodesToSelect = new NodeSet(G);
+        for (Node v : nodes) {
+            G.visitConnectedComponent(v, nodesToSelect);
+        }
+        nodesToSelect.removeAll(getSelectedNodes());
+        if (nodesToSelect.size() > 0)
+            setSelected(nodesToSelect, true);
+    }
+
+    public JFrame getFrame() {
+        return frame;
+    }
+
+    public void setFrame(JFrame frame) {
+        this.frame = frame;
+    }
+}
+
+// EOF
diff --git a/src/jloda/graphview/GraphViewBase.java b/src/jloda/graphview/GraphViewBase.java
new file mode 100644
index 0000000..d53809f
--- /dev/null
+++ b/src/jloda/graphview/GraphViewBase.java
@@ -0,0 +1,32 @@
+/**
+ * GraphViewBase.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+/**
+ * @author Daniel Huson
+ * @version $Id: GraphViewBase.java,v 1.2 2005-06-28 14:06:12 huson Exp $
+ *          <p/>
+ *          Base class for GraphView
+ */
+
+public class GraphViewBase {
+}
+
+// EOF
diff --git a/src/jloda/graphview/GraphViewListener.java b/src/jloda/graphview/GraphViewListener.java
new file mode 100644
index 0000000..1142cf4
--- /dev/null
+++ b/src/jloda/graphview/GraphViewListener.java
@@ -0,0 +1,1247 @@
+/**
+ * GraphViewListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+/**
+ * @version $Id: GraphViewListener.java,v 1.100 2010-06-14 13:34:40 huson Exp $
+ *
+ * Listener for all graphview events.
+ *
+ * @author Daniel Huson
+ */
+
+import jloda.graph.*;
+import jloda.util.Basic;
+import jloda.util.Cursors;
+import jloda.util.Geometry;
+import jloda.util.NotOwnerException;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.geom.Point2D;
+import java.util.LinkedList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+
+/**
+ * Listener for all GraphView events
+ */
+public class GraphViewListener implements IGraphViewListener {
+    private final static ExecutorService service = Executors.newFixedThreadPool(1);
+    private boolean inWait = false;
+
+    private final GraphView viewer;
+    private final Transform trans;
+
+    private final int inClick = 1;
+    private final int inMove = 2;
+    private final int inRubberband = 3;
+    private final int inNewEdge = 4;
+    private final int inMoveNodeLabel = 5;
+    private final int inMoveEdgeLabel = 6;
+    private final int inMoveInternalEdgePoint = 7;
+    private final int inScrollByMouse = 8;
+    private final int inMoveMagnifier = 9;
+    private final int inResizeMagnifier = 10;
+
+    private int current;
+    private int downX;
+    private int downY;
+    private Rectangle selRect;
+    private Point prevPt;
+    private Point offset; // used by move node label
+
+    private boolean allowDeselectAllByMouseClick = true;
+    private boolean allowSelectConnectedComponent = false;
+
+
+    private NodeSet hitNodes;
+    private NodeSet hitNodeLabels;
+    private EdgeSet hitEdges;
+    private EdgeSet hitEdgeLabels;
+
+    private boolean nodeLabelsHaveMoved = false;
+    private boolean edgeLabelsHaveMoved = false;
+
+    private boolean inPopup = false;
+
+    // is mouse still pressed?
+    private boolean stillDownWithoutMoving = false;
+
+    /**
+     * Constructor
+     *
+     * @param graphView GraphView
+     */
+    public GraphViewListener(GraphView graphView) {
+        this.viewer = graphView;
+        this.trans = graphView.trans;
+        hitNodes = new NodeSet(this.viewer.getGraph());
+        hitNodeLabels = new NodeSet(this.viewer.getGraph());
+        hitEdges = new EdgeSet(this.viewer.getGraph());
+        hitEdgeLabels = new EdgeSet(this.viewer.getGraph());
+    }
+
+    /**
+     * Mouse pressed.
+     *
+     * @param me MouseEvent
+     */
+    public void mousePressed(MouseEvent me) {
+        downX = me.getX();
+        downY = me.getY();
+        selRect = null;
+        prevPt = null;
+        offset = new Point();
+        nodeLabelsHaveMoved = false;
+        edgeLabelsHaveMoved = false;
+        stillDownWithoutMoving = true;
+
+        if (viewer.getGraphDrawer() == null)
+            return;
+
+        int magnifierHit = trans.getMagnifier().hit(downX, downY);
+
+        if (magnifierHit != Magnifier.HIT_NOTHING) {
+            switch (magnifierHit) {
+                case Magnifier.HIT_MOVE:
+                    current = inMoveMagnifier;
+                    viewer.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+                    break;
+                case Magnifier.HIT_RESIZE:
+                    current = inResizeMagnifier;
+                    viewer.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
+                    break;
+                case Magnifier.HIT_INCREASE_MAGNIFICATION:
+                    if (viewer.trans.getMagnifier().increaseDisplacement())
+                        viewer.repaint();
+                    break;
+                case Magnifier.HIT_DECREASE_MAGNIFICATION:
+                    if (viewer.trans.getMagnifier().decreaseDisplacement())
+                        viewer.repaint();
+                    break;
+                default:
+                    break;
+            }
+            return;
+        }
+
+        hitNodes = viewer.getGraphDrawer().getHitNodes(downX, downY);
+        int numHitNodes = hitNodes.size();
+        hitNodeLabels = viewer.getGraphDrawer().getHitNodeLabels(downX, downY);
+        int numHitNodeLabels = hitNodeLabels.size();
+        hitEdges = viewer.getGraphDrawer().getHitEdges(downX, downY);
+        int numHitEdges = hitEdges.size();
+        hitEdgeLabels = viewer.getGraphDrawer().getHitEdgeLabels(downX, downY);
+        int numHitEdgeLabels = hitEdgeLabels.size();
+
+        if (me.isPopupTrigger()) {
+            inPopup = true;
+            viewer.setCursor(Cursor.getDefaultCursor());
+            if (numHitNodes != 0) {
+                viewer.fireNodePopup(me, hitNodes);
+            } else if (numHitNodeLabels != 0) {
+                viewer.fireNodeLabelPopup(me, hitNodeLabels);
+            } else if (numHitEdges != 0) {
+                viewer.fireEdgePopup(me, hitEdges);
+            } else if (numHitEdgeLabels != 0) {
+                viewer.fireEdgeLabelPopup(me, hitEdgeLabels);
+            } else {
+                viewer.firePanelClicked(me);
+                viewer.firePanelPopup(me);
+            }
+            viewer.resetCursor();
+            return;
+        }
+
+        viewer.fireDoPress(hitNodes);
+        viewer.fireDoPress(hitEdges);
+
+        if (numHitNodes == 0 && numHitNodeLabels == 0 && numHitEdges == 0 && numHitEdgeLabels == 0) {
+            if (me.isShiftDown()) {
+                current = inRubberband;
+                viewer.setCursor(Cursor.getDefaultCursor());
+            } else {
+                current = inScrollByMouse;
+                viewer.setCursor(Cursors.getClosedHand());
+
+                if (!inWait) {
+                    service.execute(new Runnable() {
+                        public void run() {
+                            try {
+                                inWait = true;
+                                synchronized (this) {
+                                    Thread.sleep(500);
+                                }
+                            } catch (InterruptedException e) {
+                            }
+                            if (stillDownWithoutMoving) {
+                                current = inRubberband;
+                                viewer.setCursor(Cursor.getDefaultCursor());
+                            }
+                            inWait = false;
+                        }
+                    });
+                }
+            }
+        } else {
+            viewer.setCursor(Cursor.getDefaultCursor());
+            if (viewer.getAllowEdit() && numHitNodes == 1 && me.isAltDown() && !me.isShiftDown())
+                current = inNewEdge;
+            else if (numHitNodes == 0 && numHitEdges == 0 && numHitNodeLabels > 0) {
+                Node v = hitNodeLabels.getFirstElement();
+                //viewer.setSelected(v, true);
+                if (viewer.getLabel(v) == null)
+                    return;
+                current = inMoveNodeLabel;
+                viewer.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+            } else if (numHitNodes == 0 && numHitEdges == 0 && numHitNodeLabels == 0 && numHitEdgeLabels > 0) {
+                Edge e = hitEdgeLabels.getFirstElement();
+                if (!viewer.getSelected(e) || viewer.getLabel(e) == null)
+                    return; // move labels only of selected edges
+                current = inMoveEdgeLabel;
+                viewer.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+
+            } else if (numHitNodes > 0 && !me.isAltDown() && !me.isShiftDown()) {
+                if (!viewer.getAllowMoveNodes() && viewer.getNumberSelectedNodes() <
+                        viewer.getGraph().getNumberOfNodes())
+                    return;
+                // if no hit node selected, deselect all and then select node
+                boolean found = false;
+                for (Node v = hitNodes.getFirstElement(); v != null;
+                     v = hitNodes.getNextElement(v)) {
+                    if (viewer.getSelectedNodes().contains(v)) {
+                        found = true;
+                        break;
+                    }
+                }
+                if (found) {
+                    current = inMove;
+                    viewer.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+                }
+            } else if (viewer.isAllowMoveInternalEdgePoints() && (numHitEdges >= 1
+                    && viewer.getSelectedEdges().size() == 1 && hitEdges.contains(viewer.getSelectedEdges().getFirstElement()))) {
+                current = inMoveInternalEdgePoint;
+                viewer.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+            }
+        }
+    }
+
+    /**
+     * Mouse released.
+     *
+     * @param me MouseEvent
+     */
+    public void mouseReleased(MouseEvent me) {
+        if (me.isShiftDown())
+            viewer.setCursor(Cursor.getDefaultCursor());
+        else
+            viewer.resetCursor();
+        stillDownWithoutMoving = false;
+
+        if (viewer.getGraphDrawer() == null)
+            return;
+
+        // check whether we have scrolled by mouse:
+        if (current == inScrollByMouse && !(me.getX() == downX && me.getY() == downY)) {
+            return;
+        }
+
+        NodeSet hitNodes = viewer.getGraphDrawer().getHitNodes(me.getX(), me.getY());
+        EdgeSet hitEdges = viewer.getGraphDrawer().getHitEdges(me.getX(), me.getY());
+
+        if (me.isPopupTrigger()) {
+            inPopup = true;
+            viewer.setCursor(Cursor.getDefaultCursor());
+            if (hitNodes.size() != 0)
+                viewer.fireNodePopup(me, hitNodes);
+            else if (hitNodeLabels.size() != 0)
+                viewer.fireNodeLabelPopup(me, hitNodeLabels);
+            else if (hitEdges.size() != 0)
+                viewer.fireEdgePopup(me, hitEdges);
+            else if (hitEdgeLabels.size() != 0)
+                viewer.fireEdgeLabelPopup(me, hitEdgeLabels);
+            else {
+                viewer.firePanelClicked(me);
+                viewer.firePanelPopup(me);
+            }
+            viewer.resetCursor();
+            return;
+        }
+
+        viewer.fireDoRelease(hitNodes);
+        viewer.fireDoRelease(hitEdges);
+        if (hitNodes.size() == 0 && hitEdges.size() == 0) {
+            // try again with more tolerance
+            hitNodes = viewer.getGraphDrawer().getHitNodes(me.getX(), me.getY(), 8);
+            viewer.fireDoRelease(hitNodes);
+        }
+
+        if (current == inRubberband) {
+            Rectangle rect = new Rectangle(downX, downY, 0, 0);
+            rect.add(me.getX(), me.getY());
+            selectNodesEdges(viewer.getGraphDrawer().getHitNodes(rect), viewer.getGraphDrawer().getHitEdges(rect), me.isShiftDown(), me.getClickCount());
+            viewer.repaint();
+        } else if (current == inNewEdge) {
+            NodeSet firstHit = viewer.getGraphDrawer().getHitNodes(downX, downY);
+            if (firstHit.size() == 1) {
+                Node v = firstHit.getFirstElement();
+                NodeSet secondHit = viewer.getGraphDrawer().getHitNodes(me.getX(), me.getY());
+
+                Node w;
+                if (secondHit.size() == 0) {
+                    int x = me.getX();
+                    int y = me.getY();
+                    Point2D location = trans.d2w(x, y);
+                    viewer.setDefaultNodeLocation(location);
+                    Edge e = viewer.newEdge(v, null);
+                    if (e != null) {
+                        w = viewer.getGraph().getTarget(e);
+                        viewer.setLocation(w, location);
+                    }
+                } else if (secondHit.size() == 1) {
+                    w = secondHit.getFirstElement();
+
+                    if (w != null) {
+                        if (v != w) {
+                            viewer.newEdge(v, w);
+                        }
+                    }
+                }
+                viewer.repaint();
+            }
+        } else if (current == inMoveNodeLabel) {
+            if (nodeLabelsHaveMoved) {
+                viewer.fireDoNodeLabelsMoved(viewer.getGraphDrawer().getHitNodeLabels(me.getX(), me.getY()));
+                viewer.repaint();
+            }
+        } else if (current == inMoveEdgeLabel) {
+            if (edgeLabelsHaveMoved) {
+                viewer.fireDoEdgeLabelsMoved(viewer.getGraphDrawer().getHitEdgeLabels(me.getX(), me.getY()));
+                viewer.repaint();
+            }
+        } else if (current == inMove)
+            viewer.fireDoNodesMoved();
+    }
+
+    /**
+     * Mouse entered.
+     *
+     * @param me MouseEvent
+     */
+    public void mouseEntered(MouseEvent me) {
+    }
+
+    /**
+     * Mouse exited.
+     *
+     * @param me MouseEvent
+     */
+    public void mouseExited(MouseEvent me) {
+        stillDownWithoutMoving = false;
+    }
+
+    /**
+     * Mouse clicked.
+     *
+     * @param me MouseEvent
+     */
+    public void mouseClicked(MouseEvent me) {
+        if (inPopup) {
+            inPopup = false;
+            return;
+        }
+        if (viewer.getGraphDrawer() == null)
+            return;
+
+        int meX = me.getX();
+        int meY = me.getY();
+
+        NodeSet hitNodes = viewer.getGraphDrawer().getHitNodes(meX, meY);
+        EdgeSet hitEdges = viewer.getGraphDrawer().getHitEdges(meX, meY);
+        NodeSet hitNodeLabels = viewer.getGraphDrawer().getHitNodeLabels(meX, meY);
+        EdgeSet hitEdgeLabels = viewer.getGraphDrawer().getHitEdgeLabels(meX, meY);
+
+        if (current == inScrollByMouse) // in navigation mode, double-click to lose selection
+        {
+            if (hitNodes.size() == 0 && hitEdges.size() == 0 && hitNodeLabels.size() == 0 && hitEdgeLabels.size() == 0) {
+                if (isAllowDeselectAllByMouseClick()) {
+                    viewer.selectAllNodes(false);
+                    viewer.selectAllEdges(false);
+                    viewer.repaint();
+                }
+                viewer.firePanelClicked(me);
+                return;
+            }
+        }
+        current = inClick;
+
+        if (hitNodes.size() == 0 && hitEdges.size() == 0 && hitNodeLabels.size() == 0 && hitEdgeLabels.size() == 0) {
+            viewer.firePanelClicked(me);
+            return;
+        }
+
+        if (hitNodes.size() != 0)
+            viewer.fireDoClick(hitNodes, me.getClickCount());
+        if (hitEdges.size() != 0)
+            viewer.fireDoClick(hitEdges, me.getClickCount());
+        if (hitNodeLabels.size() != 0)
+            viewer.fireDoClickLabel(hitNodeLabels, me.getClickCount());
+        if (hitEdgeLabels.size() != 0)
+            viewer.fireDoClickLabel(hitEdgeLabels, me.getClickCount());
+
+
+        if (me.getClickCount() == 1 && (hitNodes.size() > 0) || hitEdges.size() > 0)
+            selectNodesEdges(hitNodes, hitEdges, me.isShiftDown(), me.getClickCount());
+        else if (me.getClickCount() == 1 && (hitNodeLabels.size() > 0) || hitEdgeLabels.size() > 0)
+            selectNodesEdges(hitNodeLabels, hitEdgeLabels, me.isShiftDown(), me.getClickCount());
+
+        if (viewer.getAllowEdit() && hitNodes.size() == 0 && hitEdges.size() == 0 && me.getClickCount() == 2) {
+            // New node:
+            if (viewer.getAllowNewNodeDoubleClick()) {
+                viewer.setDefaultNodeLocation(trans.d2w(meX, meY));
+                Node v = viewer.newNode();
+                if (v != null) {
+                    viewer.setLocation(v, trans.d2w(meX, meY));
+                    viewer.setDefaultNodeLocation(trans.d2w(meX + 10, meY + 10));
+                    viewer.repaint();
+                }
+            }
+        } else if (viewer.isAllowInternalEdgePoints() && hitNodes.size() == 0 && hitEdges.size() == 1 && me.getClickCount() == 3) {
+            Edge e = hitEdges.getFirstElement();
+            EdgeView ev = viewer.getEV(e);
+            Point vp = trans.w2d(viewer.getLocation(viewer.getGraph().getSource(e)));
+            Point wp = trans.w2d(viewer.getLocation(viewer.getGraph().getTarget(e)));
+            int index = ev.hitEdgeRank(vp, wp, trans, me.getX(), meY, 3);
+            java.util.List<Point2D> list = viewer.getInternalPoints(e);
+            Point2D aptWorld = trans.d2w(me.getX(), meY);
+            if (list == null) {
+                list = new LinkedList<>();
+                list.add(aptWorld);
+                viewer.setInternalPoints(e, list);
+            } else
+                list.add(index, aptWorld);
+        } else if (me.getClickCount() == 2
+                && ((viewer.isAllowEditNodeLabelsOnDoubleClick() && hitNodeLabels.size() > 0)
+                || (viewer.isAllowEditNodeLabelsOnDoubleClick() && hitNodes.size() > 0))) {
+// undo node label
+            Node v;
+            if (viewer.isAllowEditNodeLabelsOnDoubleClick() && hitNodeLabels.size() > 0)
+                v = hitNodeLabels.getLastElement();
+            else
+                v = hitNodes.getLastElement();
+            String label = viewer.getLabel(v);
+            label = JOptionPane.showInputDialog(viewer, "Edit Node Label:", label);
+            if (label != null && !label.equals(viewer.getLabel(v))) {
+                viewer.setLabel(v, label);
+                viewer.setLabelVisible(v, label.length() > 0);
+                viewer.repaint();
+            }
+
+        } else if (me.getClickCount() == 2 && ((viewer.isAllowEditEdgeLabelsOnDoubleClick() && hitEdgeLabels.size() > 0)
+                || (viewer.isAllowEditEdgeLabelsOnDoubleClick() && hitEdges.size() > 0))) {
+            Edge e;
+            if (viewer.isAllowEditEdgeLabelsOnDoubleClick() && hitEdgeLabels.size() > 0)
+                e = hitEdgeLabels.getLastElement();
+            else
+                e = hitEdges.getLastElement();
+            String label = viewer.getLabel(e);
+            label = JOptionPane.showInputDialog(viewer, "Edit Edge Label:", label);
+            if (label != null && !label.equals(viewer.getLabel(e))) {
+                viewer.setLabel(e, label);
+                viewer.setLabelVisible(e, label.length() > 0);
+                viewer.repaint();
+            }
+        } else if (me.getClickCount() == 2 && hitNodes.size() > 0) {
+            // select connected component:
+            if (allowSelectConnectedComponent) {
+                viewer.selectConnectedComponents(hitNodes);
+            }
+        } else if (me.getClickCount() == 2 && hitNodeLabels.size() > 0) {
+            // select connected component:
+            if (allowSelectConnectedComponent) {
+                viewer.selectConnectedComponents(hitNodeLabels);
+            }
+        } else if (me.getClickCount() == 2 && hitEdges.size() > 0) {
+            // viewer.selectAllBelow(hitEdges);
+        }
+
+        current = 0;
+    }
+
+
+    /**
+     * Mouse dragged.
+     *
+     * @param me MouseEvent
+     */
+    public void mouseDragged(MouseEvent me) {
+        stillDownWithoutMoving = false;
+
+        if (me.isPopupTrigger())
+            return;
+
+        if (current == inScrollByMouse) {
+            viewer.setCursor(Cursors.getClosedHand());
+
+            JScrollPane scrollPane = viewer.getScrollPane();
+            int dX = me.getX() - downX;
+            int dY = me.getY() - downY;
+
+            if (dY != 0) {
+                JScrollBar scrollBar = scrollPane.getVerticalScrollBar();
+                int amount = Math.round(dY * (scrollBar.getMaximum() - scrollBar.getMinimum()) / viewer.getHeight());
+                if (amount != 0) {
+                    scrollBar.setValue(scrollBar.getValue() - amount);
+                }
+            }
+            if (dX != 0) {
+                JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
+                int amount = Math.round(dX * (scrollBar.getMaximum() - scrollBar.getMinimum()) / viewer.getWidth());
+                if (amount != 0) {
+                    scrollBar.setValue(scrollBar.getValue() - amount);
+                }
+            }
+        } else if (current == inRubberband) {
+            Graphics2D gc = (Graphics2D) viewer.getGraphics();
+
+            if (gc != null) {
+                Color color = viewer.getCanvasColor() != null ? viewer.getCanvasColor() : Color.WHITE;
+                gc.setXORMode(color);
+                if (selRect != null)
+                    gc.drawRect(selRect.x, selRect.y, selRect.width, selRect.height);
+                selRect = new Rectangle(downX, downY, 0, 0);
+                selRect.add(me.getX(), me.getY());
+                gc.drawRect(selRect.x, selRect.y, selRect.width, selRect.height);
+            }
+        } else if (current == inMove) {
+            Point2D p2 = trans.d2w(me.getX(), me.getY());
+            Point2D p1 = trans.d2w(downX, downY);
+            downX = me.getX();
+            downY = me.getY();
+            Point2D diff = new Point2D.Double(p2.getX() - p1.getX(),
+                    p2.getY() - p1.getY());
+
+            boolean moveAll = false;
+            double origLength = -1; // use in maintain edge lengths
+
+            if (viewer.getMaintainEdgeLengths()) {
+                origLength = canMaintainEdgeLengths();
+                if (origLength == -1)
+                    //moveAll = true;
+                    return; // move nothing...
+                // can't maintain edge lengths, move everything
+                // else Basic.message("Can Maintain: "+origLength);
+            }
+
+            try {
+                Graph G = viewer.getGraph();
+                for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                    if (viewer.selectedNodes.contains(v) || moveAll) {
+                        Point2D p = viewer.getLocation(v);
+                        viewer.setLocation(v, p.getX() + diff.getX(),
+                                p.getY() + diff.getY());
+                    }
+                }
+                for (Edge e = G.getFirstEdge(); e != null; e = G.getNextEdge(e)) {
+                    if (viewer.getSelected(e.getSource()) && viewer.getSelected(e.getTarget())) {
+                        java.util.List internalPoints = viewer.getInternalPoints(e);
+                        if (internalPoints != null) {
+                            for (Object internalPoint : internalPoints) {
+                                Point2D apt = (Point2D) internalPoint;
+                                apt.setLocation(apt.getX() + diff.getX(), apt.getY() + diff.getY());
+                            }
+                        }
+                    }
+                }
+            } catch (NotOwnerException ex) {
+                Basic.caught(ex);
+            } finally {
+                if (viewer.getMaintainEdgeLengths() && origLength != -1)
+                    maintainEdgeLengths(origLength);
+                viewer.repaint();
+            }
+        } else if (current == inMoveInternalEdgePoint) {
+            if (viewer.isAllowInternalEdgePoints()) {
+                Point p1 = new Point(downX, downY); // old [pos
+                Edge e = hitEdges.getFirstElement();
+                if (e != null && viewer.getEV(e).getShape() != EdgeView.ARC_LINE_EDGE) {
+                    downX = me.getX();
+                    downY = me.getY();
+                    Point p2 = new Point(downX, downY);     // new pos
+                    if (e != null) {
+                        viewer.getEV(e).moveInternalPoint(trans, p1, p2);
+                        viewer.repaint();
+                        viewer.getGraphDrawer().setEdgesHasMovedInternalPoints(e);
+                    }
+                }
+            }
+        } else if (current == inMoveNodeLabel) {
+            if (hitNodeLabels.size() > 0) {
+                Node v = hitNodeLabels.getFirstElement();
+                if (!viewer.getSelected(v))
+                    return; // move labels only of selected node
+                NodeView nv = viewer.getNV(v);
+                INodeDrawer nodeDrawer = viewer.getGraphDrawer().getNodeDrawer();
+
+                if (nv.getLabel() == null)
+                    return;
+
+                Graphics2D gc = (Graphics2D) viewer.getGraphics();
+
+                if (gc != null) {
+                    Point apt = trans.w2d(nv.getLocation());
+                    int meX = me.getX();
+                    int meY = me.getY();
+                    gc.setXORMode(viewer.getCanvasColor());
+                    if (prevPt != null) {
+                        gc.drawLine(apt.x, apt.y, prevPt.x, prevPt.y);
+                    } else {
+                        prevPt = new Point(downX, downY);
+                        Point labPt = nv.getLabelPosition(trans);
+                        offset.x = labPt.x - downX;
+                        offset.y = labPt.y - downY;
+                    }
+                    gc.drawLine(apt.x, apt.y, meX, meY);
+                    nv.hiliteLabel(gc, viewer.trans, viewer.getFont());
+
+                    int labX = meX + offset.x;
+                    int labY = meY + offset.y;
+
+                    nv.setLabelPositionRelative(labX - apt.x, labY - apt.y);
+                    nv.hiliteLabel(gc, viewer.trans, viewer.getFont());
+
+                    prevPt.x = meX;
+                    prevPt.y = meY;
+                    nodeLabelsHaveMoved = true;
+                    viewer.getGraphDrawer().setNodeHasMovedLabel(v);
+                }
+            }
+        } else if (current == inMoveEdgeLabel) {
+            if (hitEdgeLabels.size() > 0) {
+                try {
+                    final Edge e = hitEdgeLabels.getFirstElement();
+                    if (!viewer.getSelected(e))
+                        return; // move labels only of selected edges
+                    EdgeView ev = viewer.getEV(e);
+
+                    if (ev.getLabel() == null)
+                        return;
+
+                    final Graph G = viewer.getGraph();
+                    final NodeView vv = viewer.getNV(G.getSource(e));
+                    final NodeView wv = viewer.getNV(G.getTarget(e));
+
+                    Point2D nextToV = wv.getLocation();
+                    Point2D nextToW = vv.getLocation();
+                    if (viewer.getInternalPoints(e) != null) {
+                        if (viewer.getInternalPoints(e).size() != 0) {
+                            nextToV = viewer.getInternalPoints(e).get(0);
+                            nextToW = viewer.getInternalPoints(e).get(viewer.getInternalPoints(e).size() - 1);
+                        }
+                    }
+                    Point pv = vv.computeConnectPoint(nextToV, trans);
+                    Point pw = wv.computeConnectPoint(nextToW, trans);
+
+                    if (G.findDirectedEdge(G.getTarget(e), G.getSource(e)) != null)
+                        viewer.adjustBiEdge(pv, pw); // want parallel bi-edges
+
+                    final Graphics2D gc = (Graphics2D) viewer.getGraphics();
+
+                    if (gc != null) {
+                        ev.setLabelReferenceLocation(nextToV, nextToW, trans);
+                        ev.setLabelSize(gc);
+
+                        Point apt = ev.getLabelReferencePoint();
+                        int meX = me.getX();
+                        int meY = me.getY();
+                        gc.setXORMode(viewer.getCanvasColor());
+                        if (prevPt != null)
+                            gc.drawLine(apt.x, apt.y, prevPt.x, prevPt.y);
+                        else {
+                            prevPt = new Point(downX, downY);
+                            Point labPt = ev.getLabelPosition(trans);
+                            offset.x = labPt.x - downX;
+                            offset.y = labPt.y - downY;
+                        }
+                        gc.drawLine(apt.x, apt.y, meX, meY);
+                        ev.drawLabel(gc, trans, viewer.getSelected(e));
+                        int labX = meX + offset.x;
+                        int labY = meY + offset.y;
+
+                        ev.setLabelPositionRelative(labX - apt.x, labY - apt.y);
+                        ev.setLabelLayout(EdgeView.USER);
+                        ev.drawLabel(gc, trans, viewer.getSelected(e));
+
+                        prevPt.x = meX;
+                        prevPt.y = meY;
+                        edgeLabelsHaveMoved = true;
+                        viewer.getGraphDrawer().setEdgesHasMovedLabel(e);
+
+                    }
+                } catch (NotOwnerException ex) {
+                    Basic.caught(ex);
+                }
+            }
+        } else if (current == inNewEdge) {
+            final Graphics gc = viewer.getGraphics();
+
+            if (gc != null) {
+                gc.setXORMode(viewer.getCanvasColor());
+                if (selRect != null) // we misuse the selRect here...
+                    gc.drawLine(downX, downY, selRect.x, selRect.y);
+                selRect = new Rectangle(me.getX(), me.getY(), 0, 0);
+                gc.drawLine(downX, downY, me.getX(), me.getY());
+            }
+        } else if (current == inMoveMagnifier) {
+            int meX = me.getX();
+            int meY = me.getY();
+            if (meX != downX || meY != downY) {
+                trans.getMagnifier().move(downX, downY, meX, meY);
+                downX = meX;
+                downY = meY;
+                viewer.repaint();
+            }
+        } else if (current == inResizeMagnifier) {
+            int meY = me.getY();
+            if (meY != downY) {
+                trans.getMagnifier().resize(downY, meY);
+                downX = me.getX();
+                downY = meY;
+                viewer.repaint();
+            }
+        }
+    }
+
+    /**
+     * Mouse moved
+     *
+     * @param me MouseEvent
+     */
+    public void mouseMoved(MouseEvent me) {
+        stillDownWithoutMoving = false;
+        if (viewer.getGraphDrawer() != null && viewer.getGraph().getNumberOfNodes() <= 50000) {
+            NodeSet nodes = viewer.getGraphDrawer().getHitNodes(me.getX(), me.getY());
+            if (nodes.size() == 0)
+                nodes = viewer.getGraphDrawer().getHitNodeLabels(me.getX(), me.getY());
+            if (nodes.size() > 0) {
+                viewer.setToolTipText(nodes.getFirstElement());
+                return;
+            }
+        }
+        viewer.setToolTipText((String) null);
+    }
+
+    /**
+     * Updates the selection of nodes and edges.
+     *
+     * @param hitNodes NodeSet
+     * @param hitEdges EdgeSet
+     * @param shift    boolean
+     * @param clicks   boolean
+     */
+    void selectNodesEdges(NodeSet hitNodes, EdgeSet hitEdges, boolean shift, int clicks) {
+        if (hitNodes.size() == 1) // in this case, only do node selection
+            hitEdges.clear();
+
+        Graph G = viewer.getGraph();
+
+        boolean changed = false;
+
+        // synchronized (G)
+        {
+            // no shift, deselect everything:
+            if (!shift && (viewer.getNumberSelectedNodes() > 0 || viewer.getNumberSelectedEdges() > 0)) {
+                viewer.selectAllNodes(false);
+                viewer.selectAllEdges(false);
+                changed = true;
+            }
+
+            try {
+                if ((clicks > 0 || viewer.isAllowRubberbandNodes()) && hitNodes.size() > 0) {
+                    NodeSet select = new NodeSet(G);
+                    NodeSet deSelect = new NodeSet(G);
+                    for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                        if (hitNodes.contains(v)) {
+                            if (!shift) {
+                                select.add(v);
+                                viewer.setSelected(v, true);
+                                if (clicks > 1)
+                                    break;
+                            } else // shift==true
+                            {
+                                if (!viewer.getSelected(v))
+                                    select.add(v);
+                                else //
+                                    deSelect.add(v);
+                            }
+                        }
+                    }
+                    changed = select.size() > 0 || deSelect.size() > 0;
+                    viewer.setSelected(select, true);
+                    viewer.setSelected(deSelect, false);
+                }
+
+                if ((clicks > 0 || viewer.isAllowRubberbandEdges()) && hitEdges.size() > 0) {
+                    EdgeSet select = new EdgeSet(G);
+                    EdgeSet deSelect = new EdgeSet(G);
+
+                    for (Edge e = G.getFirstEdge(); e != null; e = G.getNextEdge(e)) {
+                        if (hitEdges.contains(e)) {
+                            if (!shift) {
+                                if (clicks == 0 || viewer.getNumberSelectedNodes() == 0) {
+                                    select.add(e);
+                                    // selectedNodes.insert(G.source(e));
+                                    // selectedNodes.insert(G.target(e));
+                                }
+                                if (clicks > 1)
+                                    break;
+                            } else // shift==true
+                            {
+                                if (!viewer.getSelected(e)) {
+                                    select.add(e);
+                                    // selectedNodes.insert(G.source(e));
+                                    // selectedNodes.insert(G.target(e));
+                                } else // selectedEdges.member(e)
+                                    deSelect.add(e);
+                            }
+                        }
+                    }
+                    changed = select.size() > 0 || deSelect.size() > 0;
+                    viewer.setSelected(select, true);
+                    viewer.setSelected(deSelect, false);
+                }
+            } finally {
+                if (changed)
+                    viewer.repaint();
+            }
+        }
+    }
+
+    // KeyListener methods:
+
+    /**
+     * Key typed
+     *
+     * @param ke Keyevent
+     */
+    public void keyTyped(KeyEvent ke) {
+    }
+
+    /**
+     * Key pressed
+     *
+     * @param ke KeyEvent
+     */
+    public void keyPressed(KeyEvent ke) {
+        int r = 1; // rotate angle
+        double s = 1.05; // scale factor
+        if ((ke.getModifiers() & InputEvent.ALT_MASK) != 0) {
+            s = 1.5;
+            r = 5;
+        } else if ((ke.getModifiers() & InputEvent.CTRL_MASK) != 0) {
+            s = 4;
+            r = 90;
+        }
+
+        JScrollPane scrollPane = viewer.getScrollPane();
+        boolean xyLocked = viewer.isKeepAspectRatio();
+
+        if (ke.getKeyCode() == KeyEvent.VK_LEFT) {
+            JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
+            if (!ke.isShiftDown() && scrollBar.getVisibleAmount() < scrollBar.getMaximum()) {
+                scrollBar.setValue(scrollBar.getValue() + scrollBar.getBlockIncrement(1));
+            } else {
+                if (xyLocked) {
+                    if (viewer.isAllowRotation()) {
+                        ScrollPaneAdjuster spa = new ScrollPaneAdjuster(viewer.getScrollPane(), trans);
+                        double angle = trans.getAngle() - r * Math.PI / 100.0;
+                        trans.setAngle(angle);
+                        spa.adjust(true, true);
+                        viewer.resetLabelPositions(true);
+                        // final ICommand cmd = new RotateCommand(viewer, viewer.trans, angle);
+                        //new Edit(cmd, "rotate left").execute(viewer.getUndoSupportNetwork());
+                    }
+                } else   // zoom rectilinear
+                {
+                    ScrollPaneAdjuster spa = new ScrollPaneAdjuster(viewer.getScrollPane(), trans);
+                    trans.composeScale(1.0 / s, 1);
+                    spa.adjust(false, true);
+                    //final ICommand cmd = new ZoomCommand(viewer, viewer.trans, 1.0 / s, 1);
+                    //new Edit(cmd, "Zoom In").execute(viewer.getUndoSupportNetwork());
+                }
+            }
+        } else if (ke.getKeyCode() == KeyEvent.VK_RIGHT) {
+            JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
+            if (!ke.isShiftDown() && scrollBar.getVisibleAmount() < scrollBar.getMaximum()) {
+                scrollBar.setValue(scrollBar.getValue() - scrollBar.getBlockIncrement(1));
+            } else { //scale
+                if (xyLocked) {
+                    if (viewer.isAllowRotation()) {
+                        ScrollPaneAdjuster spa = new ScrollPaneAdjuster(viewer.getScrollPane(), trans);
+                        double angle = trans.getAngle() + r * Math.PI / 100.0;
+                        trans.setAngle(angle);
+                        spa.adjust(true, true);
+                        viewer.resetLabelPositions(true);
+                    }
+                    //final ICommand cmd = new RotateCommand(viewer, viewer.trans, angle);
+                    //new Edit(cmd, "rotate right").execute(viewer.getUndoSupportNetwork());
+                } else   // zoom rectilinear
+                {
+                    ScrollPaneAdjuster spa = new ScrollPaneAdjuster(viewer.getScrollPane(), trans);
+                    trans.composeScale(s, 1);
+                    spa.adjust(true, false);
+
+                    //final ICommand cmd = new ZoomCommand(viewer, viewer.trans, s, 1);
+                    //new Edit(cmd, "Zoom Out").execute(viewer.getUndoSupportNetwork());
+                }
+            }
+        } else if (ke.getKeyCode() == KeyEvent.VK_UP) {
+            JScrollBar scrollBar = scrollPane.getVerticalScrollBar();
+            if (!ke.isShiftDown() && scrollBar.getVisibleAmount() < scrollBar.getMaximum()) {
+                scrollBar.setValue(scrollBar.getValue() - scrollBar.getBlockIncrement(1));
+            } else { //scale
+                ScrollPaneAdjuster spa = new ScrollPaneAdjuster(viewer.getScrollPane(), trans);
+                double f = xyLocked ? 1.0 / s : 1.0;
+                trans.composeScale(f, 1.0 / s);
+                spa.adjust(f != 1.0, true);
+
+                //final ICommand cmd = new ZoomCommand(viewer, viewer.trans, f, 1.0 / s);
+                //new Edit(cmd, "Zoom In").execute(viewer.getUndoSupportNetwork());
+            }
+        } else if (ke.getKeyCode() == KeyEvent.VK_DOWN) {
+            JScrollBar scrollBar = scrollPane.getVerticalScrollBar();
+            if (!ke.isShiftDown() && scrollBar.getVisibleAmount() < scrollBar.getMaximum()) {
+                scrollBar.setValue(scrollBar.getValue() + scrollBar.getBlockIncrement(1));
+            } else { //scale
+                ScrollPaneAdjuster spa = new ScrollPaneAdjuster(viewer.getScrollPane(), trans);
+                double f = (xyLocked ? s : 1.0);
+                trans.composeScale(f, s);
+                spa.adjust(f != 1.0, true);
+                //final ICommand cmd = new ZoomCommand(viewer, viewer.trans, f, s);
+                //new Edit(cmd, "zoom Out").execute(viewer.getUndoSupportNetwork());
+            }
+        } else if (ke.getKeyCode() == KeyEvent.VK_PAGE_UP) {
+            ScrollPaneAdjuster spa = new ScrollPaneAdjuster(viewer.getScrollPane(), trans);
+            trans.composeScale(1.0 / s, 1.0 / s);
+            spa.adjust(true, true);
+            //final ICommand cmd = new ZoomCommand(viewer, viewer.trans, 1.0 / s, 1.0 / s);
+            //new Edit(cmd, "zoom In").execute(viewer.getUndoSupportNetwork());
+        } else if (ke.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
+            ScrollPaneAdjuster spa = new ScrollPaneAdjuster(viewer.getScrollPane(), trans);
+            trans.composeScale(s, s);
+            spa.adjust(true, true);
+            //final ICommand cmd = new ZoomCommand(viewer, viewer.trans, s, s);
+            //new Edit(cmd, "zoom Out").execute(viewer.getUndoSupportNetwork());
+        } else if (viewer.getAllowEdit() && (ke.getKeyCode() == KeyEvent.VK_DELETE || ke.getKeyCode() == KeyEvent.VK_BACK_SPACE)) {
+            viewer.delSelectedNodes();
+            viewer.delSelectedEdges();
+            viewer.repaint();
+        } else if ((ke.getModifiers() & InputEvent.SHIFT_MASK) != 0) {
+            viewer.setCursor(Cursor.getDefaultCursor());
+        }
+    }
+
+    /**
+     * Key released
+     *
+     * @param ke KeyEvent
+     */
+    public void keyReleased(KeyEvent ke) {
+        if ((ke.getModifiers() & InputEvent.SHIFT_MASK) != 0) {
+            viewer.resetCursor();
+        }
+    }
+
+    // ComponentListener methods:
+
+    /**
+     * component hidded
+     *
+     * @param ev ComponentEvent
+     */
+    public void componentHidden(ComponentEvent ev) {
+    }
+
+    /**
+     * component moved
+     *
+     * @param ev ComponentEvent
+     */
+    public void componentMoved(ComponentEvent ev) {
+    }
+
+    /**
+     * component resized
+     *
+     * @param ev ComponentEvent
+     */
+    public void componentResized(ComponentEvent ev) {
+        viewer.setSize(viewer.getSize());
+    }
+
+    /**
+     * component shown
+     *
+     * @param ev ComponentEvent
+     */
+    public void componentShown(ComponentEvent ev) {
+    }
+
+    /**
+     * If edge lengths can be maintained in user interaction, returns
+     * the length of any edge connecting the selected from the non-selected
+     * nodes. Otherwise, returns -1
+     * We can maintain edge lengths if every edge in the set of edges that
+     * separate the selected from the none-selected nodes
+     * has the same angle and length
+     *
+     * @return firstlength double
+     */
+
+    private double canMaintainEdgeLengths() {
+        Graph graph = viewer.getGraph();
+        boolean first = true;
+        double firstAngle = 0;
+        double firstLength = 0;
+
+        try {
+            for (Edge e = graph.getFirstEdge(); e != null; e = graph.getNextEdge(e)) {
+                if (!graph.isSpecial(e)) {
+                    Node v = graph.getSource(e);
+                    Node w = graph.getTarget(e);
+                    Point2D pv;
+                    Point2D pw;
+                    if (viewer.selectedNodes.contains(v) && !viewer.selectedNodes.contains(w)) {
+                        pv = viewer.getLocation(v);
+                        pw = viewer.getLocation(w);
+                    } else if (!viewer.selectedNodes.contains(v) && viewer.selectedNodes.contains(w)) {
+                        pv = viewer.getLocation(w);
+                        pw = viewer.getLocation(v);
+                    } else
+                        continue;
+                    if (pv == null || pw == null)
+                        continue;
+                    Point2D q = new Point2D.Double(pw.getX() - pv.getX(), pw.getY() - pv.getY());
+                    double angle = Geometry.computeAngle(q);
+                    double length = pv.distance(pw);
+                    if (first) {
+                        firstAngle = angle;
+                        firstLength = length;
+                        first = false;
+                    } else // compare with first line
+                    {
+                        if ((Math.abs(angle - firstAngle) > 0.01
+                                && Math.abs(angle - firstAngle - 6.28318530717958647692) > 0.01)
+                                || Math.abs(length - firstLength) > 0.01 * firstLength)
+                            return -1;
+                    }
+                }
+            }
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+        return firstLength;
+    }
+
+    /**
+     * Recompute coordinates so that edge lengths are maintained
+     * Assumes canMaintainEdgeLengths returned true!
+     *
+     * @param origLength double
+     */
+    private void maintainEdgeLengths(double origLength) {
+        Graph G = viewer.getGraph();
+        NodeSet visited = new NodeSet(G);
+
+        double length = -1;
+        Point2D diff = null;
+
+        try {
+            // put all selected nodes into visited set:
+            for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v))
+                if (viewer.selectedNodes.contains(v))
+                    visited.add(v);
+
+            for (Edge e = G.getFirstEdge(); e != null; e = G.getNextEdge(e)) {
+                if (!G.isSpecial(e)) {
+                    Node v = G.getSource(e);
+                    Node w = G.getTarget(e);
+                    Node z;
+                    Point2D pv;
+                    Point2D pw;
+                    if (viewer.selectedNodes.contains(v) && !viewer.selectedNodes.contains(w)
+                            && !visited.contains(w)) {
+                        pv = viewer.getLocation(v);
+                        pw = viewer.getLocation(w);
+                        z = w;
+                    } else if (!viewer.selectedNodes.contains(v) && viewer.selectedNodes.contains(w)
+                            && !visited.contains(v)) {
+                        pv = viewer.getLocation(w);
+                        pw = viewer.getLocation(v);
+                        z = v;
+                    } else
+                        continue;
+                    if (pv == null || pw == null)
+                        continue;
+
+                    if (length == -1) // use first edge to define diff
+                    {
+                        length = pv.distance(pw);
+
+                        if (Math.abs(length - origLength) < 0.001 * length)
+                            return; // no change of length, return
+
+                        diff = new Point2D.Double((length - origLength) * (pw.getX() - pv.getX()) / length,
+                                (length - origLength) * (pw.getY() - pv.getY()) / length);
+                    }
+
+                    shiftAllNodesRecursively(G, z, diff, visited);
+                }
+            }
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * recursively shifts all nodes necessary to maintain edge lengths
+     *
+     * @param G       Graph
+     * @param z       Node
+     * @param diff    Point2D
+     * @param visited NodeSet
+     */
+    private void shiftAllNodesRecursively(Graph G, Node z, Point2D diff, NodeSet visited) {
+        try {
+            if (!visited.contains(z)) {
+                if (viewer.getLocation(z) != null) {
+                    viewer.setLocation(z, viewer.getLocation(z).getX() - diff.getX(),
+                            viewer.getLocation(z).getY() - diff.getY());
+                }
+                visited.add(z);
+                for (Edge e = G.getFirstAdjacentEdge(z); e != null; e = G.getNextAdjacentEdge(e, z)) {
+                    Node v = G.getOpposite(z, e);
+                    shiftAllNodesRecursively(G, v, diff, visited);
+                }
+            }
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * react to a mouse wheel event
+     *
+     * @param e
+     */
+    public void mouseWheelMoved(MouseWheelEvent e) {
+        if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
+            boolean xyLocked = viewer.isKeepAspectRatio();
+
+            boolean doScaleVertical = !e.isMetaDown() && !e.isAltDown() && !e.isShiftDown() && !xyLocked;
+            boolean doScaleHorizontal = !e.isMetaDown() && !e.isControlDown() && !e.isAltDown() && e.isShiftDown();
+            boolean doScrollVertical = !e.isMetaDown() && e.isAltDown() && !e.isShiftDown() && !xyLocked;
+            boolean doScrollHorizontal = !e.isMetaDown() && e.isAltDown() && e.isShiftDown();
+            boolean doScaleBoth = (e.isMetaDown() || xyLocked) && !e.isAltDown() && !e.isShiftDown();
+            boolean doRotate = !e.isMetaDown() && e.isAltDown() && !e.isShiftDown() && xyLocked;
+
+            boolean useMag = trans.getMagnifier().isActive();
+            trans.getMagnifier().setActive(false);
+
+            if (doScrollVertical) {
+                viewer.getScrollPane().getVerticalScrollBar().setValue(viewer.getScrollPane().getVerticalScrollBar().getValue() + e.getUnitsToScroll());
+            } else if (doScaleVertical) {
+                ScrollPaneAdjuster spa = new ScrollPaneAdjuster(viewer.getScrollPane(), trans, e.getPoint());
+                double toScroll = 1.0 + (e.getUnitsToScroll() / 100.0);
+                double s = (toScroll > 0 ? 1.0 / toScroll : toScroll);
+                double scale = s * trans.getScaleY();
+                if (scale >= GraphView.YMIN_SCALE && scale <= GraphView.YMAX_SCALE) {
+                    trans.composeScale(1, s);
+                    viewer.repaint();
+                    spa.adjust(false, true);
+                }
+            } else if (doScaleBoth) {
+                ScrollPaneAdjuster spa = new ScrollPaneAdjuster(viewer.getScrollPane(), trans, e.getPoint());
+                double toScroll = 1.0 + (e.getUnitsToScroll() / 100.0);
+                double s = (toScroll > 0 ? 1.0 / toScroll : toScroll);
+                double scaleX = s * trans.getScaleX();
+                double scaleY = s * trans.getScaleY();
+                if (scaleX >= GraphView.XMIN_SCALE && scaleX <= GraphView.XMAX_SCALE && scaleY >= GraphView.YMIN_SCALE && scaleY <= GraphView.YMAX_SCALE) {
+                    trans.composeScale(s, s);
+                    viewer.repaint();
+                    spa.adjust(true, true);
+                }
+            } else if (doScrollHorizontal) {
+                viewer.getScrollPane().getHorizontalScrollBar().setValue(viewer.getScrollPane().getHorizontalScrollBar().getValue() + e.getUnitsToScroll());
+            } else if (doScaleHorizontal && !xyLocked) { //scale
+                ScrollPaneAdjuster spa = new ScrollPaneAdjuster(viewer.getScrollPane(), trans, e.getPoint());
+                double toScroll = 1.0 + (e.getUnitsToScroll() / 100.0);
+                double s = (toScroll > 0 ? 1.0 / toScroll : toScroll);
+                double scale = s * trans.getScaleX();
+                if (scale >= GraphView.XMIN_SCALE && scale <= GraphView.XMAX_SCALE) {
+                    trans.composeScale(s, 1);
+                    viewer.repaint();
+                    spa.adjust(true, false);
+                }
+            } else if (doRotate) {
+                if (viewer.isAllowRotation()) {
+                    ScrollPaneAdjuster spa = new ScrollPaneAdjuster(viewer.getScrollPane(), trans, e.getPoint());
+                    double angle = trans.getAngle() - e.getUnitsToScroll() * Math.PI / 1000.0;
+                    trans.setAngle(angle);
+                    viewer.getGraphDrawer().resetLabelPositions(false);
+                    viewer.repaint();
+                    spa.adjust(true, true);
+                }
+            }
+            trans.getMagnifier().setActive(useMag);
+        }
+    }
+
+    /**
+     * is user allowed to deselect all by mouse click off graph?
+     *
+     * @return true, if allowed
+     */
+    public boolean isAllowDeselectAllByMouseClick() {
+        return allowDeselectAllByMouseClick;
+    }
+
+    /**
+     * is user allowed to deselect all by mouse click off graph?
+     *
+     * @param allowDeselectAllByMouseClick
+     */
+    public void setAllowDeselectAllByMouseClick(boolean allowDeselectAllByMouseClick) {
+        this.allowDeselectAllByMouseClick = allowDeselectAllByMouseClick;
+    }
+
+    public boolean isAllowSelectConnectedComponent() {
+        return allowSelectConnectedComponent;
+    }
+
+    public void setAllowSelectConnectedComponent(boolean allowSelectConnectedComponent) {
+        this.allowSelectConnectedComponent = allowSelectConnectedComponent;
+    }
+}
+
+// EOF
diff --git a/src/jloda/graphview/IGraphDrawer.java b/src/jloda/graphview/IGraphDrawer.java
new file mode 100644
index 0000000..b7493a4
--- /dev/null
+++ b/src/jloda/graphview/IGraphDrawer.java
@@ -0,0 +1,225 @@
+/**
+ * IGraphDrawer.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.graph.Edge;
+import jloda.graph.EdgeSet;
+import jloda.graph.Node;
+import jloda.graph.NodeSet;
+
+import java.awt.*;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * implementation of a graph dendroscope
+ * Daniel Huson, 12.2006
+ */
+public interface IGraphDrawer {
+    String DESCRIPTION = "Graph Drawer";
+
+    /**
+     * setup the graph view
+     *
+     * @param graphView
+     */
+    void setupGraphView(GraphView graphView);
+
+    /**
+     * paint the graph. If rect is non-null, need only cover the rect
+     *
+     * @param graphics
+     * @param rect     rectangle in device coordinates
+     */
+    void paint(Graphics graphics, Rectangle rect);
+
+
+    /**
+     * compute an embedding of the graph.
+     *
+     * @param toScale if true, embedding should be to-scale
+     * @return true, if embedding was computed
+     */
+    boolean computeEmbedding(boolean toScale);
+
+    /**
+     * get all nodes hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return nodes hit
+     */
+    NodeSet getHitNodes(int x, int y);
+
+    /**
+     * get all nodes hit by mouse at (x,y) at a tolerance of d pixels
+     *
+     * @param x
+     * @param y
+     * @param d
+     * @return nodes hit
+     */
+    NodeSet getHitNodes(int x, int y, int d);
+
+    /**
+     * get all node labels hit by mouse at (x,y)
+     * @param x
+     * @param y
+     * @return node labels
+     */
+
+    /**
+     * get all node labels hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return nodes hit
+     */
+    NodeSet getHitNodeLabels(int x, int y);
+
+    /**
+     * get all nodes contained in rect
+     *
+     * @param rect
+     * @return nodes contained in rect
+     */
+    NodeSet getHitNodes(Rectangle rect);
+
+    /**
+     * get all node labels contained in rect
+     *
+     * @param rect
+     * @return node labels contained in rect
+     */
+    NodeSet getHitNodeLabels(Rectangle rect);
+
+    /**
+     * get all edges hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return edges hits
+     */
+    EdgeSet getHitEdges(int x, int y);
+
+    /**
+     * get all edge labels hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return edge labels
+     */
+    EdgeSet getHitEdgeLabels(int x, int y);
+
+    /**
+     * get all edges contained in rect
+     *
+     * @param rect
+     * @return edges contained in rect
+     */
+    EdgeSet getHitEdges(Rectangle rect);
+
+    /**
+     * get all edge labels contained in rect
+     *
+     * @param rect
+     * @return edges contained in rect
+     */
+    EdgeSet getHitEdgeLabels(Rectangle rect);
+
+    /**
+     * get the set of collapsed nodes
+     *
+     * @return collapsed nodes
+     */
+    NodeSet getCollapsedNodes();
+
+    /**
+     * set the set of collapsed nodes
+     */
+    void setCollapsedNodes(NodeSet collapsedNodes);
+
+    /**
+     * to support bounding-box oriented drawers, report any node whose label has been interavtively moved
+     *
+     * @param v
+     */
+    void setNodeHasMovedLabel(Node v);
+
+    /**
+     * to support bounding-box oriented drawers, report any edge whose label has been interavtively moved
+     *
+     * @param e
+     */
+    void setEdgesHasMovedLabel(Edge e);
+
+    /**
+     * to support bounding-box oriented drawers, report any edge whose internal points have been interavtively moved
+     *
+     * @param e
+     */
+    void setEdgesHasMovedInternalPoints(Edge e);
+
+    /**
+     * set the default label positions for nodes and edges
+     *
+     * @param resetAll if true, reset positions for user-placed labels, too
+     */
+    void resetLabelPositions(boolean resetAll);
+
+    /**
+     * gets the label overlap avoider
+     *
+     * @return label overlap avoider
+     */
+    LabelOverlapAvoider getLabelOverlapAvoider();
+
+    /**
+     * gets the bounding box of the graph in world coordinates
+     *
+     * @return bounding box
+     */
+    Rectangle2D getBBox();
+
+    /**
+     * rotate node labels to match edge directions?
+     *
+     * @param rotateLabels
+     */
+    void setRadialLabels(boolean rotateLabels);
+
+    /**
+     * set the auxilary parameter
+     *
+     * @param parameter
+     */
+    void setAuxilaryParameter(int parameter);
+
+    /**
+     * get the auxilary parameter
+     *
+     * @return auxilary parameter
+     */
+    int getAuxilaryParameter();
+
+    INodeDrawer getNodeDrawer();
+
+    void setNodeDrawer(INodeDrawer nodeDrawer);
+
+}
diff --git a/src/jloda/graphview/IGraphViewListener.java b/src/jloda/graphview/IGraphViewListener.java
new file mode 100644
index 0000000..e914fbe
--- /dev/null
+++ b/src/jloda/graphview/IGraphViewListener.java
@@ -0,0 +1,27 @@
+/**
+ * IGraphViewListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import java.awt.event.*;
+
+/**
+ */
+public interface IGraphViewListener extends MouseListener, MouseMotionListener, KeyListener, ComponentListener, MouseWheelListener {
+}
diff --git a/src/jloda/graphview/INodeDrawer.java b/src/jloda/graphview/INodeDrawer.java
new file mode 100644
index 0000000..e1d8317
--- /dev/null
+++ b/src/jloda/graphview/INodeDrawer.java
@@ -0,0 +1,61 @@
+/**
+ * INodeDrawer.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.graph.Node;
+
+import java.awt.*;
+
+/**
+ * interface for drawing nodes
+ * Daniel Huson, 1.2012
+ */
+public interface INodeDrawer {
+    /**
+     * setup data
+     *
+     * @param graphView
+     * @param gc
+     */
+    void setup(GraphView graphView, Graphics2D gc);
+
+    /**
+     * draw the node
+     *
+     * @param selected
+     */
+    void draw(Node v, boolean selected);
+
+    /**
+     * draw the label of the node
+     *
+     * @param selected
+     */
+    void drawLabel(Node v, boolean selected);
+
+    /**
+     * draw the node and the label
+     *
+     * @param selected
+     */
+    void drawNodeAndLabel(Node v, boolean selected);
+
+
+}
diff --git a/src/jloda/graphview/INodeEdgeFormatable.java b/src/jloda/graphview/INodeEdgeFormatable.java
new file mode 100644
index 0000000..abe31d0
--- /dev/null
+++ b/src/jloda/graphview/INodeEdgeFormatable.java
@@ -0,0 +1,143 @@
+/**
+ * INodeEdgeFormatable.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.graph.EdgeSet;
+import jloda.graph.NodeSet;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * used to format nodes and edges
+ * Daniel Huson, 7.2010
+ */
+public interface INodeEdgeFormatable {
+    Color getColorSelectedNodes();
+
+    Color getBackgroundColorSelectedNodes();
+
+    Color getLabelColorSelectedNodes();
+
+    Color getLabelBackgroundColorSelectedNodes();
+
+    boolean setColorSelectedNodes(Color a);
+
+    boolean setBackgroundColorSelectedNodes(Color a);
+
+    boolean setLabelColorSelectedNodes(Color a);
+
+    boolean setLabelBackgroundColorSelectedNodes(Color a);
+
+    Font getFontSelected();
+
+    int getWidthSelectedNodes();
+
+    int getHeightSelectedNodes();
+
+    int getLineWidthSelectedNodes();
+
+    byte getShapeSelectedNodes();
+
+    boolean setFontSelectedEdges(String family, int bold, int italics, int size);
+
+    boolean setFontSelectedNodes(String family, int bold, int italics, int size);
+
+
+    void setWidthSelectedNodes(byte a);
+
+    void setHeightSelectedNodes(byte a);
+
+    void setLineWidthSelectedNodes(byte a);
+
+    void setShapeSelectedNodes(byte a);
+
+
+    Color getColorSelectedEdges();
+
+    Color getLabelColorSelectedEdges();
+
+    Color getLabelBackgroundColorSelectedEdges();
+
+    boolean setColorSelectedEdges(Color a);
+
+    boolean setLabelBackgroundColorSelectedEdges(Color a);
+
+    boolean setLabelColorSelectedEdges(Color a);
+
+    int getLineWidthSelectedEdges();
+
+    int getDirectionSelectedEdges();
+
+    byte getShapeSelectedEdges();
+
+    void setLineWidthSelectedEdges(byte a);
+
+    void setDirectionSelectedEdges(byte a);
+
+    void setShapeSelectedEdges(byte a);
+
+    void addNodeActionListener(NodeActionListener nal);
+
+    void removeNodeActionListener(NodeActionListener nal);
+
+    void addEdgeActionListener(EdgeActionListener eal);
+
+    void removeEdgeActionListener(EdgeActionListener eal);
+
+    boolean hasSelectedNodes();
+
+    boolean hasSelectedEdges();
+
+    void setLabelVisibleSelectedNodes(boolean visible);
+
+    boolean hasLabelVisibleSelectedNodes();
+
+    void setLabelVisibleSelectedEdges(boolean visible);
+
+    boolean hasLabelVisibleSelectedEdges();
+
+    void repaint();
+
+    boolean getLockXYScale();
+
+    void rotateLabelsSelectedNodes(int percent);
+
+    void rotateLabelsSelectedEdges(int percent);
+
+    JPanel getPanel();
+
+    JScrollPane getScrollPane();
+
+    void setRandomColorsSelectedNodes(boolean foreground, boolean background, boolean labelforeground, boolean labelbackgrond);
+
+    void setRandomColorsSelectedEdges(boolean foreground, boolean labelforeground, boolean labelbackgrond);
+
+    NodeSet getSelectedNodes();
+
+    EdgeSet getSelectedEdges();
+
+    boolean selectAllNodes(boolean select);
+
+    boolean selectAllEdges(boolean select);
+
+    JFrame getFrame();
+
+}
diff --git a/src/jloda/graphview/IPopupListener.java b/src/jloda/graphview/IPopupListener.java
new file mode 100644
index 0000000..7ad597b
--- /dev/null
+++ b/src/jloda/graphview/IPopupListener.java
@@ -0,0 +1,70 @@
+/**
+ * IPopupListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.graph.EdgeSet;
+import jloda.graph.NodeSet;
+
+import java.awt.event.MouseEvent;
+
+/**
+ * listen for popmenu events
+ */
+public interface IPopupListener {
+    /**
+     * popup menu on node
+     *
+     * @param me
+     * @param nodes
+     */
+    void doNodePopup(MouseEvent me, NodeSet nodes);
+
+    /**
+     * popup menu on node label
+     *
+     * @param me
+     * @param nodes
+     */
+    void doNodeLabelPopup(MouseEvent me, NodeSet nodes);
+
+    /**
+     * popup menu on edge
+     *
+     * @param me
+     * @param edges
+     */
+    void doEdgePopup(MouseEvent me, EdgeSet edges);
+
+    /**
+     * popup menu on edge
+     *
+     * @param me
+     * @param edges
+     */
+    void doEdgeLabelPopup(MouseEvent me, EdgeSet edges);
+
+    /**
+     * popup menu not on graph
+     *
+     * @param me
+     */
+    void doPanelPopup(MouseEvent me);
+
+}
diff --git a/src/jloda/graphview/ITransformChangeListener.java b/src/jloda/graphview/ITransformChangeListener.java
new file mode 100644
index 0000000..518d3d6
--- /dev/null
+++ b/src/jloda/graphview/ITransformChangeListener.java
@@ -0,0 +1,28 @@
+/**
+ * ITransformChangeListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+/**
+ * listen for any changes of transformation
+ * daniel huson, 1.2006
+ */
+public interface ITransformChangeListener {
+    void hasChanged(Transform trans);
+}
diff --git a/src/jloda/graphview/LabelLayoutRTree.java b/src/jloda/graphview/LabelLayoutRTree.java
new file mode 100644
index 0000000..b4c825f
--- /dev/null
+++ b/src/jloda/graphview/LabelLayoutRTree.java
@@ -0,0 +1,87 @@
+/**
+ * LabelLayoutRTree.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.graph.Node;
+import jloda.util.Basic;
+import jloda.util.RTree;
+
+import java.awt.*;
+
+/**
+ * layout labels using an RTree
+ * Daniel Huson, 9.2012
+ */
+public class LabelLayoutRTree {
+    /**
+     * layout nodes for drawing
+     *
+     * @param graphView
+     * @param gc
+     */
+    public void layout(GraphView graphView, Graphics gc) {
+        Transform trans = graphView.trans;
+
+        RTree<Node> rTree = new RTree<>();
+        // add all nodes to avoid:
+        Point center = new Point();
+        int count = 0;
+        for (Node v = graphView.getGraph().getFirstNode(); v != null; v = v.getNext()) {
+            NodeView nv = graphView.getNV(v);
+            String label = nv.getLabel();
+            if (label != null && nv.getLabelVisible()) {
+                nv.setLabelAngle(0);
+                Rectangle bbox = nv.getBox(trans);
+                //  if (nv.getHeight() > 1 || nv.getWidth() > 1) {
+                rTree.add(bbox, v);
+                //  }
+                center.x += bbox.x + bbox.width / 2;
+                center.y += bbox.y + bbox.height / 2;
+                count++;
+                if (count > 500)
+                    return;
+                if (nv.getLabelLayout() != NodeView.LAYOUT) {
+                    Rectangle rect = nv.getLabelRect(trans);
+                    if (rect != null)
+                        rTree.add(rect, v);
+                }
+            }
+        }
+        if (count > 0) {
+            center.x /= count;
+            center.y /= count;
+        }
+        for (Node v = graphView.getGraph().getFirstNode(); v != null; v = v.getNext()) {
+            NodeView nv = graphView.getNV(v);
+            String label = nv.getLabel();
+            if (label != null && nv.getLabelVisible() && nv.getLabelLayout() == NodeView.LAYOUT) {
+                Dimension labelSize = Basic.getStringSize(gc, label, nv.getFont()).getSize();
+                Point location = graphView.trans.w2d(nv.getLocation());
+                boolean left = (location.x < center.x);
+                // location.x += nv.getWidth() / 2;
+                location.y -= labelSize.getHeight() / 2;
+                location = rTree.addCloseTo(v.getId(), location, nv.getWidth() / 2, nv.getHeight() / 2, left, labelSize.getSize(), v);
+                nv.setLabelPosition(location.x, location.y + labelSize.height, graphView.trans);
+            }
+        }
+        //rTree.draw(gc);
+        rTree.clear();
+    }
+}
diff --git a/src/jloda/graphview/LabelLayouter.java b/src/jloda/graphview/LabelLayouter.java
new file mode 100644
index 0000000..10795d0
--- /dev/null
+++ b/src/jloda/graphview/LabelLayouter.java
@@ -0,0 +1,267 @@
+/**
+ * LabelLayouter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.graph.Edge;
+import jloda.graph.Graph;
+import jloda.graph.Node;
+import jloda.util.Basic;
+import jloda.util.Geometry;
+import jloda.util.ProgramProperties;
+
+import java.awt.*;
+import java.awt.geom.Point2D;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * automatic layout of labels
+ *
+ * @author huson
+ *         Date: 09-Jan-2004
+ */
+public class LabelLayouter {
+    final Graphics2D gc;
+    final List<Rectangle> rects;
+
+    /**
+     * constructor
+     *
+     * @param gc
+     */
+    public LabelLayouter(Graphics2D gc) {
+        this.gc = gc;
+        rects = new LinkedList<>();
+    }
+
+    /**
+     * layouts out label so that it does not cover any already visible
+     *
+     * @param graphView
+     * @param v
+     */
+    private int layout(GraphView graphView, Node v, boolean changeLocations) {
+        NodeView nv = graphView.getNV(v);
+        Point apt = graphView.trans.w2d(nv.getLocation());
+        Rectangle arect = nv.getLabelRect(graphView.trans);
+
+        if (arect == null)
+            return 0;
+
+        if (nv != null)
+            arect.y -= nv.getLabelSize().height;
+
+        int count = 0;
+        boolean ok;
+
+        int sign;
+        do {
+            if ((count % 2) == 0)
+                sign = 1;
+            else
+                sign = -1;
+            ok = true;
+            for (Rectangle brect : rects) {
+                if (brect != null && arect.intersects(brect)) {
+                    arect.translate(0, sign * count * 5);
+                    ok = false;
+                    break;
+                }
+            }
+            count++;
+        } while (!ok && count < 200);
+
+        //gc.drawRect(arect.x, arect.y, arect.width, arect.height);
+
+        rects.add((Rectangle) arect.clone());
+
+        if (nv.getLabelSize() != null)
+            arect.y += nv.getLabelSize().height;
+
+        apt.x = arect.x - apt.x;
+        apt.y = arect.y - apt.y;
+
+        if (changeLocations)
+            nv.setLabelPositionRelative(apt);
+
+        return count * count;
+    }
+
+
+    /**
+     * initial layout of node labels opposite the edges entering the node
+     *
+     * @param trans
+     * @param graphView
+     */
+    public void initialLayout(Transform trans, GraphView graphView) {
+        Graph graph = graphView.getGraph();
+
+        for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+            NodeView nv = graphView.getNV(v);
+            if (nv.getLabelLayout() == NodeView.LAYOUT && nv.getLabel() != null && nv.getLabel().length() > 0) {
+                nv.setLabelAngle(0);
+                Point2D vpt = trans.w2d(nv.getLocation());
+                Rectangle vrect = nv.getLabelRect(trans);
+                if (vrect == null)
+                    continue;
+                int count = 0;
+                Point2D apt = new Point2D.Double();
+                for (Edge e = v.getFirstAdjacentEdge(); e != null; e = v.getNextAdjacentEdge(e)) {
+                    Node w = v.getOpposite(e);
+                    Point2D bpt = trans.w2d(graphView.getLocation(w));
+                    List internalPoints = graphView.getInternalPoints(e);
+                    if (internalPoints != null && internalPoints.size() > 0) {
+                        if (e.getSource() == v)
+                            bpt = trans.w2d((Point2D) internalPoints.get(0));
+                        else
+                            bpt = trans.w2d((Point2D) internalPoints.get(internalPoints.size() - 1));
+                    }
+                    apt.setLocation(apt.getX() + bpt.getX(), apt.getY() + bpt.getY());
+                    count++;
+                }
+                if (count > 0)
+                    apt.setLocation(apt.getX() / count, apt.getY() / count);
+                apt.setLocation(apt.getX() - vpt.getX(), apt.getY() - vpt.getY());
+
+                double angle;
+                if (Math.abs(apt.getX()) < 0.0001 && Math.abs(apt.getY()) < 0.0001)
+                    angle = Math.PI;
+                else
+                    angle = Geometry.moduloTwoPI(Geometry.computeAngle(apt));
+
+                apt = Geometry.translateByAngle(new Point2D.Double(0, 0), angle + Math.PI, 12);
+
+                Point pt = new Point();
+                pt.setLocation(apt);
+                int width = vrect.width;
+                int height = vrect.height;
+                int xoffset;
+                int yoffset;
+                if (angle >= 0.25 * Math.PI && angle <= 0.75 * Math.PI) // north
+                {
+                    xoffset = -width / 2;
+                    yoffset = height / 2;
+                } else if (angle >= 0.75 * Math.PI && angle <= 1.25 * Math.PI)   // east
+                {
+                    xoffset = 0;
+                    yoffset = (3 * height) / 2;
+                } else if (angle >= 1.25 * Math.PI && angle <= 1.75 * Math.PI) //south
+                {
+                    xoffset = -width / 2;
+                    yoffset = 2 * height;
+                } else // west
+                {
+                    xoffset = -width;
+                    yoffset = (3 * height) / 2;
+                }
+                pt.x += xoffset;
+                pt.y += yoffset;
+                nv.setLabelPositionRelative(pt);
+                nv.setLabelLayout(NodeView.LAYOUT);
+            }
+        }
+    }
+
+    /**
+     * lays out all node labels so that they don't overlap
+     *
+     * @param trans
+     * @param graphView
+     */
+    public void nonOverlappingLayout(Transform trans, GraphView graphView) {
+        Graph G = graphView.getGraph();
+
+        int bestCost = Integer.MAX_VALUE;
+        int bestRun = 0;
+
+        List<Node> nodes = new LinkedList<>();
+        for (Node v = G.getFirstNode(); v != null; v = v.getNext()) {
+            nodes.add(v);
+        }
+
+        int runs = ProgramProperties.get("label-layout-iterations", 10);
+        for (int i = 0; i < runs; i++) {
+            rects.clear();
+
+            for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                NodeView nv = graphView.getNV(v);
+                if (nv.getLabelColor() != null && nv.getLabel() != null && nv.getLabel().length() > 0) {
+
+                    if (nv.getLabelLayout() != NodeView.LAYOUT) {
+                        {
+                            Rectangle arect = nv.getLabelRect(trans);
+                            rects.add(arect);
+                            // gc.drawRect(arect.x, arect.y, arect.width, arect.height);
+
+                        }
+                    }
+                }
+            }
+
+            int cost = 0;
+            for (Iterator it = Basic.randomize(nodes.iterator(), 17 * i); it.hasNext(); ) {
+                Node v = (Node) it.next();
+                NodeView nv = graphView.getNV(v);
+                if (nv.getLabelColor() != null && nv.getLabel() != null && nv.getLabel().length() > 0) {
+
+                    if (nv.getLabelLayout() == NodeView.LAYOUT) {
+                        cost += layout(graphView, v, false);
+                    }
+                }
+            }
+
+            if (cost < bestCost) {
+                //System.err.println("Cost: " + bestCost+" -> "+cost);
+                bestCost = cost;
+                bestRun = i;
+            }
+        }
+
+        rects.clear();
+
+        for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+            NodeView nv = graphView.getNV(v);
+            if (nv.getLabelColor() != null && nv.getLabel() != null && nv.getLabel().length() > 0) {
+
+                if (nv.getLabelLayout() != NodeView.LAYOUT) {
+                    {
+                        Rectangle arect = nv.getLabelRect(trans);
+                        rects.add(arect);
+                        // gc.drawRect(arect.x, arect.y, arect.width, arect.height);
+
+                    }
+                }
+            }
+        }
+
+        for (Iterator it = Basic.randomize(nodes.iterator(), 17 * bestRun); it.hasNext(); ) {
+            Node v = (Node) it.next();
+            NodeView nv = graphView.getNV(v);
+            if (nv.getLabelColor() != null && nv.getLabel() != null && nv.getLabel().length() > 0) {
+
+                if (nv.getLabelLayout() == NodeView.LAYOUT) {
+                    layout(graphView, v, true);
+                }
+            }
+        }
+    }
+}
diff --git a/src/jloda/graphview/LabelOverlapAvoider.java b/src/jloda/graphview/LabelOverlapAvoider.java
new file mode 100644
index 0000000..d3a4374
--- /dev/null
+++ b/src/jloda/graphview/LabelOverlapAvoider.java
@@ -0,0 +1,185 @@
+/**
+ * LabelOverlapAvoider.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.graph.*;
+
+import java.awt.*;
+import java.awt.geom.Area;
+
+/**
+ * this class helps to avoid overlapping labels by suppressing some
+ * Daniel Huson, 1.2007
+ */
+public class LabelOverlapAvoider {
+    private final Transform trans;
+
+    // the following code is used to ensure that labels do not overlap:
+    private final ViewBase[] history;
+    private int historyA = 0;  // first filled pos
+    private int historyB = 0; //first empty pos
+    private final NodeSet visibleNodeLabels;
+    private final EdgeSet visibleEdgeLabels;
+    private boolean enabled = false;
+    Shape firstShape; // keep first shape to avoid wrap-around problem
+
+    /**
+     * constructor
+     *
+     * @param graphView
+     * @param length    number of labels to remember
+     */
+    public LabelOverlapAvoider(GraphView graphView, int length) {
+        this.trans = graphView.trans;
+        visibleNodeLabels = new NodeSet(graphView.getGraph());
+        visibleEdgeLabels = new EdgeSet(graphView.getGraph());
+        history = new NodeView[length];
+        historyA = 0;
+        historyB = 0;
+        firstShape = null;
+    }
+
+    // reset the code
+
+    public void resetHasNoOverlapToPreviouslyDrawnLabels() {
+        historyA = 0;
+        historyB = 0;
+        visibleNodeLabels.clear();
+        firstShape = null;
+    }
+
+    /**
+     * determine whether to draw a label
+     *
+     * @param v  node or edge
+     * @param vb nodeview or edgeview
+     * @return true, if this label will not overlap the last couple drawn
+     */
+    public boolean hasNoOverlapToPreviouslyDrawnLabels(NodeEdge v, ViewBase vb) {
+        if (!isEnabled())
+            return true;
+        if (!vb.isLabelVisible())
+            return true;
+        if (vb.getLabel() == null || vb.getLabel().length() == 0)
+            return false;
+
+        Shape shape = vb.getLabelShape(trans);
+
+
+        Area area = new Area(shape);
+
+        if (firstShape != null && intersects(area, firstShape))
+            return false;
+
+        if (historyA <= historyB) {
+            for (int i = historyA; i < historyB; i++) {
+                if (intersects(area, history[i].getLabelShape(trans)))
+                    return false;
+            }
+            history[historyB] = vb;
+            historyB++;
+            if (historyB == history.length) {
+                historyB = 0;
+                if (historyA == 0)
+                    historyA++;
+            }
+        } else {
+            for (int i = 0; i < historyB; i++) {
+                if (intersects(area, history[i].getLabelShape(trans)))
+                    return false;
+            }
+            for (int i = historyA; i < history.length; i++) {
+                if (intersects(area, history[i].getLabelShape(trans)))
+                    return false;
+            }
+            history[historyB] = vb;
+            historyB++;
+            if (historyB == historyA) {
+                historyA++;
+                if (historyA == history.length)
+                    historyA = 0;
+            }
+        }
+        if (firstShape == null)
+            firstShape = shape;
+
+        if (v instanceof Node)
+            visibleNodeLabels.add((Node) v);
+        else if (v instanceof Edge)
+            visibleEdgeLabels.add((Edge) v);
+        return true;
+    }
+
+    /**
+     * does the shape s intersect the shape b? a is the area of s
+     *
+     * @param a
+     * @param b
+     * @return true, if a and b intersect
+     */
+    private boolean intersects(Area a, Shape b) {
+        if (b instanceof Rectangle) {
+            if (a.intersects((Rectangle) b))
+                return true;
+        } else {
+            Area inter = (Area) a.clone();
+            inter.intersect(new Area(b));
+            if (!inter.isEmpty())
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * gets the set of all nodes whose labels were permitted
+     *
+     * @return nodes with visible labels
+     */
+    public boolean isVisible(Node v) {
+        return !isEnabled() || visibleNodeLabels.contains(v);
+    }
+
+    /**
+     * gets the set of all edges whose labels were permitted
+     *
+     * @return edges with visible labels
+     */
+    public boolean isVisible(Edge e) {
+        return !isEnabled() || visibleEdgeLabels.contains(e);
+    }
+
+    /**
+     * are we suppressing overlapping labels?
+     *
+     * @return true, if labels are being suppressed
+     */
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    /**
+     * are we suppressing overlapping labels?
+     *
+     * @param enabled
+     */
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+}
diff --git a/src/jloda/graphview/Magnifier.java b/src/jloda/graphview/Magnifier.java
new file mode 100644
index 0000000..98af1f9
--- /dev/null
+++ b/src/jloda/graphview/Magnifier.java
@@ -0,0 +1,491 @@
+/**
+ * Magnifier.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.geom.Point2D;
+
+/**
+ * manages the magnifier.
+ * Daniel Huson, 1.2007
+ */
+public class Magnifier {
+    final GraphView graphView;
+    final Transform trans;
+    final JScrollBar scrollBarX;
+    final JScrollBar scrollBarY;
+    private double centerXpercent = 50; // position of center in percent
+    private double centerYpercent = 50; // position of center in percent
+    private double radiusPercent = 90;  // radius in percent
+    private double magnificationFactor = 1;
+    private double displacement = 0.75; // new distance (*radius) from axis of a point that originally had distance 0.5*radius
+    private boolean active = false;
+    private boolean inRectilinearMode = false; // in this mode, don't need to add internal nodes to edges
+
+    private boolean hyperbolicMode = false;
+
+    // what did mouse down hit?
+    public final static int HIT_NOTHING = 0;
+    public final static int HIT_RESIZE = 1;
+    public final static int HIT_MOVE = 2;
+    public final static int HIT_INCREASE_MAGNIFICATION = 4;
+    public final static int HIT_DECREASE_MAGNIFICATION = 8;
+
+    /**
+     * constructor
+     *
+     * @param graphView
+     */
+    public Magnifier(GraphView graphView, Transform trans) {
+        this.graphView = graphView;
+        this.trans = trans;
+        scrollBarX = graphView.getScrollPane().getHorizontalScrollBar();
+        scrollBarY = graphView.getScrollPane().getVerticalScrollBar();
+    }
+
+    /**
+     * does mouse click hit magnifier?
+     *
+     * @param x
+     * @param y
+     * @return what was hit
+     */
+    public int hit(int x, int y) {
+        if (isActive()) {
+            if (!isInRectilinearMode()) {
+                double centerX = scrollBarX.getValue() + getCenterX() * scrollBarX.getVisibleAmount() / 100;
+                double centerY = scrollBarY.getValue() + getCenterY() * scrollBarY.getVisibleAmount() / 100;
+                double min = Math.min(scrollBarX.getVisibleAmount(), scrollBarY.getVisibleAmount());
+                double r = getRadius() * min / 200;
+                Rectangle rect = new Rectangle((int) (centerX - r), (int) (centerY - r), (int) (2 * r), (int) (2 * r));
+                ArrowButton ab1 = new ArrowButton(rect.x + rect.width / 2, rect.y, true);
+                if (ab1.hit(x, y))
+                    return HIT_RESIZE;
+                ArrowButton ab2 = new ArrowButton(rect.x + rect.width / 2, rect.y, false);
+                if (ab2.hit(x, y))
+                    return HIT_RESIZE;
+                ZoomButton zb1 = new ZoomButton(rect.x + rect.width, rect.y + rect.height / 2, true);
+                if (zb1.hit(x, y))
+                    return HIT_INCREASE_MAGNIFICATION;
+                ZoomButton zb2 = new ZoomButton(rect.x + rect.width, rect.y + rect.height / 2, false);
+                if (zb2.hit(x, y))
+                    return HIT_DECREASE_MAGNIFICATION;
+
+                double distance = new Point2D.Double(centerX, centerY).distance(x, y);
+                if (Math.abs(distance - r) < 15)
+                    return HIT_MOVE;
+            } else {
+                double centerY = scrollBarY.getValue() + getCenterY() * scrollBarY.getVisibleAmount() / 100;
+                double r = getRadius() * scrollBarY.getVisibleAmount() / 200;
+                Rectangle rect = new Rectangle(scrollBarX.getValue() + 1, (int) (centerY - r + 1), scrollBarX.getVisibleAmount() - 2, (int) (2 * r - 2));
+
+                ArrowButton ab1 = new ArrowButton(rect.x + rect.width - 10, rect.y, true);
+                if (ab1.hit(x, y))
+                    return HIT_RESIZE;
+                ArrowButton ab2 = new ArrowButton(rect.x + rect.width - 10, rect.y, false);
+                if (ab2.hit(x, y))
+                    return HIT_RESIZE;
+                // draw zoom controls
+                ZoomButton zb1 = new ZoomButton(rect.x + 20, rect.y + 5, true);
+                if (zb1.hit(x, y))
+                    return HIT_INCREASE_MAGNIFICATION;
+                ZoomButton zb2 = new ZoomButton(rect.x + 20, rect.y + 5, false);
+                if (zb2.hit(x, y))
+                    return HIT_DECREASE_MAGNIFICATION;
+                if (Math.abs(Math.abs(centerY - y) - r) < 15)
+                    return HIT_MOVE;
+            }
+        }
+        return HIT_NOTHING;
+    }
+
+    /**
+     * move the magnification center by the given difference of coordinates
+     *
+     * @param xOld
+     * @param yOld
+     * @param xNew
+     * @param yNew
+     */
+    public void move(int xOld, int yOld, int xNew, int yNew) {
+        double dXPercent = 0;
+        if (!isInRectilinearMode())
+            dXPercent = 100.0 * (xNew - xOld) / (double) scrollBarX.getVisibleAmount();
+        double dYPercent = 100.0 * (yNew - yOld) / (double) scrollBarY.getVisibleAmount();
+
+        double newX = getCenterX() + dXPercent;
+        double newY = getCenterY() + dYPercent;
+        if (newX > 0 && newX < 100 && newY > 0 && newY < 100)
+            setCenter(getCenterX() + dXPercent, getCenterY() + dYPercent);
+    }
+
+    /**
+     * resize the magnification center by the given difference of coordinates
+     *
+     * @param yOld
+     * @param yNew
+     */
+    public void resize(int yOld, int yNew) {
+        double dYPercent = 200.0 * (yOld - yNew) / (double) scrollBarY.getVisibleAmount();
+        setRadius(Math.max(0, Math.min(100, getRadius() + dYPercent)));
+    }
+
+
+    /**
+     * get the magnification radius between 0 and 100 %
+     *
+     * @return magnification factor
+     */
+    public double getRadius() {
+        return radiusPercent;
+    }
+
+    /**
+     * set the magnification radius between 0 and 100 %
+     *
+     * @param radiusPercent
+     */
+    public void setRadius(double radiusPercent) {
+        this.radiusPercent = Math.max(0, Math.min(100, radiusPercent));
+
+        // reset factor:
+        int min = Math.min(scrollBarX.getVisibleAmount(), scrollBarY.getVisibleAmount());
+        double r = this.radiusPercent * min / 200;
+        magnificationFactor = 0.5 * r * (1 - displacement) / (displacement - 0.5);
+    }
+
+    /**
+     * set the magnification center in percent of window width and height
+     *
+     * @param xPercent
+     * @param yPercent
+     */
+    public void setCenter(double xPercent, double yPercent) {
+        centerXpercent = xPercent;
+        centerYpercent = yPercent;
+    }
+
+    /**
+     * set the magnification center in percent of window width
+     *
+     * @return x percentage
+     */
+    public double getCenterX() {
+        return centerXpercent;
+    }
+
+    /**
+     * set the magnification center in percent of window height
+     *
+     * @return y percentage
+     */
+    public double getCenterY() {
+        return centerYpercent;
+    }
+
+    /**
+     * get the magnification displacement
+     *
+     * @return displacement
+     */
+    public double getDisplacement() {
+        return displacement;
+    }
+
+    /**
+     * set the displacement. This must be a number between 0.5 and 1
+     *
+     * @param displacement >0.5 and <1
+     */
+    public void setDisplacement(double displacement) {
+        this.displacement = displacement;
+        int min = Math.min(scrollBarX.getVisibleAmount(), scrollBarY.getVisibleAmount());
+        double r = this.radiusPercent * min / 200;
+        magnificationFactor = 0.5 * r * (1 - displacement) / (displacement - 0.5);
+    }
+
+    /**
+     * increase the displacment
+     *
+     * @return true, if changed
+     */
+    public boolean increaseDisplacement() {
+        setDisplacement(getDisplacement() + 0.1 * (1.0 - getDisplacement()));
+        return true;
+
+    }
+
+    /**
+     * decrease the displacment
+     *
+     * @return true, if changed
+     */
+    public boolean decreaseDisplacement() {
+        double d = (10.0 * getDisplacement() - 1.0) / 9.0;
+        if (d <= 0.5)
+            d = (0.5 * (0.5 + getDisplacement()));
+        if (d > 0.5) {
+            setDisplacement(d);
+            return true;
+        }
+        return false;
+    }
+
+
+    /**
+     * set the magnifier on or off
+     *
+     * @param active
+     */
+    public void setActive(boolean active) {
+        this.active = active;
+        if (active)
+            setDisplacement(getDisplacement()); // make sure everything is uptodate
+    }
+
+    /**
+     * get the magnifier on or off
+     *
+     * @return true, if magnifier is being used
+     */
+    public boolean isActive() {
+        return active;
+    }
+
+    /**
+     * turn magnification on
+     *
+     * @param centerXpercent x coordinate of center of magnification in percent of width
+     * @param centerYpercent y coordinate of center of magnification in percent of height
+     * @param radiusPercent  radius or height of magnifier in percent of maximal possible
+     */
+    public void setMagnifier(int centerXpercent, int centerYpercent, int radiusPercent, double displacement) {
+        this.centerXpercent = centerXpercent;
+        this.centerYpercent = centerYpercent;
+        this.radiusPercent = radiusPercent;
+        this.displacement = displacement;
+        int min = Math.min(scrollBarX.getVisibleAmount(), scrollBarY.getVisibleAmount());
+        double r = this.radiusPercent * min / 200;
+        magnificationFactor = 0.5 * r * (1 - displacement) / (displacement - 0.5);
+
+    }
+
+    /**
+     * apply the magnifier to a point. This is used by Transform
+     *
+     * @param x     in device coordinates before application   of magnification
+     * @param y     in device coordinates before application   of magnification
+     * @param point result in device   after magnifiation
+     */
+    public void applyMagnifier(double x, double y, Point2D point) {
+        if (inRectilinearMode)  // magnify along the center horizontal axis
+        {
+            double z = scrollBarY.getValue() + centerYpercent * scrollBarY.getVisibleAmount() / 100;
+            double h = radiusPercent * scrollBarY.getVisibleAmount() / 200;
+
+            if (!hyperbolicMode && y > z - h && y < z + h) {
+                double yNew;
+
+                if (y <= z)
+                    yNew = z - (h * (z - y) / (z - y + magnificationFactor)) * (1 + magnificationFactor / h);
+                else
+                    yNew = z + (h * (y - z) / (y - z + magnificationFactor)) * (1 + magnificationFactor / h);
+                point.setLocation(x, yNew);
+            } else if (hyperbolicMode) {
+                double yNew;
+
+                if (y <= z)
+                    yNew = z - h * (z - y) / (z - y + magnificationFactor);
+                else
+                    yNew = z + h * (y - z) / (y - z + magnificationFactor);
+                point.setLocation(x, yNew);
+            } else
+                point.setLocation(x, y);
+
+        } else // centralized magnification
+        {
+            int min = Math.min(scrollBarX.getVisibleAmount(), scrollBarY.getVisibleAmount());
+
+            double centerX = scrollBarX.getValue() + centerXpercent * scrollBarX.getVisibleAmount() / 100;
+            double centerY = scrollBarY.getValue() + centerYpercent * scrollBarY.getVisibleAmount() / 100;
+            double radius = radiusPercent * min / 200;
+
+            double d12 = (x - centerX) * (x - centerX) + (y - centerY) * (y - centerY);
+
+            if (!hyperbolicMode && d12 > 0 && d12 < radius * radius) {
+                double d1 = Math.sqrt(d12);
+                double d2 = radius * (d1 / (d1 + magnificationFactor));
+                double xNew = centerX + ((x - centerX) * d2 / d1) * (1 + magnificationFactor / radius);
+                double yNew = centerY + ((y - centerY) * d2 / d1) * (1 + magnificationFactor / radius);
+                point.setLocation(xNew, yNew);
+            } else if (hyperbolicMode) {
+                double d1 = Math.sqrt(d12);
+                double d2 = radius * (d1 / (d1 + magnificationFactor));
+                double xNew = centerX + (x - centerX) * d2 / d1;
+                double yNew = centerY + (y - centerY) * d2 / d1;
+                point.setLocation(xNew, yNew);
+            } else {
+                point.setLocation(x, y);
+            }
+        }
+    }
+
+    /**
+     * are we in rectilinear mode?
+     * In this mode, all edges are drawn either horizontal or vertical and zoom is vertical only. No need
+     * to add internal nodes to edges. Used by TreeDrawerParallel
+     *
+     * @return true, if so
+     */
+    public boolean isInRectilinearMode() {
+        return inRectilinearMode;
+    }
+
+    /**
+     * set rectilinear mode
+     *
+     * @param inRectilinearMode
+     */
+    public void setInRectilinearMode(boolean inRectilinearMode) {
+        this.inRectilinearMode = inRectilinearMode;
+    }
+
+    /**
+     * draw the magnifier
+     *
+     * @param gc
+     */
+    public void draw(Graphics2D gc) {
+        if (isActive()) {
+            gc.setColor(Color.GREEN);
+
+            if (!inRectilinearMode) {
+                double centerX = scrollBarX.getValue() + getCenterX() * scrollBarX.getVisibleAmount() / 100;
+                double centerY = scrollBarY.getValue() + getCenterY() * scrollBarY.getVisibleAmount() / 100;
+                int min = Math.min(scrollBarX.getVisibleAmount(), scrollBarY.getVisibleAmount());
+                double r = getRadius() * min / 200.0;
+                Rectangle rect = new Rectangle((int) (centerX - r), (int) (centerY - r), (int) (2 * r), (int) (2 * r));
+                // draw circle:
+                gc.drawArc(rect.x, rect.y, rect.width, rect.height, 0, 360);
+                // draw up and down arrows:
+                ArrowButton ab1 = new ArrowButton(rect.x + rect.width / 2, rect.y, true);
+                ab1.draw(gc);
+                ArrowButton ab2 = new ArrowButton(rect.x + rect.width / 2, rect.y, false);
+                ab2.draw(gc);
+
+                // draw zoom controls
+                ZoomButton zb1 = new ZoomButton(rect.x + rect.width, rect.y + rect.height / 2, true);
+                zb1.draw(gc);
+                ZoomButton zb2 = new ZoomButton(rect.x + rect.width, rect.y + rect.height / 2, false);
+                zb2.draw(gc);
+
+                //gc.drawString("" + radiusPercent, rect.x + 10, rect.y + rect.height - 10);
+                // gc.drawString("" + getDisplacement(), rect.x + 10, rect.y + rect.height - 10);
+
+            } else {
+                double centerY = scrollBarY.getValue() + getCenterY() * scrollBarY.getVisibleAmount() / 100;
+                double r = getRadius() * scrollBarY.getVisibleAmount() / 200;
+                Rectangle rect = new Rectangle(scrollBarX.getValue() + 1, (int) (centerY - r + 1), scrollBarX.getVisibleAmount() - 2, (int) (2 * r - 2));
+                // draw rect:
+                gc.draw(rect);
+                ArrowButton ab1 = new ArrowButton(rect.x + rect.width - 10, rect.y, true);
+                ab1.draw(gc);
+                ArrowButton ab2 = new ArrowButton(rect.x + rect.width - 10, rect.y, false);
+                ab2.draw(gc);
+
+                // draw zoom controls
+                ZoomButton zb1 = new ZoomButton(rect.x + 20, rect.y + 5, true);
+                zb1.draw(gc);
+                ZoomButton zb2 = new ZoomButton(rect.x + 20, rect.y + 5, false);
+                zb2.draw(gc);
+
+                //gc.drawString("" + radiusPercent, rect.x + 10, rect.y + rect.height - 10);
+                // gc.drawString("" + getDisplacement(), rect.x + 10, rect.y + rect.height - 10);
+            }
+        }
+    }
+
+
+    class ArrowButton {
+        final Polygon polygon;
+
+        ArrowButton(int x, int y, boolean up) {
+            if (up)
+                polygon = new Polygon(new int[]{x - 5, x, x + 5}, new int[]{y, y - 5, y}, 3);
+            else
+                polygon = new Polygon(new int[]{x - 5, x, x + 5}, new int[]{y, y + 5, y}, 3);
+
+        }
+
+        void draw(Graphics2D gc) {
+            gc.fill(polygon);
+        }
+
+        boolean hit(int x, int y) {
+            return polygon.contains(x, y);
+        }
+    }
+
+    class ZoomButton {
+        final boolean up;
+        final Rectangle rect;
+
+        ZoomButton(int x, int y, boolean up) {
+            this.up = up;
+            if (up)
+                rect = new Rectangle(x - 10, y - 5, 10, 10);
+            else
+                rect = new Rectangle(x, y - 5, 10, 10);
+        }
+
+        void draw(Graphics2D gc) {
+            gc.draw(rect);
+            if (up) {
+                gc.drawLine(rect.x + 2, rect.y + rect.height / 2, rect.x + rect.width - 2, rect.y + rect.height / 2);
+                gc.drawLine(rect.x + rect.width / 2, rect.y + 2, rect.x + rect.width / 2, rect.y + rect.height - 2);
+            } else {
+                gc.drawLine(rect.x + 2, rect.y + rect.height / 2, rect.x + rect.width - 2, rect.y + rect.height / 2);
+            }
+        }
+
+        boolean hit(int x, int y) {
+            return rect.contains(x, y);
+        }
+    }
+
+    /**
+     * get hyperbolic mode
+     *
+     * @return mode
+     */
+    public boolean isHyperbolicMode() {
+        return hyperbolicMode;
+    }
+
+    /**
+     * set hyperbolic mode
+     *
+     * @param hyperbolicMode
+     */
+    public void setHyperbolicMode(boolean hyperbolicMode) {
+        this.hyperbolicMode = hyperbolicMode;
+    }
+}
diff --git a/src/jloda/graphview/MagnifierUtil.java b/src/jloda/graphview/MagnifierUtil.java
new file mode 100644
index 0000000..a0d5f39
--- /dev/null
+++ b/src/jloda/graphview/MagnifierUtil.java
@@ -0,0 +1,124 @@
+/**
+ * MagnifierUtil.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.graph.Edge;
+
+import java.awt.geom.Point2D;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * utilities used when drawing edges under magnification
+ * Daniel Huson, 1.2007
+ */
+public class MagnifierUtil {
+    int spacing = 7;
+    private final GraphView graphView;
+    private final Transform trans;
+
+    private List oldInternalPoints = null;
+
+    /**
+     * constructor
+     *
+     * @param graphView
+     */
+    public MagnifierUtil(GraphView graphView) {
+        this.graphView = graphView;
+        this.trans = graphView.trans;
+    }
+
+    /**
+     * add internal points to approximate curved edges
+     *
+     * @param e
+     * @return original internal points
+     */
+    public List addInternalPoints(Edge e) {
+
+        // TODO: add additional points between existing ones!
+        oldInternalPoints = graphView.getInternalPoints(e);
+        if (trans.getMagnifier().isActive() && !trans.getMagnifier().isInRectilinearMode()) {
+            Point2D prevPt = graphView.getNV(e.getSource()).getLocation();
+
+            java.util.List internalPoints = new LinkedList();
+
+            if (oldInternalPoints != null) {
+                for (Object oldInternalPoint : oldInternalPoints) {
+                    final Point2D curPt = (Point2D) oldInternalPoint;
+                    addInternalPoints(prevPt, curPt, internalPoints);
+                    internalPoints.add(curPt);
+                    prevPt = curPt;
+                }
+            }
+            addInternalPoints(prevPt, graphView.getNV(e.getTarget()).getLocation(), internalPoints);
+            graphView.setInternalPoints(e, internalPoints);
+        }
+        return oldInternalPoints;
+    }
+
+    /**
+     * add points between two internal points
+     *
+     * @param pv             support point in world coordinates
+     * @param pw             support point in world coordinates
+     * @param internalPoints
+     */
+    private void addInternalPoints(Point2D pv, Point2D pw, java.util.List internalPoints) {
+        int count = (int) trans.w2d(pv).distance(trans.w2d(pw)) / spacing;
+        if (count > 0) {
+            double dX = (pw.getX() - pv.getX()) / count;
+            double dY = (pw.getY() - pv.getY()) / count;
+            for (int i = 1; i < count; i++) {
+                double x = pv.getX() + dX * i;
+                double y = pv.getY() + dY * i;
+                internalPoints.add(new Point2D.Double(x, y));
+            }
+        }
+    }
+
+    /**
+     * remove the added points
+     *
+     * @param e
+     */
+    public void removeAddedInternalPoints(Edge e) {
+        graphView.setInternalPoints(e, oldInternalPoints);
+    }
+
+    /**
+     * get spacing between points
+     *
+     * @return spacing in pixels
+     */
+    public int getSpacing() {
+        return spacing;
+    }
+
+    /**
+     * set spacing between points
+     *
+     * @param spacing
+     */
+    public void setSpacing(int spacing) {
+        this.spacing = spacing;
+    }
+}
diff --git a/src/jloda/graphview/NodeActionAdapter.java b/src/jloda/graphview/NodeActionAdapter.java
new file mode 100644
index 0000000..4067bb7
--- /dev/null
+++ b/src/jloda/graphview/NodeActionAdapter.java
@@ -0,0 +1,121 @@
+/**
+ * NodeActionAdapter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.graph.Node;
+import jloda.graph.NodeSet;
+
+//import jloda.util.*;
+
+/**
+ * Adapter for actions performed on nodes during GraphView interaction.
+ */
+public class NodeActionAdapter implements NodeActionListener {
+    /**
+     * Called when creating a new node.
+     *
+     * @param v newly created node
+     */
+    public void doNew(Node v) {
+    }
+
+    /**
+     * Called when creating a new node in conjunction with a new edge.
+     *
+     * @param v the source node
+     * @param w the newly created node
+     */
+    public void doNew(Node v, Node w) {
+    }
+
+    /**
+     * Called when deleting a node.
+     *
+     * @param v node that is about to be deleted
+     */
+    public void doDelete(Node v) {
+    }
+
+    /**
+     * Called when nodes are clicked on.
+     *
+     * @param nodes  set of nodes that have been clicked
+     * @param clicks number of clicks
+     */
+    public void doClick(NodeSet nodes, int clicks) {
+    }
+
+    /**
+     * Called when nodes are pressed.
+     *
+     * @param nodes set of nodes that have been pressed
+     */
+    public void doPress(NodeSet nodes) {
+    }
+
+    /**
+     * Called when nodes are released.
+     *
+     * @param nodes set of nodes that have been released
+     */
+    public void doRelease(NodeSet nodes) {
+    }
+
+    /**
+     * Called when nodes are selected.
+     *
+     * @param nodes set of nodes that have become selected
+     */
+    public void doSelect(NodeSet nodes) {
+    }
+
+    /**
+     * Called when nodes are de-selected.
+     *
+     * @param nodes set of nodes that have become de-selected
+     */
+    public void doDeselect(NodeSet nodes) {
+    }
+
+    /**
+     * called when node label is clicked on
+     *
+     * @param nodes
+     * @param clicks
+     */
+    public void doClickLabel(NodeSet nodes, int clicks) {
+    }
+
+    /**
+     * called when node label was moved
+     *
+     * @param nodes
+     */
+    public void doMoveLabel(NodeSet nodes) {
+    }
+
+    /**
+     * called when nodes moved
+     */
+    public void doNodesMoved() {
+    }
+}
+
+// EOF
diff --git a/src/jloda/graphview/NodeActionListener.java b/src/jloda/graphview/NodeActionListener.java
new file mode 100644
index 0000000..53b18be
--- /dev/null
+++ b/src/jloda/graphview/NodeActionListener.java
@@ -0,0 +1,117 @@
+/**
+ * NodeActionListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+/**
+ * @version $Id: NodeActionListener.java,v 1.8 2007-09-11 12:33:14 kloepper Exp $
+ *
+ * Actions performed during interaction.
+ *
+ * @author Daniel Huson
+ * 6.2001
+ */
+
+import jloda.graph.Node;
+import jloda.graph.NodeSet;
+
+/**
+ * Listener for actions performed on nodes during GraphView interaction.
+ */
+public interface NodeActionListener {
+    /**
+     * Called when creating a new node.
+     *
+     * @param v newly created node
+     */
+    void doNew(Node v);
+
+    /**
+     * Called when creating a new node in conjunction with a new edge.
+     *
+     * @param v the source node
+     * @param w the newly created node
+     */
+    void doNew(Node v, Node w);
+
+    /**
+     * Called when deleting a node.
+     *
+     * @param v node that is about to be deleted
+     */
+    void doDelete(Node v);
+
+    /**
+     * Called when nodes are clicked on.
+     *
+     * @param nodes  set of nodes that have been clicked
+     * @param clicks number of clicks
+     */
+    void doClick(NodeSet nodes, int clicks);
+
+    /**
+     * Called when nodes are pressed.
+     *
+     * @param nodes set of nodes that have been pressed
+     */
+    void doPress(NodeSet nodes);
+
+    /**
+     * Called when nodes are released.
+     *
+     * @param nodes set of nodes that have been released
+     */
+    void doRelease(NodeSet nodes);
+
+    /**
+     * Called when nodes are selected.
+     *
+     * @param nodes set of nodes that have become selected
+     */
+    void doSelect(NodeSet nodes);
+
+    /**
+     * Called when nodes are de-selected.
+     *
+     * @param nodes set of nodes that have become de-selected
+     */
+    void doDeselect(NodeSet nodes);
+
+    /**
+     * called when node label is clicked on
+     *
+     * @param nodes
+     * @param clicks
+     */
+    void doClickLabel(NodeSet nodes, int clicks);
+
+    /**
+     * called when node label was moved
+     *
+     * @param nodes
+     */
+    void doMoveLabel(NodeSet nodes);
+
+    /**
+     * called when nodes moved
+     */
+    void doNodesMoved();
+}
+
+// EOF
diff --git a/src/jloda/graphview/NodeImage.java b/src/jloda/graphview/NodeImage.java
new file mode 100644
index 0000000..8d613b4
--- /dev/null
+++ b/src/jloda/graphview/NodeImage.java
@@ -0,0 +1,263 @@
+/**
+ * NodeImage.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.util.Geometry;
+import jloda.util.ProgramProperties;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.ImageObserver;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * image associated with a node
+ * Daniel Huson, 7.2007
+ */
+public class NodeImage {
+    final ImageObserver observer;
+    Image image;
+    Image scaledImage;
+    int width = -1;
+    int height = 50;
+    boolean visible = true;
+    byte layout = NodeView.RADIAL;
+    final Rectangle boundingBox = new Rectangle();
+
+    /**
+     * constructor
+     *
+     * @param observer
+     */
+    public NodeImage(ImageObserver observer) {
+        this.observer = observer;
+    }
+
+    /**
+     * construct from file
+     *
+     * @param file
+     * @param observer
+     * @throws IOException
+     */
+    public NodeImage(File file, ImageObserver observer) throws IOException {
+        this(observer);
+        read(file);
+    }
+
+    /**
+     * read image from a file
+     *
+     * @param file
+     * @throws IOException
+     */
+    public void read(File file) throws IOException {
+        setImage(ImageIO.read(file));
+    }
+
+    /**
+     * draw the image
+     *
+     * @param nv
+     * @param trans
+     * @param gc
+     * @param hilite
+     */
+    public void draw(NodeView nv, Transform trans, Graphics2D gc, boolean hilite) {
+        // draw the image:
+        Image scaledImage = getScaledImage();
+        if (observer != null && scaledImage != null) {
+            Shape shape = nv.getLabelShape(trans);
+            if (shape != null) {
+                Rectangle rect = shape.getBounds();
+                if (!nv.isLabelVisible()) {
+                    Point location = nv.getLabelPosition(trans);
+                    rect = new Rectangle(location.x, location.y, 0, 0);
+                }
+                int x;
+                int y;
+                byte useLayout = layout;
+                if (layout == ViewBase.RADIAL) {
+                    double useAngle = Geometry.moduloTwoPI(nv.getLabelAngle());
+
+                    double eightsOfPi = 8 * useAngle / Math.PI;
+                    if (eightsOfPi < 1)
+                        useLayout = ViewBase.EAST;
+                    else if (eightsOfPi < 3)
+                        useLayout = ViewBase.SOUTHEAST;
+                    else if (eightsOfPi < 5)
+                        useLayout = ViewBase.SOUTH;
+                    else if (eightsOfPi < 7)
+                        useLayout = ViewBase.SOUTHWEST;
+                    else if (eightsOfPi < 9)
+                        useLayout = ViewBase.WEST;
+                    else if (eightsOfPi < 11)
+                        useLayout = ViewBase.NORTHWEST;
+                    else if (eightsOfPi < 13)
+                        useLayout = ViewBase.NORTH;
+                    else if (eightsOfPi < 15)
+                        useLayout = ViewBase.NORTHEAST;
+                    else
+                        useLayout = ViewBase.EAST;
+                }
+                switch (useLayout) {
+                    case ViewBase.NORTHWEST:
+                        x = (int) (rect.getX() - scaledImage.getWidth(observer) - 5);
+                        y = (int) (rect.getY() - scaledImage.getHeight(observer) - 3);
+                        break;
+                    case ViewBase.NORTHEAST:
+                        x = (int) (rect.getX() + rect.getWidth() + 5);
+                        y = (int) (rect.getY() - scaledImage.getHeight(observer) - 3);
+                        break;
+                    case ViewBase.NORTH:
+                        x = (int) (rect.getX() + 0.5 * (rect.getWidth() - scaledImage.getWidth(observer)));
+                        y = (int) (rect.getY() - scaledImage.getHeight(observer) - 3);
+                        break;
+                    case ViewBase.SOUTHWEST:
+                        x = (int) (rect.getX() - scaledImage.getWidth(observer) - 5);
+                        y = (int) (rect.getY() + rect.getHeight() + 3);
+                        break;
+                    case ViewBase.SOUTHEAST:
+                        x = (int) (rect.getX() + rect.getWidth() + 5);
+                        y = (int) (rect.getY() + rect.getHeight() + 3);
+                        break;
+
+                    case ViewBase.SOUTH:
+                        x = (int) (rect.getX() + 0.5 * (rect.getWidth() - scaledImage.getWidth(observer)));
+                        y = (int) (rect.getY() + rect.getHeight() + 3);
+                        break;
+                    case ViewBase.WEST:
+                        x = (int) (rect.getX() - scaledImage.getWidth(observer) - 5);
+                        y = (int) (rect.getY() + 0.5 * (rect.getHeight() - scaledImage.getHeight(observer)));
+                        break;
+                    default:
+                    case ViewBase.EAST:
+                        x = (int) (rect.getX() + rect.getWidth() + 5);
+                        y = (int) (rect.getY() + 0.5 * (rect.getHeight() - scaledImage.getHeight(observer)));
+                        break;
+
+                }
+                gc.drawImage(scaledImage, x, y, observer);
+                boundingBox.setRect(x, y, scaledImage.getWidth(observer), scaledImage.getHeight(observer));
+                if (hilite) {
+                    gc.setStroke(NodeView.HEAVY_STROKE);
+                    gc.setColor(ProgramProperties.SELECTION_COLOR);
+                    gc.draw(boundingBox);
+                }
+            }
+        }
+    }
+
+    /**
+     * does point hit this?
+     *
+     * @param x
+     * @param y
+     * @return true, if hit
+     *         todo: this is broken
+     */
+    public boolean contains(int x, int y) {
+        //System.err.println("x y: "+x+" "+y);
+        //System.err.println("rect: "+ boundingBox);
+        return boundingBox.contains(x, y);
+    }
+
+    /**
+     * gets the image
+     *
+     * @return image
+     */
+    public Image getImage() {
+        return image;
+    }
+
+    /**
+     * set the image and the scaled image
+     *
+     * @param image
+     */
+    public void setImage(Image image) {
+        this.image = image;
+        if (image != null)
+            scaledImage = image.getScaledInstance(width, height, Image.SCALE_SMOOTH);
+        else
+            scaledImage = null;
+    }
+
+    /**
+     * get the scaled image
+     *
+     * @return scaled image
+     */
+    public Image getScaledImage() {
+        if (scaledImage == null && image != null)
+            setImage(image);
+        return scaledImage;
+    }
+
+    /**
+     * get the width or -1
+     *
+     * @return width or -1
+     */
+    public int getWidth() {
+        return width;
+    }
+
+    /**
+     * set the width. use -1 for no constraint
+     *
+     * @param width
+     */
+    public void setWidth(int width) {
+        this.width = width;
+        setImage(image);
+    }
+
+    public int getHeight() {
+        return height;
+    }
+
+    public void setHeight(int height) {
+        this.height = height;
+        setImage(image);
+    }
+
+    public boolean isVisible() {
+        return visible;
+    }
+
+    public void setVisible(boolean visible) {
+        this.visible = visible;
+    }
+
+    public byte getLayout() {
+        return layout;
+    }
+
+    public void setLayout(byte layout) {
+        this.layout = layout;
+    }
+
+    public Rectangle getRectangle() {
+        return boundingBox;
+    }
+}
diff --git a/src/jloda/graphview/NodeView.java b/src/jloda/graphview/NodeView.java
new file mode 100644
index 0000000..b17b20f
--- /dev/null
+++ b/src/jloda/graphview/NodeView.java
@@ -0,0 +1,1001 @@
+/**
+ * NodeView.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * Node visualization
+ *
+ * @version $Id: NodeView.java,v 1.82 2010-05-27 14:17:33 huson Exp $
+ *
+ * @author Daniel Huson
+ */
+package jloda.graphview;
+
+import gnu.jpdf.PDFGraphics;
+import jloda.util.Basic;
+import jloda.util.Geometry;
+import jloda.util.ProgramProperties;
+import jloda.util.parse.NexusStreamParser;
+
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.Writer;
+
+final public class NodeView extends ViewBase implements Cloneable {
+    private int height = 2;
+    private int width = 2;
+
+    private final int GAPSIZE = 2; // gap between edge of node and start of edge
+
+    private Color borderColor = null;
+    private byte shape = OVAL_NODE;
+    //private byte imageLayout = NORTH;
+    protected Point2D location = null;
+    boolean fixedSize = true;
+
+    public static final byte RECT_NODE = 1;
+    public static final byte OVAL_NODE = 2;
+    public static final byte TRIANGLE_NODE = 3;
+    public static final byte DIAMOND_NODE = 4;
+    public static final byte NONE_NODE = 0;
+
+    private NodeImage image = null;
+    protected Color bgColor = Color.WHITE;
+
+    public static boolean END_EDGES_AT_BORDER_OF_NODES = true;
+
+
+    public static Writer descriptionWriter = null;
+
+    /**
+     * Construct a node view.
+     */
+    public NodeView() {
+        labelLayout = LAYOUT;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param src NodeView
+     */
+    public NodeView(NodeView src) {
+        this();
+        copy(src);
+    }
+
+    /**
+     * copies the values of the source node view
+     *
+     * @param src
+     */
+    public void copy(NodeView src) {
+        super.copy(src);
+        setLocation(src.getLocation());
+        setBackgroundColor(src.getBackgroundColor());
+        height = src.height;
+        width = src.width;
+        shape = src.shape;
+        fixedSize = src.getFixedSize();
+    }
+
+    /**
+     * Gets the location.
+     *
+     * @return location Point2D
+     */
+    public Point2D getLocation() {
+        return location;
+    }
+
+    /**
+     * Computes the connection point for an edge in device coordinates.
+     *
+     * @param other NodeView
+     * @param trans Transform
+     * @return p Point
+     */
+    public Point computeConnectPoint(Point2D other, Transform trans) {
+        if (location == null || other == null)
+            return null;
+
+        Point apt = trans.w2d(getLocation());
+        if (shape == NONE_NODE)
+            return apt;
+
+        int scaledWidth;
+        int scaledHeight;
+        if (fixedSize) {
+            scaledWidth = width;
+            scaledHeight = height;
+        } else {
+            scaledWidth = computeScaledWidth(trans, width);
+            scaledHeight = computeScaledHeight(trans, height);
+        }
+
+        Point bpt = trans.w2d(other);
+
+        int x = bpt.x - apt.x;
+        int y = bpt.y - apt.y;
+
+        int radius1 = scaledWidth >> 1;
+        int radius2 = scaledHeight >> 1;
+
+        Point p = new Point();
+
+        if (shape == RECT_NODE) {
+            if (y >= x && y >= -x) // top
+            {
+                p.x = apt.x;
+                p.y = apt.y + radius2 + 2;
+            } else if (y >= x && y <= -x) // left
+            {
+                p.x = apt.x - radius1 - 2;
+                p.y = apt.y;
+            } else if (y <= x && y <= -x) // bottom
+            {
+                p.x = apt.x;
+                p.y = apt.y - radius2 - 2;
+            } else if (y <= x && y >= -x) // right
+            {
+                p.x = apt.x + radius1 + 2;
+                p.y = apt.y;
+            }
+        } else {
+            int radius = Math.max(radius1, radius2) + GAPSIZE;
+            double dist = apt.distance(bpt);
+            if (dist == 0)
+                p = apt;
+            else {
+                double factor = radius / dist;
+                p = new Point((int) (apt.x + factor * (bpt.x - apt.x)),
+                        (int) (apt.y + factor * (bpt.y - apt.y)));
+            }
+        }
+        return p;
+    }
+
+    /**
+     * Gets the width.
+     *
+     * @return width int
+     */
+    public int getWidth() {
+        return width;
+    }
+
+    /**
+     * Gets the height.
+     *
+     * @return height int
+     */
+    public int getHeight() {
+        return height;
+    }
+
+    /**
+     * Gets the bounding box in device coordinates
+     *
+     * @param trans Transform
+     * @return Rectangle
+     */
+    public Rectangle getBox(Transform trans) {
+        if (location == null)
+            return null;
+
+        int scaledWidth;
+        int scaledHeight;
+        if (shape == NONE_NODE) {
+            scaledWidth = scaledHeight = 2;
+        } else {
+            if (fixedSize) {
+                scaledWidth = width;
+                scaledHeight = height;
+            } else {
+                scaledWidth = computeScaledWidth(trans, width);
+                scaledHeight = computeScaledHeight(trans, height);
+            }
+        }
+
+        int w = Math.max(6, scaledWidth);
+        int h = Math.max(6, scaledHeight);
+        Point apt = trans.w2d(location);
+        apt.x -= (w / 2);
+        apt.y -= (h / 2);
+
+        return new Rectangle(apt.x, apt.y, w, h);
+    }
+
+    /**
+     * Sets the width.
+     *
+     * @param a int
+     */
+    public void setWidth(int a) {
+        if (a < 0)
+            a = Byte.MAX_VALUE;
+        width = a;
+    }
+
+    /**
+     * Sets the height.
+     *
+     * @param a int
+     */
+    public void setHeight(int a) {
+        if (a < 0)
+            a = Byte.MAX_VALUE;
+        height = a;
+    }
+
+    /**
+     * Sets the node shape.
+     *
+     * @param a int
+     */
+    public void setShape(byte a) {
+        shape = a;
+    }
+
+    /**
+     * Gets the node shape.
+     *
+     * @return the shape
+     */
+
+    public byte getShape() {
+        return shape;
+    }
+
+    /**
+     * Gets the border color
+     *
+     * @return the borger color
+     */
+    public Color getBorderColor() {
+        return borderColor;
+    }
+
+    /**
+     * Sets the border color
+     *
+     * @param borderColor
+     */
+    public void setBorderColor(Color borderColor) {
+        this.borderColor = borderColor;
+    }
+
+    /**
+     * Draw the node.
+     *
+     * @param gc      Graphics
+     * @param trans   Transform
+     * @param hilited
+     */
+    public void draw(Graphics gc, Transform trans, boolean hilited) {
+        if (hilited)
+            hilite(gc, trans);
+        draw(gc, trans);
+    }
+
+    /**
+     * Draw the node.
+     *
+     * @param gc    Graphics
+     * @param trans Transform
+     */
+    public void draw(Graphics gc, Transform trans) {
+        if (location == null)
+            return; // no location, don't draw
+        Point apt = trans.w2d(location);
+
+        int scaledWidth;
+        int scaledHeight;
+        if (fixedSize) {
+            scaledWidth = width;
+            scaledHeight = height;
+        } else {
+            scaledWidth = computeScaledWidth(trans, width);
+            scaledHeight = computeScaledHeight(trans, height);
+        }
+
+        apt.x -= (scaledWidth >> 1);
+        apt.y -= (scaledHeight >> 1);
+
+        if (this.borderColor != null) {
+            if (enabled)
+                gc.setColor(this.borderColor);
+            else
+                gc.setColor(DISABLED_COLOR);
+            if (shape == OVAL_NODE) {
+                gc.drawOval(apt.x - 2, apt.y - 2, scaledWidth + 4, scaledHeight + 4);
+                gc.drawOval(apt.x - 3, apt.y - 3, scaledWidth + 6, scaledHeight + 6);
+            } else if (shape == RECT_NODE) {
+// default shape==GraphView.RECT_NODE
+                gc.drawRect(apt.x - 2, apt.y - 2, scaledWidth + 4, scaledHeight + 4);
+                gc.drawRect(apt.x - 3, apt.y - 3, scaledWidth + 6, scaledHeight + 6);
+            }
+// else draw nothing
+        }
+
+        if (bgColor != null) {
+            if (enabled)
+                gc.setColor(bgColor);
+            else
+                gc.setColor(Color.WHITE);
+            if (shape == OVAL_NODE)
+                gc.fillOval(apt.x, apt.y, scaledWidth, scaledHeight);
+            else if (shape == RECT_NODE)
+                gc.fillRect(apt.x, apt.y, scaledWidth, scaledHeight);
+
+        }
+        if (fgColor != null) {
+            if (enabled)
+                gc.setColor(fgColor);
+            else
+                gc.setColor(DISABLED_COLOR);
+            if (shape == OVAL_NODE)
+                gc.drawOval(apt.x, apt.y, scaledWidth, scaledHeight);
+            else if (shape == RECT_NODE)
+                gc.drawRect(apt.x, apt.y, scaledWidth, scaledHeight);
+        }
+    }
+
+    /**
+     * gets the scaled width
+     *
+     * @param trans
+     * @param width
+     * @return scaled width
+     */
+    public static int computeScaledWidth(Transform trans, int width) {
+        int scaledWidth = (int) (width / Math.max(1 / trans.getScaleX(), 1 / trans.getScaleY()));
+        if (width > 0) scaledWidth = Math.max(1, scaledWidth);
+        return scaledWidth;
+
+    }
+
+    /**
+     * gets the scaled height
+     *
+     * @param trans
+     * @param height
+     * @return scaled height
+     */
+    public static int computeScaledHeight(Transform trans, int height) {
+        int scaledHeight = (int) (height / Math.max(1 / trans.getScaleX(), 1 / trans.getScaleY()));
+        if (height > 0) scaledHeight = Math.max(1, scaledHeight);
+        return scaledHeight;
+    }
+
+
+    /**
+     * Highlights the node.
+     *
+     * @param gc    Graphics
+     * @param trans Transform
+     */
+    public void hilite(Graphics gc, Transform trans) {
+        if (location == null)
+            return;
+        int scaledWidth;
+        int scaledHeight;
+        if (shape == NONE_NODE) {
+            scaledWidth = scaledHeight = 2;
+        } else {
+            if (fixedSize) {
+                scaledWidth = width;
+                scaledHeight = height;
+            } else {
+                scaledWidth = computeScaledWidth(trans, width);
+                scaledHeight = computeScaledHeight(trans, height);
+            }
+        }
+
+        Point apt = trans.w2d(location);
+        apt.x -= (scaledWidth >> 1);
+        apt.y -= (scaledHeight >> 1);
+
+        gc.setColor(ProgramProperties.SELECTION_COLOR);
+        gc.drawRect(apt.x - 2, apt.y - 2, scaledWidth + 4, scaledHeight + 4);
+    }
+
+
+    /**
+     * Highlights the node label
+     *
+     * @param gc          Graphics
+     * @param trans       Transform
+     * @param defaultFont font to use if node has no font set
+     */
+    public void hiliteLabel(Graphics2D gc, Transform trans, Font defaultFont) {
+        if (location == null)
+            return;
+
+        if (labelColor != null && label != null && labelVisible && label.length() > 0) {
+            gc.setStroke(NORMAL_STROKE);
+            if (getFont() != null)
+                gc.setFont(getFont());
+            else
+                gc.setFont(defaultFont);
+            gc.setColor(ProgramProperties.SELECTION_COLOR);
+            Shape shape = getLabelShape(trans);
+            gc.fill(shape);
+            gc.setColor(ProgramProperties.SELECTION_COLOR_DARKER);
+            final Stroke oldStroke = gc.getStroke();
+            gc.setStroke(NORMAL_STROKE);
+            gc.draw(shape);
+            gc.setStroke(oldStroke);
+        }
+    }
+
+    /**
+     * Draws the node's label and the image, if set
+     *
+     * @param gc    Graphics
+     * @param trans Transform
+     */
+    public void drawLabel(Graphics2D gc, Transform trans, Font defaultFont) {
+        drawLabel(gc, trans, defaultFont, false);
+    }
+
+
+    /**
+     * Draws the node's label and the image, if set
+     *
+     * @param gc    Graphics
+     * @param trans Transform
+     */
+    public void drawLabel(Graphics2D gc, Transform trans, Font defaultFont, boolean hilited) {
+        if (location == null)
+            return;
+
+        if (labelColor != null && label != null && label.length() > 0) {
+            //labelShape = null;
+            //gc.setColor(Color.WHITE);
+            //gc.fill(getLabelRect(trans));
+            if (labelBackgroundColor != null && labelVisible && enabled) {
+                gc.setColor(labelBackgroundColor);
+                gc.fill(getLabelShape(trans));
+            }
+
+            if (getFont() != null)
+                gc.setFont(getFont());
+            else
+                gc.setFont(defaultFont);
+
+            if (hilited)
+                hiliteLabel(gc, trans, defaultFont);
+
+            if (enabled)
+                gc.setColor(labelColor);
+            else
+                gc.setColor(DISABLED_COLOR);
+
+            Point2D apt = getLabelPosition(trans);
+
+            if (apt != null) {
+                if (labelVisible) {
+                    if (labelAngle == 0) {
+                        gc.drawString(label, (int) apt.getX(), (int) apt.getY());
+                    } else {
+                        float labelAngle = this.labelAngle + 0.00001f; // to ensure that labels all get same orientation in
+
+                        Dimension labelSize = getLabelSize();
+                        if (gc instanceof PDFGraphics) {
+                            if (labelAngle >= 0.5 * Math.PI && labelAngle <= 1.5 * Math.PI) {
+                                apt = Geometry.translateByAngle(apt, labelAngle, labelSize.getWidth());
+                                ((PDFGraphics) gc).drawString(label, (float) (apt.getX()), (float) (apt.getY()), (float) (labelAngle - Math.PI));
+                            } else {
+                                ((PDFGraphics) gc).drawString(label, (float) (apt.getX()), (float) (apt.getY()), labelAngle);
+                            }
+                        } else {
+                            // save current transform:
+                            AffineTransform saveTransform = gc.getTransform();
+                            // a vertical phylogram view
+
+                            /*
+                            AffineTransform localTransform =  gc.getTransform();
+                            // rotate label to desired angle
+                            if (labelAngle >= 0.5 * Math.PI && labelAngle <= 1.5 * Math.PI) {
+                                double d = getLabelSize().getWidth();
+                                apt = Geometry.translateByAngle(apt, labelAngle, d);
+                                localTransform.rotate(Geometry.moduloTwoPI(labelAngle - Math.PI), apt.getX(), apt.getY());
+                            } else
+                                localTransform.rotate(labelAngle, apt.getX(), apt.getY());
+                           gc.setTransform(localTransform);
+                            */
+                            // todo: this doesn't work well as the angles aren't drawn correctly
+
+                            // rotate label to desired angle
+                            if (labelAngle >= 0.5 * Math.PI && labelAngle <= 1.5 * Math.PI) {
+                                apt = Geometry.translateByAngle(apt, labelAngle, getLabelSize().getWidth());
+                                gc.rotate(Geometry.moduloTwoPI(labelAngle - Math.PI), apt.getX(), apt.getY());
+                            } else {
+                                gc.rotate(labelAngle, apt.getX(), apt.getY());
+                            }
+                            gc.drawString(label, (int) apt.getX(), (int) apt.getY());
+                            gc.setTransform(saveTransform);
+                        }
+                    }
+                }
+            }
+        }
+        // draw the image:
+        if (getImage() != null && getImage().isVisible()) {
+            getImage().draw(this, trans, gc, hilited);
+        }
+
+        if (descriptionWriter != null && getLabelVisible() && getLabel() != null && getLabel().length() > 0) {
+            Rectangle bounds;
+            if (labelAngle == 0) {
+                bounds = getLabelRect(trans).getBounds();
+            } else {
+                bounds = getLabelShape(trans).getBounds();
+            }
+            try {
+                descriptionWriter.write(String.format("%s; x=%d y=%d w=%d h=%d\n", getLabel(), bounds.x, bounds.y, bounds.width, bounds.height));
+            } catch (IOException e) {
+                // silently ignore
+            }
+        }
+    }
+
+    /**
+     * Sets the position of the label in device coordinates.
+     *
+     * @param x     int
+     * @param y     int
+     * @param trans Transform
+     */
+    public void setLabelPosition(int x, int y, Transform trans) {
+        Point apt = trans.w2d(location);
+        if (labelLayout != USER && labelLayout != LAYOUT)
+            setLabelLayout(USER);
+        dxLabel = x - apt.x;
+        dyLabel = y - apt.y;
+    }
+
+    /**
+     * gets the relative position of the label in device coordinates
+     *
+     * @return relative position
+     */
+    public Point getLabelPositionRelative(Transform trans) {
+        if (labelLayout == USER)
+            return new Point(dxLabel, dyLabel);
+        else {
+            Point aPt = trans.w2d(getLocation());
+            Point bPt = getLabelPosition(trans);
+            return new Point(bPt.x - aPt.x, bPt.y - aPt.y);
+        }
+    }
+
+    /**
+     * Gets the label position in device coordinates
+     *
+     * @param trans Transform
+     * @return locations
+     */
+    public Point getLabelPosition(Transform trans) {
+        if (location == null)
+            return null;
+
+        if (label == null || labelSize == null)
+            return null;
+
+        int scaledWidth;
+        int scaledHeight;
+
+        if (shape == NONE_NODE)
+            scaledWidth = scaledHeight = 2;
+        else {
+            if (fixedSize) {
+                scaledWidth = width;
+                scaledHeight = height;
+            } else {
+                scaledWidth = computeScaledWidth(trans, width);
+                scaledHeight = computeScaledHeight(trans, height);
+            }
+        }
+        Point apt = trans.w2d(location);
+        Dimension size = getLabelSize();
+        switch (labelLayout) {
+            case RADIAL:
+            case USER:
+            case LAYOUT:
+                apt.x += dxLabel;
+                apt.y += dyLabel;
+                break;
+            case CENTRAL:
+                apt.x -= size.width / 2;
+                apt.y += size.height / 2;
+                break;
+            case NORTH:
+                apt.x -= size.width / 2;
+                apt.y -= (scaledHeight / 2 + 3);
+                break;
+            case NORTHEAST:
+                apt.x += (scaledWidth / 2 + 3);
+                apt.y -= (scaledHeight / 2 + 3);
+                break;
+            case EAST:
+                apt.x += (scaledWidth / 2 + 3);
+                apt.y += size.height / 2;
+                break;
+            case SOUTHEAST:
+                apt.x -= (scaledWidth / 2 + 3);
+                apt.y += (scaledHeight / 2 + 3) + size.height;
+                break;
+            case SOUTH:
+                apt.x -= size.width / 2;
+                apt.y += (scaledHeight / 2 + 3) + size.height;
+                break;
+            case SOUTHWEST:
+                apt.x -= (scaledWidth / 2 + size.width + 3);
+                apt.y += (scaledHeight / 2 + 3) + size.height;
+                break;
+            case WEST:
+                apt.x -= (scaledWidth / 2 + size.width + 3);
+                apt.y += size.height / 2;
+                break;
+            case NORTHWEST:
+                apt.x -= (scaledWidth / 2 + size.width + 3);
+                apt.y -= (scaledHeight / 2 + 3);
+                break;
+        }
+        return apt;
+    }
+
+    /**
+     * gets the bounding box of the label in device coordinates
+     *
+     * @param trans
+     * @return bounding box
+     */
+    public Rectangle getLabelRect(Transform trans) {
+        if (labelSize != null) {
+            Point apt = getLabelPosition(trans);
+            if (apt != null)
+                return new Rectangle(apt.x, apt.y - labelSize.height + 1, labelSize.width, labelSize.height);
+        }
+        return null;
+    }
+
+    /**
+     * gets the bounding box of the label in device coordinates as a shape (rectangle or polygon)
+     *
+     * @param trans
+     * @return bounding box
+     */
+    public Shape getLabelShape(Transform trans) {
+        if (labelSize != null) {
+            Point2D apt = getLabelPosition(trans);
+            if (apt != null) {
+                if (labelAngle == 0) {
+                    return new Rectangle((int) apt.getX(), (int) apt.getY() - labelSize.height + 1, labelSize.width, labelSize.height);
+                } else {
+                    double labelAngle = this.labelAngle + 0.0001; // to ensure that labels all get same orientation in
+
+                    AffineTransform localTransform = new AffineTransform();
+                    // rotate label to desired angle
+                    if (labelAngle >= 0.5 * Math.PI && labelAngle <= 1.5 * Math.PI) {
+                        double d = getLabelSize().getWidth();
+                        apt = Geometry.translateByAngle(apt, labelAngle, d);
+                        localTransform.rotate(Geometry.moduloTwoPI(labelAngle - Math.PI), apt.getX(), apt.getY());
+                    } else
+                        localTransform.rotate(labelAngle, apt.getX(), apt.getY());
+                    double[] pts = new double[]{apt.getX(), apt.getY(),
+                            apt.getX() + labelSize.width, apt.getY(),
+                            apt.getX() + labelSize.width, apt.getY() - labelSize.height,
+                            apt.getX(), apt.getY() - labelSize.height};
+                    localTransform.transform(pts, 0, pts, 0, 4);
+                    return new Polygon(new int[]{(int) pts[0], (int) pts[2], (int) pts[4], (int) pts[6]}, new int[]{(int) pts[1], (int) pts[3],
+                            (int) pts[5], (int) pts[7]}, 4);
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Sets the location
+     *
+     * @param p Point2D
+     */
+    public void setLocation(Point2D p) {
+        location = p;
+    }
+
+    /**
+     * Sets the location
+     *
+     * @param x
+     * @param y
+     */
+    public void setLocation(double x, double y) {
+        location = new Point2D.Double(x, y);
+    }
+
+    /**
+     * draw this noded at a fixed size?
+     *
+     * @return fixed-size mode?
+     */
+    public boolean getFixedSize() {
+        return fixedSize;
+    }
+
+    /**
+     * draw this node at a fixed size?
+     *
+     * @param fixedSize
+     */
+    public void setFixedSize(boolean fixedSize) {
+        this.fixedSize = fixedSize;
+    }
+
+    /**
+     * writes this node view
+     *
+     * @param w
+     */
+    public void write(Writer w) throws IOException {
+        w.write(toString(null));
+        w.write("\n");
+    }
+
+    /**
+     * gets a string representation of this node view, including coordinates
+     *
+     * @return string representation
+     */
+    public String toString() {
+        return toString(null, true, true);
+    }
+
+    /**
+     * gets a string representation of this node view
+     *
+     * @param withCoordinates show coordinates as well?
+     * @return string representation
+     */
+    public String toString(boolean withCoordinates) {
+        return toString(null, withCoordinates, true);
+    }
+
+
+    /**
+     * writes this node view
+     *
+     * @param w
+     * @param previousNV if not null, only write those fields that differ from the values in previousNV
+     */
+    public void write(Writer w, NodeView previousNV) throws IOException {
+        w.write(toString(previousNV));
+        w.write("\n");
+    }
+
+    /**
+     * gets a string representation of this node view
+     *
+     * @param previousNV if not null, only write those fields that differ from the values in previousNV
+     * @return string representation
+     */
+    public String toString(NodeView previousNV) {
+        return toString(previousNV, true, true);
+    }
+
+    /**
+     * gets a string representation of this node view
+     *
+     * @param previousNV if not null, only write those fields that differ from the values in previousNV
+     * @return string representation
+     */
+    public String toString(NodeView previousNV, boolean withCoordinates, boolean withLabel) {
+        StringBuilder buf = new StringBuilder();
+        if (previousNV == null || height != previousNV.height)
+            buf.append(" nh=").append(height);
+        if (previousNV == null || width != previousNV.width)
+            buf.append(" nw=").append(width);
+        if (fgColor != null && (previousNV == null || previousNV.fgColor == null || !fgColor.equals(previousNV.fgColor)))
+            buf.append(" fg=").append(Basic.toString3Int(fgColor));
+        if (bgColor != null && (previousNV == null || previousNV.bgColor == null || !bgColor.equals(previousNV.bgColor)))
+            buf.append(" bg=").append(Basic.toString3Int(bgColor));
+        if (borderColor != null && (previousNV == null || previousNV.borderColor == null || !borderColor.equals(previousNV.borderColor)))
+            buf.append(" bd=").append(Basic.toString3Int(borderColor));
+        if (previousNV == null || linewidth != previousNV.linewidth)
+            buf.append(" w=").append(linewidth);
+        if (previousNV == null || shape != previousNV.shape)
+            buf.append(" sh=").append(shape);
+        if (withCoordinates && location != null) {
+            buf.append(" x=").append((float) location.getX());
+            buf.append(" y=").append((float) location.getY());
+        }
+        if (previousNV == null || fixedSize != previousNV.fixedSize)
+            buf.append(" fx=").append(fixedSize ? 1 : 0);
+
+        if (labelColor != null && (previousNV == null || previousNV.labelColor == null || !labelColor.equals(previousNV.labelColor)))
+            buf.append(" lc=").append(Basic.toString3Int(labelColor));
+        if (previousNV == null || ((previousNV.labelBackgroundColor == null) != (labelBackgroundColor == null))
+                ||
+                (previousNV.labelBackgroundColor != null && labelBackgroundColor != null && !previousNV.labelBackgroundColor.equals(labelBackgroundColor))) {
+            buf.append(" lk=").append(Basic.toString3Int(labelBackgroundColor));
+        }
+
+        if (font != null && (previousNV == null || previousNV.font == null || !font.equals(previousNV.font)))
+            buf.append(" ft='").append(Basic.getCode(font)).append("'");
+        if (previousNV == null || dxLabel != previousNV.dxLabel)
+            buf.append(" lx=").append(dxLabel);
+        if (previousNV == null || dyLabel != previousNV.dyLabel)
+            buf.append(" ly=").append(dyLabel);
+        if (previousNV == null || labelLayout != previousNV.labelLayout)
+            buf.append(" ll=").append(labelLayout);
+        if (previousNV == null || labelVisible != previousNV.labelVisible)
+            buf.append(" lv=").append(labelVisible ? 1 : 0);
+        if (labelAngle != 0)
+            buf.append(" la=").append(labelAngle);
+        if (withLabel && label != null && label.length() > 0)
+            buf.append(" lb='").append(label).append("'");
+
+        buf.append(";");
+        return buf.toString();
+    }
+
+    /**
+     * read node format from a string
+     *
+     * @param src
+     * @throws IOException
+     */
+    public void read(String src) throws IOException {
+        read(src, this);
+    }
+
+    /**
+     * read node format from a string. Use prevNV for defaults
+     *
+     * @param src
+     * @param prevNV
+     * @throws IOException
+     */
+    public void read(String src, NodeView prevNV) throws IOException {
+        NexusStreamParser np = new NexusStreamParser(new StringReader(src));
+        java.util.List<String> tokens = np.getTokensRespectCase(null, ";");
+        read(np, tokens, prevNV != null ? prevNV : this);
+    }
+
+    /**
+     * reads a node view from a line
+     *
+     * @param tokens
+     * @param prevNV this must be !=null, for example can be set to graphView.defaultNodeView
+     */
+    public void read(NexusStreamParser np, java.util.List<String> tokens, NodeView prevNV) throws IOException {
+        if (prevNV == null)
+            throw new IOException("prevNV=null");
+        height = (byte) np.findIgnoreCase(tokens, "nh=", prevNV.height);
+        width = (byte) np.findIgnoreCase(tokens, "nw=", prevNV.width);
+        fgColor = np.findIgnoreCase(tokens, "fg=", prevNV.fgColor);
+        bgColor = np.findIgnoreCase(tokens, "bg=", prevNV.bgColor);
+        borderColor = np.findIgnoreCase(tokens, "bd=", prevNV.borderColor);
+        linewidth = (byte) np.findIgnoreCase(tokens, "w=", prevNV.linewidth);
+        shape = (byte) np.findIgnoreCase(tokens, "sh=", prevNV.shape);
+
+        if ((prevNV != null && prevNV != this) || (tokens.contains("x=") && tokens.contains("y="))) {
+            double x = np.findIgnoreCase(tokens, "x=", prevNV.getLocation() != null ? (float) prevNV.getLocation().getX() : 0);
+            double y = np.findIgnoreCase(tokens, "y=", prevNV.getLocation() != null ? (float) prevNV.getLocation().getY() : 0);
+            setLocation(new Point2D.Double(x, y));
+        }
+
+        fixedSize = (np.findIgnoreCase(tokens, "fx=", prevNV.fixedSize ? 1 : 0) != 0);
+
+        labelColor = np.findIgnoreCase(tokens, "lc=", prevNV.labelColor);
+        labelBackgroundColor = np.findIgnoreCase(tokens, "lk=", prevNV.labelBackgroundColor);
+
+        String fontName = np.findIgnoreCase(tokens, "ft=", null, "");
+        if (fontName != null && fontName.length() > 0)
+            font = Font.decode(fontName);
+        else if (prevNV.getFont() != null && prevNV != this)
+            font = prevNV.getFont(); // will use default font
+        else
+            font = GraphView.defaultNodeView.getFont();
+
+        dxLabel = (int) np.findIgnoreCase(tokens, "lx=", prevNV.dxLabel);
+        dyLabel = (int) np.findIgnoreCase(tokens, "ly=", prevNV.dyLabel);
+        setLabelAngle(np.findIgnoreCase(tokens, "la=", 0));
+        labelLayout = (byte) np.findIgnoreCase(tokens, "ll=", prevNV.labelLayout);
+        labelVisible = (np.findIgnoreCase(tokens, "lv=", prevNV.labelVisible ? 1 : 0) != 0);
+
+        label = np.findIgnoreCase(tokens, "lb=", null, "");
+        if (label != null && label.length() == 0)
+            label = null;
+        setLabel(label);
+
+        if (tokens.size() > 0) {
+            throw new IOException("Unexpected tokens: " + tokens);
+        }
+    }
+
+    /**
+     * get the image associated with this node
+     *
+     * @return
+     */
+    public NodeImage getImage() {
+        return image;
+    }
+
+    /**
+     * set the image associated with this node
+     *
+     * @param image
+     */
+    public void setImage(NodeImage image) {
+        this.image = image;
+    }
+
+    /**
+     * does node contain mouse click
+     *
+     * @param trans
+     * @param x
+     * @param y
+     * @return true, if hit
+     */
+    public boolean contains(Transform trans, int x, int y) {
+        Rectangle box = getBox(trans);
+        return box != null && box.contains(x, y) || image != null && image.isVisible() && image.contains(x, y);
+    }
+
+    /**
+     * does node intersect rectangle?
+     *
+     * @param trans
+     * @param rect
+     * @return
+     */
+    public boolean intersects(Transform trans, Rectangle rect) {
+        Rectangle box = getBox(trans);
+
+        return box != null && box.intersects(rect) || image != null && image.isVisible() && image.getRectangle().intersects(rect);
+    }
+
+    /**
+     * Gets the background color.
+     *
+     * @return bgcol Color the background color
+     */
+    public Color getBackgroundColor() {
+        return bgColor;
+    }
+
+    /**
+     * Sets the background color.
+     *
+     * @param a Color
+     */
+    public void setBackgroundColor(Color a) {
+        bgColor = a;
+    }
+}
+
+// EOF
diff --git a/src/jloda/graphview/PanelActionListener.java b/src/jloda/graphview/PanelActionListener.java
new file mode 100644
index 0000000..b54f71e
--- /dev/null
+++ b/src/jloda/graphview/PanelActionListener.java
@@ -0,0 +1,30 @@
+/**
+ * PanelActionListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import java.awt.event.MouseEvent;
+
+/**
+ * actions to be called when panel clicked
+ * Daniel Huson, 6.2010
+ */
+public interface PanelActionListener {
+    void doMouseClicked(MouseEvent mouseEvent);
+}
diff --git a/src/jloda/graphview/ScrollPaneAdjuster.java b/src/jloda/graphview/ScrollPaneAdjuster.java
new file mode 100644
index 0000000..fdd18ee
--- /dev/null
+++ b/src/jloda/graphview/ScrollPaneAdjuster.java
@@ -0,0 +1,104 @@
+/**
+ * ScrollPaneAdjuster.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.geom.Point2D;
+
+/**
+ * this is used when zooming or rotating the graph in a graphview to adjust the
+ * scroll pane so that the content in the middle of the window stays fixed
+ * Daniel Huson, 12.2006
+ */
+public class ScrollPaneAdjuster {
+    private final JScrollPane scrollPane;
+    private final JScrollBar scrollBarX;
+    private final JScrollBar scrollBarY;
+    private final Transform trans;
+    private final Point centerDC;
+    private final Point2D centerWC;
+
+    /**
+     * construct object and "remember" how scrollpane is currently centered around middle of screen
+     *
+     * @param scrollPane
+     * @param trans
+     */
+    public ScrollPaneAdjuster(JScrollPane scrollPane, Transform trans) {
+        this(scrollPane, trans, null);
+    }
+
+    /**
+     * construct object and "remember" how scrollpane is currently centered
+     *
+     * @param scrollPane
+     * @param trans
+     * @param centerDC   center point in device coordinates
+     */
+    public ScrollPaneAdjuster(JScrollPane scrollPane, Transform trans, Point centerDC) {
+        this.scrollPane = scrollPane;
+        this.trans = trans;
+        scrollBarX = scrollPane.getHorizontalScrollBar();
+        scrollBarY = scrollPane.getVerticalScrollBar();
+
+        if (centerDC == null) // if no point given, center on window
+            centerDC = new Point(scrollBarX.getValue() + scrollBarX.getVisibleAmount() / 2,
+                    scrollBarY.getValue() + scrollBarY.getVisibleAmount() / 2);
+        this.centerDC = centerDC;
+
+        // save world coordinates of center
+        if (trans != null)
+            centerWC = trans.d2w(this.centerDC);
+        else {
+            centerWC = (Point) centerDC.clone(); // todo: this is broken for the chartviewer
+        }
+    }
+
+    /**
+     * adjusts the scroll bars to recenter on world coordinates that were previously in
+     * center of window
+     *
+     * @param horizontal adjust horizontally
+     * @param vertical   adjust vertically
+     */
+    public void adjust(boolean horizontal, boolean vertical) {
+        Point newPosDC;
+        if (trans != null) {
+            boolean useMagnifier = trans.getMagnifier().isActive();
+            trans.getMagnifier().setActive(false);
+            newPosDC = trans.w2d(centerWC);
+            trans.getMagnifier().setActive(useMagnifier);
+        } else {
+            newPosDC = centerDC; // todo: fix this
+        }
+        if (horizontal) {
+            int diff = (int) Math.round(newPosDC.getX()) - centerDC.x;
+            diff -= 1;
+            scrollBarX.setValue(scrollBarX.getValue() + diff);
+        }
+        if (vertical) {
+            int diff = (int) Math.round(newPosDC.getY()) - centerDC.y;
+            if (trans != null && trans.getFlipV())
+                diff = -diff;
+            scrollBarY.setValue(scrollBarY.getValue() + diff);
+        }
+    }
+}
diff --git a/src/jloda/graphview/Transform.java b/src/jloda/graphview/Transform.java
new file mode 100644
index 0000000..ee2b601
--- /dev/null
+++ b/src/jloda/graphview/Transform.java
@@ -0,0 +1,823 @@
+/**
+ * Transform.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.util.Basic;
+import jloda.util.Geometry;
+import jloda.util.PolygonDouble;
+import jloda.util.parse.NexusStreamParser;
+
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.LinkedList;
+import java.util.Random;
+
+/**
+ * transformation class used to draw graph onto scrollpanel. Ensures that the rotated bounding box of
+ * the world coordinates always has xmin,ymin corner at 0,0
+ * Daniel Huson
+ */
+public class Transform {
+    final Rectangle2D coordRect;
+    final Rectangle2D rotatedCoordRect;
+    final Point2D centerOfCoordRect;
+    final Point2D centerOfRotatedCoordRect;
+    double angle;
+    double sinAngle;
+    double cosAngle;
+    double scaleX;
+    double scaleY;
+    boolean lockXYScale;
+    boolean flipH;
+    boolean flipV;
+    int bottomMargin = 50;
+    int topMargin = 50;
+    int leftMargin = 50;
+    int rightMargin = 50;
+    private Random rand;
+    Magnifier magnifier = null;
+
+    final java.util.List<ITransformChangeListener> changeListeners = new LinkedList<>();
+
+    /**
+     * default constructor
+     */
+    public Transform() {
+        coordRect = new Rectangle2D.Double();
+        rotatedCoordRect = new Rectangle2D.Double();
+        centerOfCoordRect = new Point2D.Double();
+        centerOfRotatedCoordRect = new Point2D.Double();
+
+        angle = 0;
+        sinAngle = Math.sin(angle);
+        cosAngle = Math.cos(angle);
+
+        scaleX = 1;
+        scaleY = 1;
+        lockXYScale = true;
+        flipH = false;
+        flipV = false;
+    }
+
+
+    /**
+     * constructor with given magnifier
+     *
+     * @param graphView
+     */
+    public Transform(GraphView graphView) {
+        this();
+        this.magnifier = new Magnifier(graphView, this);
+    }
+
+    /**
+     * returns a clone (with no listeners)
+     *
+     * @return clone
+     */
+    public Object clone() throws CloneNotSupportedException {
+        //super.clone();
+        Transform trans = new Transform();
+        trans.copy(this);
+        return trans;
+    }
+
+    /**
+     * copies a transform (except for its listeners)
+     *
+     * @param src
+     */
+    public void copy(Transform src) {
+        coordRect.setRect(src.coordRect);
+        rotatedCoordRect.setRect(src.rotatedCoordRect);
+        centerOfCoordRect.setLocation(src.centerOfCoordRect);
+        centerOfRotatedCoordRect.setLocation(src.centerOfRotatedCoordRect);
+        angle = src.angle;
+        sinAngle = src.sinAngle;
+        cosAngle = src.cosAngle;
+        scaleX = src.scaleX;
+        scaleY = src.scaleY;
+        lockXYScale = src.lockXYScale;
+        flipH = src.flipH;
+        flipV = src.flipV;
+        bottomMargin = src.bottomMargin;
+        topMargin = src.topMargin;
+        leftMargin = src.leftMargin;
+        rightMargin = src.rightMargin;
+    }
+
+    /**
+     * reset scale, angle and flips
+     */
+    public void reset() {
+        angle = 0;
+        sinAngle = Math.sin(angle);
+        cosAngle = Math.cos(angle);
+        scaleX = 1;
+        scaleY = 1;
+        flipH = false;
+        flipV = false;
+        fireHasChanged();
+    }
+
+    /**
+     * is the world empty
+     *
+     * @return true, if coordinate rectangle has width 0 and height 0
+     */
+    public boolean isEmpty() {
+        return coordRect.getWidth() == 0 && coordRect.getHeight() == 0;
+    }
+
+    /**
+     * gets a string representation
+     */
+    public String toString() {
+        StringWriter sw = new StringWriter();
+        try {
+            write(sw);
+        } catch (IOException ex) {
+            Basic.caught(ex);
+        }
+        return sw.toString();
+    }
+
+    /**
+     * writes the object
+     *
+     * @param w
+     * @throws IOException
+     */
+    public void write(Writer w) throws IOException {
+        w.write("angle:" + (float) angle + " scaleX:" + (float) scaleX + " scaleY:" + (float) scaleY + " flipH:" + (flipH ? 1 : 0) + " flipV:" + (flipV ? 1 : 0) +
+                " leftMargin:" + leftMargin +
+                " rightMargin:" + rightMargin +
+                " topMargin:" + topMargin +
+                " bottomMargin:" + bottomMargin +
+                ";");
+    }
+
+    /**
+     * read the object
+     *
+     * @param r
+     * @throws IOException
+     */
+    public void read(Reader r) throws IOException {
+        read(new NexusStreamParser(r));
+
+    }
+
+    /**
+     * read the object
+     *
+     * @param np
+     * @throws IOException
+     */
+    public void read(NexusStreamParser np) throws IOException {
+        java.util.List<String> tokens = np.getTokensLowerCase(null, ";");
+        angle = np.findIgnoreCase(tokens, "angle:", (float) angle);
+        sinAngle = Math.sin(angle);
+        cosAngle = Math.cos(angle);
+        scaleX = (double) np.findIgnoreCase(tokens, "scaleX:", (float) scaleX);
+        scaleY = (double) np.findIgnoreCase(tokens, "scaleY:", (float) scaleY);
+        flipH = (np.findIgnoreCase(tokens, "flipH:", flipH ? 1 : 0) != 0);
+        flipV = (np.findIgnoreCase(tokens, "flipV:", flipV ? 1 : 0) != 0);
+        leftMargin = (int) np.findIgnoreCase(tokens, "leftMargin:", leftMargin);
+        rightMargin = (int) np.findIgnoreCase(tokens, "rightMargin:", rightMargin);
+        topMargin = (int) np.findIgnoreCase(tokens, "topMargin:", topMargin);
+        bottomMargin = (int) np.findIgnoreCase(tokens, "bottomMargin:", bottomMargin);
+
+        if (tokens.size() > 0)
+            throw new IOException("Transform.read: illegal tokens: " + tokens);
+    }
+
+    /**
+     * tell the transformation what the current bounding box for user coordinates is
+     *
+     * @param x
+     * @param y
+     * @param width
+     * @param height
+     */
+    public void setCoordinateRect(double x, double y, double width, double height) {
+        coordRect.setRect(x, y, width, height);
+        centerOfCoordRect.setLocation(coordRect.getCenterX(), coordRect.getCenterY());
+        setRotatedCoordinateRect();
+        fireHasChanged();
+    }
+
+    /**
+     * tell the transformation what the current bounding box for user coordinates is
+     *
+     * @param rect
+     */
+    public void setCoordinateRect(Rectangle2D rect) {
+        coordRect.setRect(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight());
+        centerOfCoordRect.setLocation(coordRect.getCenterX(), coordRect.getCenterY());
+        setRotatedCoordinateRect();
+        fireHasChanged();
+    }
+
+    /**
+     * sets the rotated coordinate rect
+     */
+    private void setRotatedCoordinateRect() {
+        if (angle == 0)
+            rotatedCoordRect.setRect(coordRect);
+        else {
+            final Point2D p00 = Geometry.rotateAbout(new Point2D.Double(coordRect.getMinX(), coordRect.getMinY()), angle, centerOfCoordRect);
+            final Point2D p01 = Geometry.rotateAbout(new Point2D.Double(coordRect.getMaxX(), coordRect.getMinY()), angle, centerOfCoordRect);
+            final Point2D p10 = Geometry.rotateAbout(new Point2D.Double(coordRect.getMinX(), coordRect.getMaxY()), angle, centerOfCoordRect);
+            final Point2D p11 = Geometry.rotateAbout(new Point2D.Double(coordRect.getMaxX(), coordRect.getMaxY()), angle, centerOfCoordRect);
+
+            rotatedCoordRect.setRect(p00.getX(), p00.getY(), 0, 0);
+            rotatedCoordRect.add(p01);
+            rotatedCoordRect.add(p10);
+            rotatedCoordRect.add(p11);
+        }
+        centerOfRotatedCoordRect.setLocation(rotatedCoordRect.getCenterX(), rotatedCoordRect.getCenterY());
+    }
+
+
+    public Point2D getDeviceRotationCenter() {
+        Point2D wp = (Point2D) centerOfRotatedCoordRect.clone();
+        return w2d(wp);
+    }
+
+    public Point2D getWorldRotationCenter() {
+        return centerOfRotatedCoordRect;
+    }
+
+    public AffineTransform getAffineTransform() {
+        Point2D w00 = new Point2D.Double(0, 0);
+        Point2D w10 = new Point2D.Double(0, 1);
+        Point2D w11 = new Point2D.Double(1, 0);
+        Point2D d00 = w2d(w00);
+        Point2D d10 = w2d(w10);
+        Point2D d11 = w2d(w11);
+
+        double ratio1 = (w10.getX() - w00.getX()) / (w00.getY() - w10.getY());
+        double ratio2 = (d00.getX() - d10.getX()) / (w00.getY() - w10.getY());
+
+        double m00 = (d11.getX() - w11.getY() * ratio2 - d00.getX() + w00.getY() * ratio2) / (w11.getX() + w11.getY() * ratio1 - w00.getX() - w00.getY() * ratio1);
+        double m01 = ratio1 * m00 + ratio2;
+        double m02 = d00.getX() - w00.getX() * m00 - w00.getY() * m01;
+
+        double ratio3 = (d00.getY() - d10.getY()) / (w00.getY() - w10.getY());
+
+        double m10 = (d11.getY() - w11.getY() * ratio3 - d00.getY() + w00.getY() * ratio3) / (w11.getX() + w11.getY() * ratio1 - w00.getX() - w00.getY() * ratio1);
+        double m11 = ratio1 * m10 + ratio3;
+        double m12 = d00.getY() - w00.getX() * m10 - w00.getY() * m11;
+
+        AffineTransform re = new AffineTransform();
+        re.setTransform(m00, m10, m01, m11, m02, m12);
+        return re;
+    }
+
+    /**
+     * Computes the angle between a and b as observed from the center from device to world coordiante system
+     *
+     * @param deviceCenter
+     * @param a
+     * @param b
+     * @return
+     */
+    public double computeObservedWorldAngle(Point2D deviceCenter, Point2D a, Point2D b) {
+        Point2D wC = d2w(deviceCenter);
+        Point2D wA = d2w(a);
+        Point2D wB = d2w(b);
+        return Geometry.computeObservedAngle(wC, wA, wB);
+    }
+
+    /**
+     * transform from world coordinates to device coordinates
+     *
+     * @param wp
+     * @param dp
+     */
+    public void w2d(Point2D wp, Point dp) {
+        w2d(wp.getX(), wp.getY(), dp);
+    }
+
+    /**
+     * transform from world coordinates to device coordinates
+     *
+     * @param x
+     * @param y
+     * @param dp
+     */
+    public void w2d(double x, double y, Point2D dp) {
+        Point2D wp = new Point2D.Double(x,
+                y);
+
+        if (angle != 0) {
+            // Geometry.rotateAbout(dd, angle, centerOfRotatedCoordRect, dd);
+            // compute directly here to avoid overhead:
+            wp.setLocation(wp.getX() - centerOfRotatedCoordRect.getX(), wp.getY() - centerOfRotatedCoordRect.getY());
+            wp.setLocation(wp.getX() * cosAngle - wp.getY() * sinAngle + centerOfRotatedCoordRect.getX(),
+                    wp.getX() * sinAngle + wp.getY() * cosAngle + centerOfRotatedCoordRect.getY());
+
+        }
+        wp = new Point2D.Double(flipH ? 2 * centerOfRotatedCoordRect.getX() - wp.getX() : wp.getX(),
+                flipV ? 2 * centerOfRotatedCoordRect.getY() - wp.getY() : wp.getY());
+
+        wp.setLocation(wp.getX() - rotatedCoordRect.getX(), wp.getY() - rotatedCoordRect.getY());
+
+        wp.setLocation(scaleX != 1.0 ? wp.getX() * scaleX : wp.getX(), scaleY != 1.0 ? wp.getY() * scaleY : wp.getY());
+
+
+        if (magnifier != null && magnifier.isActive())
+            magnifier.applyMagnifier(wp.getX() + leftMargin, wp.getY() + topMargin, dp);
+        else
+            dp.setLocation(wp.getX() + leftMargin, wp.getY() + topMargin);
+    }
+
+    /**
+     * gets the point in device coordinates
+     *
+     * @param wp point in world coordinatess
+     * @return point in device coordinates
+     */
+    public Point w2d(Point2D wp) {
+        final Point dp = new Point();
+        w2d(wp, dp);
+        return dp;
+    }
+
+    /**
+     * gets the point in device coordinates
+     *
+     * @param x point in world coordinates
+     * @param y point in world coordinates
+     * @return point in device coordinates
+     */
+    public Point w2d(double x, double y) {
+        final Point dp = new Point();
+        w2d(new Point2D.Double(x, y), dp);
+        return dp;
+    }
+
+    /**
+     * transform from device coordinates to world coordinates
+     *
+     * @param dp
+     * @param wp
+     */
+    public void d2w(Point2D dp, Point2D wp) {
+        double x = dp.getX() - leftMargin;
+        double y = dp.getY() - topMargin;
+        if (scaleX != 1.0)
+            x /= scaleX;
+        if (scaleY != 1.0)
+            y /= scaleY;
+        x += rotatedCoordRect.getX();
+        y += rotatedCoordRect.getY();
+
+        wp.setLocation(flipH ? 2 * centerOfRotatedCoordRect.getX() - x : x, flipV ? 2 * centerOfRotatedCoordRect.getY() - y : y);
+
+        if (angle != 0)
+            Geometry.rotateAbout(wp, -angle, centerOfRotatedCoordRect, wp);
+    }
+
+    /**
+     * gets the point in world coordinates
+     *
+     * @param dp point in device coordinatess
+     * @return point in world coordinates
+     */
+    public Point2D d2w(Point2D dp) {
+        final Point2D wp = new Point2D.Double();
+        d2w(dp, wp);
+        return wp;
+    }
+
+    /**
+     * gets the point in world coordinates
+     *
+     * @return point in world coordinates
+     */
+    public Point2D d2w(int x, int y) {
+        return d2w(new Point(x, y));
+    }
+
+    /**
+     * transform from world coordinates to device coordinates.
+     * Note that the location and the size of the rectangle are modified, but not
+     * not its orientation, that is, the rectangle is NOT rotated
+     *
+     * @param rectWC input: rectangle in world coordinates
+     * @param polyDC output: rectangular polygon in device coordinates
+     */
+    public void w2d(Rectangle2D rectWC, Polygon polyDC) {
+        if (rectWC != null && polyDC != null) {
+            polyDC.reset();
+            Point a = w2d(rectWC.getX(), rectWC.getY());
+            polyDC.addPoint(a.x, a.y);
+            a = w2d(rectWC.getX(), rectWC.getY() + rectWC.getHeight());
+            polyDC.addPoint(a.x, a.y);
+            a = w2d(rectWC.getX() + rectWC.getWidth(), rectWC.getY() + rectWC.getHeight());
+            polyDC.addPoint(a.x, a.y);
+            a = w2d(rectWC.getX() + rectWC.getWidth(), rectWC.getY());
+            polyDC.addPoint(a.x, a.y);
+        }
+    }
+
+    /**
+     * transform from world coordinates to device coordinates.
+     * Note that the location and the size of the rectangle are modified, but not
+     * not its orientation, that is, the rectangle is NOT rotated
+     *
+     * @param wp
+     * @param dp
+     */
+    public Rectangle w2d(Rectangle2D wp, Rectangle dp) {
+        if (wp != null && dp != null) {
+            Point2D anchor = w2d(new Point2D.Double(wp.getX(), wp.getY()));
+            double width = scaleX * wp.getWidth();
+            double height = scaleY * wp.getHeight();
+            dp.setRect(anchor.getX() - (flipH ? width : 0), anchor.getY() - (flipV ? height : 0), width, height);
+            return dp;
+        }
+        return null;
+    }
+
+    /**
+     * transforms from world coordinates to device coordinates.
+     *
+     * @param wp rect in world coordinatess
+     * @return polygon in device coordinates
+     */
+    public Polygon w2d(Rectangle2D wp) {
+        final Polygon dp = new Polygon();
+        w2d(wp, dp);
+        return dp;
+    }
+
+
+    /**
+     * transform from device coordinates to world coordinates. Note that the rectangle is not rotated
+     *
+     * @param dp
+     * @param wp
+     */
+    public void d2w(Rectangle2D dp, Rectangle2D wp) {
+        Point2D anchor = d2w(new Point2D.Double(dp.getX(), dp.getY()));
+        wp.setRect(anchor.getX(), anchor.getY(), dp.getWidth() / scaleX, dp.getHeight() / scaleY);
+    }
+
+    /**
+     * gets the rectangle in device coordinates. Note that it is not rotated
+     *
+     * @param dp rectangle in device coordinatess
+     * @return rectangle in world coordinates
+     */
+    public Rectangle2D d2w(Rectangle2D dp) {
+        final Rectangle2D wp = new Rectangle2D.Double();
+        d2w(dp, wp);
+        return wp;
+    }
+
+    public double getAngle() {
+        return angle;
+    }
+
+    public void setAngle(double angle) {
+        this.angle = Geometry.moduloTwoPI(angle);
+        sinAngle = Math.sin(this.angle);
+        cosAngle = Math.cos(this.angle);
+        setRotatedCoordinateRect();
+        fireHasChanged();
+    }
+
+    public void composeAngle(double delta) {
+        this.angle += delta;
+        sinAngle = Math.sin(angle);
+        cosAngle = Math.cos(angle);
+        setRotatedCoordinateRect();
+        fireHasChanged();
+    }
+
+    public boolean getFlipH() {
+        return flipH;
+    }
+
+    public void setFlipH(boolean flipH) {
+        this.flipH = flipH;
+        fireHasChanged();
+    }
+
+    public boolean getFlipV() {
+        return flipV;
+    }
+
+    public void setFlipV(boolean flipV) {
+        this.flipV = flipV;
+        fireHasChanged();
+    }
+
+    public boolean getLockXYScale() {
+        return lockXYScale;
+    }
+
+    public void setLockXYScale(boolean lockXYScale) {
+        this.lockXYScale = lockXYScale;
+    }
+
+    public double getScaleX() {
+        return scaleX;
+    }
+
+    public void setScaleX(double scaleX) {
+        this.scaleX = scaleX;
+        fireHasChanged();
+    }
+
+    public double getScaleY() {
+        return scaleY;
+    }
+
+    public void setScaleY(double scaleY) {
+        this.scaleY = scaleY;
+        fireHasChanged();
+    }
+
+    public void setScale(double scaleX, double scaleY) {
+        this.scaleX = scaleX;
+        this.scaleY = scaleY;
+        fireHasChanged();
+    }
+
+    public void composeScale(double deltaX, double deltaY) {
+        this.scaleX *= deltaX;
+        this.scaleY *= deltaY;
+        fireHasChanged();
+    }
+
+    public void composeScaleCentered(double deltaX, double deltaY) {
+        this.scaleX *= deltaX;
+        this.scaleY *= deltaY;
+        fireHasChanged();
+    }
+
+    public void composeScaleX(double deltaX) {
+        this.scaleX *= deltaX;
+        fireHasChanged();
+    }
+
+    public void composeScaleY(double deltaY) {
+        this.scaleY *= deltaY;
+        fireHasChanged();
+    }
+
+
+    /**
+     * register a new change listener
+     *
+     * @param listener
+     */
+    public void addChangeListener(ITransformChangeListener listener) {
+        changeListeners.add(listener);
+    }
+
+    /**
+     * remove a registered change listener
+     *
+     * @param listener
+     */
+    public void removeChangeListener(ITransformChangeListener listener) {
+        changeListeners.remove(listener);
+    }
+
+    /**
+     * remove all change listeners
+     */
+    public void removeAllChangeListeners() {
+        changeListeners.clear();
+    }
+
+    /**
+     * fire the change listeners
+     */
+    public void fireHasChanged() {
+        for (ITransformChangeListener changeListener : changeListeners) {
+            changeListener.hasChanged(this);
+        }
+    }
+
+    /**
+     * gets the dimensions of the bounding box in device coordinates
+     *
+     * @return preferred size
+     */
+    public Dimension getPreferredSize() {
+        Polygon rect = w2d(coordRect);
+        return new Dimension((int) Math.round(rect.getBounds().getWidth() + getLeftMargin() + getRightMargin()),
+                (int) Math.round(rect.getBounds().getHeight() + getBottomMargin() + getTopMargin()));
+    }
+
+    /**
+     * gets the preferred rectangle in device coordinates
+     *
+     * @return preferred size
+     */
+    public Rectangle getPreferredRect() {
+        Rectangle rect = w2d(coordRect).getBounds();
+        rect.setRect(rect.x - getLeftMargin(), rect.y - getTopMargin(),
+                (int) rect.getBounds().getWidth() + getLeftMargin() + getRightMargin(),
+                (int) rect.getBounds().getHeight() + getBottomMargin() + getTopMargin());
+        return rect;
+    }
+
+
+    public int getBottomMargin() {
+        return bottomMargin;
+    }
+
+    public void setBottomMargin(int bottomMargin) {
+        this.bottomMargin = bottomMargin;
+    }
+
+    public int getLeftMargin() {
+        return leftMargin;
+    }
+
+    public void setLeftMargin(int leftMargin) {
+        this.leftMargin = leftMargin;
+    }
+
+    public int getRightMargin() {
+        return rightMargin;
+    }
+
+    public void setRightMargin(int rightMargin) {
+        this.rightMargin = rightMargin;
+    }
+
+    public int getTopMargin() {
+        return topMargin;
+    }
+
+    public void setTopMargin(int topMargin) {
+        this.topMargin = topMargin;
+    }
+
+    /**
+     * Return a point that is contained in the area the world that is
+     * currently mapped onto the device rectangle.
+     *
+     * @return a random point in the currently visible world
+     */
+    public Point2D getRandomVisibleLocation() {
+        if (rand == null)
+            rand = new Random();
+        return new Point2D.Double
+                (coordRect.getX() + rand.nextDouble() * coordRect.getWidth(),
+                        coordRect.getY() + rand.nextDouble() * coordRect.getHeight());
+    }
+
+    /**
+     * gets a copy of the current world rectangle
+     *
+     * @return world rectangle
+     */
+    public Rectangle2D getWorldRect() {
+        return (Rectangle2D) rotatedCoordRect.clone();
+    }
+
+    /**
+     * set scale so that  the rotated coordinate rectangle has the given size
+     *
+     * @param size
+     */
+    public void fitToSize(Dimension size) {
+        fitToSize(rotatedCoordRect, size);
+    }
+
+    /**
+     * zoom such that worldRect fits exactly into a device of the given size
+     *
+     * @param worldRect the worldrectangle to fit
+     * @param size      the device size to fit into
+     */
+    public void fitToSize(Rectangle2D worldRect, Dimension size) {
+        if (rotatedCoordRect.getWidth() == 0)
+            scaleX = 1;
+        else if (size.getWidth() - getLeftMargin() - getRightMargin() > 0)
+            scaleX = (size.getWidth() - getLeftMargin() - getRightMargin()) / worldRect.getWidth();
+        else
+            scaleX = size.getWidth() / rotatedCoordRect.getWidth();
+        if (rotatedCoordRect.getHeight() == 0)
+            scaleY = 1;
+        else if (size.getHeight() - getTopMargin() - getBottomMargin() > 0)
+            scaleY = (size.getHeight() - getTopMargin() - getBottomMargin()) / worldRect.getHeight();
+        else
+            scaleY = size.getHeight() / rotatedCoordRect.getHeight();
+
+        if (lockXYScale) {
+            scaleX = scaleY = Math.min(scaleX, scaleY);
+        }
+        if (scaleX == 0 && scaleY == 0)
+            scaleX = scaleY = 1;
+        fireHasChanged();
+    }
+
+
+    /**
+     * transforms a world polygon into a device polygon
+     *
+     * @param wp
+     * @return device polygon
+     */
+    public Polygon w2d(PolygonDouble wp) {
+        if (wp.npoints == 0)
+            return new Polygon();
+
+        java.util.List<Point> list = new LinkedList<>();
+
+        double prevXw = wp.xpoints[0];
+        double prevYw = wp.ypoints[0];
+        Point prevPtd = w2d(prevXw, prevYw);
+
+        list.add(prevPtd);
+
+        for (int i = 1; i < wp.npoints; i++) {
+            double currentXw = wp.xpoints[i];
+            double currentYw = wp.ypoints[i];
+            Point currentPtd = w2d(currentXw, currentYw);
+            double dist = prevPtd.distance(currentPtd);
+            int count = (int) (dist / 10);
+            if (count > 0) {
+                double dX = (currentXw - prevXw) / count;
+                double dY = (currentYw - prevYw) / count;
+
+                for (int j = 1; j < count; j++)
+                    list.add(w2d(prevXw + j * dX, prevYw + j * dY));
+            }
+            list.add(currentPtd);
+            prevXw = currentXw;
+            prevYw = currentYw;
+            prevPtd = currentPtd;
+
+        }
+        Polygon polygon = new Polygon();
+        for (Point apt : list) {
+            polygon.addPoint(apt.x, apt.y);
+        }
+        return polygon;
+    }
+
+    /**
+     * gets the magnifier
+     *
+     * @return magnifier
+     */
+    public Magnifier getMagnifier() {
+        return magnifier;
+    }
+
+    /**
+     * adjusts angle so that it is either north, south, east or west
+     */
+    public void adjustAngleToNorthSouthEastWest() {
+        double newAngle = getAngle();
+        if (newAngle >= 0.25 * Math.PI && newAngle < 0.75 * Math.PI) // north
+        {
+            newAngle = 0.5 * Math.PI;
+        } else if (newAngle >= 0.75 * Math.PI && newAngle < 1.25 * Math.PI) // west
+        {
+            newAngle = Math.PI;
+        } else if (newAngle >= 1.25 * Math.PI && newAngle < 1.75 * Math.PI) // south
+        {
+            newAngle = 1.5 * Math.PI;
+        } else // east
+        {
+            newAngle = 0;
+        }
+        if (newAngle != getAngle())
+            setAngle(newAngle);
+    }
+}
diff --git a/src/jloda/graphview/ViewBase.java b/src/jloda/graphview/ViewBase.java
new file mode 100644
index 0000000..f927204
--- /dev/null
+++ b/src/jloda/graphview/ViewBase.java
@@ -0,0 +1,397 @@
+/**
+ * ViewBase.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.graphview;
+
+import jloda.util.Basic;
+import jloda.util.Geometry;
+
+import java.awt.*;
+
+/**
+ * class of stuff common both to a NodeView and an EdgeView
+ * Daniel Huson, 2.2006
+ */
+public abstract class ViewBase {
+    public static final Stroke NORMAL_STROKE = new BasicStroke(1);
+    public static final Stroke HEAVY_STROKE = new BasicStroke(2);
+
+    protected Color labelColor = Color.black;
+    protected Color labelBackgroundColor = null;
+    protected Font font = null;
+    protected int dxLabel = 0; //6
+    protected int dyLabel = 0;    // 5
+    protected float labelAngle = 0;
+    protected byte labelLayout = WEST;
+    protected String label;
+    protected boolean labelVisible = true;
+    protected boolean enabled = true;
+    protected Dimension labelSize = null;
+    /**
+     * Label positions
+     */
+    public static final byte NORTH = 1;
+    public static final byte NORTHWEST = 2;
+    public static final byte WEST = 3;
+    public static final byte SOUTHWEST = 4;
+    public static final byte SOUTH = 5;
+    public static final byte SOUTHEAST = 6;
+    public static final byte EAST = 7;
+    public static final byte NORTHEAST = 8;
+    public static final byte USER = 9;
+    public static final byte LAYOUT = 10;
+    public static final byte CENTRAL = 11;
+    public static final byte RADIAL = 12;
+    public static final byte MAXLAYOUT = RADIAL;
+
+    public static final Color DISABLED_COLOR = Color.GRAY;
+    protected byte linewidth = 1;
+    protected Color fgColor = Color.black;
+
+    /**
+     * Gets the label.
+     *
+     * @return label String
+     */
+    public String getLabel() {
+        return label;
+    }
+
+    /**
+     * copy
+     *
+     * @param src
+     */
+    public void copy(ViewBase src) {
+        setLabelColor(src.getLabelColor());
+        setLabelBackgroundColor(src.getLabelBackgroundColor());
+        setFont(src.getFont());
+        setColor(src.getColor());
+        this.dxLabel = src.dxLabel;
+        this.dyLabel = src.dyLabel;
+        this.labelAngle = src.labelAngle;
+        labelLayout = src.labelLayout;
+        this.label = src.label;
+        this.labelVisible = src.labelVisible;
+        this.enabled = src.enabled;
+        this.labelSize = src.labelSize;
+        linewidth = src.linewidth;
+    }
+
+
+    /**
+     * Gets the label color.
+     *
+     * @return labelcol Color
+     */
+    public Color getLabelColor() {
+        return labelColor;
+    }
+
+    /**
+     * Gets the label background color.
+     *
+     * @return labelcol Color
+     */
+    public Color getLabelBackgroundColor() {
+        return labelBackgroundColor;
+    }
+
+    /**
+     * set the size of the label rect in device coordinates
+     *
+     * @param size
+     */
+    public void setLabelSize(Dimension size) {
+        labelSize = size;
+    }
+
+    /**
+     * set the size of the label rect in device coordinates
+     *
+     * @param gc graphics context
+     */
+    public void setLabelSize(Graphics gc) {
+        setLabelSize(Basic.getStringSize(gc, getLabel(), font).getSize());
+    }
+
+    /**
+     * gets the set label size
+     *
+     * @return label size
+     */
+    public Dimension getLabelSize() {
+        return labelSize;
+    }
+
+    /**
+     * gets the font
+     *
+     * @return font used for drawing label, or null, if default is to be used
+     */
+    public Font getFont() {
+        return font;
+    }
+
+    /**
+     * sets the font
+     *
+     * @param font
+     */
+    public void setFont(Font font) {
+        this.font = font;
+    }
+
+    /**
+     * Sets the label.
+     *
+     * @param a String
+     */
+    public void setLabel(String a) {
+        label = a;
+        labelSize = null;
+    }
+
+    /**
+     * gets the label layout
+     *
+     * @return the label position such as NORTH, etc
+     */
+    public byte getLabelLayout() {
+        return labelLayout;
+    }
+
+    /**
+     * sets the label layout, such as NORTH etc
+     *
+     * @param labelLayout
+     */
+    public void setLabelLayout(byte labelLayout) {
+        this.labelLayout = labelLayout;
+    }
+
+    /**
+     * sets the label layout to NORTH etc, approximating the given angle
+     *
+     * @param radian
+     */
+    public void setLabelLayoutFromAngle(double radian) {
+        final double PI_8 = Math.PI / 8.0;
+        radian = Geometry.moduloTwoPI(radian);
+        if (radian < PI_8)
+            setLabelLayout(EAST);
+        else if (radian < 3 * PI_8)
+            setLabelLayout(SOUTHEAST);
+        else if (radian < 5 * PI_8)
+            setLabelLayout(SOUTH);
+        else if (radian < 7 * PI_8)
+            setLabelLayout(SOUTHWEST);
+        else if (radian < 9 * PI_8)
+            setLabelLayout(WEST);
+        else if (radian < 11 * PI_8)
+            setLabelLayout(NORTHWEST);
+        else if (radian < 13 * PI_8)
+            setLabelLayout(NORTH);
+        else if (radian < 15 * PI_8)
+            setLabelLayout(NORTHEAST);
+        else
+            setLabelLayout(EAST);
+    }
+
+    /**
+     * Sets the label color.
+     *
+     * @param a Color
+     */
+    public void setLabelColor(Color a) {
+        labelColor = a;
+    }
+
+    /**
+     * Sets the label color.
+     *
+     * @param a Color
+     */
+    public void setLabelBackgroundColor(Color a) {
+        labelBackgroundColor = a;
+    }
+
+    /**
+     * Is the label visible ?
+     *
+     * @return is the label visible?
+     */
+    public boolean isLabelVisible() {
+        return labelVisible;
+    }
+
+    /**
+     * Set label visibility
+     *
+     * @param labelVisible
+     */
+    public void setLabelVisible(boolean labelVisible) {
+        this.labelVisible = labelVisible;
+    }
+
+    /**
+     * is label visible?
+     *
+     * @return true, if  visible
+     */
+
+    public boolean getLabelVisible() {
+        return labelVisible;
+    }
+
+    /**
+     * Sets the relative position of the label in device coordinates.
+     *
+     * @param x int
+     * @param y int
+     */
+    public void setLabelPositionRelative(int x, int y) {
+        if (labelLayout != USER && labelLayout != LAYOUT)
+            labelLayout = USER;
+        dxLabel = x;
+        dyLabel = y;
+    }
+
+    /**
+     * Sets the relative position of the label in device coordinates.
+     *
+     * @param apt
+     */
+    public void setLabelPositionRelative(Point apt) {
+        if (labelLayout != USER && labelLayout != LAYOUT)
+            labelLayout = USER;
+        dxLabel = apt.x;
+        dyLabel = apt.y;
+    }
+
+    /**
+     * gets the offset used in USER_POS
+     *
+     * @return offset in device coordinates
+     */
+    public Point getLabelOffset() {
+        return new Point(dxLabel, dyLabel);
+    }
+
+    /**
+     * sets the offset used by USER_POS layout, in device coordinates
+     *
+     * @param offset
+     */
+    public void setLabelOffset(Point offset) {
+        dxLabel = offset.x;
+        dyLabel = offset.y;
+    }
+
+    /**
+     * gets the angle at which the label will be drawn
+     *
+     * @return angle
+     */
+    public float getLabelAngle() {
+        return labelAngle;
+    }
+
+    /**
+     * sets the angle at which label will be drawn
+     *
+     * @param labelAngle
+     */
+    public void setLabelAngle(float labelAngle) {
+        this.labelAngle = (float) Geometry.moduloTwoPI(labelAngle);
+    }
+
+    /**
+     * get the label rectangle
+     *
+     * @param trans
+     * @return rectangle
+     */
+    abstract public Rectangle getLabelRect(Transform trans);
+
+    /**
+     * gets the label shape
+     *
+     * @param trans
+     * @return shape of label
+     */
+    abstract public Shape getLabelShape(Transform trans);
+
+    /**
+     * is this node or edge enabled? If not, it will be drawn in grey
+     *
+     * @return true, if enabled
+     */
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    /**
+     * enable or disable this node or edge
+     *
+     * @param enabled
+     */
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    /**
+     * Gets the line width.
+     *
+     * @return linewidth int
+     */
+    public int getLineWidth() {
+        return linewidth;
+    }
+
+    /**
+     * Sets the line width.
+     *
+     * @param a int
+     */
+    public void setLineWidth(byte a) {
+        if (a < 0)
+            a = Byte.MAX_VALUE;
+        linewidth = a;
+    }
+
+    /**
+     * Gets the foreground color.
+     *
+     * @return fgcol Color the foreground color
+     */
+    public Color getColor() {
+        return fgColor;
+    }
+
+    /**
+     * Sets the foreground color.
+     *
+     * @param a Color
+     */
+    public void setColor(Color a) {
+        fgColor = a;
+    }
+
+}
diff --git a/src/jloda/gui/About.java b/src/jloda/gui/About.java
new file mode 100644
index 0000000..1dfc16e
--- /dev/null
+++ b/src/jloda/gui/About.java
@@ -0,0 +1,275 @@
+/**
+ * About.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import jloda.util.Basic;
+import jloda.util.ProgramProperties;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.image.BufferedImage;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * splashes an about window on the screen
+ *
+ * @author huson
+ *         Date: 11-Feb-2004
+ */
+public class About {
+    private String versionString;
+    static Point versionStringOffset = new Point(20, 20);
+    private final BufferedImage aboutImage;
+    private final JDialog aboutDialog;
+    boolean hasPainted = false;
+    private String additionalString;
+    private int additionalStringVerticalPosition = 100;
+
+    static private About instance=null;
+
+    /**
+     * get the current about window
+     * @return about window or null
+     */
+    public static About getAbout() {
+        return instance;
+    }
+
+    /**
+     * set the current about window
+     * @param packageName
+     * @param fileName
+     * @param version
+     */
+    public static void setAbout (String packageName, String fileName, final String version){
+        setAbout(packageName, fileName, version, JDialog.HIDE_ON_CLOSE);
+
+    }
+
+    /**
+     * set the current about window
+     * @param packageName
+     * @param fileName
+     * @param version
+     * @param closeOperation
+     */
+    public static void setAbout(String packageName, String fileName, String version, int closeOperation) {
+    instance=new About(packageName,fileName,version,closeOperation);
+    }
+
+    /**
+     * constructs an about message for splashing the screen
+     *
+     * @param packageName    name of package containing image file
+     * @param fileName       name of image file
+     * @param version0       version string to include in message
+     * @param closeOperation default close operation, e.g. JDialog.HIDE_ON_CLOSE
+     */
+    private About(String packageName, String fileName, String version0, int closeOperation) {
+        this.versionString = version0;
+
+        BufferedImage image = null;
+        try {
+            image = ImageIO.read(Basic.getBasicClassLoader().getResourceAsStream(packageName.replaceAll("\\.", "/") + "/" + fileName));
+        } catch (Exception e) {
+            Basic.caught(e);
+            //new Alert("ERROR: couldn't find SPLASH screen, corrupt installation?");
+        }
+        aboutImage = image;
+
+        //if this fails with null, check whether the resources are in place
+//                File file = Basic.getFileInPackage(packageName, fileName);
+//                if (file == null || file.isFile() == false) {
+//                    return;
+//                }
+//                // system.err.println("Found file: "+file);
+//
+//                if (aboutImage == null)
+//                    aboutImage = ImageIO.read(file);
+        aboutDialog = new JDialog(null, Dialog.ModalityType.APPLICATION_MODAL);
+        aboutDialog.setUndecorated(true);
+        aboutDialog.setTitle("About " + versionString);
+        aboutDialog.setDefaultCloseOperation(closeOperation);
+        int width = (aboutImage != null ? aboutImage.getWidth() : 200);
+        int height = (aboutImage != null ? aboutImage.getHeight() : 200);
+        aboutDialog.setSize(width, height);
+        Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
+        aboutDialog.setLocation((d.width - width) / 2, (d.height - height) / 2);
+
+        JPanel pane = new JPanel();
+        pane.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+        pane.setLayout(new BorderLayout());
+        pane.add(new Component() {
+            public void paint(Graphics gc) {
+                gc.setFont(new Font(Font.DIALOG, Font.PLAIN, 11));
+                if (aboutImage != null)
+                    gc.drawImage(aboutImage, 0, 0, this);
+                else {
+                    gc.setColor(Color.WHITE);
+                    ((Graphics2D) gc).fill(this.getBounds());
+                }
+                gc.setColor(Color.BLACK);
+                if (versionString != null) {
+                    String[] tokens = Basic.split(versionString, '\n');
+                    for (int i = 0; i < tokens.length; i++) {
+                        gc.drawString(tokens[i], versionStringOffset.x, versionStringOffset.y + 14 * i);
+                    }
+                }
+                if (additionalString != null) {
+                    Dimension labelSize = Basic.getStringSize(gc, additionalString, gc.getFont()).getSize();
+                    gc.drawString(additionalString, (getWidth() - labelSize.width) / 2, additionalStringVerticalPosition);
+                }
+                if (!hasPainted) {
+                    hasPainted = true;
+                    synchronized (aboutDialog) {
+                        aboutDialog.notifyAll();
+                    }
+                }
+            }
+        }, BorderLayout.CENTER);
+
+        aboutDialog.getContentPane().setLayout(new BorderLayout());
+        aboutDialog.getContentPane().add(pane, BorderLayout.CENTER);
+
+        aboutDialog.addMouseListener(new MouseAdapter() {
+            public void mouseClicked(MouseEvent event) {
+                hideSplash();
+            }
+        });
+        aboutDialog.addKeyListener(new KeyAdapter() {
+            public void keyPressed(KeyEvent event) {
+                hideSplash();
+            }
+        });
+
+        aboutDialog.addWindowFocusListener(new WindowFocusListener() {
+            public void windowGainedFocus(WindowEvent event) {
+            }
+
+            public void windowLostFocus(WindowEvent event) {
+                if (false) hideSplash();
+            }
+        });
+    }
+
+    /**
+     * shows the about message on the screen
+     */
+    public void showAboutModal() {
+        ProgramProperties.checkState();
+
+        if (aboutDialog != null) {
+            hasPainted = false;
+            aboutDialog.setModal(true);
+            aboutDialog.setVisible(true);
+            aboutDialog.toFront();
+            aboutDialog.setAlwaysOnTop(true);
+        } else
+            JOptionPane.showMessageDialog(null, versionString);
+    }
+
+    /**
+     * splashs the about message on the screen until hideAbout is called
+     */
+    public void showAbout() {
+        if (aboutDialog != null) {
+            hasPainted = false;
+            aboutDialog.setModal(false);
+            aboutDialog.setVisible(true);
+            aboutDialog.toFront();
+            aboutDialog.setAlwaysOnTop(true);
+
+            // give user chance to see splash screen:
+            while (!hasPainted) {
+                synchronized (aboutDialog) {
+                    try {
+                        aboutDialog.wait();
+                    } catch (Exception ex) {
+                        Basic.caught(ex);
+                        break;
+                    }
+                }
+            }
+            hideAfter(4000);
+        }
+    }
+
+    /**
+     * hide the splash screen again
+     */
+    public void hideSplash() {
+        if (aboutDialog != null)
+            aboutDialog.setVisible(false);
+    }
+
+    /**
+     * set the version string offset
+     */
+    static public void setVersionStringOffset(int x, int y) {
+        versionStringOffset = new Point(x, y);
+    }
+
+    public void setVersion(String version) {
+        this.versionString = version;
+    }
+
+    public static boolean isSet() {
+        return instance!=null;
+    }
+
+    public String getAdditionalString() {
+        return additionalString;
+    }
+
+    public void setAdditionalString(String additionalString) {
+        this.additionalString = additionalString;
+    }
+
+    public int getAdditionalStringVerticalPosition() {
+        return additionalStringVerticalPosition;
+    }
+
+    public void setAdditionalStringVerticalPosition(int additionalStringVerticalPosition) {
+        this.additionalStringVerticalPosition = additionalStringVerticalPosition;
+    }
+
+    /**
+     * hide after given number of milliseconds
+     *
+     * @param millis time to wait before hiding
+     */
+    public void hideAfter(final int millis) {
+        final ExecutorService executor = Executors.newSingleThreadExecutor();
+        executor.execute(new Runnable() {
+            public void run() {
+                try {
+                    Thread.sleep(millis);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                hideSplash();
+                executor.shutdown();
+            }
+        });
+    }
+}
diff --git a/src/jloda/gui/ActionJList.java b/src/jloda/gui/ActionJList.java
new file mode 100644
index 0000000..6e4c6f4
--- /dev/null
+++ b/src/jloda/gui/ActionJList.java
@@ -0,0 +1,73 @@
+/**
+ * ActionJList.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import javax.swing.*;
+import java.awt.event.*;
+import java.util.List;
+
+/*
+** sends ACTION_PERFORMED event for double-click
+** and ENTER key
+*/
+
+public class ActionJList<E> extends JList<E> {
+    ActionListener al;
+
+    public ActionJList(ListModel<E> dataModel) {
+        super(dataModel);
+
+        addMouseListener(new MouseAdapter() {
+            public void mouseClicked(MouseEvent me) {
+                if (al == null) return;
+                final List<E> selectedValuesList = getSelectedValuesList();
+                if (selectedValuesList.size() != 1) return;
+                if (me.getClickCount() == 2) {
+                    //System.out.println("Sending ACTION_PERFORMED to ActionListener");
+                    al.actionPerformed(new ActionEvent(this,
+                            ActionEvent.ACTION_PERFORMED,
+                            selectedValuesList.get(0).toString()));
+                    me.consume();
+                }
+            }
+        });
+
+        addKeyListener(new KeyAdapter() {
+            public void keyReleased(KeyEvent ke) {
+                if (al == null) return;
+                final List<E> selectedValuesList = getSelectedValuesList();
+                if (selectedValuesList.size() != 1) return;
+                if (ke.getKeyCode() == KeyEvent.VK_ENTER) {
+                    //System.out.println("Sending ACTION_PERFORMED to ActionListener");
+                    al.actionPerformed(new ActionEvent(this,
+                            ActionEvent.ACTION_PERFORMED,
+                            selectedValuesList.get(0).toString()));
+                    ke.consume();
+                }
+            }
+        });
+        //this.setSelectedIndex(0); //All are selected when we open the pane
+        this.setSelectedIndex(-1); //None are selected when we open the pane
+    }
+
+    public void addActionListener(ActionListener al) {
+        this.al = al;
+    }
+}
diff --git a/src/jloda/gui/AppleStuff.java b/src/jloda/gui/AppleStuff.java
new file mode 100644
index 0000000..baf2c31
--- /dev/null
+++ b/src/jloda/gui/AppleStuff.java
@@ -0,0 +1,109 @@
+/**
+ * AppleStuff.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+
+import com.apple.eawt.*;
+
+import javax.swing.*;
+
+/**
+ * Apple specific stuff
+ * Daniel Huson, 3.2014
+ */
+public class AppleStuff {
+    static private AppleStuff instance;
+    private final Application application;
+    private boolean isQuitDefined;
+    private boolean isAboutDefined;
+    private boolean isPreferencesDefined;
+
+    /**
+     * constructor
+     */
+    private AppleStuff() {
+        application = Application.getApplication();
+    }
+
+    /**
+     * get instance
+     *
+     * @return instance
+     */
+    public static AppleStuff getInstance() {
+        if (instance == null)
+            instance = new AppleStuff();
+        return instance;
+    }
+
+    /**
+     * sets the quit action
+     *
+     * @param action
+     */
+    public void setQuitAction(final Action action) {
+        isQuitDefined = true;
+        application.setQuitHandler(new QuitHandler() {
+            @Override
+            public void handleQuitRequestWith(AppEvent.QuitEvent quitEvent, QuitResponse quitResponse) {
+                action.actionPerformed(null);
+                quitResponse.cancelQuit();
+            }
+        });
+    }
+
+    /**
+     * set the about action
+     *
+     * @param action
+     */
+    public void setAboutAction(final Action action) {
+        isAboutDefined = true;
+        application.setAboutHandler(new AboutHandler() {
+            @Override
+            public void handleAbout(AppEvent.AboutEvent aboutEvent) {
+                action.actionPerformed(null);
+            }
+        });
+    }
+
+    public void setPreferencesAction(final Action action) {
+        isPreferencesDefined = true;
+        application.setPreferencesHandler(new PreferencesHandler() {
+            @Override
+            public void handlePreferences(AppEvent.PreferencesEvent preferencesEvent) {
+                action.actionPerformed(null);
+            }
+        });
+    }
+
+    public boolean isQuitDefined() {
+        return isQuitDefined;
+    }
+
+    public boolean isAboutDefined() {
+        return isAboutDefined;
+    }
+
+    public boolean isPreferencesDefined() {
+        return isPreferencesDefined;
+    }
+
+}
diff --git a/src/jloda/gui/ChooseColorDialog.java b/src/jloda/gui/ChooseColorDialog.java
new file mode 100644
index 0000000..a6722d5
--- /dev/null
+++ b/src/jloda/gui/ChooseColorDialog.java
@@ -0,0 +1,67 @@
+/**
+ * ChooseColorDialog.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import jloda.util.Single;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+/**
+ * choose a color
+ * Daniel Huson, 4.2011
+ */
+public class ChooseColorDialog {
+    public final static JColorChooser colorChooser = new JColorChooser();
+
+    /**
+     * show a choose color dialog
+     *
+     * @param parent
+     * @param title
+     * @param defaultColor
+     * @return color chosen or null
+     */
+    public static Color showChooseColorDialog(JFrame parent, String title, Color defaultColor) {
+        if (defaultColor != null)
+            colorChooser.setColor(defaultColor);
+
+        final Single<Color> result = new Single<>();
+
+        ActionListener okListener = new ActionListener() {
+            public void actionPerformed(ActionEvent actionEvent) {
+                result.set(colorChooser.getColor());
+            }
+        };
+
+        ActionListener cancelListener = new ActionListener() {
+            public void actionPerformed(ActionEvent actionEvent) {
+                result.set(null);
+            }
+        };
+
+        JDialog chooser = JColorChooser.createDialog(parent, title, true, colorChooser, okListener, cancelListener);
+        chooser.setVisible(true);
+
+        return result.get();
+    }
+}
diff --git a/src/jloda/gui/ChooseFileDialog.java b/src/jloda/gui/ChooseFileDialog.java
new file mode 100644
index 0000000..841109b
--- /dev/null
+++ b/src/jloda/gui/ChooseFileDialog.java
@@ -0,0 +1,299 @@
+/**
+ * ChooseFileDialog.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import jloda.util.Basic;
+import jloda.util.ProgramProperties;
+
+import javax.swing.*;
+import javax.swing.filechooser.FileFilter;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.Arrays;
+import java.util.LinkedList;
+
+/**
+ * file chooser
+ * Daniel Huson, 9.2008
+ */
+public class ChooseFileDialog {
+    /**
+     * choose file to open dialog
+     *
+     * @param parent
+     * @param lastOpenFile
+     * @param fileFilter
+     * @param fileNameFilter
+     * @param event
+     * @param message
+     * @return file or null
+     */
+    public static File chooseFileToOpen(Component parent, File lastOpenFile, FileFilter fileFilter, FilenameFilter fileNameFilter, ActionEvent event, String message) {
+        File file = null;
+
+        JFrame frame = null;
+        if (parent != null && parent instanceof JFrame)
+            frame = (JFrame) parent;
+        if (frame != null && frame.getJMenuBar() != null) {
+            // frame.getJMenuBar().setEnabled(false); // todo: to do this we need to remember the state of all menu items and then reenable them below...
+        }
+
+        try {
+            if (ProgramProperties.isMacOS() && (event == null || (event.getModifiers() & Event.SHIFT_MASK) == 0)) {
+                //Use native file dialog on mac
+                java.awt.FileDialog dialog;
+                if (parent != null && parent instanceof JFrame)
+                    dialog = new java.awt.FileDialog((JFrame) parent, message, java.awt.FileDialog.LOAD);
+                else if (parent != null && parent instanceof Dialog)
+                    dialog = new java.awt.FileDialog((Dialog) parent, message, java.awt.FileDialog.LOAD);
+                else
+                    dialog = new java.awt.FileDialog((JFrame) null, message, java.awt.FileDialog.LOAD);
+                if (parent != null)
+                    dialog.setLocationRelativeTo(parent);
+                //dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
+                dialog.setModal(true);
+                if (fileNameFilter != null)
+                    dialog.setFilenameFilter(fileNameFilter);
+                if (lastOpenFile != null) {
+                    dialog.setDirectory(lastOpenFile.getParent());
+                    dialog.setFile(lastOpenFile.getName());
+                }
+                dialog.setVisible(true);
+                if (dialog.getFile() != null) {
+                    file = new File(dialog.getDirectory(), dialog.getFile());
+                }
+            } else {
+                JFileChooser chooser;
+                try {
+                    chooser = new JFileChooser(lastOpenFile);
+                    chooser.setSelectedFile(lastOpenFile);
+                } catch (Exception ex) {
+                    chooser = new JFileChooser();
+                }
+                chooser.setAcceptAllFileFilterUsed(true);
+                if (fileFilter != null)
+                    chooser.setFileFilter(fileFilter);
+
+                int result = chooser.showOpenDialog(parent);
+                if (result == JFileChooser.APPROVE_OPTION) {
+                    file = chooser.getSelectedFile();
+                }
+            }
+        } finally {
+            if (frame != null && frame.getJMenuBar() != null) {
+                // frame.getJMenuBar().setEnabled(true);
+            }
+        }
+        return file;
+    }
+
+    /**
+     * choose file to open dialog
+     *
+     * @param parent
+     * @param lastOpenFile
+     * @param fileFilter
+     * @param fileNameFilter
+     * @param event
+     * @param message
+     * @return file or null
+     */
+    public static java.util.List<File> chooseFilesToOpen(Component parent, File lastOpenFile, FileFilter fileFilter, FilenameFilter fileNameFilter, ActionEvent event, String message) {
+        final LinkedList<File> list = new LinkedList<>();
+
+        final JFrame frame;
+        if (parent != null && parent instanceof JFrame)
+            frame = (JFrame) parent;
+        else
+            frame = null;
+
+        if (ProgramProperties.isMacOS() && (event == null || (event.getModifiers() & Event.SHIFT_MASK) == 0)) {
+            //Use native file dialog on mac
+            java.awt.FileDialog dialog;
+            if (parent != null && parent instanceof JFrame)
+                dialog = new java.awt.FileDialog((JFrame) parent, message, java.awt.FileDialog.LOAD);
+            else if (parent != null && parent instanceof Dialog)
+                dialog = new java.awt.FileDialog((Dialog) parent, message, java.awt.FileDialog.LOAD);
+            else
+                dialog = new java.awt.FileDialog((JFrame) null, message, java.awt.FileDialog.LOAD);
+            dialog.setMultipleMode(true);
+            if (frame != null)
+                dialog.setLocationRelativeTo(frame);
+            //dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
+            dialog.setModal(true);
+            if (fileNameFilter != null)
+                dialog.setFilenameFilter(fileNameFilter);
+            if (lastOpenFile != null) {
+                dialog.setDirectory(lastOpenFile.getParent());
+                dialog.setFile(lastOpenFile.getName());
+            }
+            dialog.setVisible(true);
+            list.addAll(Arrays.asList(dialog.getFiles()));
+
+            dialog.setVisible(false);
+        } else {
+            JFileChooser chooser;
+            try {
+                chooser = new JFileChooser(lastOpenFile);
+                chooser.setSelectedFile(lastOpenFile);
+            } catch (Exception ex) {
+                chooser = new JFileChooser();
+            }
+            chooser.setMultiSelectionEnabled(true);
+            chooser.setAcceptAllFileFilterUsed(true);
+            if (fileFilter != null)
+                chooser.setFileFilter(fileFilter);
+
+            int result = chooser.showOpenDialog(parent);
+            if (result == JFileChooser.APPROVE_OPTION) {
+                list.addAll(Arrays.asList(chooser.getSelectedFiles()));
+            }
+        }
+
+        return list;
+    }
+
+    /**
+     * choose file to save dialog
+     *
+     * @param frame
+     * @param lastOpenFile
+     * @param fileFilter
+     * @param fileNameFilter
+     * @param event
+     * @param message
+     * @return file or null
+     */
+    public static File chooseFileToSave(JFrame frame, File lastOpenFile, FileFilter fileFilter, FilenameFilter fileNameFilter, ActionEvent event, String message) {
+        return chooseFileToSave(frame, lastOpenFile, fileFilter, fileNameFilter, event, message, null);
+    }
+
+    /**
+     * choose file to save dialog
+     *
+     * @param parent
+     * @param lastOpenFile
+     * @param fileFilter
+     * @param fileNameFilter
+     * @param event
+     * @param message
+     * @param defaultSuffix  .suff or null
+     * @return file or null
+     */
+    public static File chooseFileToSave(Component parent, File lastOpenFile, FileFilter fileFilter, FilenameFilter fileNameFilter, ActionEvent event, String message,
+                                        String defaultSuffix) {
+        if (defaultSuffix != null && !defaultSuffix.startsWith("."))
+            defaultSuffix = "." + defaultSuffix;
+        File file = null;
+
+        boolean okToWrite = false;
+        while (!okToWrite) {
+            if (ProgramProperties.isMacOS() && (event == null || (event.getModifiers() & Event.SHIFT_MASK) == 0)) {
+                //Use native file dialog on mac
+                java.awt.FileDialog dialog;
+                if (parent != null && parent instanceof JFrame)
+                    dialog = new java.awt.FileDialog((JFrame) parent, message, java.awt.FileDialog.SAVE);
+                else if (parent != null && parent instanceof Dialog)
+                    dialog = new java.awt.FileDialog((Dialog) parent, message, java.awt.FileDialog.SAVE);
+                else
+                    dialog = new java.awt.FileDialog((JFrame) null, message, java.awt.FileDialog.SAVE);
+
+                if (fileNameFilter != null)
+                    dialog.setFilenameFilter(fileNameFilter);
+                if (lastOpenFile != null) {
+                    if (lastOpenFile.getParentFile() != null && lastOpenFile.getParentFile().exists())
+                        dialog.setDirectory(lastOpenFile.getParent());
+                    //if (lastOpenFile.exists())
+                    dialog.setFile(lastOpenFile.getName());
+                }
+                dialog.setVisible(true);
+                if (dialog.getFile() != null) {
+                    file = new File(dialog.getDirectory(), dialog.getFile());
+                    okToWrite = true;
+                    if (defaultSuffix != null) {
+                        String suffix = Basic.getSuffix(file.getName());
+                        if (suffix == null || suffix.equals(file.getName())) {
+                            file = new File(file.getParent(), file.getName() + defaultSuffix);
+                            // todo: don't seem to need this:
+                                /*
+                                if (file.exists()) {
+                                    int result = JOptionPane.showConfirmDialog(parent, "This file already exists. Overwrite the existing file?",
+                                            "Save File", JOptionPane.YES_NO_OPTION);
+                                    if (result != JOptionPane.YES_OPTION)
+                                        okToWrite = false;
+                                }
+                                */
+                        }
+                    }
+                } else
+                    return file;
+            } else {
+                JFileChooser chooser;
+                try {
+                    chooser = new JFileChooser(lastOpenFile);
+                    chooser.setSelectedFile(lastOpenFile);
+                } catch (Exception ex) {
+                    chooser = new JFileChooser();
+                }// Add the FileFilter for the Import Plugins
+
+                chooser.setAcceptAllFileFilterUsed(true);
+                if (fileFilter != null)
+                    chooser.setFileFilter(fileFilter);
+
+                int result = chooser.showSaveDialog(parent);
+                if (result != JFileChooser.APPROVE_OPTION) {
+                    System.err.println("Save canceled");
+                    return null;
+                }
+                file = chooser.getSelectedFile();
+                okToWrite = true;
+
+                if (defaultSuffix != null) {
+                    String suffix = Basic.getSuffix(file.getName());
+                    if (suffix == null || suffix.equals(file.getName())) {
+                        file = new File(file.getParent(), file.getName() + defaultSuffix);
+                    }
+                }
+                if (file.exists()) {
+                    switch (
+                            JOptionPane.showConfirmDialog(parent,
+                                    "This file already exists. Overwrite the existing file?",
+                                    "Save File",
+                                    JOptionPane.YES_NO_CANCEL_OPTION)) {
+                        case JOptionPane.YES_OPTION:
+                            okToWrite = true;
+                            break;
+                        case JOptionPane.NO_OPTION:
+                            okToWrite = false;
+                            break;
+                        case JOptionPane.CANCEL_OPTION:
+                            return file;
+                    }
+                } else
+                    okToWrite = true;
+
+            }
+        }
+
+        return file;
+    }
+}
diff --git a/src/jloda/gui/ChooseFontDialog.java b/src/jloda/gui/ChooseFontDialog.java
new file mode 100644
index 0000000..effcd23
--- /dev/null
+++ b/src/jloda/gui/ChooseFontDialog.java
@@ -0,0 +1,237 @@
+/**
+ * ChooseFontDialog.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import jloda.util.Basic;
+import jloda.util.Pair;
+import jloda.util.Single;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+/**
+ * choose a font
+ * Daniel Huson, 9.2012
+ */
+public class ChooseFontDialog {
+    static private Pair<Font, Color> result = null;
+
+    /**
+     * show a choose font dialog
+     *
+     * @param parent
+     * @param title
+     * @param defaultFont
+     * @return color chosen or null
+     */
+    public static Pair<Font, Color> showChooseFontDialog(JFrame parent, String title, Font defaultFont, Color defaultColor) {
+        final Single<Color> theColor = new Single<>(defaultColor);
+
+        final JDialog dialog = new JDialog(parent, title);
+        dialog.setLocationRelativeTo(parent);
+        dialog.setSize(new Dimension(500, 180));
+        dialog.setModal(true);
+        JPanel mainPanel = new JPanel();
+        mainPanel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
+        dialog.getContentPane().add(mainPanel);
+        mainPanel.setLayout(new BorderLayout());
+
+        // Top panel:
+        JPanel topPanel = new JPanel();
+        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.X_AXIS));
+        topPanel.setPreferredSize(new Dimension(1000, 50));
+        topPanel.setMaximumSize(new Dimension(1000, 50));
+        topPanel.add(new JLabel("Font:"));
+        final JComboBox fontNames = makeFontNames();
+        if (defaultFont != null)
+            fontNames.setSelectedItem(defaultFont.getFamily());
+        topPanel.add(fontNames);
+
+        topPanel.add(new JLabel("Size:"));
+        final JComboBox fontSizes = makeFontSizes();
+        if (defaultFont != null)
+            fontSizes.setSelectedItem("" + defaultFont.getSize());
+
+        topPanel.add(fontSizes);
+
+        final JCheckBox boldCBox = new JCheckBox("Bold");
+        if (defaultFont != null && defaultFont.isBold())
+            boldCBox.setSelected(true);
+
+        topPanel.add(boldCBox);
+
+        final JCheckBox italicCBox = new JCheckBox("Italic");
+        if (defaultFont != null && defaultFont.isItalic())
+            italicCBox.setSelected(true);
+
+        topPanel.add(italicCBox);
+        mainPanel.add(topPanel, BorderLayout.NORTH);
+
+        // middle panel:
+        JPanel middlePanel = new JPanel();
+        middlePanel.setLayout(new BorderLayout());
+
+        // text area in middle panel:
+        final JTextArea preview = new JTextArea();
+        preview.setEditable(true);
+        preview.setText("The quick brown fox jumps over the lazy dog");
+        preview.setFont(defaultFont);
+        preview.setBorder(BorderFactory.createBevelBorder(1));
+
+        fontNames.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent actionEvent) {
+                preview.setFont(getCurrentFont(fontNames, fontSizes, boldCBox, italicCBox));
+            }
+        });
+        fontSizes.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent actionEvent) {
+                preview.setFont(getCurrentFont(fontNames, fontSizes, boldCBox, italicCBox));
+            }
+        });
+        boldCBox.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent actionEvent) {
+                preview.setFont(getCurrentFont(fontNames, fontSizes, boldCBox, italicCBox));
+            }
+        });
+        italicCBox.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent actionEvent) {
+                preview.setFont(getCurrentFont(fontNames, fontSizes, boldCBox, italicCBox));
+            }
+        });
+        middlePanel.add(preview, BorderLayout.CENTER);
+
+        // color choice in middle panel:
+        JPanel colorPanel = new JPanel();
+        colorPanel.setLayout(new BoxLayout(colorPanel, BoxLayout.X_AXIS));
+
+        colorPanel.add(new JLabel("Color:"));
+
+        JRadioButton defaultColorButton = new JRadioButton(new AbstractAction("Default") {
+            public void actionPerformed(ActionEvent actionEvent) {
+                preview.setForeground(Color.BLACK);
+                theColor.set(null);
+            }
+        });
+        if (defaultColor != null)
+            preview.setForeground(defaultColor);
+        defaultColorButton.setSelected(defaultColor == null);
+        colorPanel.add(defaultColorButton);
+
+        final JRadioButton userColorButton = new JRadioButton();
+        userColorButton.setAction(new AbstractAction("Choose...") {
+            public void actionPerformed(ActionEvent actionEvent) {
+                if (!userColorButton.isSelected()) {
+                    preview.setForeground(Color.BLACK);
+                    theColor.set(null);
+                    preview.repaint();
+                } else {
+                    Color color = JColorChooser.showDialog(dialog, "Choose Font Color", preview.getForeground());
+                    if (color != null) {
+                        preview.setForeground(color);
+                        theColor.set(color);
+                        preview.repaint();
+                    }
+                }
+            }
+        });
+        colorPanel.add(userColorButton);
+        userColorButton.setSelected(defaultColor != null);
+
+        ButtonGroup group = new ButtonGroup();
+        group.add(defaultColorButton);
+        group.add(userColorButton);
+
+        middlePanel.add(colorPanel, BorderLayout.SOUTH);
+
+        mainPanel.add(middlePanel, BorderLayout.CENTER);
+
+        // bottom panel:
+        JPanel bottom = new JPanel();
+        bottom.setLayout(new BoxLayout(bottom, BoxLayout.X_AXIS));
+
+        bottom.add(Box.createHorizontalGlue());
+
+        JButton cancelButton = new JButton(new AbstractAction("Cancel") {
+            public void actionPerformed(ActionEvent actionEvent) {
+                result = null;
+                dialog.setVisible(false);
+            }
+        });
+        bottom.add(cancelButton);
+        dialog.getRootPane().setDefaultButton(cancelButton);
+
+        bottom.add(new JButton(new AbstractAction("Default") {
+            public void actionPerformed(ActionEvent actionEvent) {
+                Font font = Font.decode(null);
+                boldCBox.setSelected(font.isBold());
+                italicCBox.setSelected(font.isItalic());
+                fontNames.setSelectedItem(font.getFamily());
+                fontSizes.setSelectedItem("" + font.getSize());
+            }
+        }));
+
+        bottom.add(new JButton(new AbstractAction("Apply") {
+            public void actionPerformed(ActionEvent actionEvent) {
+                String name = fontNames.getSelectedItem().toString().trim();
+                int size = Basic.parseInt(fontSizes.getSelectedItem().toString().trim());
+                if (name != null && size > 0) {
+                    result = new Pair<>(getCurrentFont(fontNames, fontSizes, boldCBox, italicCBox), theColor.get());
+                }
+                dialog.setVisible(false);
+            }
+        }));
+
+        mainPanel.add(bottom, BorderLayout.SOUTH);
+
+        dialog.setVisible(true);
+        return result;
+    }
+
+    static private Font getCurrentFont(JComboBox fontNames, JComboBox fontSizes, JCheckBox boldCBox, JCheckBox italicCBox) {
+        String name = fontNames.getSelectedItem().toString().trim();
+        int size = Basic.parseInt(fontSizes.getSelectedItem().toString().trim());
+        if (name != null && size > 0) {
+            int style = 0;
+            if (boldCBox.isSelected())
+                style |= Font.BOLD;
+            if (italicCBox.isSelected())
+                style |= Font.ITALIC;
+            return new Font(name, style, size);
+        }
+        return Font.decode(null);
+    }
+
+    static private JComboBox makeFontNames() {
+        JComboBox box = new JComboBox(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames());
+        box.setMinimumSize(box.getPreferredSize());
+        return box;
+    }
+
+    static private JComboBox makeFontSizes() {
+        Object[] possibleValues = {"8", "10", "12", "14", "16", "18", "20", "22", "24", "26", "28", "32", "36", "40", "44"};
+        JComboBox box = new JComboBox(possibleValues);
+        box.setEditable(true);
+        box.setMinimumSize(box.getPreferredSize());
+        return box;
+    }
+
+}
diff --git a/src/jloda/gui/ColorTable.java b/src/jloda/gui/ColorTable.java
new file mode 100644
index 0000000..def3018
--- /dev/null
+++ b/src/jloda/gui/ColorTable.java
@@ -0,0 +1,179 @@
+/*
+ *  Copyright (C) 2015 Daniel H. Huson
+ *
+ *  (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package jloda.gui;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.util.Collection;
+
+/**
+ * a color table
+ * Created by huson on 1/31/16.
+ */
+public class ColorTable {
+    private final String name;
+    private final Color[] colors;
+
+    /**
+     * constructor
+     *
+     * @param name
+     * @param colors
+     */
+    public ColorTable(String name, Color... colors) {
+        this.name = name;
+        this.colors = colors;
+    }
+
+    /**
+     * constructor
+     *
+     * @param name
+     * @param colors
+     */
+    public ColorTable(String name, Collection<Color> colors) {
+        this.name = name;
+        this.colors = colors.toArray(new Color[colors.size()]);
+    }
+
+    /**
+     * get name of color scheme
+     *
+     * @return name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * get color
+     *
+     * @param i (is used modulo number of colors)
+     * @return color
+     */
+    public Color get(int i) {
+        return colors[Math.abs(i) % colors.length];
+    }
+
+    /**
+     * get the i-th color
+     *
+     * @param i
+     * @param alpha
+     * @return color
+     */
+    public Color getWithAlpha(int i, int alpha) {
+        Color color = get(i);
+        if (color.getRed() > 210 && color.getGreen() > 210 && color.getBlue() > 210)
+            color = color.darker();
+
+        if (color.getAlpha() == alpha)
+            return color;
+        else
+            return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
+    }
+
+    public Color[] getColors() {
+        return colors;
+    }
+
+    public int size() {
+        return colors.length;
+    }
+
+    /**
+     * gets the command needed to undo this command
+     *
+     * @return undo command
+     */
+    public String getUndo() {
+        return null;
+    }
+
+    /**
+     * make an icon for this color table
+     *
+     * @return icon
+     */
+    public ImageIcon makeIcon() {
+        final BufferedImage image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB);
+
+        int patchSize = 16 / (int) (1 + Math.sqrt(size()));
+
+        for (int x = 0; x < 16; x++)
+            for (int y = 0; y < 16; y++)
+                image.setRGB(x, y, Color.LIGHT_GRAY.getRGB());
+
+        int row = 0;
+        int col = 0;
+        for (Color color : getColors()) {
+            for (int y = 0; y < patchSize; y++) {
+
+                if (row + patchSize >= 16) {
+                    row = 0;
+                    col += patchSize;
+                }
+
+                for (int x = 0; x < patchSize; x++) {
+                    if (row + x < 16 && col + y < 16)
+                        image.setRGB(row + x, col + y, color.getRGB());
+                }
+            }
+            row += patchSize;
+        }
+        return new ImageIcon(image);
+    }
+
+    /**
+     * this is used in the node drawer of the main viewer
+     *
+     * @param count
+     * @param inverseLogMaxReads
+     * @return color on a log scale
+     */
+    public Color getColorLogScale(int count, double inverseLogMaxReads) {
+        int index = Math.max(1, Math.min(colors.length - 1, (int) Math.round(colors.length * Math.log(count + 1) * inverseLogMaxReads)));
+        return get(index);
+    }
+
+    /**
+     * this is used in the node drawer of the main viewer
+     *
+     * @param count
+     * @param inverseSqrtMaxReads
+     * @return color on a log scale
+     */
+    public Color getColorSqrtScale(int count, double inverseSqrtMaxReads) {
+        int index = Math.max(1, Math.min(colors.length - 1, (int) Math.round(colors.length * Math.sqrt(count) * inverseSqrtMaxReads)));
+        return get(index);
+    }
+
+    /**
+     * get color on linear scale
+     *
+     * @param count
+     * @return color
+     */
+    public Color getColor(int count, int maxCount) {
+        int index = Math.min(colors.length - 1, (count * colors.length) / maxCount);
+        return get(index);
+    }
+}
diff --git a/src/jloda/gui/ColorTableManager.java b/src/jloda/gui/ColorTableManager.java
new file mode 100644
index 0000000..cb0e81a
--- /dev/null
+++ b/src/jloda/gui/ColorTableManager.java
@@ -0,0 +1,221 @@
+/*
+ *  Copyright (C) 2015 Daniel H. Huson
+ *
+ *  (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package jloda.gui;
+
+import jloda.util.Basic;
+import jloda.util.ProgramProperties;
+
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * color tables
+ * Daniel Huson, 1.2016
+ */
+public class ColorTableManager {
+    private static final String DefaultColorTableName = "Fews8";
+    private static final String DefaultColorTableHeatMap = "White-Green";
+
+    public static final String[] BuiltInColorTables = {
+            "Fews8;8;0x5da6dc;0xfba53a;0x60be68;0xf27db0;0xb39230;0xb376b2;0xdfd040;0xf15954;",
+            "Caspian8;8;0xf64d1b;0x8633bc;0x41a744;0x747474;0x2746bc;0xff9301;0xc03150;0x2198bc;",
+            "Sea9;9;0xffffdb;0xedfbb4;0xc9ecb6;0x88cfbc;0x56b7c4;0x3c90bf;0x345aa7;0x2f2b93;0x121858;",
+            "Pale12;12;0xdbdada;0xf27e75;0xba7bbd;0xceedc5;0xfbf074;0xf8cbe5;0xf9b666;0xfdffb6;0x86b0d2;0x95d6c8;0xb3e46c;0xbfb8da;",
+            "Rainbow13;13;0xed1582;0xf73e43;0xee8236;0xe5ae3d;0xe5da45;0xa1e443;0x22da27;0x21d18e;0x21c8c7;0x1ba2fc;0x2346fb;0x811fd9;0x9f1cc5;",
+            "Retro29;29;0xf4d564;0x97141d;0xe9af6b;0x82ae92;0x356c7c;0x5c8c83;0x3a2b27;0xe28b90;0x242666;0xc2a690;0xb80614;0x35644f;0xe3a380;0xb9a253;" +
+                    "0x72a283;0x73605b;0x94a0ad;0xf7a09d;0xe5c09e;0x4a4037;0xcec07c;0x6c80bb;0x7fa0a4;0xb9805b;0xd5c03f;0xdd802e;0x8b807f;0xc42030;0xc2603d;",
+            "Pairs12;12;0x267ab2;0xa8cfe3;0x399f34;0xb4df8e;0xe11f27;0xfa9b9b;0xfe7f23;0xfcbf75;0x6a4199;0xcab3d6;0xb05a2f;0xffff9f;",
+            "Random250;250;0x912805;0xc75efd;0x289;0x76feb4;0xdd65;0xccef95;0x68022a;0x510083;0x74fe43;0xc47de8;0xdccca9;0x5e59;0x3b64fc;0xb7bb29;0x2091;" +
+                    "0xfc267f;0x200101;0xa44670;0x62fe22;0x250d41;0x72aebe;0xfc866a;0x526300;0xb77aab;0xfc4473;0xabfe8b;0xfffe40;0xc87a79;0x147c4;0x8e8e8e;" +
+                    "0xe84925;0xdac8f4;0x20a089;0x40fdd3;0x1385ea;0xa12766;0x9255ba;0xe50100;0x3bf574;0xb2cbac;0x8fd2fe;0x316f00;0xfefd05;0xffffac;0x6ba899;" +
+                    "0xb60200;0x22509b;0x7a4f52;0x736787;0x68268f;0xfb3f9e;0xfb4607;0x52b6f5;0xf3bdd3;0xf2d10f;0x76ecc;0x2f03;0x6a6ffa;0x823024;0x51b9cd;" +
+                    "0xfe4dfd;0xd4c7c8;0x4a0000;0x2975a6;0x2f5a4d;0xe79dcb;0x153bf9;0xbdf8fe;0xb177;0xfefd88;0x57f9a;0x70a400;0xfa129e;0x5c014c;0xb209fa;" +
+                    "0x7e5123;0x1ebf6e;0xb05ca0;0x8834b5;0xb0c6ec;0xafa7ee;0x27a5a9;0x1a59fb;0x68ad56;0xbcc051;0xaec96f;0x4b519f;0x48992b;0x4dac99;0x232563;" +
+                    "0xe56ccc;0x1ff553;0xb610f;0xb4c2cb;0x1f4306;0xea6655;0x8ccdcf;0xfb003b;0x61fedc;0xbcd08e;0xb6b31;0xfd8f42;0xfb006f;0x6fcca;0xcf0091;" +
+                    "0x5f2225;0x7b2c49;0xe416c0;0xa4935;0xfb820b;0x30e8fe;0x2e7528;0xb19ac2;0xf2d33d;0xbc40fc;0x363645;0x92a4c5;0x304d27;0x7b29d4;0x31b5cb;" +
+                    "0x2346cf;0xdcefff;0x452201;0x42bc5c;0x38c46;0xcaf0da;0x56b27b;0x5096;0xc8eab9;0xb1009f;0xaca39f;0x8085b6;0x77e67;0x309400;0xe72500;" +
+                    "0x5fdafe;0xa4f4;0x638300;0xdff977;0x486faa;0x70865d;0xbc4a44;0xf1eadd;0x9fe500;0xfddab4;0x2fd99c;0x108c00;0x8e8700;0xfeb42f;0x72cb50;" +
+                    "0x50daa1;0xcb264f;0xfc6835;0x2d2e24;0xfcaa01;0xf33f;0x8e49d9;0x515280;0xe6fd;0x8d0604;0x6b8780;0xa6a9;0x5bdece;0xf5aca3;0x6f00d1;" +
+                    "0x9c502c;0x6a0507;0xd04f83;0x584417;0x47fda4;0x2232;0x98f1fe;0xfeaa73;0xaca87b;0x3c7d58;0xdb9e4f;0x8d0c3a;0x8eb532;0x253296;0x5a783c;" +
+                    "0x5b4435;0xf4d764;0xda9bfa;0x479d4f;0x722d67;0x9c7859;0xfe8c9d;0x989855;0xd69c87;0xfd4747;0xdc8ca7;0xd79319;0xc7002e;0xc3004d;0x1f5875;" +
+                    "0xdaf758;0xac7e17;0x9d1f8a;0xff77fd;0x26c1;0xb6261a;0xbe4e23;0xbf4b00;0x65c832;0x432766;0x90fd69;0xfc2422;0xfc0dfd;0xffb2fe;0x31;0x3e8480;" +
+                    "0x98ce50;0xba8a52;0xa9e832;0x2567ca;0x28b33e;0xd284;0x2e62;0x3fc104;0x54;0xcb0c73;0x4e80d1;0x4bfc00;0x94d7af;0xa53e94;0x9c0458;0x9092fb;" +
+                    "0x370020;0x975300;0x966e7b;0x9b239;0x8fb1fd;0x785b04;0x8d8e35;0xeb4dcc;0x2ba6f5;0x88b872;0xe46ea3;0xac00;0x565653;0x21be00;0xd643a6;0xd01e97;" +
+                    "0x11b1c8;0x6e9d2b;0x76468a;0xc7fc;0xb958;0x2c0760;0xfd1e5b;",
+            "Blue-Red;203;0x4156be;0x4055c2;0x4459c2;0x4259c6;0x465dc5;0x455dc9;0x4961c9;0x4860cd;0x4b64ce;0x4c67d2;0x4e68ce;0x4f6bd3;0x506cd7;0x536ed3;0x5471d8;0x5874d7;0x5674dc;" +
+                    "0x5a77db;0x5978e0;0x5d7bdd;0x5c7ce2;0x607fdf;0x5f80e4;0x6483e2;0x6384e7;0x6588e9;0x6787e5;0x698ae9;0x6c8eeb;0x6e91ef;0x7091ea;0x7195f1;0x7395ed;0x7598f1;" +
+                    "0x789bf5;0x799bf0;0x7b9ff6;0x7d9ff1;0x7fa2f5;0x82a6f9;0x83a5f4;0x86a9fa;0x86a9f5;0x8aadfb;0x8aacf6;0x8db1fb;0x8eb0f7;0x91b5fc;0x92b4f8;0x95b8fc;0x96b6f8;" +
+                    "0x99bafd;0x9ab9f8;0x9dbdfd;0x9ebcf8;0xa1c0fd;0xa2bff8;0xa5c3fd;0xa6c3f8;0xa9c6fd;0xaac5f7;0xadcafc;0xaec9f6;0xb1ccfb;0xb2cbf5;0xb5cffa;0xb6cdf4;0xb9d2f9;" +
+                    "0xbad1f5;0xbbd0f1;0xbed4f7;0xbfd4f3;0xbfd2ef;0xc3d6f4;0xc3d2ed;0xc7d6f2;0xc8d6ee;0xc7d5ea;0xccdaee;0xcbd6e8;0xcedaea;0xced7e4;0xd2dbe9;0xd3dae5;0xd2d8e1;" +
+                    "0xd7dde5;0xd7dbe1;0xd6d9dd;0xdbdee1;0xdbdbdd;0xdad8d9;0xdfdcdc;0xdedad8;0xded7d4;0xe2dcd8;0xe2d9d4;0xe0d6d0;0xe6dad3;0xe5d7cf;0xe4d4cb;0xe9d8cf;0xe8d5cb;" +
+                    "0xe6d1c7;0xead3c7;0xe7cfc3;0xebd1c3;0xe9cdbf;0xedcfbf;0xebccbb;0xefcdbb;0xecc8b6;0xf0cab7;0xeec5b2;0xf2c7b3;0xefc2ae;0xf3c4af;0xf0bfaa;0xf4c2ab;0xf1bca6;" +
+                    "0xf5bea7;0xf2baa2;0xf7bda3;0xf1b79e;0xf6b99f;0xf5b59c;0xf2b198;0xf7b398;0xf2ad94;0xf6af95;0xf1aa90;0xf6ac91;0xf6a98d;0xf1a78c;0xf0a389;0xf6a589;0xf0a085;" +
+                    "0xf4a185;0xef9c81;0xf49e81;0xf39a7e;0xee987d;0xf2967b;0xed947a;0xf19277;0xeb9076;0xef8e73;0xea8c73;0xee8a70;0xe9886f;0xed866d;0xe8846c;0xec8269;0xe68069;" +
+                    "0xea7e66;0xe47c65;0xe97a64;0xe37863;0xe77660;0xe1755f;0xe5725d;0xe0705c;0xe46e5a;0xdd6c5a;0xe16a57;0xdb6857;0xdf6654;0xda6553;0xdc6150;0xd86151;0xda5d4f;" +
+                    "0xd55d4f;0xd8594b;0xd3594c;0xd65549;0xd1554a;0xd45147;0xcf5148;0xd24d46;0xcd4d46;0xd04943;0xca4944;0xcd4540;0xc84541;0xc9413e;0xc93d3b;0xc53e3d;0xc5393a;" +
+                    "0xc1383b;0xc53538;0xbf3439;0xc23037;0xbd3038;0xc02c35;0xbb2b35;0xbe2733;0xb92533;0xbb2130;0xb72132;0xba1d30;0xb51d31;0xb7192f;0xb31830;0xb4142e;",
+            "White-Green;165;0xfdfefd;0xfbfdfb;0xfafdf9;0xf8fcf7;0xf6fcf5;0xf4fbf3;0xf2faf1;0xf0f9ef;0xeff8ed;0xedf7eb;0xebf7e9;0xe9f6e7;0xe8f5e5;0xe6f4e3;0xe4f4e2;0xe2f3e0;" +
+                    "0xe0f2de;0xdff1db;0xddf0d9;0xdbefd7;0xdaeed5;0xd8edd3;0xd6edd1;0xd4eccf;0xd3ebcd;0xd1eacb;0xd0eac9;0xcee9c7;0xcce8c5;0xcae7c3;0xc8e7c2;0xc7e6c0;0xc5e6be;" +
+                    "0xc3e5bc;0xc1e4bb;0xc0e3b9;0xbee2b7;0xbce2b5;0xbae1b2;0xb8e0b0;0xb6dfae;0xb5deac;0xb3deab;0xb1dda9;0xb0dda7;0xaedca5;0xacdba3;0xaadaa1;0xa8da9f;0xa7d99d;" +
+                    "0xa5d89b;0xa3d799;0xa1d697;0x9fd695;0x9ed593;0x9dd491;0x9bd48f;0x99d38d;0x97d28b;0x95d189;0x93d087;0x91cf85;0x8fce82;0x8dcd80;0x8bcd7e;0x89cc7d;0x87cb7b;" +
+                    "0x86cb79;0x85ca77;0x83ca75;0x81c873;0x7fc771;0x7dc770;0x7bc66e;0x79c56c;0x77c46b;0x75c46a;0x73c368;0x71c267;0x6fc266;0x6dc165;0x6cc063;0x6abf61;0x68be5f;" +
+                    "0x66be5f;0x64bd5d;0x62bc5c;0x60bb5b;0x5fba59;0x5db958;0x5bb856;0x59b755;0x57b754;0x55b653;0x53b651;0x51b550;0x50b44e;0x4eb24c;0x4cb24b;0x4ab14a;0x48b149;" +
+                    "0x47b047;0x45af46;0x43af45;0x42ae43;0x40ad42;0x3dac41;0x3cac3f;0x3aab3e;0x39aa3c;0x37aa3b;0x35a93b;0x34a839;0x32a838;0x30a736;0x2fa535;0x2ca533;0x2aa431;" +
+                    "0x29a230;0x27a22f;0x25a12d;0x23a02c;0x229f2a;0x209f29;0x1e9e27;0x1d9c26;0x1b9c25;0x1b9a24;0x1a9824;0x199623;0x199422;0x189223;0x199024;0x188e24;0x188c24;" +
+                    "0x178a23;0x168822;0x178624;0x168424;0x168224;0x158024;0x147f22;0x147d23;0x147b24;0x157924;0x157725;0x137624;0x127423;0x117223;0x127023;0x136f25;0x126d25;" +
+                    "0x126b25;0x116924;0x106723;0x106523;0x106525;0x116326;0x106126;0x105f25;0xf5d25;0xd5b24;0xe5926;0xf5727;0xf5526;"
+    };
+
+    private static void init() {
+        if (name2ColorTable.size() == 0)
+            parseTables(BuiltInColorTables);
+    }
+
+    private static final Map<String, ColorTable> name2ColorTable = new TreeMap<>();
+
+    /**
+     * get a named color table
+     *
+     * @param name
+     * @return color table
+     */
+    public static ColorTable getColorTable(String name) {
+        init();
+        if (name != null && name2ColorTable.keySet().contains(name)) {
+            return name2ColorTable.get(name);
+        }
+        else
+            return name2ColorTable.get(DefaultColorTableName);
+    }
+
+    /**
+     * get a named color table
+     *
+     * @param name
+     * @return color table
+     */
+    public static ColorTable getColorTableHeatMap(String name) {
+        init();
+        if (name != null && name2ColorTable.keySet().contains(name)) {
+            return name2ColorTable.get(name);
+        } else
+            return name2ColorTable.get(DefaultColorTableHeatMap);
+    }
+
+    public static int size() {
+        return name2ColorTable.size();
+    }
+
+    /**
+     * get all names of defined tables
+     *
+     * @return names
+     */
+    public static String[] getNames() {
+        init();
+        return name2ColorTable.keySet().toArray(new String[name2ColorTable.size()]);
+    }
+
+    /**
+     * get all names of defined tables ordered
+     *
+     * @return names
+     */
+    public static String[] getNamesOrdered() {
+        init();
+        ArrayList<String> list = new ArrayList<>(BuiltInColorTables.length);
+        for (String aLine : BuiltInColorTables) {
+            list.add(aLine.split(";")[0]);
+        }
+        for (String name : name2ColorTable.keySet()) {
+            if (!list.contains(name))
+                list.add(name);
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
+    /**
+     * parse the definition of tables
+     *
+     * @param tables
+     */
+    public static void parseTables(String... tables) {
+        int alpha = Math.max(0, Math.min(255, ProgramProperties.get("ColorAlpha", 255)));
+
+        for (String table : tables) {
+            final String[] tokens = Basic.split(table, ';');
+            if (tokens.length > 0) {
+                int i = 0;
+                while (i < tokens.length) {
+                    String name = tokens[i++];
+                    int numberOfColors = Integer.valueOf(tokens[i++]);
+                    final ArrayList<Color> colors = new ArrayList<>(numberOfColors);
+                    for (int k = 0; k < numberOfColors; k++) {
+                        Color color = new Color(Integer.decode(tokens[i++]));
+                        if (alpha < 255)
+                            color = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
+                        colors.add(color);
+                    }
+                    if (colors.size() > 0 && !name2ColorTable.containsKey(name))
+                        name2ColorTable.put(name, new ColorTable(name, colors));
+                }
+            }
+        }
+    }
+
+    /**
+     * gets the default color table
+     *
+     * @return default color table
+     */
+    public static ColorTable getDefaultColorTable() {
+        String name = ProgramProperties.get("DefaultColorTableName", DefaultColorTableName);
+        if (name2ColorTable.keySet().contains(name))
+            return getColorTable(name);
+        else
+            return getColorTable(DefaultColorTableName);
+    }
+
+    public static void setDefaultColorTable(String name) {
+        if (name2ColorTable.keySet().contains(name))
+            ProgramProperties.put("DefaultColorTableName", name);
+    }
+
+    /**
+     * gets the default color table
+     *
+     * @return default color table
+     */
+    public static ColorTable getDefaultColorTableHeatMap() {
+        String name = ProgramProperties.get("DefaultColorTableHeatMap", DefaultColorTableHeatMap);
+        if (name2ColorTable.keySet().contains(name))
+            return getColorTable(name);
+        else
+            return getColorTable(DefaultColorTableHeatMap);
+    }
+
+    public static void setDefaultColorTableHeatMap(String name) {
+        if (name2ColorTable.keySet().contains(name))
+            ProgramProperties.put("DefaultColorTableHeatMap", name);
+
+    }
+}
diff --git a/src/jloda/gui/DefaultLabelGetter.java b/src/jloda/gui/DefaultLabelGetter.java
new file mode 100644
index 0000000..dcdf6ec
--- /dev/null
+++ b/src/jloda/gui/DefaultLabelGetter.java
@@ -0,0 +1,37 @@
+/**
+ * DefaultLabelGetter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+/**
+ * default label getter
+ * Daniel Huson, 4.2013
+ */
+public class DefaultLabelGetter implements ILabelGetter {
+    /**
+     * returns name as label
+     *
+     * @param name
+     * @return label
+     */
+    @Override
+    public String getLabel(String name) {
+        return name;
+    }
+}
diff --git a/src/jloda/gui/GraphViewPopupListener.java b/src/jloda/gui/GraphViewPopupListener.java
new file mode 100644
index 0000000..c14f645
--- /dev/null
+++ b/src/jloda/gui/GraphViewPopupListener.java
@@ -0,0 +1,141 @@
+/**
+ * GraphViewPopupListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import jloda.graph.EdgeSet;
+import jloda.graph.NodeSet;
+import jloda.graphview.GraphView;
+import jloda.graphview.IPopupListener;
+import jloda.gui.commands.CommandManager;
+
+import javax.swing.*;
+import java.awt.event.MouseEvent;
+
+/**
+ * constructs a graph popuplistener
+ * Daniel Huson, 8.2010
+ */
+public class GraphViewPopupListener implements IPopupListener {
+    final JPopupMenu nodeMenu;
+    //JPopupMenu nodeLabelMenu;
+    final JPopupMenu edgeMenu;
+    //JPopupMenu EdgeLabelMenu;
+    final JPopupMenu panelMenu;
+    private final GraphView viewer;
+
+    /**
+     * construct the popup menus
+     *
+     * @param viewer
+     * @param nodeConfig
+     * @param edgeConfig
+     * @param panelConfig
+     * @param commandManager
+     */
+    public GraphViewPopupListener(GraphView viewer, String nodeConfig, String edgeConfig, String panelConfig, CommandManager commandManager) {
+        this.viewer = viewer;
+        nodeMenu = new PopupMenu(nodeConfig, commandManager);
+        edgeMenu = new PopupMenu(edgeConfig, commandManager);
+        panelMenu = new PopupMenu(panelConfig, commandManager);
+    }
+
+    /**
+     * popup menu on node
+     *
+     * @param me
+     * @param nodes
+     */
+    public void doNodePopup(MouseEvent me, NodeSet nodes) {
+        if (nodes.size() != 0) {
+            /*
+            if (me.isShiftDown() == false) {
+                viewer.selectAllNodes(false);
+                viewer.selectAllEdges(false);
+            }
+            if (!viewer.getSelected(nodes.getFirstElement()))
+                viewer.setSelected(nodes.getFirstElement(), true);
+                */
+            nodeMenu.show(me.getComponent(), me.getX(), me.getY());
+            viewer.repaint(); // stuff gets messed up
+        }
+    }
+
+    /**
+     * popup menu on node label
+     *
+     * @param me
+     * @param nodes
+     */
+    public void doNodeLabelPopup(MouseEvent me, NodeSet nodes) {
+        doNodePopup(me, nodes);
+    }
+
+    /**
+     * popup menu on edge
+     *
+     * @param me
+     * @param edges
+     */
+    public void doEdgePopup(MouseEvent me, EdgeSet edges) {
+        if (edges.size() != 0) {
+            /*
+            if (me.isShiftDown() == false) {
+                viewer.selectAllNodes(false);
+                viewer.selectAllEdges(false);
+            }
+            if (!viewer.getSelected(edges.getFirstElement()))
+                viewer.setSelected(edges.getFirstElement(), true);
+                */
+            edgeMenu.show(me.getComponent(), me.getX(), me.getY());
+            viewer.repaint(); // stuff gets messed up
+        }
+    }
+
+    /**
+     * popup menu on edge
+     *
+     * @param me
+     * @param edges
+     */
+    public void doEdgeLabelPopup(MouseEvent me, EdgeSet edges) {
+        doEdgePopup(me, edges);
+    }
+
+    /**
+     * popup menu not on graph
+     *
+     * @param me
+     */
+    public void doPanelPopup(MouseEvent me) {
+        panelMenu.show(me.getComponent(), me.getX(), me.getY());
+    }
+
+    public JPopupMenu getNodeMenu() {
+        return nodeMenu;
+    }
+
+    public JPopupMenu getEdgeMenu() {
+        return edgeMenu;
+    }
+
+    public JPopupMenu getPanelMenu() {
+        return panelMenu;
+    }
+}
diff --git a/src/jloda/gui/HistogramPanel.java b/src/jloda/gui/HistogramPanel.java
new file mode 100644
index 0000000..c67e7a0
--- /dev/null
+++ b/src/jloda/gui/HistogramPanel.java
@@ -0,0 +1,502 @@
+/**
+ * HistogramPanel.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import jloda.util.Alert;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.geom.Rectangle2D;
+import java.util.BitSet;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * a panel containing a histogram
+ * Daniel Huson , 3.2006
+ */
+public class HistogramPanel extends JPanel {
+    final List data;
+    int numberOfBuckets = 64;
+    float minValue = 0;
+    float maxValue = 0;
+    float minCount = 0;
+    float maxCount = 0;
+    float bucketWidth = 0;
+    float[] buckets = null;
+
+    float threshold = 0;
+    String text = "";
+    boolean includeZero = false;
+    boolean integerSteps = false;
+
+    final JPanel topPanel;
+    final JPanel centerPanel;
+    final JPanel bottomPanel;
+    final JLabel label;
+    final JTextField input;
+    final JSlider slider;
+    boolean reverse = false;
+
+    Color color = Color.BLACK;
+    private int decimalDigits = 8; // digits after "."
+
+
+    /**
+     * constructor
+     */
+    public HistogramPanel() {
+        super();
+        setLayout(new BorderLayout());
+
+        topPanel = new JPanel();
+        topPanel.setBorder(BorderFactory.createEmptyBorder(0, 100, 5, 100));
+        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.LINE_AXIS));
+        input = new JTextField(8);
+        input.addActionListener(new AbstractAction() {
+            /**
+             * Invoked when an action occurs.
+             */
+            public void actionPerformed(ActionEvent e) {
+                try {
+                    setThreshold(new Float(input.getText()));
+                } catch (Exception ex) {
+                }
+            }
+        });
+        /*
+        input.addKeyListener(new KeyAdapter(){
+            public void keyTyped(KeyEvent keyEvent) {
+                try
+                {
+                setThreshold((new Float(input.getText())).floatValue());
+            }
+                catch(Exception ex)
+                {
+                }
+            }
+        });
+        */
+        topPanel.add(input);
+        label = new JLabel(text);
+        topPanel.add(label);
+        add(topPanel, BorderLayout.NORTH);
+
+        centerPanel = new CenterPanel();
+        add(centerPanel, BorderLayout.CENTER);
+
+        slider = new JSlider();
+        slider.addChangeListener(new ChangeListener() {
+            /**
+             * Invoked when the target of the listener has changed its state.
+             *
+             * @param e a ChangeEvent object
+             */
+            public void stateChanged(ChangeEvent e) {
+                int newValue = ((JSlider) e.getSource()).getValue();
+                if (!inSetThreshold)  // in explicit setThreshold, don't clobber the set value
+                    setThreshold(minValue + newValue * bucketWidth);
+                centerPanel.repaint();
+            }
+        });
+        bottomPanel = new JPanel();
+        bottomPanel.setLayout(new BorderLayout(0, 0));
+        bottomPanel.add(slider, BorderLayout.CENTER);
+        add(bottomPanel, BorderLayout.SOUTH);
+
+        data = new LinkedList();
+    }
+
+    /**
+     * set values
+     *
+     * @param values
+     */
+    public void setValues(int[] values) {
+        data.clear();
+        for (int value : values) data.add((float) value);
+        computeBuckets();
+    }
+
+    /**
+     * set values
+     *
+     * @param values
+     */
+    public void setValues(BitSet values) {
+        data.clear();
+        for (int i = values.nextSetBit(0); i >= 0; i = values.nextSetBit(i + 1))
+            data.add((float) i);
+    }
+
+    /**
+     * set values
+     *
+     * @param values
+     */
+    public void setValues(float[] values) {
+        data.clear();
+        for (float value : values) data.add(value);
+        computeBuckets();
+    }
+
+    /**
+     * set values
+     *
+     * @param values
+     */
+    public void setValues(double[] values) {
+        data.clear();
+        for (double value : values) data.add(new Float(value));
+        computeBuckets();
+    }
+
+    /**
+     * set the values
+     *
+     * @param values list of Float values
+     */
+    public void setValues(List values) {
+        data.clear();
+        data.addAll(values);
+        computeBuckets();
+    }
+
+    /**
+     * erase the data
+     */
+    public void clear() {
+        minValue = maxValue = bucketWidth = 0;
+        minCount = maxCount = 0;
+        buckets = new float[numberOfBuckets];
+    }
+
+    /**
+     * compute the buckets
+     */
+    private void computeBuckets() {
+        clear();
+        if (data.size() > 0) {
+            if (includeZero)
+                minValue = 0;
+            else
+                minValue = Float.MAX_VALUE;
+            maxValue = -Float.MAX_VALUE;
+            minCount = 0;
+            maxCount = 0;
+            for (Object aData1 : data) {
+                float f = (Float) aData1;
+                if (f < minValue)
+                    minValue = f;
+                if (f > maxValue)
+                    maxValue = f;
+            }
+            if (minValue == maxValue) {
+                maxValue += 1;
+            }
+
+
+            bucketWidth = (maxValue - minValue) / (numberOfBuckets - 1);
+            for (Object aData : data) {
+                float f = (Float) aData;
+                int i = getBucketForValue(f);
+                buckets[i]++;
+                if (buckets[i] < minCount)
+                    minCount = buckets[i];
+                if (buckets[i] > maxCount)
+                    maxCount = buckets[i];
+            }
+        }
+
+        if (isReverse())
+            minValue -= (maxValue - minValue) / numberOfBuckets;
+
+
+        slider.setMinimum(0);
+        slider.setMaximum(numberOfBuckets);
+        // reset slider:
+        setThreshold(getThreshold());
+    }
+
+    /**
+     * given a value f, returns its bucket
+     *
+     * @param f
+     * @return bucket
+     */
+    private int getBucketForValue(float f) {
+        return (int) Math.floor((f - minValue) / bucketWidth);
+    }
+
+    /**
+     * center panel needs to be able to paint itself
+     */
+    class CenterPanel extends JPanel {
+        CenterPanel() {
+            super();
+            setPreferredSize(new Dimension(400, 100));
+        }
+
+        /**
+         * paint the histogram
+         *
+         * @param g0
+         */
+        public void paint(Graphics g0) {
+            super.paint(g0);
+            Graphics2D g = (Graphics2D) g0;
+            float xoffset = 10;
+            float width = (float) getBounds().getWidth();
+            float height = (float) getBounds().getHeight();
+            float dy = maxCount - minCount;
+            int countIn = 0;
+            int countTotal = 0;
+            for (int i = 0; i < numberOfBuckets; i++) {
+                countTotal += buckets[i];
+                float x = i * (width - 2 * xoffset) / numberOfBuckets + xoffset;
+
+                float y = height - buckets[i] * height / dy;
+                int currentBucket = getBucketForValue(getThreshold());
+
+                if (isReverse()) {
+                    x = width - x - width / numberOfBuckets;
+                    if (i < currentBucket) {
+
+                        g.setColor(color);
+                        countIn += buckets[i];
+                    } else {
+                        g.setColor(Color.GRAY);
+                    }
+                } else {
+                    if (i < currentBucket) {
+                        g.setColor(Color.GRAY);
+                    } else {
+                        g.setColor(color);
+                        countIn += buckets[i];
+                    }
+                }
+                g.fill(new Rectangle2D.Float(x, y, width / numberOfBuckets, height - y));
+                g.setColor(color);
+                g.draw(new Rectangle2D.Float(x, y, width / numberOfBuckets, height - y));
+            }
+            if (isIntegerSteps()) {
+                input.setText("" + getThresholdInt());
+                label.setText(" (" + countIn + " of " + countTotal + ")");
+            } else {
+                String str = "" + getThreshold();
+                int p = str.lastIndexOf(".");
+                if (p > -1 && p < str.length() - (decimalDigits + 1))
+                    str = str.substring(0, p + decimalDigits + 1);
+                input.setText("" + str);
+                label.setText(" (" + countIn + " of " + countTotal + ")");
+            }
+        }
+    }
+
+    public float getThreshold() {
+        return threshold;
+    }
+
+    public int getThresholdInt() {
+        if (!reverse)
+            return (int) Math.ceil(getThreshold());
+        else
+            return (int) Math.floor(getThreshold());
+
+    }
+
+    boolean inSetThreshold = false;
+
+    public void setThreshold(float threshold) {
+        this.threshold = threshold;
+        int i = (int) ((threshold - minValue) / bucketWidth);
+        inSetThreshold = true;
+        slider.setValue(i);
+        inSetThreshold = false;
+    }
+
+    public Color getColor() {
+        return color;
+    }
+
+    public void setColor(Color color) {
+        this.color = color;
+    }
+
+    public void setNumberOfBuckets(int numberOfBuckets) {
+        this.numberOfBuckets = numberOfBuckets;
+        computeBuckets();
+        repaint();
+    }
+
+    public int getNumberOfBuckets() {
+        return numberOfBuckets;
+    }
+
+    public String getText() {
+        return text;
+    }
+
+    public void setText(String text) {
+        this.text = text;
+    }
+
+    public boolean isIncludeZero() {
+        return includeZero;
+    }
+
+    public void setIncludeZero(boolean includeZero) {
+        this.includeZero = includeZero;
+        computeBuckets();
+    }
+
+    public boolean isReverse() {
+        return reverse;
+    }
+
+    public void setReverse(boolean reverse) {
+        this.reverse = reverse;
+        slider.setInverted(reverse);
+        computeBuckets();
+        if (reverse)
+            setThreshold(maxValue);
+        else
+            setThreshold(minValue);
+    }
+
+    public boolean isIntegerSteps() {
+        return integerSteps;
+    }
+
+    public void setIntegerSteps(boolean integerSteps) {
+        this.integerSteps = integerSteps;
+    }
+
+    /**
+     * open a dialog to choose dialog
+     *
+     * @param parent
+     * @param title
+     * @param initialThreshold
+     * @return threshold chosen, or null, if canceled
+     */
+    public Float showThresholdDialog(JFrame parent, String title, float initialThreshold) {
+        setThreshold(initialThreshold);
+        return showThresholdDialog(parent, title);
+    }
+
+    Float result;
+
+    /**
+     * open a dialog to choose dialog
+     *
+     * @param parent
+     * @param title
+     * @return threshold chosen, or null, if canceled
+     */
+    public Float showThresholdDialog(final JFrame parent, String title) {
+        final JDialog dialog = new JDialog(parent, title, true);
+        dialog.setSize(400, 150);
+        dialog.setLocationRelativeTo(parent);
+
+        dialog.getContentPane().setLayout(new BorderLayout());
+        this.setBorder(BorderFactory.createEtchedBorder());
+        dialog.getContentPane().add(this, BorderLayout.CENTER);
+        JPanel buttonsPanel = new JPanel();
+        buttonsPanel.setBorder(BorderFactory.createEtchedBorder());
+
+        JButton lessButton = new JButton(new AbstractAction() {
+            /**
+             * Invoked when an action occurs.
+             */
+            public void actionPerformed(ActionEvent e) {
+                setNumberOfBuckets(Math.max(2, getNumberOfBuckets() / 2));
+            }
+        });
+        lessButton.setText("-");
+        buttonsPanel.add(lessButton);
+
+        JButton moreButton = new JButton(new AbstractAction() {
+            /**
+             * Invoked when an action occurs.
+             */
+            public void actionPerformed(ActionEvent e) {
+                setNumberOfBuckets(Math.min(1024, 2 * getNumberOfBuckets()));
+            }
+        });
+        moreButton.setText("+");
+        buttonsPanel.add(moreButton);
+
+        JButton cancelButton = new JButton(new AbstractAction() {
+            /**
+             * Invoked when an action occurs.
+             */
+            public void actionPerformed(ActionEvent e) {
+                dialog.setVisible(false);
+                dialog.dispose();
+                result = null;
+            }
+        });
+        cancelButton.setText("Cancel");
+        buttonsPanel.add(cancelButton);
+
+        JButton applyButton = new JButton(new AbstractAction() {
+            /**
+             * Invoked when an action occurs.
+             */
+            public void actionPerformed(ActionEvent e) {
+                dialog.setVisible(false);
+                dialog.dispose();
+                try {
+                    result = new Float(input.getText());
+                } catch (Exception ex) {
+                    new Alert(parent, "Illegal input: " + input.getText());
+                }
+            }
+        });
+        applyButton.setText("Apply");
+        buttonsPanel.add(applyButton);
+        dialog.getContentPane().add(buttonsPanel, BorderLayout.SOUTH);
+
+        dialog.setVisible(true);
+        return result;
+    }
+
+    /**
+     * get max number of decimal digits
+     *
+     * @return factional digits
+     */
+    public int getDecimalDigits() {
+        return decimalDigits;
+    }
+
+    /**
+     * set decimal digits
+     *
+     * @param decimalDigits
+     */
+    public void setDecimalDigits(int decimalDigits) {
+        this.decimalDigits = decimalDigits;
+    }
+}
diff --git a/src/jloda/gui/ILabelGetter.java b/src/jloda/gui/ILabelGetter.java
new file mode 100644
index 0000000..a7c3437
--- /dev/null
+++ b/src/jloda/gui/ILabelGetter.java
@@ -0,0 +1,34 @@
+/**
+ * ILabelGetter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+/**
+ * gets a label for a given name
+ * Daniel Huson, 4.2013
+ */
+public interface ILabelGetter {
+    /**
+     * gets the label for a given name
+     *
+     * @param name
+     * @return label
+     */
+    String getLabel(String name);
+}
diff --git a/src/jloda/gui/IMenuModifier.java b/src/jloda/gui/IMenuModifier.java
new file mode 100644
index 0000000..bcd1834
--- /dev/null
+++ b/src/jloda/gui/IMenuModifier.java
@@ -0,0 +1,32 @@
+/**
+ * IMenuModifier.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package jloda.gui;
+
+import jloda.gui.commands.CommandManager;
+
+import javax.swing.*;
+
+/**
+ * menu modifier interface
+ * Daniel Huson, 5.2015
+ */
+public interface IMenuModifier {
+    void apply(JMenu menu, CommandManager commandManager);
+}
diff --git a/src/jloda/gui/IPopupMenuModifier.java b/src/jloda/gui/IPopupMenuModifier.java
new file mode 100644
index 0000000..33de3a6
--- /dev/null
+++ b/src/jloda/gui/IPopupMenuModifier.java
@@ -0,0 +1,32 @@
+/**
+ * IPopupMenuModifier.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package jloda.gui;
+
+import jloda.gui.commands.CommandManager;
+
+import javax.swing.*;
+
+/**
+ * menu modifier interface
+ * Daniel Huson, 5.2015
+ */
+public interface IPopupMenuModifier {
+    void apply(JPopupMenu menu, CommandManager commandManager);
+}
diff --git a/src/jloda/gui/IToolBarModifier.java b/src/jloda/gui/IToolBarModifier.java
new file mode 100644
index 0000000..ac84625
--- /dev/null
+++ b/src/jloda/gui/IToolBarModifier.java
@@ -0,0 +1,32 @@
+/**
+ * IToolBarModifier.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package jloda.gui;
+
+import jloda.gui.commands.CommandManager;
+
+import javax.swing.*;
+
+/**
+ * toolbar modifier interface
+ * Daniel Huson, 5.2015
+ */
+public interface IToolBarModifier {
+    void apply(JToolBar toolBar, CommandManager commandManager);
+}
diff --git a/src/jloda/gui/ListTransferHandler.java b/src/jloda/gui/ListTransferHandler.java
new file mode 100644
index 0000000..f176d5e
--- /dev/null
+++ b/src/jloda/gui/ListTransferHandler.java
@@ -0,0 +1,184 @@
+/**
+ * ListTransferHandler.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import javax.swing.*;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.StringSelection;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+
+/**
+ * list transfer handler
+ */
+public class ListTransferHandler extends TransferHandler {
+    private int[] indices = null;
+    private int addIndex = -1;
+    private int addCount = 0;
+
+    /**
+     * constructor
+     */
+    public ListTransferHandler() {
+    }
+
+    /**
+     * export string
+     *
+     * @param component
+     * @return string
+     */
+    protected String exportString(JComponent component) {
+        final JList list = (JList) component;
+        this.indices = list.getSelectedIndices();
+        final StringBuilder buff = new StringBuilder();
+        for (Object obj : list.getSelectedValuesList()) {
+            buff.append(obj != null ? obj.toString() : "");
+            buff.append("\n");
+
+        }
+        return buff.toString();
+    }
+
+    /**
+     * import a string
+     *
+     * @param component
+     * @param str
+     */
+    protected void importString(JComponent component, String str) {
+        JList target = (JList) component;
+        DefaultListModel listModel = (DefaultListModel) target.getModel();
+        int targetIndex = target.getSelectedIndex();
+
+        if (this.indices.length > 1)
+            if (targetIndex >= this.indices[0] - 1 && targetIndex <= this.indices[this.indices.length - 1]) {
+                this.indices = null;
+                return;
+            }
+
+        int max = listModel.getSize();
+        if (targetIndex < 0) {
+            targetIndex = max;
+        } else {
+            if (targetIndex > 0) {
+                //targetIndex++;
+                if (targetIndex > max)
+                    targetIndex = max;
+            }
+        }
+        if (targetIndex - this.indices[0] > 0) //shift downwards
+            targetIndex++;
+
+        this.addIndex = targetIndex;
+        String[] values = str.split("\n");
+        this.addCount = values.length;
+        for (String value : values) {
+            listModel.add(targetIndex, value);
+            targetIndex++;
+        }
+    }
+
+
+    /**
+     * cleanup
+     *
+     * @param component
+     * @param remove
+     */
+    protected void cleanup(JComponent component, boolean remove) {
+
+        if (remove && this.indices != null) {
+            JList source = (JList) component;
+            DefaultListModel m = (DefaultListModel) source.getModel();
+            source.clearSelection();
+
+            //If we are moving items around in the same list, we
+            //need to adjust the indices accordingly, since those
+            //after the insertion point have moved.
+            if (this.addCount > 0) {
+                for (int i = 0; i < this.indices.length; i++) {
+                    if (this.indices[i] > this.addIndex)
+                        this.indices[i] += this.addCount;
+                }
+            }
+            for (int i = this.indices.length - 1; i >= 0; i--) {
+                m.remove(this.indices[i]);
+            }
+        }
+        this.indices = null;
+        this.addCount = 0;
+        this.addIndex = -1;
+    }
+
+    /**
+     * create transferable
+     */
+    @Override
+    protected Transferable createTransferable(JComponent component) {
+        return new StringSelection(this.exportString(component));
+    }
+
+    /**
+     * get source acitons
+     */
+    @Override
+    public int getSourceActions(JComponent c) {
+        return MOVE;
+    }
+
+    /**
+     * import data
+     */
+    @Override
+    public boolean importData(JComponent component, Transferable t) {
+        if (this.canImport(component, t.getTransferDataFlavors())) {
+            try {
+                String str = (String) t.getTransferData(DataFlavor.stringFlavor);
+                this.importString(component, str);
+                return true;
+            } catch (UnsupportedFlavorException | IOException ufe) {
+            }
+        }
+        return false;
+    }
+
+    /**
+     * finished export
+     */
+    @Override
+    protected void exportDone(JComponent component, Transferable data, int action) {
+        this.cleanup(component, action == MOVE);
+    }
+
+    /**
+     * can we import?
+     */
+    @Override
+    public boolean canImport(JComponent component, DataFlavor[] flavors) {
+        for (DataFlavor flavor : flavors) {
+            if (DataFlavor.stringFlavor.equals(flavor)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/jloda/gui/MemoryUsageManager.java b/src/jloda/gui/MemoryUsageManager.java
new file mode 100644
index 0000000..12fe99c
--- /dev/null
+++ b/src/jloda/gui/MemoryUsageManager.java
@@ -0,0 +1,80 @@
+/**
+ * MemoryUsageManager.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import jloda.util.Basic;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.InvocationTargetException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * manages memory usage message, typically displayed in status bar of a window
+ * Daniel Huson, 7.2011
+ */
+public class MemoryUsageManager {
+    static private MemoryUsageManager memoryUsageManager;
+    private final List<WeakReference<ChangeListener>> changeListeners;
+
+    /**
+     * constructor
+     */
+    private MemoryUsageManager() {
+        changeListeners = new LinkedList<>();
+
+        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
+        scheduler.scheduleAtFixedRate(new Runnable() {
+            public void run() {
+                try {
+                    SwingUtilities.invokeAndWait(new Runnable() {
+                        public void run() {
+                            ChangeEvent changeEvent = new ChangeEvent(Basic.getMemoryUsageString());
+                            synchronized (changeListeners) {
+                                for (WeakReference<ChangeListener> weak : changeListeners) {
+                                    ChangeListener listener = weak.get();
+                                    if (listener != null)
+                                        listener.stateChanged(changeEvent);
+                                }
+                            }
+                        }
+                    });
+                } catch (InterruptedException | InvocationTargetException e) {
+                    Basic.caught(e);
+                }
+            }
+        }, 0, 5, SECONDS);
+    }
+
+    public static void addChangeListener(ChangeListener changeListener) {
+        if (memoryUsageManager == null)
+            memoryUsageManager = new MemoryUsageManager();
+        synchronized (memoryUsageManager.changeListeners) {
+            memoryUsageManager.changeListeners.add(new WeakReference<>(changeListener));
+        }
+    }
+}
diff --git a/src/jloda/gui/MenuBar.java b/src/jloda/gui/MenuBar.java
new file mode 100644
index 0000000..cb7fee4
--- /dev/null
+++ b/src/jloda/gui/MenuBar.java
@@ -0,0 +1,150 @@
+/**
+ * MenuBar.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import jloda.gui.commands.CommandManager;
+import jloda.gui.commands.MenuCreator;
+import jloda.util.Basic;
+import jloda.util.PropertiesListListener;
+import jloda.util.ResourceManager;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.util.List;
+
+/**
+ * makes the menu bar
+ * Daniel Huson, 1.2007
+ */
+public class MenuBar extends JMenuBar {
+    private final JMenu windowMenu;
+    private JMenu recentFilesMenu;
+    static int numberFixedWindowMenuItems = 0;
+    private PropertiesListListener recentFilesListener;
+
+    /**
+     * creates the window menu bar
+     *
+     * @param commandManager
+     */
+    public MenuBar(MenuConfiguration configuration, CommandManager commandManager) {
+        MenuCreator menuCreator = new MenuCreator(commandManager);
+        try {
+            menuCreator.buildMenuBar("main", configuration, this);
+        } catch (Exception e) {
+            Basic.caught(e);
+            throw new RuntimeException("Failed to build menus: " + e);
+        }
+        windowMenu = MenuCreator.findMenu("Window", this, false);
+        if (windowMenu != null)
+            numberFixedWindowMenuItems = windowMenu.getItemCount();
+        JMenu recentFilesMenu = MenuCreator.findMenu("Open Recent", this, true);
+        if (recentFilesMenu != null)
+            setupRecentFilesMenu(commandManager, recentFilesMenu);
+    }
+
+    /**
+     * setup the recent files menu
+     */
+    private void setupRecentFilesMenu(final CommandManager commandManager, final JMenu recentFilesMenu) {
+        this.recentFilesMenu = recentFilesMenu;
+        recentFilesMenu.setIcon(ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Open16.gif"));
+
+        recentFilesListener = new PropertiesListListener() {
+            public boolean isInterested(String name) {
+                return name != null && name.equals("RecentFiles");
+            }
+
+            public void hasChanged(List<String> recentFileNames) {
+                recentFilesMenu.removeAll();
+                for (String fileName : recentFileNames) {
+                    recentFilesMenu.add(createOpenRecentFileAction(commandManager, fileName));
+                    recentFilesMenu.setEnabled(recentFilesMenu.getItemCount() > 0);
+                }
+            }
+        };
+    }
+
+    /**
+     * gets a recent files listener  used by the menu to listener for changes to the recent files menu
+     *
+     * @return listener
+     */
+    public PropertiesListListener getRecentFilesListener() {
+        return recentFilesListener;
+    }
+
+    /**
+     * gets the windows menu
+     *
+     * @return windows menu
+     */
+    public JMenu getWindowMenu() {
+        return windowMenu;
+    }
+
+    private AbstractAction createOpenRecentFileAction(final CommandManager commandManager, final String recentFileName) {
+        final String displayName;
+        if (recentFileName.length() <= 40)
+            displayName = recentFileName;
+        else
+            displayName = "..." + recentFileName.substring(recentFileName.length() - 35);
+
+        AbstractAction action = new AbstractAction() {
+            public void actionPerformed(ActionEvent actionEvent) {
+                commandManager.getDir().execute("open file='" + recentFileName + "';", commandManager);
+            }
+        };
+        action.putValue(AbstractAction.NAME, displayName);
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Open16.gif"));
+
+        return action;
+    }
+
+    /**
+     * turn all recent file menu items on or off
+     *
+     * @param state
+     */
+    public void setEnableRecentFileMenuItems(boolean state) {
+        if (recentFilesMenu != null)
+            for (Component component : recentFilesMenu.getMenuComponents()) {
+                if (component instanceof JMenuItem) {
+                    component.setEnabled(state);
+                }
+            }
+    }
+
+    /**
+     * find the named top-level menu
+     *
+     * @param name
+     * @return menu or null
+     */
+    public JMenu findMenu(String name) {
+        for (int i = 0; i < getMenuCount(); i++) {
+            JMenu menu = getMenu(i);
+            if (menu.getText().equals(name))
+                return menu;
+        }
+        return null;
+    }
+}
diff --git a/src/jloda/gui/MenuConfiguration.java b/src/jloda/gui/MenuConfiguration.java
new file mode 100644
index 0000000..f27d3a9
--- /dev/null
+++ b/src/jloda/gui/MenuConfiguration.java
@@ -0,0 +1,69 @@
+/**
+ * MenuConfiguration.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import java.util.Hashtable;
+
+/**
+ * menu configurator
+ * Daniel Huson, 5.2010
+ */
+public class MenuConfiguration extends Hashtable<String, String> {
+    /**
+     * Put the menu bar configuration.
+     * Example: "File;Edit;Select;Options;Tree;View;Window;"
+     *
+     * @param menuNames
+     */
+    public void defineMenuBar(String menuNames) {
+        put("MenuBar.main", menuNames);
+
+    }
+
+    /**
+     * Configure a menu.
+     * Example: "Select", "All Panels;No Panels;Invert Panels;|;"
+     *
+     * @param name
+     * @param menuItemNames
+     */
+    public void defineMenu(String name, String menuItemNames) {
+        put("Menu." + name, name + ";" + menuItemNames);
+    }
+
+    /**
+     * gets the menu bar description
+     *
+     * @return
+     */
+    public String getMenuBar() {
+        return get("MenuBar.main");
+    }
+
+    /**
+     * gets a menu description
+     *
+     * @param name
+     * @return
+     */
+    public String getMenu(String name) {
+        return get(name).replace("name;", "");
+    }
+}
diff --git a/src/jloda/gui/Message.java b/src/jloda/gui/Message.java
new file mode 100644
index 0000000..356831b
--- /dev/null
+++ b/src/jloda/gui/Message.java
@@ -0,0 +1,211 @@
+/**
+ * Message.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import jloda.util.ProgramProperties;
+import jloda.util.ResourceManager;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+
+/**
+ * show a message window
+ *
+ * @author huson
+ *         Date: 23-Feb-2004
+ */
+public class Message {
+    public static final Color PALE_YELLOW = new Color(252, 232, 131, 100);
+
+    /**
+     * create an message window with the given message and display it
+     *
+     * @param message
+     */
+    public Message(String message) {
+        this(null, message);
+    }
+
+    /**
+     * create an message window with the given message and display it
+     *
+     * @param parent  parent window
+     * @param message
+     */
+    public Message(Component parent, final String message) {
+        this(parent, message, 400, 200, null);
+    }
+
+    /**
+     * create an message window with the given message and display it
+     *
+     * @param parent  parent window
+     * @param message
+     */
+    public Message(Component parent, final String message, final String title) {
+        this(parent, message, 400, 200, title);
+    }
+
+    /**
+     * create an message window with the given message and display it
+     *
+     * @param parent  parent window
+     * @param message
+     */
+    public Message(Component parent, final String message, int width, int height) {
+        this(parent, message, width, height, null);
+    }
+
+
+    /**
+     * create an message window with the given message and display it
+     *
+     * @param parent  parent window
+     * @param message
+     */
+    public Message(Component parent, final String message, int width, int height, final String title) {
+        if (ProgramProperties.isUseGUI()) {
+            String label;
+            if (title == null) {
+                if (ProgramProperties.getProgramName() != null)
+                    label = "Message - " + ProgramProperties.getProgramName();
+                else
+                    label = "Message";
+            } else
+                label = title + " - " + ProgramProperties.getProgramName();
+            new MessageDialog(parent, message, label, width, height);
+            //new MessageBox((JFrame)parent,message,label,width,height);
+        } else
+            System.err.println("Message - " + message);
+    }
+
+}
+
+class MessageDialog extends JDialog {
+    MessageDialog(Component parent, String message, String title, int width, int height) {
+        super();
+        // setIconImage(ProgramProperties.getProgramIcon().getImage());
+        setModal(true);
+        setTitle(title);
+        setSize(width, height);
+        setLocationRelativeTo(parent);
+        Container main = getContentPane();
+        main.setLayout(new BorderLayout());
+        JPanel middle = new JPanel();
+        middle.setLayout(new BorderLayout());
+        middle.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+        JEditorPane text;
+        if (message.startsWith("<html>")) {
+            text = new JEditorPane("text/html", message);
+        } else {
+            text = new JEditorPane();
+            text.setText(message);
+        }
+        text.setEditable(false);
+        //text.setWrapStyleWord(true);
+        //text.setLineWrap(true);
+        text.setBackground(main.getBackground());
+        middle.add(new JScrollPane(text), BorderLayout.CENTER);
+        main.add(middle, BorderLayout.CENTER);
+        JPanel bottom = new JPanel();
+        bottom.setLayout(new BorderLayout());
+        //bottom.setBorder(BorderFactory.createEtchedBorder()); 
+        JButton closeButton = new JButton(getCloseAction());
+        bottom.add(closeButton, BorderLayout.EAST);
+        rootPane.setDefaultButton(closeButton);
+
+        main.add(bottom, BorderLayout.SOUTH);
+        text.setCaretPosition(0);
+        setVisible(true);
+    }
+
+    public AbstractAction getCloseAction() {
+        AbstractAction action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                MessageDialog.this.dispose();
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Close");
+        return action;
+    }
+}
+
+class MessageBox extends Window {
+    MessageBox(JFrame parent, String message, String title, int width, int height) {
+        super(parent);
+        setBackground(Message.PALE_YELLOW);
+
+        // setIconImage(ProgramProperties.getProgramIcon().getImage());
+        int x = width;
+        int y = parent.getHeight() - height;
+
+        setSize(width, height);
+        //setLocationRelativeTo(parent);
+        setLocation(x, y);
+
+        JPanel panel = new JPanel();
+        panel.setLayout(new BorderLayout());
+        panel.setBackground(Message.PALE_YELLOW);
+        add(panel);
+        JPanel middle = new JPanel();
+        middle.setBackground(panel.getBackground());
+        middle.setLayout(new BorderLayout());
+        middle.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+        JEditorPane text;
+        if (message.startsWith("<html>")) {
+            text = new JEditorPane("text/html", message);
+        } else {
+            text = new JEditorPane();
+            text.setText(message);
+        }
+        text.setEditable(false);
+        //text.setWrapStyleWord(true);
+        //text.setLineWrap(true);
+        text.setBackground(panel.getBackground());
+        JScrollPane scrollPane = new JScrollPane(text);
+        scrollPane.setBackground(panel.getBackground());
+        middle.add(scrollPane, BorderLayout.CENTER);
+        panel.add(middle, BorderLayout.CENTER);
+        JPanel top = new JPanel();
+        top.setBackground(panel.getBackground());
+        top.setLayout(new BorderLayout());
+        //bottom.setBorder(BorderFactory.createEtchedBorder());
+        JButton closeButton = new JButton(getCloseAction());
+        closeButton.setBorder(null);
+        closeButton.setBackground(panel.getBackground());
+        top.add(closeButton, BorderLayout.WEST);
+
+        panel.add(top, BorderLayout.NORTH);
+        text.setCaretPosition(0);
+        setVisible(true);
+    }
+
+    public AbstractAction getCloseAction() {
+        AbstractAction action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                MessageBox.this.dispose();
+            }
+        };
+        //action.putValue(AbstractAction.NAME, "Close");
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("Close16.gif"));
+        return action;
+    }
+}
diff --git a/src/jloda/gui/PopupMenu.java b/src/jloda/gui/PopupMenu.java
new file mode 100644
index 0000000..c14fe0c
--- /dev/null
+++ b/src/jloda/gui/PopupMenu.java
@@ -0,0 +1,89 @@
+/**
+ * PopupMenu.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import jloda.gui.commands.CommandManager;
+import jloda.gui.commands.ICommand;
+import jloda.gui.commands.TeXGenerator;
+import jloda.util.ProgramProperties;
+import jloda.util.ResourceManager;
+
+import javax.swing.*;
+
+/**
+ * popup menu
+ * Daniel Huson, 11.2010
+ */
+public class PopupMenu extends JPopupMenu {
+    /**
+     * constructor
+     *
+     * @param configuration
+     * @param commandManager
+     */
+    public PopupMenu(String configuration, CommandManager commandManager) {
+        this(configuration, commandManager, false);
+    }
+
+    /**
+     * constructor
+     *
+     * @param configuration
+     * @param commandManager
+     */
+    public PopupMenu(String configuration, CommandManager commandManager, boolean showApplicableOnly) {
+        super();
+        if (configuration != null && configuration.length() > 0) {
+            String[] tokens = configuration.split(";");
+
+            for (String token : tokens) {
+                if (token.equals("|")) {
+                    addSeparator();
+                } else {
+                    JMenuItem menuItem;
+                    ICommand command = commandManager.getCommand(token);
+                    if (command == null) {
+                        if (showApplicableOnly)
+                            continue;
+                        menuItem = new JMenuItem(token + "#");
+                        menuItem.setEnabled(false);
+                        add(menuItem);
+                    } else {
+                        if (CommandManager.getCommandsToIgnore().contains(command.getName()))
+                            continue;
+                        if (showApplicableOnly && !command.isApplicable())
+                            continue;
+                        menuItem = commandManager.getJMenuItem(command);
+                    }
+                    if (menuItem.getIcon() == null)
+                        menuItem.setIcon(ResourceManager.getIcon("Empty16.gif"));
+                    add(menuItem);
+                }
+            }
+        }
+        if (ProgramProperties.get("showtex", false)) {
+            System.out.println(TeXGenerator.getPopupMenuLaTeX(configuration, commandManager));
+        }
+        try {
+            commandManager.updateEnableState();
+        } catch (Exception ex) {
+        }
+    }
+}
diff --git a/src/jloda/gui/ProgressDialog.java b/src/jloda/gui/ProgressDialog.java
new file mode 100644
index 0000000..3f2b3a7
--- /dev/null
+++ b/src/jloda/gui/ProgressDialog.java
@@ -0,0 +1,566 @@
+/**
+ * ProgressDialog.java
+ * Copyright (C) 2016 Daniel H. Huson
+ * <p>
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package jloda.gui;
+
+import javafx.application.Platform;
+import jloda.util.Basic;
+import jloda.util.CanceledException;
+import jloda.util.ProgramProperties;
+import jloda.util.ProgressListener;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Arrays;
+import java.util.Stack;
+
+/**
+ * A progress bar dialog that updates via the swing event queue
+ *
+ * @author huson
+ *         Date: 02-Dec-2003
+ */
+public class ProgressDialog implements ProgressListener {
+    static private long delayInMilliseconds = 2000;// wait two seconds before opening progress bar
+    static private final int BITS = 30; // used to shift long values to int ones
+    private long startTime = System.currentTimeMillis();
+    private JDialog dialog;
+    private boolean closed = false;
+    private boolean visible = false;
+    private JProgressBar progressBar;
+    boolean userCancelled;
+    private JLabel taskLabel = new JLabel();
+    private JButton cancelButton;
+    private boolean closeOnCancel = true;
+    private String task;
+    private String subtask;
+    private boolean debug = false;
+
+    private long maxProgess = 100;
+    private long currentProgress = -1;
+    private boolean shiftedDown = false;
+
+    private StatusBar frameStatusBar = null;
+    private JPanel statusBarPanel = null;
+
+    private final Component owner;
+
+    private boolean cancelable = true;
+
+    /**
+     * Constructs a Progress Dialog with a given task name and subtask name. The dialog is embedded into
+     * the given frame. If frame = null then the dialog will appear as a separate window.
+     *
+     * @param taskName
+     * @param subtaskName
+     * @param owner
+     */
+    public ProgressDialog(final String taskName, final String subtaskName, final Component owner) {
+        this.owner = owner;
+        setup(taskName, subtaskName, delayInMilliseconds);
+        checkTimeAndShow();
+        if (dialog != null)
+            dialog.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+    }
+
+    public ProgressDialog(final String taskName, final String subtaskName, final Component owner, final long delayInMillisec) {
+        this.owner = owner;
+        setup(taskName, subtaskName, delayInMillisec);
+        checkTimeAndShow();
+        dialog.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+    }
+
+    /**
+     * sets up Progress Dialog with a given task name and subtask name. The dialog is embedded into
+     * the given frame. If frame = null then the dialog will appear as a separate window.
+     *  @param taskName
+     * @param subtaskName
+     */
+    private void setup(final String taskName, final String subtaskName, final long delayInMillisec) {
+        run(new Runnable() {
+            public void run() {
+                frameStatusBar = findStatusBar(owner);
+
+                userCancelled = false;
+                delayInMilliseconds = delayInMillisec;
+// the label:
+                taskLabel = new JLabel();
+                task = taskName;
+                subtask = subtaskName;
+                updateTaskLabel();
+
+// the progress bar:
+                progressBar = new JProgressBar(0, 150);
+                progressBar.setValue(-1);
+                progressBar.setIndeterminate(true);
+                progressBar.setStringPainted(false);
+                if (ProgramProperties.isMacOS()) { //On the mac - make like the standard p bar
+                    Dimension d = progressBar.getPreferredSize();
+                    d.height = 10;
+                    progressBar.setPreferredSize(d);
+                    d = progressBar.getMaximumSize();
+                    d.height = 10;
+                    progressBar.setMaximumSize(d);
+                }
+
+// the cancel button:
+                cancelButton = new JButton();
+                resetCancelButtonText();
+                cancelButton.addActionListener(new ActionListener() {
+                    public void actionPerformed(ActionEvent e) {
+                        try {
+                            setUserCancelled(true);
+                            checkForCancel();
+                        } catch (CanceledException e1) {
+                        }
+                    }
+                });
+
+                if (!isCancelable())
+                    cancelButton.setEnabled(false);
+
+                if (frameStatusBar != null) { // window appears to have a status bar that can be used for the progress bar
+                    statusBarPanel = new JPanel();
+                    statusBarPanel.setLayout(new BorderLayout());
+
+                    progressBar.setPreferredSize(new Dimension(300, 10));
+                    statusBarPanel.add(progressBar, BorderLayout.CENTER);
+
+                    cancelButton.setPreferredSize(new Dimension(60, 14));
+                    cancelButton.setMinimumSize(new Dimension(60, 14));
+                    cancelButton.setFont(new Font("Dialog", Font.PLAIN, 12));
+                    cancelButton.setBorder(BorderFactory.createEtchedBorder());
+                    statusBarPanel.add(cancelButton, BorderLayout.EAST);
+                } else { // no status bar for a program bar, show a window
+                    final JFrame parent = (owner instanceof JFrame ? (JFrame) owner : null);
+                    dialog = new JDialog(parent, "Progress...");
+                    dialog.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+
+                    if (!ProgramProperties.isMacOS()) { // none mac progress dialog:
+                        final GridBagLayout gridBag = new GridBagLayout();
+                        final JPanel pane = new JPanel(gridBag);
+                        pane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+
+                        GridBagConstraints c = new GridBagConstraints();
+
+                        c.anchor = GridBagConstraints.CENTER;
+                        c.fill = GridBagConstraints.HORIZONTAL;
+                        c.weightx = 3;
+                        c.weighty = 1;
+                        c.gridx = 1;
+                        c.gridy = 0;
+                        c.gridwidth = 3;
+                        c.gridheight = 1;
+                        pane.add(taskLabel, c);
+
+                        c.anchor = GridBagConstraints.CENTER;
+                        c.fill = GridBagConstraints.NONE;
+                        c.weightx = 1;
+                        c.weighty = 5;
+                        c.gridx = 1;
+                        c.gridy = 1;
+                        c.gridwidth = 3;
+                        c.gridheight = 1;
+                        pane.add(progressBar, c);
+
+                        c.anchor = GridBagConstraints.CENTER;
+                        c.weightx = 1;
+                        c.weighty = 1;
+                        c.gridx = 1;
+                        c.gridy = 2;
+                        c.gridwidth = 1;
+                        c.gridheight = 1;
+                        pane.add(cancelButton, c);
+
+                        dialog.getContentPane().add(pane);
+                        dialog.setSize(new Dimension(550, 120));
+                    } else {  // mac os progress dialog:
+                        final JPanel contentPane = new JPanel(new BorderLayout());
+                        contentPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+//Progress Bar and cancel button.
+                        JPanel barpane = new JPanel();
+                        barpane.setLayout(new BoxLayout(barpane, BoxLayout.LINE_AXIS));
+                        barpane.add(progressBar);
+
+                        barpane.add(cancelButton);
+
+                        JPanel taskPanel = new JPanel();
+                        taskPanel.setLayout(new BoxLayout(taskPanel, BoxLayout.PAGE_AXIS));
+                        taskPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
+                        taskPanel.add(taskLabel);
+                        taskPanel.add(Box.createHorizontalGlue());
+
+                        //Put everything into the content pane
+                        contentPane.add(barpane, BorderLayout.PAGE_START);
+                        contentPane.add(taskPanel, BorderLayout.LINE_START);
+                        dialog.setContentPane(contentPane);
+                        dialog.setSize(new Dimension(550, 120));
+                    }
+
+                    if (dialog.getParent() != null) {
+                        int x = dialog.getParent().getX();
+                        int y = dialog.getParent().getY();
+                        int dx = dialog.getParent().getWidth() - dialog.getWidth();
+                        int dy = dialog.getParent().getHeight() - dialog.getHeight();
+                        x += dx / 2;
+                        y += dy / 2;
+
+                        dialog.setLocation(x, y);
+                    }
+                    //dialog.setVisible(true);  //open once delay has passed
+                }
+            }
+        });
+    }
+
+    /**
+     * determine whether given component contains a statusbar
+     *
+     * @param component
+     * @return statusbar or null
+     */
+    private static StatusBar findStatusBar(Component component) {
+        if (component instanceof Container) {
+            Container frame = (Container) component;
+            final Stack<Component> stack = new Stack<>();
+            stack.addAll(Arrays.asList(frame.getComponents()));
+            while (stack.size() > 0) {
+                Component c = stack.pop();
+                if (c instanceof StatusBar)
+                    return (StatusBar) c;
+                else if (c instanceof Container)
+                    stack.addAll(Arrays.asList(((Container) c).getComponents()));
+            }
+        }
+        return null;
+    }
+
+
+    /**
+     * sets the steps number of steps to be done. This can be done in the event dispatch thread
+     *
+     * @param steps
+     */
+    public void setMaximum(final long steps) {
+        startTime = System.currentTimeMillis();
+
+        shiftedDown = (steps > (1 << BITS));
+
+        maxProgess = steps;
+        checkTimeAndShow();
+
+        if (progressBar != null && maxProgess != progressBar.getMaximum()) {
+            run(new Runnable() {
+                public void run() {
+                    progressBar.setMaximum((int) (shiftedDown ? steps >>> BITS : steps));
+                }
+            });
+        }
+    }
+
+    /**
+     * sets the progress. If a negative value is given, sets the progress bar to indeterminate mode
+     *
+     * @param steps
+     */
+    public void setProgress(final long steps) throws CanceledException {
+        if (steps != currentProgress) {
+            currentProgress = steps;
+            checkForCancel();
+
+            if (progressBar != null && currentProgress != progressBar.getValue()) {
+                SwingUtilities.invokeLater(new Runnable() {
+                    public void run() {
+                        if (currentProgress < 0) {
+                            progressBar.setIndeterminate(true);
+                            progressBar.setString(null);
+                        } else {
+                            progressBar.setIndeterminate(false);
+                            progressBar.setValue((int) (shiftedDown ? steps >>> BITS : steps));
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * gets the current progress
+     *
+     * @return progress
+     */
+    public long getProgress() {
+        return currentProgress;
+    }
+
+    /**
+     * increment the progress
+     *
+     * @throws CanceledException
+     */
+    public void incrementProgress() throws CanceledException {
+        if (currentProgress == -1)
+            currentProgress = 1;
+        else
+            currentProgress++;
+        checkForCancel();
+
+        if (progressBar != null && currentProgress != progressBar.getValue()) {
+            run(new Runnable() {
+                public void run() {
+                    progressBar.setValue((int) (shiftedDown ? currentProgress >>> BITS : currentProgress));
+                }
+            });
+        }
+    }
+
+    /**
+     * closes the dialog.
+     */
+    public void close() {
+        run(new Runnable() {
+            public void run() {
+                if (!closed) {
+                    if (statusBarPanel != null) {
+                        frameStatusBar.setExternalPanel1(null, false);
+                        frameStatusBar.setComponent2(statusBarPanel, false);
+                        statusBarPanel = null;
+                    }
+                    if (dialog != null) {
+                        dialog.setVisible(false);
+                        dialog.dispose();
+                        dialog = null;
+                    }
+                    closed = true;
+                    visible = false;
+                }
+            }
+        });
+    }
+
+    /**
+     * has user canceled?
+     *
+     * @throws CanceledException
+     */
+    public void checkForCancel() throws CanceledException {
+        checkTimeAndShow();
+
+        if (this.userCancelled) {
+            //dialog.setVisible(false);
+            if (closeOnCancel)
+                close();
+
+            throw new CanceledException();
+        }
+    }
+
+    /**
+     * sets the subtask name
+     *
+     * @param subtaskName
+     * @throws CanceledException
+     */
+    public void setSubtask(String subtaskName) {
+        checkTimeAndShow();
+
+        if ((subtaskName == null && subtask != null) || (subtaskName != null && (subtask == null || !subtask.equals(subtaskName)))) {
+            subtask = subtaskName;
+
+            run(new Runnable() {
+                public void run() {
+                    updateTaskLabel();
+                }
+            });
+        }
+    }
+
+
+    /**
+     * Sets the task name (first description, printed in bold)  and subtask
+     *
+     * @param taskName
+     * @param subtaskName
+     * @throws CanceledException
+     */
+    public void setTasks(String taskName, String subtaskName) {
+        checkTimeAndShow();
+
+        if ((taskName == null && task != null) || (taskName != null && (task == null || !task.equals(taskName)))
+                || (subtaskName == null && subtask != null) || (subtaskName != null && (subtask == null || !subtask.equals(subtaskName)))) {
+            task = taskName;
+            subtask = subtaskName;
+            run(new Runnable() {
+                public void run() {
+                    updateTaskLabel();
+                }
+            });
+        }
+    }
+
+    private void updateTaskLabel() {
+        String label = "<html><p style=\"font-size:" + (statusBarPanel != null ? "10pt" : "12pt") + ";\">";
+        if (this.task != null)
+            label += "<b>" + this.task + "</b>";
+        if (this.task != null && this.subtask != null)
+            label += ": ";
+        if (this.subtask != null)
+            label += this.subtask;
+        label += "</font></p>";
+        if (statusBarPanel != null) {
+            frameStatusBar.setExternalPanel1(new JLabel(label), true);
+            statusBarPanel.setToolTipText(label);
+        } else
+            taskLabel.setText(label);
+    }
+
+    public boolean isUserCancelled() {
+        return userCancelled;
+    }
+
+    public void setUserCancelled(boolean userCancelled) {
+        this.userCancelled = userCancelled;
+    }
+
+    private void checkTimeAndShow() {
+        try {
+            if (!closed && !visible && System.currentTimeMillis() - startTime > delayInMilliseconds) {
+                show();
+            }
+        } catch (Exception ex) {
+        }
+    }
+
+    /**
+     * show the progress bar
+     */
+    public void show() {
+        if (!visible) {
+            run(new Runnable() {
+                    public void run() {
+                        if (owner != null && owner instanceof Window) {
+                            // ((Window) owner).toFront(); // this causes weird effects
+                        }
+                        if (progressBar != null) {
+                            updateTaskLabel();
+                            progressBar.setMaximum((int) (shiftedDown ? maxProgess >>> BITS : maxProgess));
+                            if (currentProgress < 0) {
+                                progressBar.setIndeterminate(true);
+                                progressBar.setString(null);
+                            } else {
+                                progressBar.setIndeterminate(false);
+                                progressBar.setValue((int) (shiftedDown ? currentProgress >>> BITS : currentProgress));
+                            }
+                        }
+                        if (statusBarPanel != null) {
+                            frameStatusBar.setComponent2(statusBarPanel, !closed);
+                        } else if (dialog != null) {
+                            dialog.setVisible(true);
+                        }
+                        visible = true;
+                    }
+            });
+        }
+    }
+
+    /**
+     * run a task either directly, if in swing thread, or later, if FX thread, or invoke or wait, otherwise
+     *
+     * @param runnable
+     */
+    private static void run(Runnable runnable) {
+        if (SwingUtilities.isEventDispatchThread())
+            runnable.run();
+        else if (true || Platform.isFxApplicationThread())
+            SwingUtilities.invokeLater(runnable);
+        else // todo: this may lead to FX vs Swing deadlock. But not using this cases Inspector window to appear below current window
+            try {
+                SwingUtilities.invokeAndWait(runnable);
+            } catch (Exception e) {
+                Basic.caught(e);
+            }
+    }
+
+    public static long getDelayInMilliseconds() {
+        return delayInMilliseconds;
+    }
+
+    public static void setDelayInMilliseconds(long delayInMilliseconds) {
+        ProgressDialog.delayInMilliseconds = delayInMilliseconds;
+    }
+
+    /**
+     * in debug mode, report tasks and subtasks to stderr, too
+     *
+     * @return verbose mode
+     */
+    public boolean getDebug() {
+        return debug;
+    }
+
+    /**
+     * in debug mode, report tasks and subtasks to stderr, too
+     *
+     * @param debug
+     */
+    public void setDebug(boolean debug) {
+        this.debug = debug;
+    }
+
+    /**
+     * is user allowed to cancel?
+     *
+     * @param cancelable
+     */
+    public void setCancelable(boolean cancelable) {
+        this.cancelable = cancelable;
+        if (cancelButton != null)
+            cancelButton.setEnabled(cancelable);
+    }
+
+    /**
+     * is user allowed to cancel
+     *
+     * @return cancelable?
+     */
+    public boolean isCancelable() {
+        return cancelable;
+    }
+
+    public void setCancelButtonText(String text) {
+        cancelButton.setText(text);
+    }
+
+    public void resetCancelButtonText() {
+        if (ProgramProperties.isMacOS())
+            cancelButton.setText("Stop");
+        else
+            cancelButton.setText("Cancel");
+    }
+
+    public boolean isCloseOnCancel() {
+        return closeOnCancel;
+    }
+
+    public void setCloseOnCancel(boolean closeOnCancel) {
+        this.closeOnCancel = closeOnCancel;
+    }
+}
diff --git a/src/jloda/gui/ReorderListDialog.java b/src/jloda/gui/ReorderListDialog.java
new file mode 100644
index 0000000..a1c1115
--- /dev/null
+++ b/src/jloda/gui/ReorderListDialog.java
@@ -0,0 +1,483 @@
+/**
+ * ReorderListDialog.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.dnd.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * Dialog for reordering a list of objects
+ * <p/>
+ * Wei Wu and Daniel Huson, 6.2008
+ */
+public class ReorderListDialog extends JDialog implements DropTargetListener, ActionListener {
+    /**
+     *
+     */
+    private static final long serialVersionUID = 5234260814801310243L;
+    private final Vector originalList = new Vector();//save inputted objects in Vector
+    private boolean beApplied = false;//label for action from button apply
+    private boolean beCancelled = false;//label for action form button cancel
+
+    private final boolean showCopy; // show the copy button?
+
+    /*
+    *attributes for JDialog
+    */
+    private final JLabel originalLabel;
+    private final JLabel reorderedLabel;
+    private final JList originalJlist;
+    private final JList reorderedJlist;
+    private final JButton copy;
+    private final JButton flip;
+    private final JButton rotateUp;
+    private final JButton rotateDown;
+    private final JButton apply;
+    private final JButton cancel;
+    private final JPanel panel;
+
+    /*
+      * implements for interface DrogTargetListener
+     */
+
+    public void dragEnter(DropTargetDragEvent event) {
+
+    }
+
+    public void dragExit(DropTargetEvent event) {
+
+    }
+
+    public void dragOver(DropTargetDragEvent event) {
+
+    }
+
+    public void drop(DropTargetDropEvent event) {
+        //get the current context of the reordered list
+        DefaultListModel dropmodel = new DefaultListModel();
+        for (int i = 0; i < reorderedJlist.getModel().getSize(); i++) {
+            dropmodel.addElement(reorderedJlist.getModel().getElementAt(i));
+        }
+
+        //position to insert
+        int insertIndex = reorderedJlist.locationToIndex(event.getLocation());
+        //default inserted position is the current index. wenn now it is the last index,
+        //it must be confirmed, which it is between the current last or the future last index
+        //a new dialog about the insert position at tail of the list
+        if (insertIndex == reorderedJlist.getModel().getSize() - 1) {
+            Object[] options = {"Current last position", "Last position after", "Cancel"};
+            int n = JOptionPane.showOptionDialog(reorderedJlist,
+                    "Would you like to drop the selection at",
+                    "Drop at the current tail postion ",
+                    JOptionPane.YES_NO_CANCEL_OPTION,
+                    JOptionPane.QUESTION_MESSAGE,
+                    null,
+                    options,
+                    options[1]);
+            if ((n != JOptionPane.CANCEL_OPTION) && (n != JOptionPane.CLOSED_OPTION)) insertIndex += n;
+            else {
+                event.getDropTargetContext().dropComplete(true);
+                return;
+            }
+        }
+
+        //offset for new insert position after deleting old elements
+        int offsetForNewInsertPosition = 0;
+        //create a container saving the removed element, in order to add them at new postions again after deleting
+        Vector toBeRemoved = new Vector();
+
+        /*
+                  //at first calculate the offset ands save the indize which elements will be removed
+
+                  System.out.println(tokens.countTokens()+" items "+"selected");
+                  while(tokens.hasMoreTokens()){
+                      Object nextElement = tokens.nextElement();
+                      int j = dropmodel.indexOf(nextElement);
+                      System.out.println(j+" "+nextElement.hashCode());
+                      if (j < insertIndex) offsetForNewInsertPosition++;
+                      toBeRemoved.add(j);
+                  }
+                  */
+
+        //then delete the selected elements from the list
+        int i = 0;
+        for (int j = 0; j < reorderedJlist.getSelectedIndices().length; j++) {
+            int pointer = reorderedJlist.getSelectedIndices()[j];
+            toBeRemoved.add(dropmodel.getElementAt(pointer - i));
+            dropmodel.removeElementAt(pointer - i);
+            if (pointer < insertIndex) offsetForNewInsertPosition++;
+            i++;
+        }
+        //insert the removed elements at new position again
+        for (Object ins : toBeRemoved) {
+            dropmodel.add(insertIndex - offsetForNewInsertPosition, ins);
+            insertIndex++;
+        }
+
+        /*
+                  StringTokenizer tokensForAdding = new StringTokenizer(data, "\n") ;
+                  while(tokensForAdding.hasMoreTokens()){
+                      Object nextElement = tokensForAdding.nextElement();
+                      System.out.println("insert["+(insertIndex-offsetForNewInsertPosition)+"] "+nextElement);
+                      dropmodel.add(insertIndex-offsetForNewInsertPosition, nextElement);
+                      insertIndex++;
+                  }
+                  */
+
+        reorderedJlist.setModel(dropmodel);
+        reorderedJlist.updateUI();
+        event.getDropTargetContext().dropComplete(true);
+    }
+
+    public void dropActionChanged(DropTargetDragEvent event) {
+
+    }
+
+    /*
+      * implement for ActionListerner that buttons use
+      * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
+      */
+
+    public void actionPerformed(ActionEvent e) {
+        if (e.getSource() == apply) {
+            beApplied = true;
+            dispose();
+        } else if (e.getSource() == cancel) {
+            beCancelled = true;
+            dispose();
+        } else if (e.getSource() == copy) {
+            DefaultListModel model = new DefaultListModel();
+            model.clear();
+            for (Object anOriginalList : originalList) {
+                model.addElement(anOriginalList);
+            }
+            reorderedJlist.setModel(model);
+        } else if (e.getSource() == flip) {
+            DefaultListModel model1 = new DefaultListModel();
+            for (int i = 0; i < reorderedJlist.getModel().getSize(); i++) {
+                model1.addElement(reorderedJlist.getModel().getElementAt(i));
+            }
+            if (model1.getSize() == 0) {
+                for (Object anOriginalList : originalList) {
+                    model1.addElement(anOriginalList);
+                }
+            }
+            int size = model1.getSize();
+            for (int i = 0; i < size - 1; i++) {
+                Object element = model1.get(size - 2 - i);
+                model1.addElement(element);
+            }
+            for (int j = 0; j < size - 1; j++) {
+                model1.removeElementAt(0);
+            }
+            reorderedJlist.setModel(model1);
+        } else if (e.getSource() == rotateUp) {
+            DefaultListModel model2 = new DefaultListModel();
+            for (int i = 0; i < reorderedJlist.getModel().getSize(); i++) {
+                model2.addElement(reorderedJlist.getModel().getElementAt(i));
+            }
+            if (model2.getSize() == 0) {
+                for (Object anOriginalList : originalList) {
+                    model2.addElement(anOriginalList);
+                }
+            }
+            model2.add(model2.getSize(), model2.get(0));
+            model2.removeElementAt(0);
+            reorderedJlist.setModel(model2);
+        } else if (e.getSource() == rotateDown) {
+            DefaultListModel model3 = new DefaultListModel();
+            for (int i = 0; i < reorderedJlist.getModel().getSize(); i++) {
+                model3.addElement(reorderedJlist.getModel().getElementAt(i));
+            }
+            if (model3.getSize() == 0) {
+                for (Object anOriginalList : originalList) {
+                    model3.addElement(anOriginalList);
+                }
+            }
+            model3.add(0, model3.get(model3.getSize() - 1));
+            model3.removeElementAt(model3.getSize() - 1);
+            reorderedJlist.setModel(model3);
+        }
+    }
+
+    /*
+      * constructor
+      */
+
+    public ReorderListDialog(String title, boolean showCopy) {
+        super();
+        setTitle(title);
+        setModal(true);
+        this.showCopy = showCopy;
+
+        /*
+           * left list
+           */
+        originalLabel = new JLabel("Original");
+        originalLabel.setHorizontalAlignment(SwingConstants.CENTER);
+
+        originalJlist = new JList(originalList.toArray());
+        originalJlist.setLayoutOrientation(JList.VERTICAL);
+        JScrollPane scroll1 = new JScrollPane(originalJlist);
+        scroll1.setPreferredSize(new Dimension(240, 300));
+
+        /*
+        * right list
+        */
+        reorderedLabel = new JLabel("Reordered");
+        reorderedLabel.setHorizontalAlignment(SwingConstants.CENTER);
+
+        reorderedJlist = new JList();
+        reorderedJlist.setLayoutOrientation(JList.VERTICAL);
+        reorderedJlist.setAutoscrolls(true);
+        //set dnd on this Jlist
+        reorderedJlist.setDragEnabled(true);
+        // reorderedJlist.setDropMode(DropMode.ON_OR_INSERT );
+        reorderedJlist.setAutoscrolls(true);
+        new DropTarget(reorderedJlist, this);
+
+        JScrollPane scroll2 = new JScrollPane(reorderedJlist);
+        scroll2.setPreferredSize(new Dimension(240, 300));
+
+        /*
+           * buttons in the middle
+           */
+        copy = new JButton("Copy=>");
+        copy.setMinimumSize(new Dimension(120, 30));
+        copy.setMaximumSize(new Dimension(120, 30));
+        copy.setPreferredSize(new Dimension(120, 30));
+        copy.setAlignmentX(JComponent.CENTER_ALIGNMENT);
+        copy.addActionListener(this);
+
+        flip = new JButton("Swap=>");
+        flip.setMinimumSize(new Dimension(120, 30));
+        flip.setMaximumSize(new Dimension(120, 30));
+        flip.setPreferredSize(new Dimension(120, 30));
+        flip.setAlignmentX(JComponent.CENTER_ALIGNMENT);
+        flip.addActionListener(this);
+
+
+        rotateDown = new JButton("Rotate Down=>");
+        rotateDown.setMinimumSize(new Dimension(120, 30));
+        rotateDown.setMaximumSize(new Dimension(120, 30));
+        rotateDown.setPreferredSize(new Dimension(120, 30));
+        rotateDown.setAlignmentX(JComponent.CENTER_ALIGNMENT);
+        rotateDown.addActionListener(this);
+
+
+        rotateUp = new JButton("Rotate Up=>");
+        rotateUp.setMinimumSize(new Dimension(120, 30));
+        rotateUp.setMaximumSize(new Dimension(120, 30));
+        rotateUp.setPreferredSize(new Dimension(120, 30));
+        rotateUp.setAlignmentX(JComponent.CENTER_ALIGNMENT);
+        rotateUp.addActionListener(this);
+
+        /*
+           * botton apply, cancel
+           */
+        cancel = new JButton("Cancel");
+        cancel.setMinimumSize(new Dimension(100, 30));
+        cancel.setMaximumSize(new Dimension(100, 30));
+        cancel.setPreferredSize(new Dimension(100, 30));
+        cancel.setDisplayedMnemonicIndex(0);
+        cancel.addActionListener(this);
+        getRootPane().setDefaultButton(cancel);
+
+        apply = new JButton("Apply");
+        apply.setMinimumSize(new Dimension(100, 30));
+        apply.setMaximumSize(new Dimension(100, 30));
+        apply.setPreferredSize(new Dimension(100, 30));
+        apply.setDisplayedMnemonicIndex(0);
+        apply.addActionListener(this);
+
+        panel = new JPanel();
+
+        /*
+        * layout for the dialog
+        */
+        GridBagLayout gbl = new GridBagLayout();
+        GridBagConstraints gbc = new GridBagConstraints();
+        panel.setLayout(gbl);
+
+        gbc.gridx = 0;
+        gbc.gridy = 0;
+        gbc.gridwidth = 3;
+        gbc.gridheight = 1;
+        gbc.weightx = 1;
+        gbc.weighty = 1;
+        gbc.insets = new Insets(5, 5, 5, 5);
+        gbc.anchor = GridBagConstraints.LAST_LINE_START;
+        gbc.fill = GridBagConstraints.HORIZONTAL;
+        gbl.setConstraints(originalLabel, gbc);
+        panel.add(originalLabel);
+
+        gbc.gridx = 4;
+        gbc.gridy = 0;
+        gbc.gridwidth = 3;
+        gbc.gridheight = 1;
+        gbl.setConstraints(reorderedLabel, gbc);
+        panel.add(reorderedLabel);
+
+        gbc.gridx = 0;
+        gbc.gridy = 1;
+        gbc.gridwidth = 3;
+        gbc.gridheight = 5;
+        gbc.anchor = GridBagConstraints.CENTER;
+        gbc.fill = GridBagConstraints.BOTH;
+        gbl.setConstraints(scroll1, gbc);
+        panel.add(scroll1);
+
+        gbc.gridx = 4;
+        gbc.gridy = 1;
+        gbc.gridwidth = 3;
+        gbc.gridheight = 5;
+        gbc.fill = GridBagConstraints.BOTH;
+        gbc.anchor = GridBagConstraints.CENTER;
+        gbl.setConstraints(scroll2, gbc);
+        panel.add(scroll2);
+
+        gbc.gridy = 0;
+        gbc.gridwidth = 1;
+        gbc.gridheight = 1;
+        gbc.fill = GridBagConstraints.CENTER;
+
+
+        if (showCopy) {
+            gbc.gridx = 3;
+            gbc.gridy++;
+            gbl.setConstraints(copy, gbc);
+            panel.add(copy);
+        }
+
+        gbc.gridx = 3;
+        gbc.gridy++;
+        gbl.setConstraints(flip, gbc);
+        panel.add(flip);
+
+        gbc.gridx = 3;
+        gbc.gridy++;
+        gbl.setConstraints(rotateUp, gbc);
+        panel.add(rotateUp);
+
+        gbc.gridx = 3;
+        gbc.gridy++;
+        gbl.setConstraints(rotateDown, gbc);
+        panel.add(rotateDown);
+
+        gbc.gridx = 5;
+        gbc.gridy = 6;
+        gbl.setConstraints(cancel, gbc);
+        panel.add(cancel);
+
+        gbc.gridx = 6;
+        gbc.gridy = 6;
+        gbl.setConstraints(apply, gbc);
+        panel.add(apply);
+
+        this.setLayout(new BorderLayout());
+        this.add(panel, BorderLayout.CENTER);
+        this.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
+        this.pack();
+        this.setLocation((Toolkit.getDefaultToolkit().getScreenSize().width - this.getSize().width) / 2, (Toolkit.getDefaultToolkit().getScreenSize().height - this.getSize().height) / 2);
+    }
+
+    /**
+     * constructor
+     *
+     * @param title
+     */
+    public ReorderListDialog(String title) {
+        this(title, true);
+    }
+
+    /**
+     * constructor
+     */
+    public ReorderListDialog() {
+        this("Reorder", true);
+    }
+
+    /**
+     * show the dialog for the given list of objects
+     *
+     * @param original
+     * @return reordered list
+     */
+    public List show(List original) {
+        //load input into the link Jlist
+        originalList.addAll(original);
+        DefaultListModel model = new DefaultListModel();
+        for (Object anOriginal : original) {
+            model.addElement(anOriginal);
+        }
+        this.originalJlist.setModel(model);
+
+        if (!showCopy) {
+            model = new DefaultListModel();
+            for (Object anOriginalList : originalList) {
+                model.addElement(anOriginalList);
+            }
+            reorderedJlist.setModel(model);
+        }
+
+        this.setVisible(true);
+        this.toFront();
+
+        if (beApplied) {
+            Vector returnedList = new Vector();
+            for (int i = 0; i < reorderedJlist.getModel().getSize(); i++) {
+                returnedList.add(reorderedJlist.getModel().getElementAt(i));
+            }
+            return returnedList;
+        }
+        //only one case that beCancelled is true now
+        else {
+            return null;
+        }
+    }
+
+    /**
+     * test program
+     *
+     * @param args
+     */
+    static public void main(String[] args) throws Exception {
+        final Vector superClasses = new Vector();
+        Class rootClass = javax.swing.JList.class;
+        for (Class cls = rootClass; cls != null; cls = cls.getSuperclass()) {
+            superClasses.add(cls);
+        }
+
+        SwingUtilities.invokeAndWait(new Runnable() {
+            public void run() {
+                ReorderListDialog test = new ReorderListDialog("ReorderListDialog", false);
+                List output = test.show(superClasses);
+                System.out.println(output);
+            }
+        });
+        System.exit(0);
+    }
+
+}
diff --git a/src/jloda/gui/StatusBar.java b/src/jloda/gui/StatusBar.java
new file mode 100644
index 0000000..a41cfc5
--- /dev/null
+++ b/src/jloda/gui/StatusBar.java
@@ -0,0 +1,185 @@
+/**
+ * StatusBar.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import jloda.util.Basic;
+import jloda.util.ProgramProperties;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+
+/**
+ * StatusBar for windows
+ *
+ * @author Daniel Huson, 1.2011
+ */
+public class StatusBar extends JPanel {
+    private final JTextArea text1 = new JTextArea();
+    private final JPanel panel1 = new JPanel();
+    private final JTextArea text2 = new JTextArea();
+    private final JPanel panel2 = new JPanel();
+    private final JLabel text3 = new JLabel();
+    private final JSplitPane splitPane1;
+    private final JSplitPane splitPane2;
+
+    private final ChangeListener changeListener;
+
+    /**
+     * Constructor for the status bar of the window
+     */
+    public StatusBar() {
+        this(true);
+    }
+
+    /**
+     * Constructor for the status bar of the window
+     */
+    public StatusBar(boolean showMemoryUsage) {
+        this.setLayout(new BorderLayout());
+        this.setBorder(BorderFactory.createEtchedBorder());
+
+        text1.setFont(new Font("Dialog", Font.PLAIN, 10));
+        text1.setEditable(false);
+        text1.setBackground(text3.getBackground());
+        //text1.setFocusable(false);
+        text2.setFont(new Font("Dialog", Font.PLAIN, 10));
+        text2.setEditable(false);
+        text2.setBackground(text3.getBackground());
+        // text2.setFocusable(false);
+        text3.setFont(new Font("Dialog", Font.PLAIN, 10));
+        text3.setText(Basic.getMemoryUsageString(100));
+        text3.setFocusable(false);
+
+        panel1.add(text1);
+        panel1.setToolTipText("Number of taxa currently displayed");
+
+        panel2.add(text2);
+        panel2.setToolTipText("Number of reads, algorithm settings");
+
+        splitPane1 = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, panel1, panel2);
+        splitPane1.setBorder(BorderFactory.createEmptyBorder());
+        splitPane1.setResizeWeight(0);
+
+        if (ProgramProperties.isMacOS())
+            splitPane1.setDividerSize(10);
+        else splitPane1.setDividerSize(1);
+
+        JPanel text3Panel = new JPanel();
+        splitPane2 = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, splitPane1, text3Panel);
+        splitPane2.setBorder(BorderFactory.createEmptyBorder());
+        splitPane2.setResizeWeight(1);
+        if (ProgramProperties.isMacOS())
+            splitPane2.setDividerSize(10);
+        else splitPane2.setDividerSize(1);
+
+        this.add(splitPane2, BorderLayout.CENTER);
+
+        if (showMemoryUsage) {
+            setText3("------------");
+            text3Panel.add(text3);
+            text3Panel.setToolTipText("Memory usage");
+            this.add(Box.createHorizontalStrut(10), BorderLayout.EAST);
+
+            changeListener = new ChangeListener() {
+                public void stateChanged(ChangeEvent changeEvent) {
+                    setText3(changeEvent.getSource().toString());
+                }
+            };
+            MemoryUsageManager.addChangeListener(changeListener);
+        } else {
+            changeListener = null;
+        }
+    }
+
+    /**
+     * set the text directly
+     *
+     * @param text
+     */
+    public void setText1(String text) {
+        this.text1.setText(text);
+        splitPane1.resetToPreferredSizes();
+        splitPane2.resetToPreferredSizes();
+    }
+
+    public String getText1() {
+        return text1.getText().trim();
+    }
+
+    /**
+     * set the text directly
+     *
+     * @param text
+     */
+    public void setText2(String text) {
+        this.text2.setText(text + "   ");
+        splitPane1.resetToPreferredSizes();
+        splitPane2.resetToPreferredSizes();
+    }
+
+    public String getText2() {
+        return text2.getText().trim();
+    }
+
+    public void setExternalPanel1(JComponent externalPanel, boolean visible) {
+        panel1.removeAll();
+        if (visible)
+            panel1.add(externalPanel);
+        else
+            panel1.add(text1);
+        splitPane1.resetToPreferredSizes();
+        splitPane2.resetToPreferredSizes();
+        panel1.repaint();
+    }
+
+    public void setComponent2(JComponent externalPanel, boolean visible) {
+        panel2.removeAll();
+        if (visible)
+            panel2.add(externalPanel);
+        else
+            panel2.add(text2);
+        splitPane1.resetToPreferredSizes();
+        splitPane2.resetToPreferredSizes();
+        panel2.repaint();
+    }
+
+    /**
+     * set the text3 directly
+     *
+     * @param text3
+     */
+    public void setText3(String text3) {
+        this.text3.setText(text3 + " ");
+        if (splitPane1 != null)
+            splitPane1.resetToPreferredSizes();
+        if (splitPane2 != null)
+            splitPane2.resetToPreferredSizes();
+    }
+
+    public String getText3() {
+        return text3.getText().trim();
+    }
+
+    public void setToolTipText(String toolTipText) {
+        panel2.setToolTipText(toolTipText);
+    }
+}
diff --git a/src/jloda/gui/ToolBar.java b/src/jloda/gui/ToolBar.java
new file mode 100644
index 0000000..37450d3
--- /dev/null
+++ b/src/jloda/gui/ToolBar.java
@@ -0,0 +1,150 @@
+/**
+ * ToolBar.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import jloda.gui.commands.CommandManager;
+import jloda.gui.commands.ICommand;
+import jloda.gui.commands.TeXGenerator;
+import jloda.util.Basic;
+import jloda.util.ProgramProperties;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+/**
+ * tool bar generator
+ * Daniel Huson, 16.2010
+ */
+public class ToolBar extends JToolBar {
+    static private IToolBarModifier toolBarModifier;
+
+    /**
+     * construct a tool bar using the given configuation
+     * Example:  New...;Save...;|;Print...;|;Select All;
+     * To add a button with text label, tooltip and popup menu, use this syntax:
+     * {;label(tooltip);command1;command2;command3;};
+     *
+     * @param configuration
+     * @param commandManager
+     * @throws Exception
+     */
+    public ToolBar(String configuration, CommandManager commandManager) {
+        super();
+        this.setRollover(true);
+        this.setBorder(BorderFactory.createEtchedBorder());
+        this.setFloatable(false);
+        this.setLayout(new WrapLayout(FlowLayout.LEFT, 2, 2));
+
+        String[] tokens = configuration.split(";");
+
+        JPopupMenu popupMenu = null; // not null when in creation of popup menu
+        boolean needToAddPopupMenu = false;
+
+        for (String token : tokens) {
+            switch (token) {
+                case "|":
+                    if (popupMenu != null)
+                        popupMenu.addSeparator();
+                    else
+                        addSeparator(new Dimension(5, 10));
+                    break;
+                case "{":
+                    if (popupMenu == null) {
+                        popupMenu = new JPopupMenu();
+                        needToAddPopupMenu = true;
+                    } else
+                        System.err.println("Warning: nested popup menu in toolbar detected, not implemented");
+                    break;
+                case "}":
+                    popupMenu = null;
+                    needToAddPopupMenu = false;
+                    break;
+                default:
+                    if (CommandManager.getCommandsToIgnore().contains(token)) {
+                        if (needToAddPopupMenu) // this popup menu is disabled
+                        {
+                            popupMenu = null;
+                            needToAddPopupMenu = false;
+                        }
+                        continue;
+                    }
+
+                    if (needToAddPopupMenu) {
+                        String tooltip = null;
+                        int a = token.indexOf("(");
+                        int b = token.indexOf(")");
+                        if (a != -1 && b > a + 1) {
+                            tooltip = token.substring(a + 1, b);
+                            token = token.substring(0, a);
+                        }
+                        final JButton button = new JButton(token);
+                        Basic.changeFontSize(button, 10);
+
+                        if (tooltip != null)
+                            button.setToolTipText(tooltip);
+                        button.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEtchedBorder(), BorderFactory.createEmptyBorder(2, 0, 2, 0)));
+                        final JPopupMenu popup = popupMenu;
+                        button.addMouseListener(new MouseAdapter() {
+                            public void mousePressed(MouseEvent e) {
+                                popup.show(e.getComponent(), e.getX(), e.getY());
+                            }
+                        });
+                        add(button);
+                        needToAddPopupMenu = false;
+                        continue;
+                    }
+
+                    ICommand command = commandManager.getCommand(token);
+                    if (command == null) {
+                        JLabel label = new JLabel("(" + token + ")");
+                        label.setBorder(BorderFactory.createEmptyBorder());
+                        label.setEnabled(false);
+                        add(label);
+                    } else if (popupMenu != null) {
+                        popupMenu.add(commandManager.getJMenuItem(command));
+                    } else {
+                        AbstractButton button = commandManager.getButtonForToolBar(command);
+                        //button = new JToggleButton(button.getAction());
+                        //button.setText(null);
+                        button.setBorder(BorderFactory.createEtchedBorder());
+                        add(button);
+                    }
+                    break;
+            }
+        }
+        if (toolBarModifier != null)
+            toolBarModifier.apply(this, commandManager);
+
+        if (ProgramProperties.get("showtex", false)) {
+            System.out.println(TeXGenerator.getToolBarLaTeX(configuration, commandManager));
+        }
+    }
+
+    public static IToolBarModifier getToolBarModifier() {
+        return toolBarModifier;
+    }
+
+    public static void setToolBarModifier(IToolBarModifier toolBarModifier) {
+        ToolBar.toolBarModifier = toolBarModifier;
+    }
+
+}
diff --git a/src/jloda/gui/TwoInputOptionsPanel.java b/src/jloda/gui/TwoInputOptionsPanel.java
new file mode 100644
index 0000000..7f2db5e
--- /dev/null
+++ b/src/jloda/gui/TwoInputOptionsPanel.java
@@ -0,0 +1,72 @@
+/*
+ *  Copyright (C) 2015 Daniel H. Huson
+ *
+ *  (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package jloda.gui;
+
+import jloda.util.ProgramProperties;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * two input options panel
+ * Created by huson on 8/24/16.
+ */
+public class TwoInputOptionsPanel<T, S> {
+
+    /**
+     * show a two value input dialog
+     *
+     * @param title
+     * @param label1
+     * @param value1
+     * @param label2
+     * @param value2
+     * @return true, if not canceled
+     */
+    public static String[] show(Component parent, String title, String label1, String value1, String toolTip1, String label2, String value2, String toolTip2) {
+        final JTextField field1 = new JTextField(8);
+        field1.setText(value1);
+        field1.setToolTipText(toolTip1);
+
+        final JLabel jLabel1 = new JLabel(label1 + ":  ");
+        jLabel1.setToolTipText(toolTip1);
+
+        final JTextField field2 = new JTextField(8);
+        field2.setText(value2);
+        field2.setToolTipText(toolTip2);
+
+        final JLabel jLabel2 = new JLabel(label2 + ":  ");
+        jLabel2.setToolTipText(toolTip2);
+
+        final JPanel myPanel = new JPanel();
+        myPanel.setLayout(new GridLayout(2, 2));
+        myPanel.add(jLabel1);
+        myPanel.add(field1);
+        myPanel.add(jLabel2);
+        myPanel.add(field2);
+
+        final int result = JOptionPane.showConfirmDialog(parent, myPanel, title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
+                ProgramProperties.getProgramIcon());
+        if (result == JOptionPane.OK_OPTION) {
+            return new String[]{field1.getText(), field2.getText()};
+        } else
+            return null;
+    }
+}
diff --git a/src/jloda/gui/WindowListenerAdapter.java b/src/jloda/gui/WindowListenerAdapter.java
new file mode 100644
index 0000000..aa88d17
--- /dev/null
+++ b/src/jloda/gui/WindowListenerAdapter.java
@@ -0,0 +1,52 @@
+/**
+ * WindowListenerAdapter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+
+/**
+ * adapter for window listener
+ *
+ * @author huson
+ *         Date: 27-Nov-2003
+ */
+public class WindowListenerAdapter implements WindowListener {
+    public void windowActivated(WindowEvent event) {
+    }
+
+    public void windowClosed(WindowEvent event) {
+    }
+
+    public void windowClosing(WindowEvent event) {
+    }
+
+    public void windowDeactivated(WindowEvent event) {
+    }
+
+    public void windowDeiconified(WindowEvent event) {
+    }
+
+    public void windowIconified(WindowEvent event) {
+    }
+
+    public void windowOpened(WindowEvent event) {
+    }
+}
diff --git a/src/jloda/gui/WrapLayout.java b/src/jloda/gui/WrapLayout.java
new file mode 100644
index 0000000..25f258a
--- /dev/null
+++ b/src/jloda/gui/WrapLayout.java
@@ -0,0 +1,189 @@
+/**
+ * WrapLayout.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * FlowLayout subclass that fully supports wrapping of components.
+ */
+public class WrapLayout extends FlowLayout {
+    private Dimension preferredLayoutSize;
+
+    /**
+     * Constructs a new <code>WrapLayout</code> with a left
+     * alignment and a default 5-unit horizontal and vertical gap.
+     */
+    public WrapLayout() {
+        super();
+    }
+
+    /**
+     * Constructs a new <code>FlowLayout</code> with the specified
+     * alignment and a default 5-unit horizontal and vertical gap.
+     * The value of the alignment argument must be one of
+     * FlowLayout.LEFT, FlowLayout.RIGHT, FlowLayout.CENTER, FlowLayout.LEADING, or FlowLayout.TRAILING.
+     *
+     * @param align the alignment value
+     */
+    public WrapLayout(int align) {
+        super(align);
+    }
+
+    /**
+     * Creates a new flow layout manager with the indicated alignment
+     * and the indicated horizontal and vertical gaps.
+     * <p/>
+     * The value of the alignment argument must be one of
+     * FlowLayout.LEFT, FlowLayout.RIGHT, FlowLayout.CENTER, FlowLayout.LEADING, or FlowLayout.TRAILING.
+     *
+     * @param align the alignment value
+     * @param hgap  the horizontal gap between components
+     * @param vgap  the vertical gap between components
+     */
+    public WrapLayout(int align, int hgap, int vgap) {
+        super(align, hgap, vgap);
+    }
+
+    /**
+     * Returns the preferred dimensions for this layout given the
+     * <i>visible</i> components in the specified target container.
+     *
+     * @param target the component which needs to be laid out
+     * @return the preferred dimensions to lay out the
+     * subcomponents of the specified container
+     */
+    @Override
+    public Dimension preferredLayoutSize(Container target) {
+        return layoutSize(target, true);
+    }
+
+    /**
+     * Returns the minimum dimensions needed to layout the <i>visible</i>
+     * components contained in the specified target container.
+     *
+     * @param target the component which needs to be laid out
+     * @return the minimum dimensions to lay out the
+     * subcomponents of the specified container
+     */
+    @Override
+    public Dimension minimumLayoutSize(Container target) {
+        Dimension minimum = layoutSize(target, false);
+        minimum.width -= (getHgap() + 1);
+        return minimum;
+    }
+
+    /**
+     * Returns the minimum or preferred dimension needed to layout the target
+     * container.
+     *
+     * @param target    target to get layout size for
+     * @param preferred should preferred size be calculated
+     * @return the dimension to layout the target container
+     */
+    private Dimension layoutSize(Container target, boolean preferred) {
+        synchronized (target.getTreeLock()) {
+            //  Each row must fit with the width allocated to the container.
+            //  When the container width = 0, the preferred width of the container
+            //  has not yet been calculated so lets ask for the maximum.
+
+            int targetWidth = target.getSize().width;
+
+            if (targetWidth == 0)
+                targetWidth = Integer.MAX_VALUE;
+
+            int hgap = getHgap();
+            int vgap = getVgap();
+            Insets insets = target.getInsets();
+            int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2);
+            int maxWidth = targetWidth - horizontalInsetsAndGap;
+
+            //  Fit components into the allowed width
+
+            Dimension dim = new Dimension(0, 0);
+            int rowWidth = 0;
+            int rowHeight = 0;
+
+            int nmembers = target.getComponentCount();
+
+            for (int i = 0; i < nmembers; i++) {
+                Component m = target.getComponent(i);
+
+                if (m.isVisible()) {
+                    Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();
+
+                    //  Can't add the component to current row. Start a new row.
+
+                    if (rowWidth + d.width > maxWidth) {
+                        addRow(dim, rowWidth, rowHeight);
+                        rowWidth = 0;
+                        rowHeight = 0;
+                    }
+
+                    //  Add a horizontal gap for all components after the first
+
+                    if (rowWidth != 0) {
+                        rowWidth += hgap;
+                    }
+
+                    rowWidth += d.width;
+                    rowHeight = Math.max(rowHeight, d.height);
+                }
+            }
+
+            addRow(dim, rowWidth, rowHeight);
+
+            dim.width += horizontalInsetsAndGap;
+            dim.height += insets.top + insets.bottom + vgap * 2;
+
+            //	When using a scroll pane or the DecoratedLookAndFeel we need to
+            //  make sure the preferred size is less than the size of the
+            //  target containter so shrinking the container size works
+            //  correctly. Removing the horizontal gap is an easy way to do this.
+
+            Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target);
+
+            if (scrollPane != null && target.isValid()) {
+                dim.width -= (hgap + 1);
+            }
+
+            return dim;
+        }
+    }
+
+    /*
+     *  A new row has been completed. Use the dimensions of this row
+     *  to update the preferred size for the container.
+     *
+     *  @param dim update the width and height when appropriate
+     *  @param rowWidth the width of the row to add
+     *  @param rowHeight the height of the row to add
+     */
+    private void addRow(Dimension dim, int rowWidth, int rowHeight) {
+        dim.width = Math.max(dim.width, rowWidth);
+
+        if (dim.height > 0) {
+            dim.height += getVgap();
+        }
+
+        dim.height += rowHeight;
+    }
+}
diff --git a/src/jloda/gui/commands/CommandBase.java b/src/jloda/gui/commands/CommandBase.java
new file mode 100644
index 0000000..563b979
--- /dev/null
+++ b/src/jloda/gui/commands/CommandBase.java
@@ -0,0 +1,298 @@
+/**
+ * CommandBase.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.commands;
+
+import jloda.gui.director.IDirectableViewer;
+import jloda.gui.director.IDirector;
+import jloda.util.Basic;
+import jloda.util.parse.NexusStreamParser;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.io.StringReader;
+
+/**
+ * Base class for commands
+ * Daniel Huson, 5.2010
+ */
+public abstract class CommandBase {
+    private boolean selected = false;
+    private IDirector dir;
+    private IDirectableViewer theViewer;
+    private Object theParent;
+    private Timer autoRepeatTimer;
+    private int autoRepeatInterval = 0; // if >0, will autorepeat with given number of milliseconds
+    private CommandManager commandManager;
+
+    /**
+     * constructor
+     */
+    public CommandBase() {
+    }
+
+    /**
+     * constructor
+     * @param commandManager
+     */
+    public CommandBase(CommandManager commandManager) {
+        setCommandManager(commandManager);
+        setDir(commandManager.getDir());
+        setParent(commandManager.getParent());
+        if (commandManager.getParent() instanceof IDirectableViewer)
+            setViewer((IDirectableViewer) commandManager.getParent());
+    }
+
+    /**
+     * set the director
+     *
+     * @param dir
+     */
+    public void setDir(IDirector dir) {
+        this.dir = dir;
+    }
+
+    /**
+     * get the director
+     *
+     * @return dir
+     */
+    public IDirector getDir() {
+        return dir;
+    }
+
+    /**
+     * set the command manager. This is required for all commands that call the "execute" method
+     *
+     * @param commandManager
+     */
+    public void setCommandManager(CommandManager commandManager) {
+        this.commandManager = commandManager;
+    }
+
+    /**
+     * get the command manager
+     */
+    public CommandManager getCommandManager() {
+        return commandManager;
+    }
+
+    /**
+     * get the viewer
+     */
+    public IDirectableViewer getViewer() {
+        return theViewer;
+    }
+
+    /**
+     * set the viewer
+     */
+    public void setViewer(IDirectableViewer viewer) {
+        this.theViewer = viewer;
+    }
+
+    /**
+     * sets the viewer in the case that the viewer is not an  IDirectableViewer
+     *
+     * @param viewer
+     */
+    public void setParent(Object viewer) {
+        theParent = viewer;
+    }
+
+    /**
+     * gets the  viewer in the case that the viewer is not an  IDirectableViewer
+     *
+     * @return viewer
+     */
+    public Object getParent() {
+        return theParent;
+    }
+
+    /**
+     * get an alternative name used to identify this command
+     *
+     * @return name
+     */
+    public String getAltName() {
+        return null;
+    }
+
+    /**
+     * parses the given command and executes it
+     *
+     * @param np
+     * @throws java.io.IOException
+     */
+    abstract public void apply(NexusStreamParser np) throws Exception;
+
+    /**
+     * parses the given command and executes it
+     *
+     * @param command
+     * @throws java.io.IOException
+     */
+    public void apply(String command) throws Exception {
+        apply(new NexusStreamParser(new StringReader(command)));
+    }
+
+    /**
+     * get command-line usage description
+     *
+     * @return usage
+     */
+    abstract public String getSyntax();
+
+    /**
+     * initial tokens used to identify the command
+     *
+     * @return first tokens
+     */
+    public String getStartsWith() {
+        String syntax = getSyntax();
+        if (syntax == null)
+            return null;
+        else {
+            NexusStreamParser np = new NexusStreamParser(new StringReader(syntax));
+            np.setSquareBracketsSurroundComments(false);
+
+            String startsWith = "";
+            String token;
+            try {
+                while (np.peekNextToken() != NexusStreamParser.TT_EOF) {
+                    token = np.getWordRespectCase();
+                    if (token == null || token.startsWith("[") || token.startsWith("{") || token.startsWith("<") || token.equals(";"))
+                        break;
+                    startsWith += " " + token;
+                }
+                return startsWith;
+            } catch (IOException e) {
+                Basic.caught(e);
+                return null;
+            }
+        }
+    }
+
+    /**
+     * execute a command in a separate thread
+     *
+     * @param command
+     */
+    public void execute(String command) {
+
+        if (getViewer() != null) {
+            dir.execute(command, commandManager, getViewer().getFrame());
+        } else
+            dir.execute(command, commandManager);
+    }
+
+    /**
+     * execute a command in the current thread
+     *
+     * @param command
+     */
+    public void executeImmediately(String command) {
+        dir.executeImmediately(command, commandManager);
+    }
+
+    /**
+     * set the selected status
+     *
+     * @param selected
+     */
+    public void setSelected(boolean selected) {
+        this.selected = selected;
+    }
+
+    /**
+     * is selected?
+     */
+    public boolean isSelected() {
+        return selected;
+    }
+
+    /**
+     * action to be performed
+     *
+     * @param ev
+     */
+    abstract public void actionPerformed(ActionEvent ev);
+
+    /**
+     * get the autorepeat interval. 0 means no autorepeat
+     *
+     * @return autorepeat interval
+     */
+    public int getAutoRepeatInterval() {
+        return autoRepeatInterval;
+    }
+
+    /**
+     * set  the autorepeat interval. 0 means no autorepeat
+     *
+     * @param autoRepeatInterval
+     */
+    public void setAutoRepeatInterval(int autoRepeatInterval) {
+        this.autoRepeatInterval = autoRepeatInterval;
+    }
+
+    /**
+     * Action to be performed in case of autorepeat
+     *
+     * @param ev
+     */
+    public void actionPerformedAutoRepeat(ActionEvent ev) {
+        actionPerformed(ev);
+        if (getAutoRepeatInterval() > 0) {
+            if (autoRepeatTimer == null) {
+                autoRepeatTimer = new Timer(getAutoRepeatInterval(), new ActionListener() {
+                    public void actionPerformed(ActionEvent evt) {
+                        CommandBase.this.actionPerformed(null);
+                    }
+                });
+                autoRepeatTimer.setRepeats(true);
+                JComponent component = (JComponent) ev.getSource();
+                component.addMouseListener(new MouseAdapter() {
+                    public void mousePressed(MouseEvent e) {
+                        //System.err.println("pressed");
+                        if (!autoRepeatTimer.isRunning()) autoRepeatTimer.start();
+                    }
+
+                    public void mouseReleased(MouseEvent e) {
+                        //System.err.println("released");
+                        if (autoRepeatTimer.isRunning()) autoRepeatTimer.stop();
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * gets the command needed to undo this command
+     *
+     * @return undo command
+     */
+    public String getUndo() {
+        return null;
+    }
+}
diff --git a/src/jloda/gui/commands/CommandManager.java b/src/jloda/gui/commands/CommandManager.java
new file mode 100644
index 0000000..edd699f
--- /dev/null
+++ b/src/jloda/gui/commands/CommandManager.java
@@ -0,0 +1,890 @@
+/**
+ * CommandManager.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.commands;
+
+import jloda.gui.director.IDirectableViewer;
+import jloda.gui.director.IDirector;
+import jloda.util.Basic;
+import jloda.util.CanceledException;
+import jloda.util.PluginClassLoader;
+import jloda.util.parse.NexusStreamParser;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.*;
+
+/**
+ * managers commands
+ * Daniel Huson, 7.2007
+ */
+public class CommandManager {
+    protected static final List<ICommand> globalCommands = new LinkedList<>();
+
+    protected final IDirector dir;
+    protected final List<ICommand> commands;
+    protected final Map<String, ICommand> name2Command = new HashMap<>();
+    protected final Map<String, ICommand> startsWith2Command = new HashMap<>();
+
+    // these are used to update the selection state of check box menu items
+    protected final Map<JMenuItem, ICommand> menuItem2Command = new HashMap<>();
+    protected final Map<AbstractButton, ICommand> button2Command = new HashMap<>();
+
+    public static final String ALT_NAME = "AltName";
+
+    protected String undoCommand;
+    protected boolean returnOnCommandNotFound = false;
+    final static protected Set<String> commandsToIgnore = new HashSet<>();
+
+    protected final Object parent;
+
+    /**
+     * construct a parser
+     *
+     * @param dir
+     */
+    public CommandManager(IDirector dir, List<ICommand> commands) {
+        this.dir = dir;
+        this.parent = null;
+        this.commands = commands;
+        for (ICommand command : commands) {
+            command.setDir(dir);
+        }
+    }
+
+    /**
+     * construct a parser and load all commands found for the given path
+     */
+    public CommandManager(IDirector dir, IDirectableViewer viewer, String commandsPath) {
+        this(dir, viewer, new String[]{commandsPath}, false);
+    }
+
+    /**
+     * construct a parser and load all commands found for the given paths
+     * @param viewer  usually an IDirectableViewer, but sometimes a JDialog
+     */
+    public CommandManager(IDirector dir, Object viewer, String[] commandsPaths) {
+        this(dir, viewer, commandsPaths, false);
+    }
+
+    /**
+     * construct a parser and load all commands found for the given path
+     */
+    public CommandManager(IDirector dir, IDirectableViewer viewer, String commandsPath, boolean returnOnCommandNotFound) {
+        this(dir, viewer, new String[]{commandsPath}, returnOnCommandNotFound);
+    }
+
+    /**
+     * construct a parser and load all commands found for the given paths
+     *
+     * @param viewer  usually an IDirectableViewer, but sometimes a JDialog
+     */
+    public CommandManager(IDirector dir, Object viewer, String[] commandsPaths, boolean returnOnCommandNotFound) {
+        this.dir = dir;
+        this.parent = viewer;
+        this.setReturnOnCommandNotFound(returnOnCommandNotFound);
+        this.commands = new LinkedList<>();
+
+        addCommands(viewer, globalCommands, true);
+        addCommands(viewer, commandsPaths);
+    }
+
+    /**
+     * add more commands
+     *
+     * @param viewer
+     * @param commandsPaths
+     */
+    public void addCommands(Object viewer, String[] commandsPaths) {
+        final List<ICommand> commands = new LinkedList<>();
+        for (String commandsPath : commandsPaths) {
+            for (Object obj : PluginClassLoader.getInstances(commandsPath, ICommand.class)) {
+                if (obj instanceof ICommand)
+                    commands.add((ICommand) obj);
+            }
+        }
+        addCommands(viewer, commands, false);
+    }
+
+    /**
+     * add the given list of commands
+     *
+     * @param mustWrap commands that are defined globally but used locally must be wrapped so as to preserve the correct command manager, director and viewer
+     */
+    public void addCommands(Object viewer, Collection<ICommand> commands, boolean mustWrap) {
+        for (final ICommand command0 : commands) {
+            final ICommand command;
+
+            if (mustWrap) {
+                if (command0 instanceof ICheckBoxCommand)
+                    command = new WrappedCheckBoxCommand((ICheckBoxCommand) command0);
+                else
+                    command = new WrappedCommand(command0);
+            }
+            else
+                command = command0;
+
+            command.setDir(dir);
+            if (viewer instanceof IDirectableViewer)
+                command.setViewer((IDirectableViewer) viewer);
+            else
+                command.setViewer(null);
+            command.setParent(viewer);
+            command.setCommandManager(this);
+
+            String name = command.getAltName();
+            if (name == null)
+                name = command.getName();
+            name2Command.put(name, command);
+
+            String startsWith = command.getStartsWith();
+            if (startsWith != null) {
+                final ICommand prev = startsWith2Command.get(startsWith);
+                if (prev != null) {
+                    if (prev.getClass().isAssignableFrom(command.getClass())) // command extends prev
+                    {
+                    } else if (command.getClass().isAssignableFrom(prev.getClass())) // prev extends command
+                    {
+                        startsWith2Command.put(startsWith, command);
+                    }
+                } else
+                    startsWith2Command.put(startsWith, command);
+            }
+            this.commands.add(command);
+        }
+    }
+
+    /**
+     * execute
+     *
+     * @param commandString
+     * @throws IOException
+     * @throws CanceledException
+     */
+    public void execute(String commandString) throws IOException, CanceledException {
+        commandString = Basic.protectBackSlashes(commandString);  // need this for windows paths
+        NexusStreamParser np = new NexusStreamParser(new StringReader(commandString));
+        execute(np);
+    }
+
+    /**
+     * execute a stream of commands
+     *
+     * @param np
+     */
+    public void execute(NexusStreamParser np) throws CanceledException, IOException {
+        while (np.peekNextToken() != NexusStreamParser.TT_EOF) {
+            if (np.peekMatchIgnoreCase(";")) {
+                np.matchIgnoreCase(";"); // skip empty command
+            } else {
+                boolean found = false;
+                for (ICommand command : startsWith2Command.values()) {
+                    // System.err.println("trying " + Basic.getShortName(command.getClass()));
+                    if (command.getStartsWith() != null && np.peekMatchIgnoreCase(command.getStartsWith())) {
+                        try {
+                            if (command.getName() != null && command.getName().equals("Undo"))
+                                undoCommand = null;
+                            else
+                                undoCommand = command.getUndo();
+                            command.apply(np);
+                        } catch (CanceledException e) {
+                            // System.err.println("USER canceled");
+                            throw e;
+                        } catch (Exception e) {
+                            Basic.caught(e);
+                            System.err.println("Command usage: " + command.getSyntax() + " - " + command.getDescription());
+                            throw new IOException(e.getMessage());
+                        }
+                        found = true;
+                        break;
+                    }
+                }
+                if (!found) {
+                    if (returnOnCommandNotFound) {
+                        String command = np.getWordRespectCase();
+                        System.err.println("Failed to parse command: " + command + " " + Basic.toString(np.getTokensRespectCase(null, ";"), " "));
+                        String similar = getUsageStartsWith(command);
+                        if (similar.length() > 0) {
+                            System.err.println("Similar commands:");
+                            System.err.print(similar);
+                        }
+                        return;
+                    } else
+                        System.err.println("Failed to parse command: '" + np.getWordRespectCase() + "'");
+                }
+            }
+        }
+    }
+
+    /**
+     * get the named command
+     *
+     * @param name
+     * @return command
+     */
+    public ICommand getCommand(String name) {
+        return name2Command.get(name);
+    }
+
+    /**
+     * enable or disable all critical actions
+     *
+     * @param on
+     */
+    public void setEnableCritical(boolean on) {
+        /**
+         * update selection state of all menu items
+         */
+        for (JMenuItem menuItem : menuItem2Command.keySet()) {
+            ICommand command = menuItem2Command.get(menuItem);
+            if (command != null && command.isCritical()) {
+                menuItem.setEnabled(on && command.isApplicable());
+            }
+        }
+
+        /**
+         * update selection state of all check boxes
+         */
+        for (AbstractButton button : button2Command.keySet()) {
+            ICommand command = button2Command.get(button);
+            if (command != null && command.isCritical()) {
+                button.setEnabled(on && command.isApplicable());
+                button.getAction().setEnabled(button.isEnabled());
+            }
+        }
+    }
+
+    /**
+     * update the enable state
+     */
+    public void updateEnableState() {
+        /**
+         * update selection state of all menu items
+         */
+        try {
+            for (JMenuItem menuItem : menuItem2Command.keySet()) {
+                ICommand command = menuItem2Command.get(menuItem);
+                if (command != null) {
+                    menuItem.setEnabled(command.isApplicable());
+                    if (command instanceof ICheckBoxCommand)
+                        menuItem.setSelected(((ICheckBoxCommand) command).isSelected());
+                }
+            }
+
+            /**
+             * update selection state of all check boxes
+             */
+            for (AbstractButton button : button2Command.keySet()) {
+                ICommand command = button2Command.get(button);
+                if (button.getAction() != null)
+                    button.getAction().setEnabled(command.isApplicable());
+                else
+                    button.setEnabled(command.isApplicable());
+                if (command instanceof ICheckBoxCommand) {
+                    button.setSelected(((ICheckBoxCommand) command).isSelected());
+                    if (((ICheckBoxCommand) command).isSelected())
+                        button.setBorder(BorderFactory.createBevelBorder(1));
+                    else
+                        button.setBorder(BorderFactory.createEtchedBorder());
+                    button.repaint();
+
+                }
+            }
+            } catch (Exception ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * update the enable state
+     */
+    public void updateEnableState(String commandName) {
+        for (JMenuItem menuItem : menuItem2Command.keySet()) {
+            ICommand command = menuItem2Command.get(menuItem);
+            if (command.getName().equals(commandName)) {
+                menuItem.setEnabled(command.isApplicable());
+                if (command instanceof ICheckBoxCommand)
+                    menuItem.setSelected(((ICheckBoxCommand) command).isSelected());
+            }
+        }
+
+        /**
+         * update selection state of all check boxes
+         */
+        for (AbstractButton button : button2Command.keySet()) {
+            ICommand command = button2Command.get(button);
+            if (command.getName().equals(commandName)) {
+                if (button.getAction() != null)
+                    button.getAction().setEnabled(command.isApplicable());
+                else
+                    button.setEnabled(command.isApplicable());
+                if (command instanceof ICheckBoxCommand) {
+                    button.setSelected(((ICheckBoxCommand) command).isSelected());
+                    if (((ICheckBoxCommand) command).isSelected())
+                        button.setBorder(BorderFactory.createBevelBorder(1));
+                    else
+                        button.setBorder(BorderFactory.createEtchedBorder());
+                }
+            }
+        }
+    }
+
+    /**
+     * gets the usage of all commands, ordered by menu
+     *
+     * @param menuBar
+     * @return usage
+     */
+    public String getUsage(JMenuBar menuBar) {
+        StringBuilder buf = new StringBuilder();
+        Set<ICommand> seen = new HashSet<>();
+        for (int i = 0; i < menuBar.getMenuCount(); i++) {
+            JMenu menu = menuBar.getMenu(i);
+
+            List<JMenu> subMenus = getUsageMenu("menu", menu, buf, seen);
+            while (subMenus.size() > 0) {
+                menu = subMenus.remove(0);
+                subMenus.addAll(getUsageMenu("sub-menu", menu, buf, seen));
+            }
+        }
+
+        final Set<ICommand> additionalCommands = new HashSet<>();
+        additionalCommands.addAll(commands);
+        additionalCommands.removeAll(seen);
+        if (additionalCommands.size() > 0) {
+            buf.append("Additional commands:\n");
+            SortedSet<String> lines = new TreeSet<>();
+            for (ICommand command : additionalCommands) {
+                String syntax = command.getSyntax();
+                if (syntax != null) {
+                    String description = command.getDescription();
+                    if (description != null) {
+                        if (Basic.getLastLine(syntax).length() + description.length() < 100)
+                            lines.add(syntax + " - " + description + "\n");
+                        else
+                            lines.add(syntax + "\n\t- " + description + "\n");
+                    }
+                }
+            }
+            for (String line : lines)
+                buf.append(line);
+            buf.append("\n");
+        }
+        return buf.toString();
+    }
+
+    /**
+     * writes the description of a menu and returns all hierachical menus below it
+     *
+     * @param menu
+     * @param buf
+     * @param seen
+     * @return list of sub menus
+     */
+    private List<JMenu> getUsageMenu(String label, JMenu menu, StringBuilder buf, Set<ICommand> seen) {
+        List<JMenu> subMenus = new LinkedList<>();
+
+        if (menu.getText().equals("Open Recent"))
+            return subMenus;
+        buf.append(menu.getText()).append(" ").append(label).append(":\n");
+
+        for (int j = 0; j < menu.getItemCount(); j++) {
+            JMenuItem item = menu.getItem(j);
+            if (item != null) {
+                if (item instanceof JMenu) {
+                    subMenus.add((JMenu) item);
+                } else {
+                    Action action = item.getAction();
+                    String name = null;
+                    if (action != null) {
+                        name = (String) action.getValue(ALT_NAME);
+                        if (name == null)
+                            name = (String) action.getValue(AbstractAction.NAME);
+                    }
+                    if (name == null)
+                        name = item.getText();
+                    ICommand command = getCommand(name);
+                    if (command != null && command.getSyntax() != null) {
+                        String syntax = command.getSyntax();
+                        String description = command.getDescription();
+                        if (Basic.getLastLine(syntax).length() + description.length() < 100)
+                            buf.append(syntax).append(" - ").append(description).append("\n");
+                        else
+                            buf.append(syntax).append("\n\t- ").append(description).append("\n");
+
+                        seen.add(command);
+                    }
+                }
+            }
+        }
+        buf.append("\n");
+        return subMenus;
+    }
+
+
+    /**
+     * gets the usage of all commands
+     *
+     * @return usage
+     */
+    public String getUsage() {
+        return getUsage((Set<String>) null);
+    }
+
+    /**
+     * gets the usage of all commands
+     *
+     * @param keywords set of keywords, if not null or empty, at least one of these words must appear in the syntax or description
+     * @return usage
+     */
+    public String getUsage(Set<String> keywords) {
+        SortedSet<String> lines = new TreeSet<>();
+        for (ICommand command : commands) {
+            if (command.getSyntax() != null) {
+                boolean ok = keywords == null || keywords.size() == 0;
+                if (!ok) {
+                    for (String keyword : keywords) {
+                        if (command.getSyntax().toLowerCase().contains(keyword.toLowerCase())) {
+                            ok = true;
+                            break;
+                        }
+                        if (command.getDescription() != null && command.getDescription().toLowerCase().contains(keyword.toLowerCase())) {
+                            ok = true;
+                            break;
+                        }
+                    }
+                }
+                if (ok)
+                    lines.add(command.getSyntax() + " - " + command.getDescription() + "\n");
+            }
+        }
+        StringBuilder buf = new StringBuilder();
+
+        for (String line : lines) {
+            buf.append(line);
+        }
+        return buf.toString();
+    }
+
+    /**
+     * gets the usage of all commands
+     *
+     * @return usage
+     */
+    public String getUsageStartsWith(String name) {
+        final SortedSet<String> lines = new TreeSet<>();
+        for (ICommand command : commands) {
+            String syntaxString = command.getSyntax();
+            if (syntaxString != null && syntaxString.trim().toLowerCase().startsWith(name))
+                lines.add(syntaxString + " - " + command.getDescription() + "\n");
+        }
+        StringBuilder buf = new StringBuilder();
+
+        for (String line : lines) {
+            buf.append(line);
+        }
+        return buf.toString();
+    }
+
+
+    /**
+     * gets the list of all commands
+     *
+     * @return all commands
+     */
+    public List<ICommand> getAllCommands() {
+        return commands;
+    }
+
+    public String getUndoCommand() {
+        return undoCommand;
+    }
+
+    /**
+     * get a menu item for the named command
+     *
+     * @param commandName
+     * @return menu item
+     */
+    public JMenuItem getJMenuItem(String commandName) {
+        final ICommand command = getCommand(commandName);
+        if (command != null) {
+            final JMenuItem item = getJMenuItem(command);
+            if (item != null) {
+                item.setEnabled(command.isApplicable());
+                return item;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * get a menu item for the named command
+     *
+     * @param commandName
+     * @param enabled
+     * @return menu item
+     */
+    public JMenuItem getJMenuItem(String commandName, boolean enabled) {
+        ICommand command = getCommand(commandName);
+        JMenuItem item = getJMenuItem(command);
+        if (item != null)
+            item.setEnabled(enabled && command.isApplicable());
+        return item;
+    }
+
+    /**
+     * creates a menu item for the given command
+     *
+     * @param command
+     * @return menu item
+     */
+    public JMenuItem getJMenuItem(final ICommand command) {
+        if (command == null) {
+            JMenuItem nullItem = new JMenuItem("Null");
+            nullItem.setEnabled(false);
+            return nullItem;
+        }
+        if (command instanceof ICheckBoxCommand) {
+            final ICheckBoxCommand checkBoxCommand = (ICheckBoxCommand) command;
+
+            final JCheckBoxMenuItem cbox = new JCheckBoxMenuItem();
+            AbstractAction action = new AbstractAction() {
+                public void actionPerformed(ActionEvent actionEvent) {
+                    checkBoxCommand.setSelected(cbox.isSelected());
+                    if (command.getAutoRepeatInterval() > 0)
+                        command.actionPerformedAutoRepeat(actionEvent);
+                    else
+                        command.actionPerformed(actionEvent);
+                }
+            };
+            action.putValue(AbstractAction.NAME, command.getName());
+            action.putValue(ALT_NAME, command.getAltName());
+            if (command.getDescription() != null)
+                action.putValue(AbstractAction.SHORT_DESCRIPTION, command.getDescription());
+            if (command.getIcon() != null)
+                action.putValue(AbstractAction.SMALL_ICON, command.getIcon());
+            if (command.getAcceleratorKey() != null)
+                action.putValue(AbstractAction.ACCELERATOR_KEY, command.getAcceleratorKey());
+            cbox.setAction(action);
+            cbox.setSelected(checkBoxCommand.isSelected());
+            menuItem2Command.put(cbox, checkBoxCommand);
+            return cbox;
+        } else {
+            AbstractAction action = new AbstractAction() {
+                public void actionPerformed(ActionEvent actionEvent) {
+                    if (command.getAutoRepeatInterval() > 0)
+                        command.actionPerformedAutoRepeat(actionEvent);
+                    else
+                        command.actionPerformed(actionEvent);
+                }
+            };
+            action.putValue(AbstractAction.NAME, command.getName());
+            action.putValue(ALT_NAME, command.getAltName());
+            if (command.getDescription() != null)
+                action.putValue(AbstractAction.SHORT_DESCRIPTION, command.getDescription());
+            if (command.getIcon() != null)
+                action.putValue(AbstractAction.SMALL_ICON, command.getIcon());
+            if (command.getAcceleratorKey() != null)
+                action.putValue(AbstractAction.ACCELERATOR_KEY, command.getAcceleratorKey());
+            JMenuItem menuItem = new JMenuItem(action);
+            menuItem2Command.put(menuItem, command);
+            return menuItem;
+        }
+    }
+
+    /**
+     * creates a button for the command
+     *
+     * @param commandName
+     * @return button
+     */
+    public AbstractButton getButton(String commandName) {
+        return getButton(commandName, true);
+    }
+
+    private static boolean warned = false;
+
+    /**
+     * creates a button for the command
+     *
+     * @param commandName
+     * @param enabled
+     * @return button
+     */
+    public AbstractButton getButton(String commandName, boolean enabled) {
+        AbstractButton button = getButton(getCommand(commandName));
+        button.setEnabled(enabled);
+        if (button.getText() != null && button.getText().equals("Null")) {
+            System.err.println("Failed to create button for command '" + commandName + "'");
+            if (!warned) {
+                warned = true;
+                System.err.println("Table of known commands:");
+                for (String name : name2Command.keySet()) {
+                    System.err.print(" '" + name + "'");
+                }
+                System.err.println();
+            }
+        }
+        return button;
+    }
+
+    /**
+     * creates a button for the command
+     *
+     * @param command
+     * @return button
+     */
+    public AbstractButton getButtonForToolBar(final ICommand command) {
+        if (command == null) {
+            JButton nullButton = new JButton("Null");
+            nullButton.setEnabled(false);
+            return nullButton;
+        }
+        if (command instanceof ICheckBoxCommand) {
+            final ICheckBoxCommand checkBoxCommand = (ICheckBoxCommand) command;
+
+            final JButton cbox = new JButton();
+            AbstractAction action = new AbstractAction() {
+                public void actionPerformed(ActionEvent actionEvent) {
+                    checkBoxCommand.setSelected(cbox.isSelected());
+                    if (cbox.isEnabled()) {
+                        if (command.getAutoRepeatInterval() > 0)
+                            command.actionPerformedAutoRepeat(actionEvent);
+                        else
+                            command.actionPerformed(actionEvent);
+                    }
+                }
+            };
+            action.putValue(AbstractAction.NAME, command.getName());
+            action.putValue(ALT_NAME, command.getAltName());
+            if (command.getDescription() != null)
+                action.putValue(AbstractAction.SHORT_DESCRIPTION, command.getDescription());
+            if (command.getIcon() != null)
+                action.putValue(AbstractAction.SMALL_ICON, command.getIcon());
+            if (command.getAcceleratorKey() != null)
+                action.putValue(AbstractAction.ACCELERATOR_KEY, command.getAcceleratorKey());
+            cbox.setAction(action);
+            cbox.setSelected(checkBoxCommand.isSelected());
+            button2Command.put(cbox, checkBoxCommand);
+            if (cbox.getIcon() != null)
+                cbox.setText(null);
+            return cbox;
+        } else
+            return getButton(command);
+    }
+
+    /**
+     * creates a button for the command
+     *
+     * @param command
+     * @return button
+     */
+    public AbstractButton getButton(final ICommand command) {
+        if (command == null) {
+            JButton nullButton = new JButton("Null");
+            nullButton.setEnabled(false);
+            return nullButton;
+        }
+        if (command instanceof ICheckBoxCommand) {
+            final ICheckBoxCommand checkBoxCommand = (ICheckBoxCommand) command;
+
+            final JCheckBox cbox = new JCheckBox();
+            AbstractAction action = new AbstractAction() {
+                public void actionPerformed(ActionEvent actionEvent) {
+                    checkBoxCommand.setSelected(cbox.isSelected());
+                    if (cbox.isEnabled()) {
+                        if (command.getAutoRepeatInterval() > 0)
+                            command.actionPerformedAutoRepeat(actionEvent);
+                        else
+                            command.actionPerformed(actionEvent);
+                    }
+                }
+            };
+            action.putValue(AbstractAction.NAME, command.getName());
+            action.putValue(ALT_NAME, command.getAltName());
+            if (command.getDescription() != null)
+                action.putValue(AbstractAction.SHORT_DESCRIPTION, command.getDescription());
+            if (command.getIcon() != null)
+                action.putValue(AbstractAction.SMALL_ICON, command.getIcon());
+            if (command.getAcceleratorKey() != null)
+                action.putValue(AbstractAction.ACCELERATOR_KEY, command.getAcceleratorKey());
+            cbox.setAction(action);
+            cbox.setSelected(checkBoxCommand.isSelected());
+            button2Command.put(cbox, checkBoxCommand);
+            if (cbox.getIcon() != null)
+                cbox.setText(null);
+            return cbox;
+        } else {
+            final JButton button = new JButton();
+            AbstractAction action = new AbstractAction() {
+                public void actionPerformed(ActionEvent actionEvent) {
+                    if (button.isEnabled()) {
+                        if (command.getAutoRepeatInterval() > 0)
+                            command.actionPerformedAutoRepeat(actionEvent);
+                        else
+                            command.actionPerformed(actionEvent);
+                    }
+                }
+            };
+            action.putValue(AbstractAction.NAME, command.getName());
+            action.putValue(ALT_NAME, command.getAltName());
+            if (command.getDescription() != null)
+                action.putValue(AbstractAction.SHORT_DESCRIPTION, command.getDescription());
+            if (command.getIcon() != null)
+                action.putValue(AbstractAction.SMALL_ICON, command.getIcon());
+            if (command.getAcceleratorKey() != null)
+                action.putValue(AbstractAction.ACCELERATOR_KEY, command.getAcceleratorKey());
+            button.setAction(action);
+            if (button.getIcon() != null)
+                button.setText(null);
+            button2Command.put(button, command);
+            return button;
+        }
+    }
+
+    /**
+     * creates a button for the command
+     *
+     * @param command
+     * @return button
+     */
+    public AbstractButton getRadioButton(final ICommand command) {
+        if (command == null) {
+            JButton nullButton = new JButton("Null");
+            nullButton.setEnabled(false);
+            return nullButton;
+        }
+        if (command instanceof ICheckBoxCommand) {
+            final ICheckBoxCommand checkBoxCommand = (ICheckBoxCommand) command;
+
+            final JRadioButton cbox = new JRadioButton();
+            AbstractAction action = new AbstractAction() {
+                public void actionPerformed(ActionEvent actionEvent) {
+                    checkBoxCommand.setSelected(cbox.isSelected());
+                    if (cbox.isEnabled()) {
+                        if (command.getAutoRepeatInterval() > 0)
+                            command.actionPerformedAutoRepeat(actionEvent);
+                        else
+                            command.actionPerformed(actionEvent);
+                    }
+                }
+            };
+            action.putValue(AbstractAction.NAME, command.getName());
+            action.putValue(ALT_NAME, command.getAltName());
+            if (command.getDescription() != null)
+                action.putValue(AbstractAction.SHORT_DESCRIPTION, command.getDescription());
+            if (command.getIcon() != null)
+                action.putValue(AbstractAction.SMALL_ICON, command.getIcon());
+            if (command.getAcceleratorKey() != null)
+                action.putValue(AbstractAction.ACCELERATOR_KEY, command.getAcceleratorKey());
+            cbox.setAction(action);
+            cbox.setSelected(checkBoxCommand.isSelected());
+            button2Command.put(cbox, checkBoxCommand);
+            if (cbox.getIcon() != null)
+                cbox.setText(null);
+            return cbox;
+        } else
+            return null;
+    }
+
+    /**
+     * get the director
+     *
+     * @return
+     */
+    public IDirector getDir() {
+        return dir;
+    }
+
+    /**
+     * creates an action for the command. Note that this action is not controlled by any
+     * command manager, in particular its enable state is never updated.
+     * Only use for state-free commands such as Quit
+     *
+     * @return action object
+     */
+    public static AbstractAction createAction(final ICommand command) {
+        if (command == null) {
+            AbstractAction nullAction = new AbstractAction("Null") {
+                public void actionPerformed(ActionEvent actionEvent) {
+                }
+            };
+            nullAction.setEnabled(false);
+            return nullAction;
+        }
+
+        AbstractAction action = new AbstractAction() {
+            public void actionPerformed(ActionEvent actionEvent) {
+                command.actionPerformed(actionEvent);
+            }
+        };
+        action.putValue(AbstractAction.NAME, command.getName());
+        action.putValue(ALT_NAME, command.getAltName());
+        if (command.getDescription() != null)
+            action.putValue(AbstractAction.SHORT_DESCRIPTION, command.getDescription());
+        if (command.getIcon() != null)
+            action.putValue(AbstractAction.SMALL_ICON, command.getIcon());
+        if (command.getAcceleratorKey() != null)
+            action.putValue(AbstractAction.ACCELERATOR_KEY, command.getAcceleratorKey());
+        return action;
+    }
+
+    /**
+     * should execute command return when unable to match a token?
+     *
+     * @return
+     */
+    public boolean isReturnOnCommandNotFound() {
+        return returnOnCommandNotFound;
+    }
+
+    public void setReturnOnCommandNotFound(boolean returnOnCommandNotFound) {
+        this.returnOnCommandNotFound = returnOnCommandNotFound;
+    }
+
+    /**
+     * contains set of command names that are all silently ignored when building a menu
+     *
+     * @return list of commands to ignore
+     */
+    public static Set<String> getCommandsToIgnore() {
+        return commandsToIgnore;
+    }
+
+    /**
+     * gets the parent viewer for this command parser
+     *
+     * @return parent, either IDirectableViewer or   JDialog
+     */
+    public Object getParent() {
+        return parent;
+    }
+
+    /**
+     * gets the list of global commands. These commands are added to all command managers
+     *
+     * @return global commands
+     */
+    public static List<ICommand> getGlobalCommands() {
+        return globalCommands;
+    }
+}
diff --git a/src/jloda/gui/commands/ICheckBoxCommand.java b/src/jloda/gui/commands/ICheckBoxCommand.java
new file mode 100644
index 0000000..7ccede1
--- /dev/null
+++ b/src/jloda/gui/commands/ICheckBoxCommand.java
@@ -0,0 +1,41 @@
+/**
+ * ICheckBoxCommand.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.commands;
+
+/**
+ * Checkbox command
+ * Daniel Huson, 5.2010
+ */
+public interface ICheckBoxCommand extends ICommand {
+
+    /**
+     * this is currently selected?
+     *
+     * @return selected
+     */
+    boolean isSelected();
+
+    /**
+     * set the selected status of this command
+     *
+     * @param selected
+     */
+    void setSelected(boolean selected);
+}
diff --git a/src/jloda/gui/commands/ICommand.java b/src/jloda/gui/commands/ICommand.java
new file mode 100644
index 0000000..c4431c6
--- /dev/null
+++ b/src/jloda/gui/commands/ICommand.java
@@ -0,0 +1,199 @@
+/**
+ * ICommand.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.commands;
+
+import jloda.gui.director.IDirectableViewer;
+import jloda.gui.director.IDirector;
+import jloda.util.parse.NexusStreamParser;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+
+/**
+ * basic command interface
+ * Daniel Huson, 5.2010
+ */
+public interface ICommand {
+    /**
+     * set the director
+     *
+     * @param dir
+     */
+    void setDir(IDirector dir);
+
+    /**
+     * get the director
+     *
+     * @return
+     */
+    IDirector getDir();
+
+    /**
+     * set the command manager. This is required for all commands that call the "execute" method
+     *
+     * @param commandManager
+     */
+    void setCommandManager(CommandManager commandManager);
+
+    /**
+     * get the associated command manager
+     *
+     * @return commandManager
+     */
+    CommandManager getCommandManager();
+
+    /**
+     * get the name to be used as a menu label
+     *
+     * @return name
+     */
+    String getName();
+
+    /**
+     * get an alternative name used to identify this command
+     *
+     * @return name
+     */
+    String getAltName();
+
+    /**
+     * initial tokens used to identify the command
+     *
+     * @return first tokens
+     */
+    String getStartsWith();
+
+    /**
+     * get description to be used as a tooltip
+     *
+     * @return description
+     */
+    String getDescription();
+
+    /**
+     * get icon to be used in menu or button
+     *
+     * @return icon
+     */
+    ImageIcon getIcon();
+
+    /**
+     * gets the accelerator key  to be used in menu
+     *
+     * @return accelerator key
+     */
+    javax.swing.KeyStroke getAcceleratorKey();
+
+    /**
+     * get command-line syntax. First two tokens are used to identify the command
+     *
+     * @return usage
+     */
+    String getSyntax();
+
+    /**
+     * action to be performed
+     *
+     * @param ev
+     */
+    void actionPerformed(ActionEvent ev);
+
+    /**
+     * is this a critical command that can only be executed when no other command is running?
+     *
+     * @return true, if critical
+     */
+    boolean isCritical();
+
+    /**
+     * is the command currently applicable? Used to set enable state of command
+     *
+     * @return true, if command can be applied
+     */
+    boolean isApplicable();
+
+    /**
+     * parses the given command and executes it
+     *
+     * @param np
+     * @throws IOException
+     */
+    void apply(NexusStreamParser np) throws Exception;
+
+    /**
+     * gets the command needed to undo this command
+     *
+     * @return undo command
+     */
+    String getUndo();
+
+    /**
+     * sets the  viewer
+     *
+     * @param viewer
+     */
+    void setViewer(IDirectableViewer viewer);
+
+    /**
+     * gets the  viewer
+     *
+     * @return viewer
+     */
+    IDirectableViewer getViewer();
+
+
+    /**
+     * sets the viewer in the case that the viewer is not an  IDirectableViewer
+     *
+     * @param viewer
+     */
+    void setParent(Object viewer);
+
+    /**
+     * gets the  viewer in the case that the viewer is not an  IDirectableViewer
+     *
+     * @return viewer
+     */
+    Object getParent();
+
+    /**
+     * get the autorepeat interval. 0 means no autorepeat
+     *
+     * @return
+     */
+    int getAutoRepeatInterval();
+
+
+    /**
+     * set  the autorepeat interval. 0 means no autorepeat
+     *
+     * @param autoRepeatInterval
+     */
+    void setAutoRepeatInterval(int autoRepeatInterval);
+
+
+    /**
+     * Action to be performed in case of autorepeat
+     *
+     * @param ev
+     */
+    void actionPerformedAutoRepeat(ActionEvent ev);
+}
diff --git a/src/jloda/gui/commands/MenuCreator.java b/src/jloda/gui/commands/MenuCreator.java
new file mode 100644
index 0000000..fbd9432
--- /dev/null
+++ b/src/jloda/gui/commands/MenuCreator.java
@@ -0,0 +1,333 @@
+/**
+ * MenuCreator.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.commands;
+
+import jloda.gui.AppleStuff;
+import jloda.gui.IMenuModifier;
+import jloda.util.MenuMnemonics;
+import jloda.util.ProgramProperties;
+import jloda.util.ResourceManager;
+import jloda.util.lang.Translator;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.util.*;
+import java.util.List;
+
+/**
+ * class for creating and managing menus
+ * Daniel Huson, 8.2006
+ */
+public class MenuCreator {
+    public final static String MENUBAR_TAG = "MenuBar";
+    static IMenuModifier menuModifer;
+
+    private final CommandManager commandManager;
+
+    /**
+     * constructor
+     */
+    public MenuCreator(CommandManager commandManager) {
+        this.commandManager = commandManager;
+    }
+
+    /**
+     * builds a menu bar from a set of description lines.
+     * Description must contain one menu bar line in the format:
+     * MenuBar.menuBarLabel=item;item;item...;item, where menuBarLabel must match the
+     * given name and each item is of the form Menu.menuBarLabel or simply menuBarLabel,
+     *
+     * @param menuBarLabel
+     * @param descriptions
+     * @param menuBar
+     * @throws Exception
+     */
+    public void buildMenuBar(String menuBarLabel, Hashtable<String, String> descriptions, JMenuBar menuBar) throws Exception {
+        /*
+        System.err.println("Known actions:");
+        for (Iterator it = actions.keySet().iterator(); it.hasNext();) {
+            System.err.println(it.next());
+        }
+         */
+
+        menuBarLabel = MENUBAR_TAG + "." + menuBarLabel;
+        if (!descriptions.keySet().contains(menuBarLabel))
+            throw new Exception("item not found: " + menuBarLabel);
+
+        List<String> menus = getTokens(descriptions.get(menuBarLabel));
+
+        for (String menuLabel : menus) {
+            if (!menuLabel.startsWith("Menu."))
+                menuLabel = "Menu." + menuLabel;
+            if (descriptions.keySet().contains(menuLabel)) {
+                final JMenu menu = buildMenu(menuLabel, descriptions, false);
+                addSubMenus(0, menu, descriptions);
+                MenuMnemonics.setMnemonics(menu);
+                menuBar.add(menu);
+            }
+        }
+    }
+
+    /**
+     * builds a menu from a description.
+     * Format:
+     * Menu.menuLabel=name;item;item;...;item;  where  name is menu name
+     * and item is either the menuLabel of an action, | to indicate a separator
+     * or @menuLabel to indicate menuLabel name of a submenu
+     *
+     * @param menuBarConfiguration
+     * @param menusConfigurations
+     * @param addEmptyIcon
+     * @return menu
+     * @throws Exception
+     */
+    private JMenu buildMenu(String menuBarConfiguration, Hashtable<String, String> menusConfigurations, boolean addEmptyIcon) throws Exception {
+        if (!menuBarConfiguration.startsWith("Menu."))
+            menuBarConfiguration = "Menu." + menuBarConfiguration;
+        String description = menusConfigurations.get(menuBarConfiguration);
+        if (description == null)
+            return null;
+        List<String> menuDescription = getTokens(description);
+        if (menuDescription.size() == 0)
+            return null;
+        boolean skipNextSeparator = false;  // avoid double separators
+        Iterator it = menuDescription.iterator();
+        String menuName = (String) it.next();
+        JMenu menu = new JMenu(Translator.get(menuName));
+        if (addEmptyIcon)
+            menu.setIcon(ResourceManager.getIcon("Empty16.gif"));
+        String[] labels = menuDescription.toArray(new String[menuDescription.size()]);
+        for (int i = 1; i < labels.length; i++) {
+            String label = labels[i];
+            if (i == labels.length - 2 && label.equals("|") && labels[i + 1].equals("Quit"))
+                skipNextSeparator = true; // avoid separator at bottom of File menu in mac version
+
+            if (skipNextSeparator && label.equals("|")) {
+                skipNextSeparator = false;
+                continue;
+            }
+            skipNextSeparator = false;
+
+            if (label.startsWith("@")) {
+                JMenu subMenu = new JMenu(Translator.get(label));
+                subMenu.setIcon(ResourceManager.getIcon("Empty16.gif"));
+                menu.add(subMenu);
+            } else if (label.equals("|")) {
+                menu.addSeparator();
+                skipNextSeparator = true;
+            } else {
+                if (CommandManager.getCommandsToIgnore().contains(label))
+                    continue;
+                ICommand command = commandManager.getCommand(label);
+                if (command != null) {
+                    label = command.getName(); // label might have been altName...
+                    if (CommandManager.getCommandsToIgnore().contains(label))
+                        continue;
+                    boolean done = false;
+                    if (ProgramProperties.isMacOS()) {
+                        switch (label) {
+                            case "Quit": {
+                                final Action action = createAction(command);
+                                AppleStuff.getInstance().setQuitAction(action);
+                                if (menu.getItemCount() > 0 && menu.getItem(menu.getItemCount() - 1) == null) {
+                                    skipNextSeparator = true;
+                                }
+                                done = true;
+                                break;
+                            }
+                            case "About":
+                            case "About...": {
+                                final Action action = createAction(command);
+                                AppleStuff.getInstance().setAboutAction(action);
+                                if (menu.getItemCount() > 0 && menu.getItem(menu.getItemCount() - 1) == null) {
+                                    skipNextSeparator = true;
+                                }
+                                done = true;
+                                break;
+                            }
+                            case "Preferences":
+                            case "Preferences...": {
+                                final Action action = createAction(command);
+                                AppleStuff.getInstance().setPreferencesAction(action);
+                                if (menu.getItemCount() > 0 && menu.getItem(menu.getItemCount() - 1) == null) {
+                                    skipNextSeparator = true;
+                                }
+                                done = true;
+                                break;
+                            }
+                        }
+                    }
+                    if (!done) {
+                        JMenuItem menuItem = commandManager.getJMenuItem(command);
+                        menuItem.setText(Translator.get(label));
+                        menuItem.setToolTipText(command.getDescription());
+                        menu.add(menuItem);
+                        // always add empty icon, if non is given
+                        if (menuItem.getIcon() == null)
+                            menuItem.setIcon(ResourceManager.getIcon("Empty16.gif"));
+                    }
+                } else {
+                    JMenuItem menuItem = new JMenuItem(label + " #");
+                    menuItem.setIcon(ResourceManager.getIcon("Empty16.gif"));
+                    menu.add(menuItem);
+                    menu.getItem(menu.getItemCount() - 1).setEnabled(false);
+                }
+            }
+        }
+        if (menuModifer != null)
+            menuModifer.apply(menu, commandManager);
+        if (ProgramProperties.get("showtex", false)) {
+            System.out.println(TeXGenerator.getMenuLaTeX(commandManager, menuBarConfiguration, menusConfigurations));
+        }
+
+        return menu;
+    }
+
+    /**
+     * adds submenus to a menu
+     *
+     * @param depth
+     * @param menu
+     * @param descriptions
+     * @throws Exception
+     */
+    private void addSubMenus(int depth, JMenu menu, Hashtable<String, String> descriptions) throws Exception {
+        if (depth > 5)
+            throw new Exception("Submenus: too deep: " + depth);
+        for (int i = 0; i < menu.getItemCount(); i++) {
+            JMenuItem item = menu.getItem(i);
+            if (item != null && item.getText() != null && item.getText().startsWith("@")) {
+                String name = item.getText().substring(1);
+                item.setText(name);
+                JMenu subMenu = buildMenu(name, descriptions, true);
+                if (subMenu != null) {
+                    addSubMenus(depth + 1, subMenu, descriptions);
+                    menu.remove(i);
+                    menu.add(subMenu, i);
+                }
+            }
+        }
+    }
+
+    /**
+     * find named menu
+     *
+     * @param name
+     * @param menuBar
+     * @param mayBeSubmenu also search for sub menu
+     * @return menu or null
+     */
+    public static JMenu findMenu(String name, JMenuBar menuBar, boolean mayBeSubmenu) {
+        name = Translator.get(name, false);
+        for (int i = 0; i < menuBar.getMenuCount(); i++) {
+            JMenu result = findMenu(name, menuBar.getMenu(i), mayBeSubmenu);
+            if (result != null)
+                return result;
+        }
+        return null;
+    }
+
+    /**
+     * searches for menu by name
+     *
+     * @param name
+     * @param menu
+     * @param mayBeSubmenu
+     * @return menu or null
+     */
+    public static JMenu findMenu(String name, JMenu menu, boolean mayBeSubmenu) {
+        name = Translator.get(name, false);
+        // System.err.println("TEXT: " + menu.getText());
+        if (menu.getText().equals(name))
+            return menu;
+        if (mayBeSubmenu) {
+            for (int j = 0; j < menu.getItemCount(); j++) {
+                JMenuItem item = menu.getItem(j);
+                if (item != null) {
+                    Component comp = item.getComponent();
+                    if (comp instanceof JMenu) {
+                        JMenu result = findMenu(name, (JMenu) comp, true);
+                        if (result != null)
+                            return result;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+
+    /**
+     * get the list of tokens in a description
+     *
+     * @param str
+     * @return list of tokens
+     * @throws Exception
+     */
+    static public List<String> getTokens(String str) throws Exception {
+        try {
+            int pos = str.indexOf("=");
+            str = str.substring(pos + 1).trim();
+            StringTokenizer tokenizer = new StringTokenizer(str, ";");
+            List<String> result = new LinkedList<>();
+            while (tokenizer.hasMoreTokens())
+                result.add(tokenizer.nextToken());
+            return result;
+        } catch (Exception ex) {
+            throw new Exception("failed to parse description-line: <" + str + ">: " + ex);
+        }
+    }
+
+    /**
+     * create an action for the given command
+     *
+     * @param command
+     * @return action
+     */
+    private AbstractAction createAction(final ICommand command) {
+        AbstractAction action = new AbstractAction() {
+            public void actionPerformed(ActionEvent actionEvent) {
+                if (command.getAutoRepeatInterval() > 0)
+                    command.actionPerformedAutoRepeat(actionEvent);
+                else
+                    command.actionPerformed(actionEvent);
+            }
+        };
+        action.putValue(AbstractAction.NAME, command.getName());
+        if (command.getDescription() != null)
+            action.putValue(AbstractAction.SHORT_DESCRIPTION, command.getDescription());
+        if (command.getIcon() != null)
+            action.putValue(AbstractAction.SMALL_ICON, command.getIcon());
+        if (command.getAcceleratorKey() != null)
+            action.putValue(AbstractAction.ACCELERATOR_KEY, command.getAcceleratorKey());
+        return action;
+    }
+
+    /**
+     * if set, the menu modifier is applied to each menu after it is built
+     *
+     * @param menuModifier
+     */
+    public static void setMenuModifier(IMenuModifier menuModifier) {
+        menuModifer = menuModifier;
+    }
+
+}
diff --git a/src/jloda/gui/commands/TeXGenerator.java b/src/jloda/gui/commands/TeXGenerator.java
new file mode 100644
index 0000000..765d6c8
--- /dev/null
+++ b/src/jloda/gui/commands/TeXGenerator.java
@@ -0,0 +1,158 @@
+/**
+ * TeXGenerator.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.commands;
+
+import java.io.StringWriter;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * generate LaTeX description of menus
+ * Daniel Huson, 11.2010
+ */
+public class TeXGenerator {
+    /**
+     * Get LaTeX description of menus
+     * Format:
+     * Menu.menuLabel=name;item;item;...;item;  where  name is menu name
+     * and item is either the menuLabel of an action, | to indicate a separator
+     * or @menuLabel to indicate menuLabel name of a submenu
+     *
+     * @param menuBarLayout
+     * @param menusConfigurations
+     * @return menu description in LaTeX
+     * @throws Exception
+     */
+    public static String getMenuLaTeX(CommandManager commandManager, String menuBarLayout, Hashtable<String, String> menusConfigurations) throws Exception {
+        StringWriter w = new StringWriter();
+
+        if (!menuBarLayout.startsWith("Menu."))
+            menuBarLayout = "Menu." + menuBarLayout;
+        String description = menusConfigurations.get(menuBarLayout);
+        if (description == null)
+            return null;
+        List<String> menuDescription = MenuCreator.getTokens(description);
+        if (menuDescription.size() == 0)
+            return null;
+        Iterator it = menuDescription.iterator();
+        String menuName = (String) it.next();
+
+        w.write("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n");
+        w.write("\\mysubsection{The " + menuName + " menu}\n");
+        w.write("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n");
+
+        w.write("The \\pmenu{" + menuName + "} menu contains the following items:\n\n");
+        w.write("\\begin{itemize}\n");
+
+        String[] labels = menuDescription.toArray(new String[menuDescription.size()]);
+        for (int i = 1; i < labels.length; i++) {
+            String name = labels[i];
+
+            if (name.startsWith("@")) {
+                w.write("\\item The \\pmenuitem{" + menuName + "}{" + name.substring(1) + "} submenu.\n");
+            } else if (name.equals("|")) {
+                // separator
+            } else {
+                ICommand command = commandManager.getCommand(name);
+                if (command != null) {
+                    name = command.getName(); // label might have been altName...
+                    boolean notMac = name.equals("Quit") || name.equals("About") || name.equals("About...") || name.equals("Preferences") || name.equals("Preferences...");
+                    name = name.replaceAll("_", "-");
+                    String des = command.getDescription();
+                    w.write("\\item The \\pmenuitem{" + menuName + "}{" + name + "} item: " + (des != null ? des.replaceAll("_", "-") : " NONE") +
+                            (notMac ? " (Windows and Linux only)" : "") + ".\n");
+                }
+            }
+        }
+        w.write("\\end{itemize}\n\n");
+        return w.toString();
+    }
+
+    /**
+     * get a laTeX description of a tool bar
+     *
+     * @param configuration
+     * @param commandManager
+     * @return LaTeX
+     */
+    public static String getToolBarLaTeX(String configuration, CommandManager commandManager) {
+        StringWriter w = new StringWriter();
+
+        if (configuration != null) {
+            w.write("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n");
+            w.write("\\mysubsection{The Toolbar}\n");
+            w.write("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n");
+
+            w.write("The toolbar contains the following items:\n\n");
+            w.write("\\begin{itemize}\n");
+
+            String[] tokens = configuration.split(";");
+            for (String name : tokens) {
+                if (name.equals("|")) {
+                    // separator
+                } else {
+                    ICommand command = commandManager.getCommand(name);
+                    if (command != null) {
+                        name = command.getName(); // label might have been altName...
+                        w.write("\\item The \\pbutton{" + name + "} item: " + command.getDescription().replaceAll("_", "-") + ".\n");
+                    }
+                }
+            }
+            w.write("\\end{itemize}\n\n");
+        }
+        return w.toString();
+    }
+
+    /**
+     * get a laTeX description of a tool bar
+     *
+     * @param configuration
+     * @param commandManager
+     * @return LaTeX
+     */
+    public static String getPopupMenuLaTeX(String configuration, CommandManager commandManager) {
+        StringWriter w = new StringWriter();
+
+        if (configuration != null) {
+            w.write("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n");
+            w.write("\\mysubsection{The Popup Menu}\n");
+            w.write("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n");
+
+            w.write("The popup menu contains the following items:\n\n");
+            w.write("\\begin{itemize}\n");
+
+            String[] tokens = configuration.split(";");
+            for (String name : tokens) {
+                if (name.equals("|")) {
+                    // separator
+                } else {
+                    ICommand command = commandManager.getCommand(name);
+                    if (command != null) {
+                        name = command.getName(); // label might have been altName...
+                        w.write("\\item The \\ppopupmenuitem{WHICH?}{" + name + "} item: " + command.getDescription().replaceAll("_", "-") + ".\n");
+                    }
+                }
+            }
+            w.write("\\end{itemize}\n\n");
+        }
+        return w.toString();
+    }
+}
diff --git a/src/jloda/gui/commands/WrappedCheckBoxCommand.java b/src/jloda/gui/commands/WrappedCheckBoxCommand.java
new file mode 100644
index 0000000..cd682d8
--- /dev/null
+++ b/src/jloda/gui/commands/WrappedCheckBoxCommand.java
@@ -0,0 +1,57 @@
+/**
+ * WrappedCheckBoxCommand.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.commands;
+
+/**
+ * a wrapped command
+ * This is used for globally defined commands to ensure that they are given the correct dir, parent and viewer on execution
+ * Daniel Huson, 4.2015
+ */
+public class WrappedCheckBoxCommand extends WrappedCommand implements ICheckBoxCommand {
+
+    /**
+     * constructor
+     *
+     * @param command
+     */
+    public WrappedCheckBoxCommand(ICheckBoxCommand command) {
+        super(command);
+    }
+
+    /**
+     * this is currently selected?
+     *
+     * @return selected
+     */
+    @Override
+    public boolean isSelected() {
+        return ((ICheckBoxCommand) command).isSelected();
+    }
+
+    /**
+     * set the selected status of this command
+     *
+     * @param selected
+     */
+    @Override
+    public void setSelected(boolean selected) {
+        ((ICheckBoxCommand) command).setSelected(selected);
+    }
+}
diff --git a/src/jloda/gui/commands/WrappedCommand.java b/src/jloda/gui/commands/WrappedCommand.java
new file mode 100644
index 0000000..74dea33
--- /dev/null
+++ b/src/jloda/gui/commands/WrappedCommand.java
@@ -0,0 +1,305 @@
+/**
+ * WrappedCommand.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.commands;
+
+import jloda.gui.director.IDirectableViewer;
+import jloda.gui.director.IDirector;
+import jloda.util.parse.NexusStreamParser;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+
+/**
+ * a wrapped command
+ * This is used for globally defined commands to ensure that they are given the correct dir, parent and viewer on execution
+ * Daniel Huson, 4.2015
+ */
+public class WrappedCommand implements ICommand {
+    protected final ICommand command;
+    private CommandManager commandManager;
+    private IDirector dir;
+    private Object parent;
+    private IDirectableViewer viewer;
+
+    /**
+     * constructor
+     *
+     * @param command
+     */
+    public WrappedCommand(ICommand command) {
+        this.command = command;
+    }
+
+    /**
+     * set the director
+     *
+     * @param dir
+     */
+    @Override
+    public void setDir(IDirector dir) {
+        this.dir = dir;
+        command.setDir(dir);
+    }
+
+    /**
+     * get the director
+     *
+     * @return
+     */
+    @Override
+    public IDirector getDir() {
+        return dir;
+    }
+
+    /**
+     * set the command manager. This is required for all commands that call the "execute" method
+     *
+     * @param commandManager
+     */
+    @Override
+    public void setCommandManager(CommandManager commandManager) {
+        this.commandManager = commandManager;
+    }
+
+    /**
+     * get the associated command manager
+     *
+     * @return commandManager
+     */
+    @Override
+    public CommandManager getCommandManager() {
+        return commandManager;
+    }
+
+    /**
+     * get the name to be used as a menu label
+     *
+     * @return name
+     */
+    @Override
+    public String getName() {
+        return command.getName();
+    }
+
+    /**
+     * get an alternative name used to identify this command
+     *
+     * @return name
+     */
+    @Override
+    public String getAltName() {
+        return command.getAltName();
+    }
+
+    /**
+     * initial tokens used to identify the command
+     *
+     * @return first tokens
+     */
+    @Override
+    public String getStartsWith() {
+        return command.getStartsWith();
+    }
+
+    /**
+     * get description to be used as a tooltip
+     *
+     * @return description
+     */
+    @Override
+    public String getDescription() {
+        return command.getDescription();
+    }
+
+    /**
+     * get icon to be used in menu or button
+     *
+     * @return icon
+     */
+    @Override
+    public ImageIcon getIcon() {
+        return command.getIcon();
+    }
+
+    /**
+     * gets the accelerator key  to be used in menu
+     *
+     * @return accelerator key
+     */
+    @Override
+    public KeyStroke getAcceleratorKey() {
+        return command.getAcceleratorKey();
+    }
+
+    /**
+     * get command-line syntax. First two tokens are used to identify the command
+     *
+     * @return usage
+     */
+    @Override
+    public String getSyntax() {
+        return command.getSyntax();
+    }
+
+    /**
+     * action to be performed
+     *
+     * @param ev
+     */
+    @Override
+    public void actionPerformed(ActionEvent ev) {
+        synchronized (command) {
+            command.setCommandManager(commandManager);
+            command.setViewer(viewer);
+            command.setDir(dir);
+            command.setParent(parent);
+            command.actionPerformed(ev);
+        }
+    }
+
+    /**
+     * is this a critical command that can only be executed when no other command is running?
+     *
+     * @return true, if critical
+     */
+    @Override
+    public boolean isCritical() {
+        return command.isCritical();
+    }
+
+    /**
+     * is the command currently applicable? Used to set enable state of command
+     *
+     * @return true, if command can be applied
+     */
+    @Override
+    public boolean isApplicable() {
+        synchronized (command) {
+            command.setCommandManager(commandManager);
+            command.setViewer(viewer);
+            command.setDir(dir);
+            command.setParent(parent);
+            return command.isApplicable();
+        }
+    }
+
+    /**
+     * parses the given command and executes it
+     *
+     * @param np
+     * @throws Exception
+     */
+    @Override
+    public void apply(NexusStreamParser np) throws Exception {
+        synchronized (command) {
+            command.setCommandManager(commandManager);
+            command.setViewer(viewer);
+            command.setDir(dir);
+            command.setParent(parent);
+            command.apply(np);
+        }
+    }
+
+    /**
+     * gets the command needed to undo this command
+     *
+     * @return undo command
+     */
+    @Override
+    public String getUndo() {
+        return command.getUndo();
+    }
+
+    /**
+     * sets the  viewer
+     *
+     * @param viewer
+     */
+    @Override
+    public void setViewer(IDirectableViewer viewer) {
+        this.viewer = viewer;
+    }
+
+    /**
+     * gets the  viewer
+     *
+     * @return viewer
+     */
+    @Override
+    public IDirectableViewer getViewer() {
+        return viewer;
+    }
+
+    /**
+     * sets the viewer in the case that the viewer is not an  IDirectableViewer
+     *
+     * @param parent
+     */
+    @Override
+    public void setParent(Object parent) {
+        this.parent = parent;
+    }
+
+    /**
+     * gets the  viewer in the case that the viewer is not an  IDirectableViewer
+     *
+     * @return viewer
+     */
+    @Override
+    public Object getParent() {
+        return parent;
+    }
+
+    /**
+     * get the autorepeat interval. 0 means no autorepeat
+     *
+     * @return
+     */
+    @Override
+    public int getAutoRepeatInterval() {
+        return command.getAutoRepeatInterval();
+    }
+
+    /**
+     * set  the autorepeat interval. 0 means no autorepeat
+     *
+     * @param autoRepeatInterval
+     */
+    @Override
+    public void setAutoRepeatInterval(int autoRepeatInterval) {
+        command.setAutoRepeatInterval(autoRepeatInterval);
+    }
+
+    /**
+     * Action to be performed in case of autorepeat
+     *
+     * @param ev
+     */
+    @Override
+    public void actionPerformedAutoRepeat(ActionEvent ev) {
+        synchronized (command) {
+            command.setCommandManager(commandManager);
+            command.setViewer(viewer);
+            command.setDir(dir);
+            command.setParent(parent);
+            command.actionPerformedAutoRepeat(ev);
+        }
+    }
+}
diff --git a/src/jloda/gui/director/IDirectableViewer.java b/src/jloda/gui/director/IDirectableViewer.java
new file mode 100644
index 0000000..f77e772
--- /dev/null
+++ b/src/jloda/gui/director/IDirectableViewer.java
@@ -0,0 +1,69 @@
+/**
+ * IDirectableViewer.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.director;
+
+import jloda.gui.commands.CommandManager;
+
+import javax.swing.*;
+
+/**
+ * A directable viewer is one that listens to the director updates and informs
+ * on the uptodate status
+ *
+ * @author huson
+ *         Date: 26-Nov-2003
+ */
+public interface IDirectableViewer extends IDirectorListener {
+    /**
+     * is viewer uptodate?
+     *
+     * @return uptodate
+     */
+    boolean isUptoDate();
+
+    /**
+     * return the frame associated with the viewer
+     *
+     * @return frame
+     */
+    JFrame getFrame();
+
+    /**
+     * gets the title
+     *
+     * @return title
+     */
+    String getTitle();
+
+    /**
+     * gets the associated command manager
+     *
+     * @return command manager
+     */
+    CommandManager getCommandManager();
+
+
+    /**
+     * get the name of the class
+     *
+     * @return class name
+     */
+    String getClassName();
+}
diff --git a/src/jloda/gui/director/IDirector.java b/src/jloda/gui/director/IDirector.java
new file mode 100644
index 0000000..1ebd627
--- /dev/null
+++ b/src/jloda/gui/director/IDirector.java
@@ -0,0 +1,164 @@
+/**
+ * IDirector.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.director;
+
+import jloda.gui.commands.CommandManager;
+import jloda.util.CanceledException;
+
+import java.awt.*;
+
+/**
+ * director interface
+ * Daniel Huson, 3.2007
+ */
+public interface IDirector {
+    // update targets
+    String ALL = "ALL";
+    String TITLE = "TITLE";
+    String ENABLE_STATE = "enable_state";
+
+    /**
+     * execute a command
+     *
+     * @param command
+     */
+    void execute(String command);
+
+    /**
+     * execute a command using the provided command manager
+     *
+     * @param command
+     * @param commandManager
+     */
+    void execute(String command, CommandManager commandManager);
+
+    /**
+     * execute a command using the provided command manager
+     *
+     * @param command
+     * @param commandManager
+     */
+    void execute(String command, CommandManager commandManager, Component parent);
+
+    /**
+     * execute a command
+     *
+     * @param command
+     */
+    boolean executeImmediately(String command);
+
+    /**
+     * execute a command using the provided command manager
+     *
+     * @param command
+     * @param commandManager
+     */
+    boolean executeImmediately(String command, CommandManager commandManager);
+
+    /**
+     * update viewers
+     *
+     * @param what update target
+     */
+    void notifyUpdateViewer(String what);
+
+    /**
+     * adds a viewer
+     *
+     * @param viewer
+     */
+    IDirectableViewer addViewer(IDirectableViewer viewer);
+
+    /**
+     * remove a given viewer
+     *
+     * @param viewer
+     */
+    void removeViewer(IDirectableViewer viewer);
+
+    /**
+     * get the project title
+     *
+     * @return title
+     */
+    String getTitle();
+
+    /**
+     * set the dirty flag
+     *
+     * @param dirty
+     */
+    void setDirty(boolean dirty);
+
+    /**
+     * get the dirty flag
+     *
+     * @return dirty
+     */
+    boolean getDirty();
+
+    /**
+     * set the project id
+     *
+     * @param id
+     */
+    void setID(int id);
+
+    /**
+     * get the project id
+     *
+     * @return id
+     */
+    int getID();
+
+    /**
+     * gets the main viewer associated with this director
+     *
+     * @return main viewer
+     */
+    IMainViewer getMainViewer();
+
+    /**
+     * close this director
+     */
+    void close() throws CanceledException;
+
+    /**
+     * tell  directed viewers to lock input
+     */
+    void notifyLockInput();
+
+    /**
+     * tell directed viewers to unlock input
+     */
+    void notifyUnlockInput();
+
+    /**
+     * returns a viewer of the given class
+     *
+     * @param aClass
+     * @return viewer of the given class, or null
+     */
+    IDirectableViewer getViewerByClass(Class aClass);
+
+    boolean isInternalDocument();
+
+    void setInternalDocument(boolean isInternalDocument);
+}
diff --git a/src/jloda/gui/director/IDirectorListener.java b/src/jloda/gui/director/IDirectorListener.java
new file mode 100644
index 0000000..c2b623a
--- /dev/null
+++ b/src/jloda/gui/director/IDirectorListener.java
@@ -0,0 +1,72 @@
+/**
+ * IDirectorListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * director events that viewers listen to
+ * @author huson
+ * 11.03
+ */
+package jloda.gui.director;
+
+import jloda.util.CanceledException;
+
+/**
+ * director events that viewers listen to
+ *
+ * @author huson
+ *         11.03
+ */
+public interface IDirectorListener extends IUpdateableView {
+    /**
+     * ask view to update itself. This is method is wrapped into a runnable object
+     * and put in the swing event queue to avoid concurrent modifications.
+     *
+     * @param what what should be updated? Possible values: Director.ALL or Director.TITLE
+     */
+    void updateView(String what);
+
+    /**
+     * ask view to prevent user input
+     */
+    void lockUserInput();
+
+    /**
+     * ask view to allow user input
+     */
+    void unlockUserInput();
+
+    /**
+     * is viewer currently locked?
+     *
+     * @return true, if locked
+     */
+    boolean isLocked();
+
+    /**
+     * ask view to destroy itself
+     */
+    void destroyView() throws CanceledException;
+
+    /**
+     * set uptodate state
+     *
+     * @param flag
+     */
+    void setUptoDate(boolean flag);
+}
diff --git a/src/jloda/gui/director/IMainViewer.java b/src/jloda/gui/director/IMainViewer.java
new file mode 100644
index 0000000..ecf3403
--- /dev/null
+++ b/src/jloda/gui/director/IMainViewer.java
@@ -0,0 +1,42 @@
+/**
+ * IMainViewer.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.director;
+
+import javax.swing.*;
+
+/**
+ * main viewer interface
+ * Daniel Huson, 3.2007
+ */
+public interface IMainViewer extends IDirectableViewer {
+    /**
+     * gets the window menu
+     *
+     * @return window menu
+     */
+    JMenu getWindowMenu();
+
+    /**
+     * get the quit action
+     *
+     * @return quit action
+     */
+    AbstractAction getQuit();
+}
diff --git a/src/jloda/gui/director/IProjectsChangedListener.java b/src/jloda/gui/director/IProjectsChangedListener.java
new file mode 100644
index 0000000..cf08f8f
--- /dev/null
+++ b/src/jloda/gui/director/IProjectsChangedListener.java
@@ -0,0 +1,31 @@
+/**
+ * IProjectsChangedListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.director;
+
+/**
+ * listens for changes of list of projects
+ * Daniel Huson, DATE
+ */
+public interface IProjectsChangedListener {
+    /**
+     * called after number of projects has changed
+     */
+    void doHasChanged();
+}
diff --git a/src/jloda/gui/director/IUpdateableView.java b/src/jloda/gui/director/IUpdateableView.java
new file mode 100644
index 0000000..40dd580
--- /dev/null
+++ b/src/jloda/gui/director/IUpdateableView.java
@@ -0,0 +1,30 @@
+/**
+ * IUpdateableView.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.director;
+
+/**
+ * an updatable view
+ *
+ * @author huson
+ *         Date: 28-Mar-2004
+ */
+public interface IUpdateableView {
+    void updateView(String what);
+}
diff --git a/src/jloda/gui/director/IViewerWithFindToolBar.java b/src/jloda/gui/director/IViewerWithFindToolBar.java
new file mode 100644
index 0000000..47f7150
--- /dev/null
+++ b/src/jloda/gui/director/IViewerWithFindToolBar.java
@@ -0,0 +1,47 @@
+/**
+ * IViewerWithFindToolBar.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.director;
+
+import jloda.gui.find.SearchManager;
+
+import javax.swing.*;
+
+/**
+ * viewers that have a find tool bar
+ * Daniel Huson, 2.2012
+ */
+public interface IViewerWithFindToolBar {
+    boolean isShowFindToolBar();
+
+    void setShowFindToolBar(boolean show);
+
+    SearchManager getSearchManager();
+
+    /**
+     * get name for this type of viewer
+     *
+     * @return name
+     */
+    String getClassName();
+
+    JFrame getFrame();
+
+
+}
diff --git a/src/jloda/gui/director/IViewerWithLegend.java b/src/jloda/gui/director/IViewerWithLegend.java
new file mode 100644
index 0000000..68da830
--- /dev/null
+++ b/src/jloda/gui/director/IViewerWithLegend.java
@@ -0,0 +1,36 @@
+/**
+ * IViewerWithLegend.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.director;
+
+import javax.swing.*;
+
+/**
+ * window with legend
+ * Daniel Huson, 32013
+ */
+public interface IViewerWithLegend {
+    void setShowLegend(String showLegend);
+
+    String getShowLegend();
+
+    JPanel getLegendPanel();
+
+    JScrollPane getLegendScrollPane();
+}
diff --git a/src/jloda/gui/director/ProjectManager.java b/src/jloda/gui/director/ProjectManager.java
new file mode 100644
index 0000000..dc89cf8
--- /dev/null
+++ b/src/jloda/gui/director/ProjectManager.java
@@ -0,0 +1,402 @@
+/**
+ * ProjectManager.java
+ * Copyright (C) 2016 Daniel H. Huson
+ * <p>
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package jloda.gui.director;
+
+
+import jloda.gui.find.SearchManager;
+import jloda.util.*;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.util.*;
+
+
+/**
+ * manages all the different projects
+ *
+ * @author huson
+ *         Date: 01-Dec-2003
+ */
+public class ProjectManager {
+    final static private List<IDirector> projects = Collections.synchronizedList(new LinkedList<IDirector>());
+    final static private Map<IDirector, List<IDirectableViewer>> viewersList = new HashMap<>();
+    final static private List<Pair<IDirector, JMenu>> dirAndWindowMenuPairs = Collections.synchronizedList(new LinkedList<Pair<IDirector, JMenu>>());
+    final static private Map<JMenu, Integer> menu2baseSize = new HashMap<>();
+    final static private List<IProjectsChangedListener> projectsChangedListeners = Collections.synchronizedList(new LinkedList<IProjectsChangedListener>());
+    private static boolean exitOnEmpty = true;
+    final static private HashSet<JMenu> windowMenusUnderControl = new HashSet<>();
+
+    static final BitSet currentIDs = new BitSet();
+    public static final int NEWEST = -1; // pass this to getProject to get newest project
+
+    private static boolean isQuitting = false;
+
+    /**
+     * remove a project director
+     *
+     * @param dir
+     */
+    static public void removeProject(IDirector dir) {
+        synchronized (projects) {
+            projects.remove(dir);
+            viewersList.remove(dir);
+            // any given project uses more than one menu, we need to delete them all
+            List<Pair<IDirector, JMenu>> toDelete = new LinkedList<>();
+            for (Pair<IDirector, JMenu> pair : dirAndWindowMenuPairs) {
+                if (dir == pair.getFirst())
+                    toDelete.add(pair);
+            }
+            for (Pair<IDirector, JMenu> pair : toDelete) {
+                windowMenusUnderControl.remove(pair.getSecond());
+                dirAndWindowMenuPairs.remove(pair);
+            }
+        }
+        fireProjectsChanged();
+
+        synchronized (projects) {
+            currentIDs.clear(dir.getID());
+        }
+
+        if (isExitOnEmpty() && projects.isEmpty()) {
+            ProgramProperties.store();
+            System.exit(0);
+        }
+    }
+
+    /**
+     * add a new project
+     *  @param dir    director
+     * @param viewer the main viewer associated with the director
+     */
+    static public IDirector addProject(final IDirector dir, final IMainViewer viewer) {
+        try {
+            synchronized (projects) {
+                final int id = getNextID();
+                currentIDs.set(id);
+                dir.setID(id);
+
+                projects.add(dir);
+
+                viewersList.put(dir, new LinkedList<IDirectableViewer>());
+
+                if (viewer != null) {
+                    final JMenu menu = viewer.getWindowMenu();
+                    if (menu != null && !dir.isInternalDocument()) {
+                        if (!windowMenusUnderControl.contains(menu)) {
+                            Pair<IDirector, JMenu> pair = new Pair<>(dir, menu);
+                            dirAndWindowMenuPairs.add(pair);
+                            windowMenusUnderControl.add(menu);
+                            menu2baseSize.put(menu, menu.getItemCount());
+                        }
+                    }
+                    dir.addViewer(viewer);
+                }
+            }
+            fireProjectsChanged();
+        } catch (Exception ex) {
+            Basic.caught(ex);
+        }
+        return dir;
+    }
+
+    /**
+     * use this to add additional viewers that have a window menu that they want keep upto date
+     *
+     * @param dir
+     * @param menu
+     */
+    public static void addAnotherWindowWithWindowMenu(IDirector dir, JMenu menu) {
+        if (dir != null && !dir.isInternalDocument() && !windowMenusUnderControl.contains(menu)) {
+            synchronized (projects) {
+                dirAndWindowMenuPairs.add(new Pair<>(dir, menu));
+                menu2baseSize.put(menu, menu.getItemCount());
+            }
+            fireProjectsChanged();
+        }
+    }
+
+    /**
+     * gets the number of open projects
+     *
+     * @return number of open projects
+     */
+    public static int getNumberOfProjects() {
+        return projects.size();
+    }
+
+    /**
+     * close all projects
+     */
+    public static void closeAll() throws CanceledException {
+        while (!projects.isEmpty()) {
+            synchronized (projects) {
+                // close in reverse order to save the geometry of older windows later
+                IDirector dir = projects.get(projects.size() - 1);
+                dir.close();
+            }
+        }
+    }
+
+    /**
+     * call this whenever a project opens or closes a window.
+     * Add or move frame and update all window menus
+     *
+     * @param dir
+     * @param viewer0
+     * @param opened  true, if window opened, false if closed
+     */
+    public static void projectWindowChanged(IDirector dir, IDirectableViewer viewer0, boolean opened) {
+        final List<IDirectableViewer> viewers0 = viewersList.get(dir);
+
+        if (viewers0 != null) {
+            if (opened)
+                viewers0.add(viewer0);
+            else
+                viewers0.remove(viewer0);
+        }
+        if (!dir.isInternalDocument())
+            updateWindowMenus();
+    }
+
+    private static void fireProjectsChanged() {
+        for (IProjectsChangedListener projectsChangedListener : projectsChangedListeners)
+            projectsChangedListener.doHasChanged();
+    }
+
+    /**
+     * add a projects changed listener
+     *
+     * @param projectsChangedListener
+     */
+    public static void addProjectsChangedListener(IProjectsChangedListener projectsChangedListener) {
+        projectsChangedListeners.add(projectsChangedListener);
+    }
+
+    /**
+     * remove a projects changed listener
+     *
+     * @param projectsChangedListener
+     */
+    public static void removeProjectsChangedListener(IProjectsChangedListener projectsChangedListener) {
+        projectsChangedListeners.remove(projectsChangedListener);
+    }
+
+    /**
+     * update all window menus
+     */
+    public static void updateWindowMenus() {
+        synchronized (projects) {
+            for (final Pair<IDirector, JMenu> pair : dirAndWindowMenuPairs) {
+                final JMenu menu = pair.getSecond();
+                final int windowMenuBaseSize = menu2baseSize.get(menu);
+                char mnenomicKey = '1';
+
+                // remove all windows from menu:
+                while (menu.getItemCount() > windowMenuBaseSize) {
+                    menu.remove(windowMenuBaseSize);
+                }
+
+                for (final IDirector proj : projects) {
+                    if (!proj.isInternalDocument()) {
+                        final List<IDirectableViewer> viewers = viewersList.get(proj);
+                        if (viewers != null) {
+                            boolean first = true;
+                            try {
+                                for (final IDirectableViewer viewer : viewers) {
+                                    if (viewer instanceof SearchManager)
+                                        continue; // don't show search managers in menu
+                                    final JFrame frame = viewer.getFrame();
+                                    final AbstractAction action = new AbstractAction() {
+                                        public void actionPerformed(ActionEvent e) {
+                                            frame.setVisible(true);
+                                            frame.setState(JFrame.NORMAL);
+                                            frame.toFront();
+                                        }
+                                    };
+                                    String title = frame.getTitle();
+                                    int pos = title.indexOf(" - ");
+                                    if (pos != -1)
+                                        title = title.substring(0, pos);
+                                    if (viewer instanceof IMainViewer && mnenomicKey <= '9') {
+                                        action.putValue(AbstractAction.NAME, mnenomicKey + " " + title);
+                                        action.putValue(AbstractAction.MNEMONIC_KEY, (int) mnenomicKey);
+                                        mnenomicKey++;
+                                    } else
+                                        action.putValue(AbstractAction.NAME, "  " + title);
+                                    action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("Empty16.gif"));
+                                    action.putValue(AbstractAction.SHORT_DESCRIPTION, "Bring to front: " + title);
+                                    if (first) {
+                                        menu.addSeparator();
+                                        first = false;
+                                    }
+                                    menu.add(action);
+                                }
+                            } catch (ConcurrentModificationException ex) {
+                                // System.err.println("ConcurrentModificationException");
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * gets the list of open projects
+     *
+     * @return projects
+     */
+    public static List<IDirector> getProjects() {
+        return projects;
+    }
+
+    /**
+     * get the project associated with the given project id
+     *
+     * @param pid
+     * @return project
+     */
+    public static IDirector getProject(int pid) {
+        synchronized (projects) {
+            if (pid == NEWEST) {
+                if (projects.size() > 0)
+                    return projects.get(projects.size() - 1);
+            } else {
+                for (IDirector dir : projects) {
+                    if (dir.getID() == pid)
+                        return dir;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * gets the next assignable project ID
+     *
+     * @return next assignable ID
+     */
+    public static int getNextID() {
+        return currentIDs.nextClearBit(1);
+    }
+
+    public static void setExitOnEmpty(boolean b) {
+        exitOnEmpty = b;
+    }
+
+    public static boolean isExitOnEmpty() {
+        return exitOnEmpty;
+    }
+
+    public static int getWindowMenuBaseSize(JMenu windowMenu) {
+        Integer value = menu2baseSize.get(windowMenu);
+        if (value == null)
+            return 0;
+        else
+            return value;
+    }
+
+    /**
+     * makes the file name unique
+     *
+     * @param name
+     * @return unique version of file name
+     */
+    public static String getUniqueName(String name) {
+        try {
+            boolean ok = false;
+            int i = 1;
+            String newName = name;
+            synchronized (projects) {
+                while (!ok) {
+                    ok = true;
+                    for (IDirector dir : projects) {
+                        if (i > 1) {
+                            newName = Basic.getFileBaseName(name) + "-" + i + Basic.getFileSuffix(name);
+                        }
+                        String title = dir.getMainViewer().getTitle();
+                        if (title != null && title.startsWith(newName)) {
+                            ok = false;
+                            break;
+                        }
+                    }
+                    i++;
+                }
+            }
+            return newName;
+        } catch (Exception ex) { // if any thing goes  wrong, just return the original name
+            return name;
+        }
+    }
+
+    /**
+     * attempt to quit program. If quit canceled and no projects open, opens a new empty document.
+     * Programs that use this method for quitting must set setQuitting to false if the user chooses not to quit
+     *
+     * @param runOnQuitCanceled
+     */
+    public static void doQuit(final Runnable runJustBeforeQuit, final Runnable runOnQuitCanceled) {
+        setQuitting(true);
+        setExitOnEmpty(false);
+        try {
+            while (!projects.isEmpty() && isQuitting()) {
+                synchronized (projects) {
+                    // close in reverse order to save the geometry of older windows later
+                    int oldSize = projects.size();
+                    IDirector dir = projects.get(projects.size() - 1);
+                    dir.close();
+                    if (projects.size() == oldSize) // somehow failed to remove from list of projects...
+                        projects.remove(projects.size() - 1);
+                }
+            }
+            if (isQuitting()) {
+                try {
+                    if (runJustBeforeQuit != null)
+                        runJustBeforeQuit.run();
+                } catch (Exception ex) {
+                    Basic.caught(ex);
+                }
+                ProgramProperties.store();
+                System.exit(0);
+            }
+        } catch (CanceledException ex) {
+        } finally {
+            if (projects.isEmpty()) {
+                runOnQuitCanceled.run();
+            }
+            setQuitting(false);
+        }
+    }
+
+    public static boolean isQuitting() {
+        return isQuitting;
+    }
+
+    public static void setQuitting(boolean quitting) {
+        isQuitting = quitting;
+    }
+
+    private final static HashSet<String> previouslySelectedNodeLabels = new HashSet<>();
+
+    public static Set<String> getPreviouslySelectedNodeLabels() {
+        return previouslySelectedNodeLabels;
+    }
+}
+
diff --git a/src/jloda/gui/find/CompositeObjectSearchers.java b/src/jloda/gui/find/CompositeObjectSearchers.java
new file mode 100644
index 0000000..093cbb8
--- /dev/null
+++ b/src/jloda/gui/find/CompositeObjectSearchers.java
@@ -0,0 +1,279 @@
+/**
+ * CompositeObjectSearchers.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Collection;
+
+/**
+ * Composition of two object searchers
+ * Daniel Huson, 4.2013
+ */
+public class CompositeObjectSearchers implements IObjectSearcher {
+    public static final String SEARCHER_NAME = "Composite";
+    private final Component frame;
+    private final String name;
+    private final IObjectSearcher first;
+    private final IObjectSearcher second;
+
+    private enum Which {First, Second, None}
+
+    private Which which = Which.None;
+
+
+    /**
+     * constructor
+     *
+     * @param first
+     * @param second
+     */
+    public CompositeObjectSearchers(String name, Component frame, IObjectSearcher first, IObjectSearcher second) {
+        this.name = name;
+        this.frame = frame;
+        this.first = first;
+        this.second = second;
+    }
+
+    /**
+     * get the parent component
+     *
+     * @return parent
+     */
+    public Component getParent() {
+        return frame;
+    }
+
+    /**
+     * get the name for this type of search
+     *
+     * @return name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * goto the first object
+     */
+    public boolean gotoFirst() {
+        if (first.gotoFirst()) {
+            which = Which.First;
+            return true;
+        } else if (second.gotoFirst()) {
+            which = Which.Second;
+            return true;
+        }
+        which = Which.None;
+        return false;
+    }
+
+    /**
+     * goto the next object
+     */
+    public boolean gotoNext() {
+        if (which == Which.First) {
+            if (first.gotoNext())
+                return true;
+            else if (second.gotoFirst()) {
+                which = Which.Second;
+                return true;
+            } else {
+                which = Which.None;
+                return false;
+            }
+        } else if (which == Which.Second) {
+            if (second.gotoNext())
+                return true;
+            else {
+                which = Which.None;
+                return false;
+            }
+        } else {
+            if (first.gotoFirst()) {
+                which = Which.First;
+                return true;
+            } else if (second.gotoFirst()) {
+                which = Which.Second;
+                return true;
+            } else
+                return false;
+        }
+    }
+
+    /**
+     * goto the last object
+     */
+    public boolean gotoLast() {
+        if (second.gotoLast()) {
+            which = Which.Second;
+            return true;
+        } else if (first.gotoLast()) {
+            which = Which.First;
+            return true;
+        }
+        which = Which.None;
+        return false;
+    }
+
+    /**
+     * goto the previous object
+     */
+    public boolean gotoPrevious() {
+        if (which == Which.Second) {
+            if (second.gotoPrevious())
+                return true;
+            else if (first.gotoLast()) {
+                which = Which.First;
+                return true;
+            } else {
+                which = Which.None;
+                return false;
+            }
+        } else if (which == Which.First) {
+            if (first.gotoPrevious())
+                return true;
+            else {
+                which = Which.None;
+                return false;
+            }
+        } else {
+            if (second.gotoLast()) {
+                which = Which.Second;
+                return true;
+            } else if (first.gotoLast()) {
+                which = Which.First;
+                return true;
+            } else
+                return false;
+        }
+    }
+
+    /**
+     * is the current object selected?
+     *
+     * @return true, if selected
+     */
+    public boolean isCurrentSelected() {
+        return which == Which.First && first.isCurrentSelected() || which == Which.Second && second.isCurrentSelected();
+    }
+
+    /**
+     * set selection state of current object
+     *
+     * @param select
+     */
+    public void setCurrentSelected(boolean select) {
+        if (which == Which.First)
+            first.setCurrentSelected(select);
+        else if (which == Which.Second)
+            second.setCurrentSelected(select);
+    }
+
+    /**
+     * set select state of all objects
+     *
+     * @param select
+     */
+    public void selectAll(boolean select) {
+        first.selectAll(select);
+        second.selectAll(select);
+    }
+
+    /**
+     * get the label of the current object
+     *
+     * @return label
+     */
+    public String getCurrentLabel() {
+        if (which == Which.First)
+            return first.getCurrentLabel();
+        else if (which == Which.Second)
+            return second.getCurrentLabel();
+        else
+            return null;
+    }
+
+    /**
+     * set the label of the current object
+     *
+     * @param newLabel
+     */
+    public void setCurrentLabel(String newLabel) {
+    }
+
+    /**
+     * is a global find possible?
+     *
+     * @return true, if there is at least one object
+     */
+    public boolean isGlobalFindable() {
+        return first.isGlobalFindable() || second.isGlobalFindable();
+    }
+
+    /**
+     * is a selection find possible
+     *
+     * @return true, if at least one object is selected
+     */
+    public boolean isSelectionFindable() {
+        return false;
+    }
+
+    /**
+     * is the current object set?
+     *
+     * @return true, if set
+     */
+    public boolean isCurrentSet() {
+        return which == Which.First && first.isCurrentSet() || which == Which.Second && second.isCurrentSet();
+    }
+
+    /**
+     * something has been changed or selected, update view
+     */
+    public void updateView() {
+        first.updateView();
+        second.updateView();
+    }
+
+    /**
+     * does this searcher support find all?
+     *
+     * @return true, if find all supported
+     */
+    public boolean canFindAll() {
+        return true;
+    }
+
+    /**
+     * how many objects are there?
+     *
+     * @return number of objects or -1
+     */
+    public int numberOfObjects() {
+        return first.numberOfObjects() + second.numberOfObjects();
+    }
+
+    @Override
+    public Collection<AbstractButton> getAdditionalButtons() {
+        return null;
+    }
+}
diff --git a/src/jloda/gui/find/EdgeLabelSearcher.java b/src/jloda/gui/find/EdgeLabelSearcher.java
new file mode 100644
index 0000000..2082a19
--- /dev/null
+++ b/src/jloda/gui/find/EdgeLabelSearcher.java
@@ -0,0 +1,327 @@
+/**
+ * EdgeLabelSearcher.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import jloda.graph.Edge;
+import jloda.graph.EdgeSet;
+import jloda.graph.Graph;
+import jloda.graphview.GraphView;
+import jloda.phylo.PhyloTree;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.Objects;
+
+/**
+ * Class for finding and replacing edge labels in a graph
+ * Daniel Huson, 7.2008
+ */
+public class EdgeLabelSearcher implements IObjectSearcher {
+    private final Frame frame;
+    private final String name;
+    final Graph graph;
+    final GraphView viewer;
+    Edge current = null;
+
+    final EdgeSet toSelect;
+    final EdgeSet toDeselect;
+    public static final String SEARCHER_NAME = "Edges";
+
+    /**
+     * constructor
+     *
+     * @param viewer
+     */
+    public EdgeLabelSearcher(GraphView viewer) {
+        this(null, SEARCHER_NAME, viewer);
+    }
+
+    /**
+     * constructor
+     *
+     * @param viewer
+     */
+    public EdgeLabelSearcher(Frame frame, GraphView viewer) {
+        this(frame, SEARCHER_NAME, viewer);
+    }
+
+    /**
+     * constructor
+     *
+     * @param name
+     * @param viewer
+     */
+    public EdgeLabelSearcher(Frame frame, String name, GraphView viewer) {
+        this.frame = frame;
+        this.name = name;
+        this.viewer = viewer;
+        this.graph = viewer.getGraph();
+        toSelect = new EdgeSet(graph);
+        toDeselect = new EdgeSet(graph);
+    }
+
+    /**
+     * get the parent component
+     *
+     * @return parent
+     */
+    public Component getParent() {
+        return frame;
+    }
+
+    /**
+     * get the name for this type of search
+     *
+     * @return name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * goto the first object
+     */
+    public boolean gotoFirst() {
+        current = graph.getFirstEdge();
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the next object
+     */
+    public boolean gotoNext() {
+        if (current == null)
+            gotoFirst();
+        else
+            current = current.getNext();
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the last object
+     */
+    public boolean gotoLast() {
+        current = graph.getLastEdge();
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the previous object
+     */
+    public boolean gotoPrevious() {
+        if (current == null)
+            gotoLast();
+        else
+            current = current.getPrev();
+        return isCurrentSet();
+    }
+
+    /**
+     * is the current object selected?
+     *
+     * @return true, if selected
+     */
+    public boolean isCurrentSelected() {
+        return isCurrentSet()
+                && viewer.getSelected(current);
+    }
+
+    /**
+     * set selection state of current object
+     *
+     * @param select
+     */
+    public void setCurrentSelected(boolean select) {
+        if (current != null) {
+            if (select)
+                toSelect.add(current);
+            else
+                toDeselect.add(current);
+        }
+    }
+
+    /**
+     * set select state of all objects
+     *
+     * @param select
+     */
+    public void selectAll(boolean select) {
+        viewer.selectAllEdges(select);
+        viewer.repaint();
+    }
+
+    /**
+     * get the label of the current object
+     *
+     * @return label
+     */
+    public String getCurrentLabel() {
+        if (current == null)
+            return null;
+        else
+            return viewer.getLabel(current);
+    }
+
+    /**
+     * set the label of the current object
+     *
+     * @param newLabel
+     */
+    public void setCurrentLabel(String newLabel) {
+        if (current != null && !Objects.equals(newLabel, viewer.getLabel(current))) {
+            if (newLabel == null || newLabel.length() == 0) {
+                viewer.setLabel(current, null);
+                if (viewer.getGraph() instanceof PhyloTree) {
+                    ((PhyloTree) viewer.getGraph()).setLabel(current, null);
+                }
+            } else {
+                viewer.setLabel(current, newLabel);
+                if (viewer.getGraph() instanceof PhyloTree) {
+                    ((PhyloTree) viewer.getGraph()).setLabel(current, newLabel);
+                }
+            }
+            fireLabelChangedListeners(current);
+        }
+    }
+
+    /**
+     * is a global find possible?
+     *
+     * @return true, if there is at least one object
+     */
+    public boolean isGlobalFindable() {
+        return graph.getNumberOfEdges() > 0;
+    }
+
+    /**
+     * is a selection find possible
+     *
+     * @return true, if at least one object is selected
+     */
+    public boolean isSelectionFindable() {
+        return false;
+    }
+
+    /**
+     * is the current object set?
+     *
+     * @return true, if set
+     */
+    public boolean isCurrentSet() {
+        return current != null;
+    }
+
+    /**
+     * something has been changed or selected, update view
+     */
+    public void updateView() {
+        viewer.selectedEdges.addAll(toSelect);
+        viewer.fireDoSelect(toSelect);
+        Edge edge = toSelect.getLastElement();
+        if (edge != null) {
+            final Point p = viewer.trans.w2d(viewer.getLocation(edge.getSource()));
+            final Point q = viewer.trans.w2d(viewer.getLocation(edge.getTarget()));
+
+            Rectangle rect = new Rectangle(p.x - 60, p.y - 25, 120, 50);
+            rect.add(q);
+            viewer.scrollRectToVisible(rect);
+        }
+        viewer.selectedEdges.removeAll(toDeselect);
+        viewer.fireDoDeselect(toDeselect);
+        toSelect.clear();
+        toDeselect.clear();
+
+        viewer.repaint();
+    }
+
+    /**
+     * does this searcher support find all?
+     *
+     * @return true, if find all supported
+     */
+    public boolean canFindAll() {
+        return true;
+    }
+
+    private final java.util.List labelChangedListeners = new LinkedList();
+
+    /**
+     * fire the label changed listener
+     *
+     * @param e
+     */
+    private void fireLabelChangedListeners(Edge e) {
+        for (Object labelChangedListener : labelChangedListeners) {
+            LabelChangedListener listener = (LabelChangedListener) labelChangedListener;
+            listener.doLabelHasChanged(e);
+        }
+    }
+
+    /**
+     * add a label changed listener
+     *
+     * @param listener
+     */
+    public void addLabelChangedListener(LabelChangedListener listener) {
+        labelChangedListeners.add(listener);
+    }
+
+    /**
+     * remove a label changed listener
+     *
+     * @param listener
+     */
+    public void removeLabelChangedListener(LabelChangedListener listener) {
+        labelChangedListeners.remove(listener);
+    }
+
+    /**
+     * label changed listener
+     */
+    public interface LabelChangedListener {
+        void doLabelHasChanged(Edge e);
+    }
+
+
+    /**
+     * how many objects are there?
+     *
+     * @return number of objects or -1
+     */
+    public int numberOfObjects() {
+        return graph.getNumberOfEdges();
+    }
+
+    /**
+     * how many selected objects are there?
+     *
+     * @return number of objects or -1
+     */
+    public int numberOfSelectedObjects() {
+        return viewer.getSelectedEdges().size();
+    }
+
+    @Override
+    public Collection<AbstractButton> getAdditionalButtons() {
+        return null;
+    }
+}
diff --git a/src/jloda/gui/find/EmptySearcher.java b/src/jloda/gui/find/EmptySearcher.java
new file mode 100644
index 0000000..3447216
--- /dev/null
+++ b/src/jloda/gui/find/EmptySearcher.java
@@ -0,0 +1,106 @@
+/**
+ * EmptySearcher.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Collection;
+
+/**
+ * empty searcher
+ * Daniel Huson, 4.2014
+ */
+public class EmptySearcher implements ISearcher {
+    /**
+     * get the name for this type of search
+     *
+     * @return name
+     */
+    @Override
+    public String getName() {
+        return "Null";
+    }
+
+    /**
+     * is a global find possible?
+     *
+     * @return true, if there is at least one object
+     */
+    @Override
+    public boolean isGlobalFindable() {
+        return false;
+    }
+
+    /**
+     * is a selection find possible
+     *
+     * @return true, if at least one object is selected
+     */
+    @Override
+    public boolean isSelectionFindable() {
+        return false;
+    }
+
+    /**
+     * something has been changed or selected, update view
+     */
+    @Override
+    public void updateView() {
+
+    }
+
+    /**
+     * does this searcher support find all?
+     *
+     * @return true, if find all supported
+     */
+    @Override
+    public boolean canFindAll() {
+        return false;
+    }
+
+    /**
+     * set select state of all objects
+     *
+     * @param select
+     */
+    @Override
+    public void selectAll(boolean select) {
+
+    }
+
+    /**
+     * get the parent component
+     *
+     * @return parent
+     */
+    @Override
+    public Component getParent() {
+        return null;
+    }
+
+    /**
+     * get list of additional buttons to be embedded into find tool bar, or null
+     */
+    @Override
+    public Collection<AbstractButton> getAdditionalButtons() {
+        return null;
+    }
+}
diff --git a/src/jloda/gui/find/FindToolBar.java b/src/jloda/gui/find/FindToolBar.java
new file mode 100644
index 0000000..6fc1820
--- /dev/null
+++ b/src/jloda/gui/find/FindToolBar.java
@@ -0,0 +1,469 @@
+/**
+ * FindToolBar.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import jloda.gui.director.IDirectableViewer;
+import jloda.gui.director.IDirector;
+import jloda.gui.director.IViewerWithFindToolBar;
+import jloda.util.Basic;
+import jloda.util.ProgramProperties;
+import jloda.util.RememberingComboBox;
+import jloda.util.ResourceManager;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.*;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * find and replace window
+ * Daniel Huson, 7.2008
+ */
+public class FindToolBar extends JPanel implements IFindDialog {
+    private final SearchManager searchManager;
+    private final IViewerWithFindToolBar viewer;
+
+    private final RememberingComboBox findCBox;
+    private final RememberingComboBox replaceCBox;
+
+    private final JToolBar findToolBar;
+    private final JToolBar replaceToolBar;
+
+    private boolean showReplaceBar = false;
+
+    final static private Color LIGHT_RED = new Color(255, 200, 200);
+    final static private Color LIGHT_GREEN = new Color(200, 255, 200);
+
+    private final JLabel messageLabel;
+    private final JComboBox targetCBox = new JComboBox();
+    private final SearchActions actions;
+
+    private final Map<Component, ISearcher> parent2active = new HashMap<>();   // keeps a mapping of windows to active searcher
+
+    private JFrame frame;
+
+    private boolean enabled = true;
+    private boolean closing = false;
+
+    /**
+     * constructor
+     *
+     * @param searchManager
+     * @param viewer
+     * @param actions
+     * @param additionalButtons
+     */
+    public FindToolBar(SearchManager searchManager, IViewerWithFindToolBar viewer, SearchActions actions, Collection<AbstractButton> additionalButtons) {
+        this(searchManager, viewer, actions, false, additionalButtons);
+    }
+
+    /**
+     * constructor
+     *
+     * @param searchManager
+     * @param viewer
+     * @param actions
+     * @param showReplaceBar
+     * @param additionalButtons
+     */
+    public FindToolBar(SearchManager searchManager, IViewerWithFindToolBar viewer, SearchActions actions, boolean showReplaceBar,
+                       Collection<AbstractButton> additionalButtons) {
+        this.searchManager = searchManager;
+        this.viewer = viewer;
+        this.frame = viewer.getFrame();
+        this.actions = actions;
+        this.showReplaceBar = showReplaceBar;
+
+        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+        setBorder(BorderFactory.createEtchedBorder());
+
+        findToolBar = new JToolBar();
+        findToolBar.setFloatable(false);
+        findToolBar.setRollover(true);
+        add(findToolBar);
+
+        findCBox = new RememberingComboBox();
+        findCBox.addItemsFromString(ProgramProperties.get("FindString." + viewer.getClassName(), ""), "%%%");
+
+        messageLabel = new JLabel();
+        messageLabel.setForeground(Color.DARK_GRAY);
+        setupFind(additionalButtons);
+
+        replaceToolBar = new JToolBar();
+        replaceToolBar.setFloatable(false);
+        replaceToolBar.setRollover(true);
+
+        replaceCBox = new RememberingComboBox();
+        replaceCBox.addItemsFromString(ProgramProperties.get("ReplaceString." + viewer.getClassName(), ""), "%%%");
+        setupReplace();
+
+        if (showReplaceBar)
+            add(replaceToolBar);
+
+        addMouseListener(new MouseAdapter() {
+            public void mouseExited(MouseEvent mouseEvent) {
+                if (frame != null)
+                    frame.requestFocusInWindow();
+            }
+
+            public void mouseEntered(MouseEvent mouseEvent) {
+            }
+        });
+        actions.updateEnableState();
+    }
+
+    /**
+     * setup the panel
+     */
+    private void setupFind(Collection<AbstractButton> additionalButtons) {
+        findCBox.setMinimumSize(new Dimension(200, 20));
+        findCBox.setMaximumSize(new Dimension(200, 20));
+        findCBox.setPreferredSize(new Dimension(200, 20));
+        Basic.changeFontSize(findCBox, 10);
+        findToolBar.add(findCBox);
+
+        findCBox.getEditor().addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent arg0) {
+                String word = findCBox.getEditor().getItem().toString().trim();
+                if (word.length() > 0) {
+                    findCBox.setSelectedItem(word);
+                    findCBox.getCurrentText(true);
+                    searchManager.setSearchText(word);
+                    searchManager.applyFindFirst();
+                }
+            }
+        });
+
+        JButton button = new JButton(actions.getFindFirst());
+        Basic.changeFontSize(button, 10);
+        findToolBar.add(button);
+        button = new JButton(actions.getFindNext());
+        Basic.changeFontSize(button, 10);
+        findToolBar.add(button);
+
+        button = new JButton(actions.getFindAll());
+        button.setText("All");
+        Basic.changeFontSize(button, 10);
+        findToolBar.add(button);
+
+        button = new JButton(actions.getFindFromFile());
+        button.setText("");
+        addButton(button, findToolBar);
+
+        findToolBar.addSeparator(new Dimension(5, 10));
+
+        JCheckBox cbox = new JCheckBox();
+        cbox.setAction(actions.getCaseSensitiveOption(cbox));
+        addButton(cbox, findToolBar);
+
+        cbox = new JCheckBox();
+        cbox.setAction(actions.getWholeWordsOption(cbox));
+        addButton(cbox, findToolBar);
+
+        cbox = new JCheckBox();
+        cbox.setAction(actions.getRegularExpressionOption(cbox));
+        cbox.setText("Regex");
+        addButton(cbox, findToolBar);
+
+        findToolBar.addSeparator(new Dimension(5, 10));
+
+
+        if (additionalButtons != null && additionalButtons.size() > 0) {
+            for (AbstractButton but : additionalButtons) {
+                addButton(but, findToolBar);
+            }
+            findToolBar.addSeparator(new Dimension(5, 10));
+        }
+
+        findToolBar.add(Box.createHorizontalStrut(10));
+
+        Basic.changeFontSize(messageLabel, 10);
+        messageLabel.setMinimumSize(new Dimension(200, 20));
+        messageLabel.setMaximumSize(new Dimension(200, 20));
+        messageLabel.setPreferredSize(new Dimension(100, 20));
+        findToolBar.add(messageLabel);
+        findToolBar.add(Box.createHorizontalGlue());
+
+        JButton done = new JButton(new AbstractAction() {
+            public void actionPerformed(ActionEvent actionEvent) {
+                FindToolBar.this.setClosing(true);
+                if (viewer instanceof IDirectableViewer)
+                    ((IDirectableViewer) viewer).updateView(IDirector.ENABLE_STATE);
+            }
+        });
+        done.setIcon(ResourceManager.getIcon("CloseToolBar16.gif"));
+        done.setToolTipText("Close find toolbar");
+        addButton(done, findToolBar);
+
+        findToolBar.validate();
+    }
+
+    /**
+     * setup the panel
+     */
+    private void setupReplace() {
+        replaceCBox.setMinimumSize(new Dimension(200, 20));
+        replaceCBox.setMaximumSize(new Dimension(200, 20));
+        replaceCBox.setPreferredSize(new Dimension(200, 20));
+        Basic.changeFontSize(replaceCBox, 10);
+        replaceToolBar.add(replaceCBox);
+
+        replaceCBox.getEditor().addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent arg0) {
+                String word = replaceCBox.getEditor().getItem().toString().trim();
+                if (word.length() > 0) {
+                    searchManager.setReplaceText(word);
+                    replaceCBox.setSelectedItem(word);
+                    replaceCBox.getCurrentText(true);
+                }
+            }
+        });
+
+        JButton button = new JButton(actions.getFindAndReplace());
+        Basic.changeFontSize(button, 10);
+        replaceToolBar.add(button);
+        button = new JButton(actions.getReplaceAll());
+        Basic.changeFontSize(button, 10);
+        replaceToolBar.add(button);
+
+        replaceToolBar.addSeparator(new Dimension(5, 10));
+
+        ButtonGroup group = new ButtonGroup();
+        JRadioButton globalButton = new JRadioButton();
+        group.add(globalButton);
+        globalButton.setAction(actions.getGlobalScope(globalButton));
+        addButton(globalButton, replaceToolBar);
+        JRadioButton selectionButton = new JRadioButton();
+        selectionButton.setAction(actions.getSelectionScope(selectionButton));
+        group.add(selectionButton);
+        addButton(selectionButton, replaceToolBar);
+
+        replaceToolBar.add(Box.createHorizontalGlue());
+        replaceToolBar.validate();
+    }
+
+    private void addButton(AbstractButton button, JToolBar toolBar) {
+        Basic.changeFontSize(button, 10);
+        button.setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 3));
+        toolBar.add(button);
+    }
+
+    /**
+     * show or hide the replace bar
+     *
+     * @param showReplaceBar
+     */
+    public void setShowReplaceBar(boolean showReplaceBar) {
+        if (this.showReplaceBar != showReplaceBar) {
+            removeAll();
+            add(findToolBar);
+            if (showReplaceBar)
+                add(replaceToolBar);
+            revalidate();
+            this.showReplaceBar = showReplaceBar;
+        }
+    }
+
+    /**
+     * is the replace bar showing?
+     *
+     * @return true if replace bar showing
+     */
+    public boolean isShowReplaceBar() {
+        return showReplaceBar;
+    }
+
+    /**
+     * update the targets cbox
+     */
+    public void updateTargets() {
+        parent2active.clear();
+
+        targetCBox.removeAllItems();
+
+        for (int i = 0; i < searchManager.targets.length; i++) {
+            ISearcher searcher = searchManager.targets[i];
+            targetCBox.addItem(new SearcherItem(searcher));
+            if (searcher.getParent() != null && parent2active.get(searcher.getParent()) == null)
+                parent2active.put(searcher.getParent(), searcher);
+        }
+
+        targetCBox.addItemListener(new ItemListener() {
+            public void itemStateChanged(ItemEvent event) {
+                if (event.getStateChange() == ItemEvent.SELECTED) {
+                    ISearcher searcher = ((SearcherItem) event.getItem()).getSearcher();
+                    searchManager.setSearcher(searcher);
+                    if (searcher.getParent() != null)
+                        parent2active.put(searcher.getParent(), searcher);
+                }
+            }
+        });
+    }
+
+    /**
+     * update the target selection
+     *
+     * @param name named search target
+     */
+    public boolean selectTarget(String name) {
+        for (int i = 0; i < searchManager.targets.length; i++) {
+            SearcherItem item = (SearcherItem) targetCBox.getItemAt(i);
+            if (item.toString().equals(name)) {
+                targetCBox.setSelectedIndex(i);
+                return true;
+            }
+        }
+        for (int i = 0; i < searchManager.targets.length; i++) {
+            SearcherItem item = (SearcherItem) targetCBox.getItemAt(i);
+            if (item.toString().equalsIgnoreCase(name)) {
+                targetCBox.setSelectedIndex(i);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * when activating a window, call this method to revert to the last used searcher for this window
+     *
+     * @param parent
+     */
+    public void chooseTargetForFrame(Component parent) {
+        if (parent != null) {
+            ISearcher searcher = parent2active.get(parent);
+            if (searcher != null)
+                selectTarget(searcher.getName());
+        }
+    }
+
+    /**
+     * indicates that user has pressed close button
+     *
+     * @return true, if closing
+     */
+    public boolean isClosing() {
+        return closing;
+    }
+
+    /**
+     * once closed, client show set closing to false
+     *
+     * @param closing
+     */
+    public void setClosing(boolean closing) {
+        this.closing = closing;
+    }
+
+    class SearcherItem extends JButton {
+        final ISearcher searcher;
+
+        SearcherItem(ISearcher searcher) {
+            setText(searcher.getName());
+            this.searcher = searcher;
+        }
+
+        public String toString() {
+            return searcher.getName();
+        }
+
+        public ISearcher getSearcher() {
+            return searcher;
+        }
+    }
+
+    public SearchActions getActions() {
+        return actions;
+    }
+
+    /**
+     * sets the message
+     *
+     * @param message
+     */
+    public void setMessage(String message) {
+        if (message.contains("No matches") || message.contains("Found: 0"))
+            findCBox.setBackground(LIGHT_RED);
+        else if (message.contains("Found"))
+            findCBox.setBackground(LIGHT_GREEN);
+        else
+            findCBox.setBackground(Color.WHITE);
+
+        if (message.contains("No replacements") || message.contains("Replacements: 0"))
+            replaceCBox.setBackground(LIGHT_RED);
+        else if (message.contains("Replace"))
+            replaceCBox.setBackground(LIGHT_GREEN);
+        else
+            replaceCBox.setBackground(Color.WHITE);
+        messageLabel.setText(message);
+    }
+
+    /**
+     * clears the message
+     */
+    public void clearMessage() {
+        findCBox.setBackground(Color.WHITE);
+        replaceCBox.setBackground(Color.WHITE);
+        messageLabel.setText("");
+    }
+
+    public JFrame getFrame() {
+        return frame;
+    }
+
+    public void setFrame(JFrame frame) {
+        this.frame = frame;
+    }
+
+    /**
+     * call when window is closed to remember find strings
+     */
+    public void close() {
+        ProgramProperties.put("FindString." + viewer.getClassName(), findCBox.getItemsAsString(20, "%%%"));
+        ProgramProperties.put("ReplaceString." + viewer.getClassName(), replaceCBox.getItemsAsString(20, "%%%"));
+        clearMessage();
+    }
+
+    public String getFindText() {
+        if (frame != null)
+            frame.requestFocusInWindow();
+        return findCBox.getCurrentText(true);
+    }
+
+    public String getReplaceText() {
+        return replaceCBox.getCurrentText(true);
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+        if (enabled)
+            findCBox.requestFocusInWindow(); // grab focus
+    }
+
+    public void setEnableCritical(boolean enableCritical) {
+        if (isEnabled())
+            actions.setEnableCritical(enableCritical);
+    }
+}
diff --git a/src/jloda/gui/find/FindWindow.java b/src/jloda/gui/find/FindWindow.java
new file mode 100644
index 0000000..11559c3
--- /dev/null
+++ b/src/jloda/gui/find/FindWindow.java
@@ -0,0 +1,363 @@
+/**
+ * FindWindow.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import jloda.util.ProgramProperties;
+import jloda.util.RememberingComboBox;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * find and replace window
+ * Daniel Huson, 7.2008
+ */
+public class FindWindow extends JFrame implements IFindDialog {
+    final SearchManager searchManager;
+
+    final RememberingComboBox findCBox;
+    final RememberingComboBox replaceCBox;
+
+    final JLabel messageLabel;
+    final JComboBox targetCBox = new JComboBox();
+    final SearchActions actions;
+
+    private final int WIDTH_FIND = 600;
+    private final int HEIGHT_FIND = 250;
+    private final int HEIGHT_FIND_REPLACE = 330;
+
+    private final Map<Component, ISearcher> parent2active = new HashMap<>();   // keeps a mapping of windows to active searcher
+
+
+    /**
+     * constructor
+     *
+     * @param parent
+     * @param title
+     * @param searchManager
+     */
+    public FindWindow(Component parent, String title, SearchManager searchManager, SearchActions actions) {
+        this.searchManager = searchManager;
+
+        this.setLocationRelativeTo(parent);
+        this.setTitle(title);
+        if (ProgramProperties.getProgramIcon() != null)
+            this.setIconImage(ProgramProperties.getProgramIcon().getImage());
+
+        int height = searchManager.getShowReplace() ? HEIGHT_FIND_REPLACE : HEIGHT_FIND;
+        this.setSize(WIDTH_FIND, height);
+
+        findCBox = new RememberingComboBox();
+        findCBox.setBorder(BorderFactory.createBevelBorder(1));
+        findCBox.addItemsFromString(ProgramProperties.get("FindString", ""), "%%%");
+        replaceCBox = new RememberingComboBox();
+        replaceCBox.setBorder(BorderFactory.createBevelBorder(1));
+        replaceCBox.addItemsFromString(ProgramProperties.get("ReplaceString", ""), "%%%");
+        messageLabel = new JLabel();
+        messageLabel.setForeground(Color.DARK_GRAY);
+
+        this.actions = actions;
+
+        setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+        addWindowListener(new WindowAdapter() {
+            public void windowClosing(WindowEvent event) {
+                ProgramProperties.put("FindString", findCBox.getItemsAsString(20, "%%%"));
+                ProgramProperties.put("ReplaceString", replaceCBox.getItemsAsString(20, "%%%"));
+                clearMessage();
+                getActions().getClose().actionPerformed(null);
+            }
+        });
+        setupPanel(searchManager.getShowReplace());
+        actions.updateEnableState();
+    }
+
+    /**
+     * gets the frame of this
+     *
+     * @return frame
+     */
+    public JFrame getFrame() {
+        return this;
+    }
+
+    /**
+     * setup the panel
+     */
+    private void setupPanel(boolean showReplace) {
+        int preferredHeight = (showReplace ? HEIGHT_FIND_REPLACE : HEIGHT_FIND) + (searchManager.targets.length <= 3 ? 0 :
+                (searchManager.targets.length - 3) * 30);
+        if (getSize().height != preferredHeight)
+            this.setSize((int) this.getSize().getWidth(), HEIGHT_FIND_REPLACE);
+
+        Container main = getContentPane();
+        main.removeAll();
+
+        main.setLayout(new BorderLayout());
+
+        // top panel:
+        JPanel topPanel = new JPanel();
+        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
+        JPanel findTextPanel = new JPanel(new BorderLayout());
+        findTextPanel.add(new JLabel("Text to find: "), BorderLayout.WEST);
+        findTextPanel.add(findCBox, BorderLayout.CENTER);
+        topPanel.add(findTextPanel);
+
+        JPanel replaceTextPanel = new JPanel(new BorderLayout());
+        replaceTextPanel.add(new JLabel("Replace with:"), BorderLayout.WEST);
+        replaceTextPanel.add(replaceCBox, BorderLayout.CENTER);
+        if (showReplace)
+            topPanel.add(replaceTextPanel);
+
+        main.add(topPanel, BorderLayout.NORTH);
+
+        // middle panel:
+        JPanel middlePanel = new JPanel();
+        middlePanel.setLayout(new BoxLayout(middlePanel, BoxLayout.X_AXIS));
+
+        JPanel row1 = new JPanel();
+        row1.setLayout(new GridLayout(2, 0));
+
+        // options:
+        JPanel optionsPanel = new JPanel();
+        optionsPanel.setLayout(new BoxLayout(optionsPanel, BoxLayout.Y_AXIS));
+        optionsPanel.setBorder(BorderFactory.createTitledBorder("Options"));
+
+        JCheckBox cbox = new JCheckBox();
+        cbox.setAction(actions.getCaseSensitiveOption(cbox));
+
+        optionsPanel.add(cbox);
+
+        cbox = new JCheckBox();
+        cbox.setAction(actions.getWholeWordsOption(cbox));
+        optionsPanel.add(cbox);
+
+        cbox = new JCheckBox();
+        cbox.setAction(actions.getRegularExpressionOption(cbox));
+        optionsPanel.add(cbox);
+
+        row1.add(optionsPanel);
+
+        // scope:
+        JPanel scopePanel = new JPanel();
+        scopePanel.setLayout(new BoxLayout(scopePanel, BoxLayout.Y_AXIS));
+        scopePanel.setBorder(BorderFactory.createTitledBorder("Scope"));
+
+        ButtonGroup scopeButtonGroup = new ButtonGroup();
+
+        JRadioButton globalRB = new JRadioButton();
+        scopeButtonGroup.add(globalRB);
+        globalRB.setAction(actions.getGlobalScope(globalRB));
+        scopePanel.add(globalRB);
+
+        JRadioButton selectionRB = new JRadioButton();
+        scopeButtonGroup.add(selectionRB);
+        selectionRB.setAction(actions.getSelectionScope(selectionRB));
+        scopePanel.add(selectionRB);
+
+        scopePanel.add(Box.createVerticalGlue());
+        scopePanel.add(messageLabel);
+
+        row1.add(scopePanel);
+        middlePanel.add(row1);
+
+        JPanel row2 = new JPanel();
+        row2.setLayout(new GridLayout(2, 0));
+
+        // direction
+        JPanel directionPanel = new JPanel();
+        directionPanel.setLayout(new BoxLayout(directionPanel, BoxLayout.Y_AXIS));
+        directionPanel.setBorder(BorderFactory.createTitledBorder("Direction"));
+
+        ButtonGroup directionButtonGroup = new ButtonGroup();
+        JRadioButton forwardRB = new JRadioButton();
+        directionButtonGroup.add(forwardRB);
+        forwardRB.setAction(actions.getForwardDirection(forwardRB));
+        directionPanel.add(forwardRB);
+
+        JRadioButton backwardRB = new JRadioButton();
+        directionButtonGroup.add(backwardRB);
+        backwardRB.setAction(actions.getBackwardDirection(backwardRB));
+        directionPanel.add(backwardRB);
+
+        row2.add(directionPanel);
+
+        // targets
+        JPanel targetPanel = new JPanel();
+        targetPanel.setLayout(new BoxLayout(targetPanel, BoxLayout.Y_AXIS));
+        targetPanel.setBorder(BorderFactory.createTitledBorder("Target"));
+        targetCBox.setEditable(false);
+        updateTargets();
+        targetPanel.add(targetCBox);
+
+        row2.add(targetPanel);
+        middlePanel.add(row2);
+        middlePanel.add(Box.createVerticalGlue());
+
+        main.add(middlePanel, BorderLayout.CENTER);
+
+        JPanel bottomPanel = new JPanel(new GridLayout(showReplace ? 2 : 1, 0));
+        if (showReplace) {
+            JPanel replacePanel = new JPanel();
+            replacePanel.setLayout(new BoxLayout(replacePanel, BoxLayout.X_AXIS));
+
+            replacePanel.add(new JButton(actions.getFindAndReplace()));
+            replacePanel.add(new JButton(actions.getReplaceAll()));
+            bottomPanel.add(replacePanel);
+        }
+
+        JPanel findPanel = new JPanel();
+        findPanel.setBorder(BorderFactory.createEtchedBorder());
+        findPanel.setLayout(new BoxLayout(findPanel, BoxLayout.X_AXIS));
+        findPanel.add(new JButton(actions.getClose()));
+        findPanel.add(Box.createHorizontalGlue());
+
+        findPanel.add(new JButton(actions.getFindAll()));
+        findPanel.add(new JButton(actions.getUnselectAll()));
+        findPanel.add(new JButton(actions.getFindFromFile()));
+
+        findPanel.add(Box.createHorizontalGlue());
+        findPanel.add(new JButton(actions.getFindFirst()));
+
+        JButton nextButton = new JButton(actions.getFindNext());
+        findPanel.add(nextButton);
+        rootPane.setDefaultButton(nextButton);
+        bottomPanel.add(findPanel);
+
+        main.add(bottomPanel, BorderLayout.SOUTH);
+
+        main.validate();
+    }
+
+    /**
+     * update the targets cbox
+     */
+    public void updateTargets() {
+        parent2active.clear();
+
+        targetCBox.removeAllItems();
+
+        for (int i = 0; i < searchManager.targets.length; i++) {
+            ISearcher searcher = searchManager.targets[i];
+            targetCBox.addItem(new SearcherItem(searcher));
+            if (searcher.getParent() != null && parent2active.get(searcher.getParent()) == null)
+                parent2active.put(searcher.getParent(), searcher);
+        }
+
+        targetCBox.addItemListener(new ItemListener() {
+            public void itemStateChanged(ItemEvent event) {
+                if (event.getStateChange() == ItemEvent.SELECTED) {
+                    ISearcher searcher = ((SearcherItem) event.getItem()).getSearcher();
+                    searchManager.setSearcher(searcher);
+                    if (searcher.getParent() != null)
+                        parent2active.put(searcher.getParent(), searcher);
+                }
+            }
+        });
+    }
+
+    /**
+     * update the target selection
+     *
+     * @param name named search target
+     */
+    public boolean selectTarget(String name) {
+        for (int i = 0; i < searchManager.targets.length; i++) {
+            SearcherItem item = (SearcherItem) targetCBox.getItemAt(i);
+            if (item.toString().equals(name)) {
+                targetCBox.setSelectedIndex(i);
+                return true;
+            }
+        }
+        for (int i = 0; i < searchManager.targets.length; i++) {
+            SearcherItem item = (SearcherItem) targetCBox.getItemAt(i);
+            if (item.toString().equalsIgnoreCase(name)) {
+                targetCBox.setSelectedIndex(i);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * when activating a window, call this method to revert to the last used searcher for this window
+     *
+     * @param parent
+     */
+    public void chooseTargetForFrame(Component parent) {
+        if (parent != null) {
+            ISearcher searcher = parent2active.get(parent);
+            if (searcher != null)
+                selectTarget(searcher.getName());
+        }
+    }
+
+    class SearcherItem extends JButton {
+        final ISearcher searcher;
+
+        SearcherItem(ISearcher searcher) {
+            setText(searcher.getName());
+            this.searcher = searcher;
+        }
+
+        public String toString() {
+            return searcher.getName();
+        }
+
+        public ISearcher getSearcher() {
+            return searcher;
+        }
+    }
+
+    public SearchActions getActions() {
+        return actions;
+    }
+
+    /**
+     * sets the message
+     *
+     * @param message
+     */
+    public void setMessage(String message) {
+        messageLabel.setText(message);
+    }
+
+    /**
+     * clears the message
+     */
+    public void clearMessage() {
+        messageLabel.setText("");
+    }
+
+    public String getFindText() {
+        return findCBox.getCurrentText(true);
+    }
+
+    public String getReplaceText() {
+        return replaceCBox.getCurrentText(true);
+    }
+
+
+}
diff --git a/src/jloda/gui/find/IFindDialog.java b/src/jloda/gui/find/IFindDialog.java
new file mode 100644
index 0000000..1b988da
--- /dev/null
+++ b/src/jloda/gui/find/IFindDialog.java
@@ -0,0 +1,47 @@
+/**
+ * IFindDialog.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * command interface for find dialog and find toolbar
+ * Daniel Huson, 2.2012
+ */
+public interface IFindDialog {
+    JFrame getFrame();
+
+    boolean selectTarget(String name);
+
+    void chooseTargetForFrame(Component parent);
+
+    SearchActions getActions();
+
+    void setMessage(String message);
+
+    void clearMessage();
+
+    void updateTargets();
+
+    String getFindText();
+
+    String getReplaceText();
+}
diff --git a/src/jloda/gui/find/IObjectSearcher.java b/src/jloda/gui/find/IObjectSearcher.java
new file mode 100644
index 0000000..e9d8936
--- /dev/null
+++ b/src/jloda/gui/find/IObjectSearcher.java
@@ -0,0 +1,98 @@
+/**
+ * IObjectSearcher.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+/**
+ * implement this interface to support the Find and Find-Replace dialogs
+ * Daniel Huson, 7.2008
+ */
+public interface IObjectSearcher extends ISearcher {
+
+    /**
+     * goto the first object
+     *
+     * @return true, if successful
+     */
+    boolean gotoFirst();
+
+    /**
+     * goto the next object
+     *
+     * @return true, if successful
+     */
+    boolean gotoNext();
+
+    /**
+     * goto the last object
+     *
+     * @return true, if successful
+     */
+    boolean gotoLast();
+
+    /**
+     * goto the previous object
+     *
+     * @return true, if successful
+     */
+    boolean gotoPrevious();
+
+    /**
+     * is the current object set?
+     *
+     * @return true, if set
+     */
+    boolean isCurrentSet();
+
+    /**
+     * is the current object selected?
+     *
+     * @return true, if selected
+     */
+    boolean isCurrentSelected();
+
+    /**
+     * set selection state of current object
+     *
+     * @param select
+     */
+    void setCurrentSelected(boolean select);
+
+    /**
+     * get the label of the current object
+     *
+     * @return label
+     */
+    String getCurrentLabel();
+
+    /**
+     * set the label of the current object
+     *
+     * @param newLabel
+     */
+    void setCurrentLabel(String newLabel);
+
+    /**
+     * how many objects are there?
+     *
+     * @return number of objects or -1
+     */
+    int numberOfObjects();
+}
+
diff --git a/src/jloda/gui/find/ISearcher.java b/src/jloda/gui/find/ISearcher.java
new file mode 100644
index 0000000..b909431
--- /dev/null
+++ b/src/jloda/gui/find/ISearcher.java
@@ -0,0 +1,83 @@
+/**
+ * ISearcher.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Collection;
+
+/**
+ * Base interface for searchers
+ * Daniel Huson, 7.2008
+ */
+public interface ISearcher {
+    /**
+     * get the name for this type of search
+     *
+     * @return name
+     */
+    String getName();
+
+    /**
+     * is a global find possible?
+     *
+     * @return true, if there is at least one object
+     */
+    boolean isGlobalFindable();
+
+    /**
+     * is a selection find possible
+     *
+     * @return true, if at least one object is selected
+     */
+    boolean isSelectionFindable();
+
+    /**
+     * something has been changed or selected, update view
+     */
+    void updateView();
+
+    /**
+     * does this searcher support find all?
+     *
+     * @return true, if find all supported
+     */
+    boolean canFindAll();
+
+    /**
+     * set select state of all objects
+     *
+     * @param select
+     */
+    void selectAll(boolean select);
+
+    /**
+     * get the parent component
+     *
+     * @return parent
+     */
+    Component getParent();
+
+    /**
+     * get list of additional buttons to be embedded into find tool bar, or null
+     */
+    Collection<AbstractButton> getAdditionalButtons();
+
+}
diff --git a/src/jloda/gui/find/ITextSearcher.java b/src/jloda/gui/find/ITextSearcher.java
new file mode 100644
index 0000000..d4805e7
--- /dev/null
+++ b/src/jloda/gui/find/ITextSearcher.java
@@ -0,0 +1,90 @@
+/**
+ * ITextSearcher.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+/**
+ * Interface for text-based searcher
+ * Daniel Huson, 7.2008
+ */
+public interface ITextSearcher extends ISearcher {
+    /**
+     * Find first instance
+     *
+     * @param regularExpression
+     * @return - returns boolean: true if text found, false otherwise
+     */
+    boolean findFirst(String regularExpression);
+
+    /**
+     * Find next instance
+     *
+     * @param regularExpression
+     * @return - returns boolean: true if text found, false otherwise
+     */
+    boolean findNext(String regularExpression);
+
+
+    /**
+     * Find previous instance
+     *
+     * @param regularExpression
+     * @return - returns boolean: true if text found, false otherwise
+     */
+    boolean findPrevious(String regularExpression);
+
+    /**
+     * Replace the next instance with current. Does nothing if selection invalid.
+     *
+     * @param regularExpression
+     */
+    boolean replaceNext(String regularExpression, String replaceText);
+
+
+    /**
+     * Replace all occurrences of text in document, subject to options.
+     *
+     * @param regularExpression
+     * @param replaceText
+     * @param selectionOnly
+     * @return number of instances replaced
+     */
+    int replaceAll(String regularExpression, String replaceText, boolean selectionOnly);
+
+    /**
+     * Selects all occurrences of text in document, subject to options and constraints of document type
+     *
+     * @param regularExpression
+     */
+    int findAll(String regularExpression);
+
+    /**
+     * set scope global rather than selected
+     *
+     * @param globalScope
+     */
+    void setGlobalScope(boolean globalScope);
+
+    /**
+     * get scope global rather than selected
+     *
+     * @return true, if search scope is global
+     */
+    boolean isGlobalScope();
+}
diff --git a/src/jloda/gui/find/JListSearcher.java b/src/jloda/gui/find/JListSearcher.java
new file mode 100644
index 0000000..209619e
--- /dev/null
+++ b/src/jloda/gui/find/JListSearcher.java
@@ -0,0 +1,271 @@
+/**
+ * JListSearcher.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * JList searcher
+ * Daniel Huson, 7.2012
+ */
+
+/**
+ * Class for finding labels in a JList
+ * Daniel Huson, 2.2012
+ */
+public class JListSearcher implements IObjectSearcher {
+    private final String name;
+    final JList jList;
+    final Frame frame;
+    protected int current = -1;
+
+    final Set<Integer> toSelect;
+    final Set<Integer> toDeselect;
+    public static final String SEARCHER_NAME = "JList";
+
+    /**
+     * constructor
+     *
+     * @param jList
+     */
+    public JListSearcher(JList jList) {
+        this(null, SEARCHER_NAME, jList);
+    }
+
+    /**
+     * constructor
+     *
+     * @param frame
+     * @param jList
+     */
+    public JListSearcher(Frame frame, JList jList) {
+        this(frame, SEARCHER_NAME, jList);
+    }
+
+    /**
+     * constructor
+     *
+     * @param
+     * @param jList
+     */
+    public JListSearcher(Frame frame, String name, JList jList) {
+        this.frame = frame;
+        this.name = name;
+        this.jList = jList;
+        toSelect = new HashSet<>();
+        toDeselect = new HashSet<>();
+    }
+
+    /**
+     * get the parent component
+     *
+     * @return parent
+     */
+    public Component getParent() {
+        return frame;
+    }
+
+    /**
+     * get the name for this type of search
+     *
+     * @return name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * goto the first object
+     */
+    public boolean gotoFirst() {
+        current = 0;
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the next object
+     */
+    public boolean gotoNext() {
+        if (isCurrentSet())
+            current++;
+        else
+            gotoFirst();
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the last object
+     */
+    public boolean gotoLast() {
+        current = jList.getComponentCount() - 1;
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the previous object
+     */
+    public boolean gotoPrevious() {
+        if (isCurrentSet())
+            current--;
+        else
+            gotoLast();
+        return isCurrentSet();
+    }
+
+    /**
+     * is the current object selected?
+     *
+     * @return true, if selected
+     */
+    public boolean isCurrentSelected() {
+        if (isCurrentSet()) {
+            int[] selected = jList.getSelectedIndices();
+            for (int aSelected : selected)
+                if (aSelected == current)
+                    return true;
+        }
+        return false;
+    }
+
+    /**
+     * set selection state of current object
+     *
+     * @param select
+     */
+    public void setCurrentSelected(boolean select) {
+        if (select)
+            toSelect.add(current);
+        else
+            toDeselect.add(current);
+    }
+
+    /**
+     * set select state of all objects
+     *
+     * @param select
+     */
+    public void selectAll(boolean select) {
+        if (select) {
+            jList.setSelectionInterval(0, jList.getComponentCount());
+        } else {
+            jList.clearSelection();
+        }
+    }
+
+    /**
+     * get the label of the current object
+     *
+     * @return label
+     */
+    public String getCurrentLabel() {
+        if (!isCurrentSet())
+            return null;
+        else
+            return jList.getModel().getElementAt(current).toString();
+    }
+
+    /**
+     * set the label of the current object
+     *
+     * @param newLabel
+     */
+    public void setCurrentLabel(String newLabel) {
+    }
+
+    /**
+     * is a global find possible?
+     *
+     * @return true, if there is at least one object
+     */
+    public boolean isGlobalFindable() {
+        return jList.getComponentCount() > 0;
+    }
+
+    /**
+     * is a selection find possible
+     *
+     * @return true, if at least one object is selected
+     */
+    public boolean isSelectionFindable() {
+        return false;
+    }
+
+    /**
+     * is the current object set?
+     *
+     * @return true, if set
+     */
+    public boolean isCurrentSet() {
+        return current >= 0 && current < jList.getModel().getSize();
+    }
+
+    /**
+     * something has been changed or selected, update view
+     */
+    public void updateView() {
+        selectAll(false);
+
+        int[] alreadySelected = jList.getSelectedIndices();
+        for (int i : alreadySelected) {
+            toSelect.add(i);
+        }
+        toSelect.removeAll(toDeselect);
+
+        int[] indices = new int[toSelect.size()];
+        int count = 0;
+        for (Integer i : toSelect) {
+            indices[count++] = i;
+        }
+        jList.setSelectedIndices(indices);
+
+        if (isCurrentSet())
+            jList.ensureIndexIsVisible(jList.getSelectedIndex());
+
+        toSelect.clear();
+        toDeselect.clear();
+    }
+
+    /**
+     * does this searcher support find all?
+     *
+     * @return true, if find all supported
+     */
+    public boolean canFindAll() {
+        return true;
+    }
+
+    /**
+     * how many objects are there?
+     *
+     * @return number of objects or -1
+     */
+    public int numberOfObjects() {
+        return jList.getModel().getSize();
+    }
+
+    @Override
+    public Collection<AbstractButton> getAdditionalButtons() {
+        return null;
+    }
+}
diff --git a/src/jloda/gui/find/JTableSearcher.java b/src/jloda/gui/find/JTableSearcher.java
new file mode 100644
index 0000000..e3736f5
--- /dev/null
+++ b/src/jloda/gui/find/JTableSearcher.java
@@ -0,0 +1,325 @@
+/**
+ * JTableSearcher.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import jloda.util.Pair;
+
+import javax.swing.*;
+import javax.swing.table.TableColumnModel;
+import java.awt.*;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * JTable searcher
+ * Daniel Huson, 9.2012
+ */
+
+/**
+ * Class for finding labels in a JTable
+ * Daniel Huson, 2.2012
+ */
+public class JTableSearcher implements IObjectSearcher {
+    private final String name;
+    final JTable table;
+    final Frame frame;
+    protected final Pair<Integer, Integer> current = new Pair<>(-1, -1);
+
+    final Set<Pair<Integer, Integer>> toSelect;
+    final Set<Pair<Integer, Integer>> toDeselect;
+    public static final String SEARCHER_NAME = "JTable";
+
+    /**
+     * constructor
+     *
+     * @param table
+     */
+    public JTableSearcher(JTable table) {
+        this(null, SEARCHER_NAME, table);
+    }
+
+    /**
+     * constructor
+     *
+     * @param frame
+     * @param table
+     */
+    public JTableSearcher(Frame frame, JTable table) {
+        this(frame, SEARCHER_NAME, table);
+    }
+
+    /**
+     * constructor
+     *
+     * @param
+     * @param table
+     */
+    public JTableSearcher(Frame frame, String name, JTable table) {
+        this.frame = frame;
+        this.name = name;
+        this.table = table;
+        toSelect = new HashSet<>();
+        toDeselect = new HashSet<>();
+    }
+
+    /**
+     * get the parent component
+     *
+     * @return parent
+     */
+    public Component getParent() {
+        return frame;
+    }
+
+    /**
+     * get the name for this type of search
+     *
+     * @return name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * goto the first object
+     */
+    public boolean gotoFirst() {
+        current.set1(0);
+        current.set2(0);
+        boolean tried; //  did we try a cell? If yes and the column has a non zero width, then we use it
+        do {
+            tried = false;
+            if (current.get2() < table.getModel().getColumnCount() - 1) {
+                current.set2(current.get2() + 1);
+                tried = true;
+            } else if (current.get1() < table.getModel().getRowCount() - 1) {
+                current.set1(current.get1() + 1);
+                current.set2(0);
+                tried = true;
+            }
+            if (tried) {
+                TableColumnModel model = table.getColumnModel();
+                if (model.getColumn(current.get2()).getMaxWidth() > 0)
+                    break;
+            }
+        }
+        while (tried);
+
+        if (!tried) {
+            current.set1(0);
+            current.set2(0);
+        }
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the next object
+     */
+    public boolean gotoNext() {
+        if (isCurrentSet()) {
+            boolean tried; //  did we try a cell? If yes and the column has a non zero width, then we use it
+            do {
+                tried = false;
+                if (current.get2() < table.getModel().getColumnCount() - 1) {
+                    current.set2(current.get2() + 1);
+                    tried = true;
+                } else if (current.get1() < table.getModel().getRowCount() - 1) {
+                    current.set1(current.get1() + 1);
+                    current.set2(0);
+                    tried = true;
+                }
+                if (tried) {
+                    TableColumnModel model = table.getColumnModel();
+                    if (model.getColumn(current.get2()).getMaxWidth() > 0)
+                        break;
+                }
+            }
+            while (tried);
+
+            if (!tried) {
+                current.set1(-1);
+                current.set2(-1);
+            }
+        } else
+            gotoFirst();
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the last object
+     */
+    public boolean gotoLast() {
+        current.set1(table.getModel().getRowCount() - 1);
+        current.set2(table.getModel().getColumnCount() - 1);
+
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the previous object
+     */
+    public boolean gotoPrevious() {
+        if (isCurrentSet()) {
+            if (current.get2() > 0)
+                current.set2(current.get2() - 1);
+            else if (current.get1() > 0) {
+                current.set1(current.get1() - 1);
+                current.set2(table.getModel().getColumnCount() - 1);
+            } else {
+                current.set1(-1);
+                current.set2(-1);
+            }
+        } else
+            gotoLast();
+        return isCurrentSet();
+    }
+
+    /**
+     * is the current object selected?
+     *
+     * @return true, if selected
+     */
+    public boolean isCurrentSelected() {
+        return isCurrentSet() && table.isCellSelected(current.get1(), table.getSelectedColumn());
+    }
+
+    /**
+     * set selection state of current object
+     *
+     * @param select
+     */
+    public void setCurrentSelected(boolean select) {
+        if (select)
+            toSelect.add(new Pair<>(current.get1(), current.get2()));
+        else
+            toDeselect.add(new Pair<>(current.get1(), current.get2()));
+    }
+
+    /**
+     * set select state of all objects
+     *
+     * @param select
+     */
+    public void selectAll(boolean select) {
+        if (select) {
+            table.selectAll();
+        } else {
+            table.clearSelection();
+        }
+    }
+
+    /**
+     * get the label of the current object
+     *
+     * @return label
+     */
+    public String getCurrentLabel() {
+        if (!isCurrentSet())
+            return null;
+        else
+            return table.getModel().getValueAt(current.get1(), current.get2()).toString();
+    }
+
+    /**
+     * set the label of the current object
+     *
+     * @param newLabel
+     */
+    public void setCurrentLabel(String newLabel) {
+    }
+
+    /**
+     * is a global find possible?
+     *
+     * @return true, if there is at least one object
+     */
+    public boolean isGlobalFindable() {
+        return table.getComponentCount() > 0;
+    }
+
+    /**
+     * is a selection find possible
+     *
+     * @return true, if at least one object is selected
+     */
+    public boolean isSelectionFindable() {
+        return false;
+    }
+
+    /**
+     * is the current object set?
+     *
+     * @return true, if set
+     */
+    public boolean isCurrentSet() {
+        return current.get1() >= 0 && current.get1() < table.getModel().getRowCount() && current.get2() >= 0 && current.get2() < table.getModel().getColumnCount();
+    }
+
+    /**
+     * something has been changed or selected, update view
+     */
+    public void updateView() {
+
+        for (Pair<Integer, Integer> pair : toDeselect) {
+            if (table.isCellSelected(pair.get1(), pair.get2()))
+                table.changeSelection(pair.get1(), pair.get2(), true, false);
+        }
+
+        for (Pair<Integer, Integer> pair : toSelect) {
+            if (!table.isCellSelected(pair.get1(), pair.get2())) {
+                table.changeSelection(pair.get1(), pair.get2(), true, false);
+            }
+        }
+
+        /*
+        if (isCurrentSet()) {
+            Rectangle rect = table.getCellRect(current.get1(), current.get2(), true);
+            table.scrollRectToVisible(rect);
+        }
+        */
+
+        toSelect.clear();
+        toDeselect.clear();
+    }
+
+    /**
+     * does this searcher support find all?
+     *
+     * @return true, if find all supported
+     */
+    public boolean canFindAll() {
+        return true;
+    }
+
+    /**
+     * how many objects are there?
+     *
+     * @return number of objects or -1
+     */
+    public int numberOfObjects() {
+        return table.getModel().getRowCount() * table.getModel().getColumnCount();
+    }
+
+    @Override
+    public Collection<AbstractButton> getAdditionalButtons() {
+        return null;
+    }
+}
diff --git a/src/jloda/gui/find/JTreeSearcher.java b/src/jloda/gui/find/JTreeSearcher.java
new file mode 100644
index 0000000..e959c13
--- /dev/null
+++ b/src/jloda/gui/find/JTreeSearcher.java
@@ -0,0 +1,295 @@
+/**
+ * JTreeSearcher.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import jloda.util.Basic;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.TreeNode;
+import javax.swing.tree.TreePath;
+import java.awt.*;
+import java.util.*;
+
+/**
+ * jTree searcher
+ * Daniel Huson, 2.2012
+ */
+
+/**
+ * Class for finding labels in a jTree
+ * Daniel Huson, 2.2012
+ */
+public class JTreeSearcher implements IObjectSearcher {
+    private final String name;
+    final JTree jTree;
+    final Frame frame;
+    protected DefaultMutableTreeNode current = null;
+
+    final Set<DefaultMutableTreeNode> toSelect;
+    final Set<DefaultMutableTreeNode> toDeselect;
+    public static final String SEARCHER_NAME = "Tree";
+
+    /**
+     * constructor
+     *
+     * @param jTree
+     */
+    public JTreeSearcher(JTree jTree) {
+        this(null, SEARCHER_NAME, jTree);
+    }
+
+    /**
+     * constructor
+     *
+     * @param frame
+     * @param jTree
+     */
+    public JTreeSearcher(Frame frame, JTree jTree) {
+        this(frame, SEARCHER_NAME, jTree);
+    }
+
+    /**
+     * constructor
+     *
+     * @param
+     * @param jTree
+     */
+    public JTreeSearcher(Frame frame, String name, JTree jTree) {
+        this.frame = frame;
+        this.name = name;
+        this.jTree = jTree;
+        toSelect = new HashSet<>();
+        toDeselect = new HashSet<>();
+    }
+
+    /**
+     * get the parent component
+     *
+     * @return parent
+     */
+    public Component getParent() {
+        return frame;
+    }
+
+    /**
+     * get the name for this type of search
+     *
+     * @return name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * goto the first object
+     */
+    public boolean gotoFirst() {
+        current = (DefaultMutableTreeNode) jTree.getModel().getRoot();
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the next object
+     */
+    public boolean gotoNext() {
+        if (current == null)
+            gotoFirst();
+        else
+            current = current.getNextNode();
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the last object
+     */
+    public boolean gotoLast() {
+        current = (DefaultMutableTreeNode) ((DefaultMutableTreeNode) jTree.getModel().getRoot()).getLastChild();
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the previous object
+     */
+    public boolean gotoPrevious() {
+        if (current == null)
+            gotoLast();
+        else
+            current = current.getPreviousNode();
+        return isCurrentSet();
+    }
+
+    /**
+     * is the current object selected?
+     *
+     * @return true, if selected
+     */
+    public boolean isCurrentSelected() {
+        return isCurrentSet() && jTree.getSelectionModel().isPathSelected(getPath(current));
+    }
+
+    private TreePath getPath(TreeNode node) {
+        java.util.List<TreeNode> list = new ArrayList<>();
+
+        // Add all nodes to list
+        while (node != null) {
+            list.add(node);
+            node = node.getParent();
+        }
+        Collections.reverse(list);
+
+        // Convert array of nodes to TreePath
+        return new TreePath(list.toArray());
+    }
+
+    /**
+     * set selection state of current object
+     *
+     * @param select
+     */
+    public void setCurrentSelected(boolean select) {
+        if (current != null) {
+            if (select)
+                toSelect.add(current);
+            else
+                toDeselect.add(current);
+        }
+    }
+
+    /**
+     * set select state of all objects
+     *
+     * @param select
+     */
+    public void selectAll(boolean select) {
+        if (select) {
+            int count = jTree.getRowCount();
+            TreePath[] paths = new TreePath[count];
+            for (int i = 0; i < count; i++) {
+                paths[i] = jTree.getPathForRow(i);
+            }
+            jTree.addSelectionPaths(paths);
+        } else {
+            jTree.clearSelection();
+        }
+    }
+
+    /**
+     * get the label of the current object
+     *
+     * @return label
+     */
+    public String getCurrentLabel() {
+        if (current == null)
+            return null;
+        else
+            return current.toString();
+    }
+
+    /**
+     * set the label of the current object
+     *
+     * @param newLabel
+     */
+    public void setCurrentLabel(String newLabel) {
+    }
+
+    /**
+     * is a global find possible?
+     *
+     * @return true, if there is at least one object
+     */
+    public boolean isGlobalFindable() {
+        return jTree.getComponentCount() > 0;
+    }
+
+    /**
+     * is a selection find possible
+     *
+     * @return true, if at least one object is selected
+     */
+    public boolean isSelectionFindable() {
+        return false;
+    }
+
+    /**
+     * is the current object set?
+     *
+     * @return true, if set
+     */
+    public boolean isCurrentSet() {
+        return current != null;
+    }
+
+    /**
+     * something has been changed or selected, update view
+     */
+    public void updateView() {
+        // selectAll(false);
+        toSelect.removeAll(toDeselect);
+
+        TreePath[] paths = new TreePath[toSelect.size()];
+        int count = 0;
+        for (TreeNode node : toSelect) {
+            paths[count++] = getPath(node);
+        }
+        jTree.addSelectionPaths(paths);
+
+        if (current != null) {
+            final TreePath path = getPath(current);
+            try {
+                SwingUtilities.invokeAndWait(new Runnable() {
+                    public void run() {
+                        jTree.expandPath(path);  // this just doesn't work....
+                        jTree.scrollPathToVisible(path);
+                    }
+                });
+            } catch (Exception e) {
+                Basic.caught(e);
+            }
+        }
+
+        toSelect.clear();
+        toDeselect.clear();
+    }
+
+    /**
+     * does this searcher support find all?
+     *
+     * @return true, if find all supported
+     */
+    public boolean canFindAll() {
+        return true;
+    }
+
+    /**
+     * how many objects are there?
+     *
+     * @return number of objects or -1
+     */
+    public int numberOfObjects() {
+        return jTree.getComponentCount();
+    }
+
+    @Override
+    public Collection<AbstractButton> getAdditionalButtons() {
+        return null;
+    }
+}
diff --git a/src/jloda/gui/find/NodeLabelSearcher.java b/src/jloda/gui/find/NodeLabelSearcher.java
new file mode 100644
index 0000000..c34ccb8
--- /dev/null
+++ b/src/jloda/gui/find/NodeLabelSearcher.java
@@ -0,0 +1,327 @@
+/**
+ * NodeLabelSearcher.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import jloda.graph.Graph;
+import jloda.graph.Node;
+import jloda.graph.NodeSet;
+import jloda.graphview.GraphView;
+import jloda.phylo.PhyloTree;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.Objects;
+
+/**
+ * Class for finding and replacing node labels in a graph
+ * Daniel Huson, 7.2008
+ */
+public class NodeLabelSearcher implements IObjectSearcher {
+    private final String name;
+    final Graph graph;
+    final GraphView viewer;
+    final Frame frame;
+    protected Node current = null;
+
+    final NodeSet toSelect;
+    final NodeSet toDeselect;
+    public static final String SEARCHER_NAME = "Nodes";
+
+    /**
+     * constructor
+     *
+     * @param viewer
+     */
+    public NodeLabelSearcher(GraphView viewer) {
+        this(null, SEARCHER_NAME, viewer);
+    }
+
+    /**
+     * constructor
+     *
+     * @param frame
+     * @param viewer
+     */
+    public NodeLabelSearcher(Frame frame, GraphView viewer) {
+        this(frame, SEARCHER_NAME, viewer);
+    }
+
+    /**
+     * constructor
+     *
+     * @param
+     * @param viewer
+     */
+    public NodeLabelSearcher(Frame frame, String name, GraphView viewer) {
+        this.frame = frame;
+        this.name = name;
+        this.viewer = viewer;
+        this.graph = viewer.getGraph();
+        toSelect = new NodeSet(graph);
+        toDeselect = new NodeSet(graph);
+    }
+
+    /**
+     * get the parent component
+     *
+     * @return parent
+     */
+    public Component getParent() {
+        return frame;
+    }
+
+    /**
+     * get the name for this type of search
+     *
+     * @return name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * goto the first object
+     */
+    public boolean gotoFirst() {
+        current = graph.getFirstNode();
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the next object
+     */
+    public boolean gotoNext() {
+        if (current == null || current.getOwner() == null)
+            gotoFirst();
+        else
+            current = current.getNext();
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the last object
+     */
+    public boolean gotoLast() {
+        current = graph.getLastNode();
+        return isCurrentSet();
+    }
+
+    /**
+     * goto the previous object
+     */
+    public boolean gotoPrevious() {
+        if (current == null)
+            gotoLast();
+        else
+            current = current.getPrev();
+        return isCurrentSet();
+    }
+
+    /**
+     * is the current object selected?
+     *
+     * @return true, if selected
+     */
+    public boolean isCurrentSelected() {
+        return isCurrentSet() && viewer.getSelected(current);
+    }
+
+    /**
+     * set selection state of current object
+     *
+     * @param select
+     */
+    public void setCurrentSelected(boolean select) {
+        if (current != null) {
+            if (select)
+                toSelect.add(current);
+            else
+                toDeselect.add(current);
+        }
+        if (select)
+            viewer.setFoundNode(current);
+        else
+            viewer.setFoundNode(null);
+    }
+
+    /**
+     * set select state of all objects
+     *
+     * @param select
+     */
+    public void selectAll(boolean select) {
+        viewer.selectAllNodes(select);
+        viewer.repaint();
+    }
+
+    /**
+     * get the label of the current object
+     *
+     * @return label
+     */
+    public String getCurrentLabel() {
+        if (current == null)
+            return null;
+        else
+            return viewer.getLabel(current);
+    }
+
+    /**
+     * set the label of the current object
+     *
+     * @param newLabel
+     */
+    public void setCurrentLabel(String newLabel) {
+        if (current != null && !Objects.equals(newLabel, viewer.getLabel(current))) {
+            if (newLabel == null || newLabel.length() == 0) {
+                viewer.setLabel(current, null);
+                if (viewer.getGraph() instanceof PhyloTree) {
+                    ((PhyloTree) viewer.getGraph()).setLabel(current, null);
+                }
+            } else {
+                viewer.setLabel(current, newLabel);
+                if (viewer.getGraph() instanceof PhyloTree) {
+                    ((PhyloTree) viewer.getGraph()).setLabel(current, newLabel);
+                }
+
+            }
+            fireLabelChangedListeners(current);
+        }
+    }
+
+    /**
+     * is a global find possible?
+     *
+     * @return true, if there is at least one object
+     */
+    public boolean isGlobalFindable() {
+        return graph.getNumberOfNodes() > 0;
+    }
+
+    /**
+     * is a selection find possible
+     *
+     * @return true, if at least one object is selected
+     */
+    public boolean isSelectionFindable() {
+        return viewer.getSelectedNodes().size() > 0;
+    }
+
+    /**
+     * is the current object set?
+     *
+     * @return true, if set
+     */
+    public boolean isCurrentSet() {
+        return current != null;
+    }
+
+    /**
+     * something has been changed or selected, update view
+     */
+    public void updateView() {
+        viewer.selectedNodes.addAll(toSelect);
+        viewer.fireDoSelect(toSelect);
+        Node v = toSelect.getLastElement();
+        if (v != null) {
+            final Point p = viewer.trans.w2d(viewer.getLocation(v));
+            viewer.scrollRectToVisible(new Rectangle(p.x - 60, p.y - 25, 120, 50));
+
+        }
+        viewer.selectedNodes.removeAll(toDeselect);
+        viewer.fireDoDeselect(toDeselect);
+        toSelect.clear();
+        toDeselect.clear();
+
+        viewer.repaint();
+    }
+
+    /**
+     * does this searcher support find all?
+     *
+     * @return true, if find all supported
+     */
+    public boolean canFindAll() {
+        return true;
+    }
+
+    private final java.util.List<LabelChangedListener> labelChangedListeners = new LinkedList<>();
+
+    /**
+     * fire the label changed listener
+     *
+     * @param v
+     */
+    private void fireLabelChangedListeners(Node v) {
+        for (LabelChangedListener listener : labelChangedListeners) {
+            listener.doLabelHasChanged(v);
+        }
+    }
+
+    /**
+     * add a label changed listener
+     *
+     * @param listener
+     */
+    public void addLabelChangedListener(LabelChangedListener listener) {
+        labelChangedListeners.add(listener);
+    }
+
+    /**
+     * remove a label changed listener
+     *
+     * @param listener
+     */
+    public void removeLabelChangedListener(LabelChangedListener listener) {
+        labelChangedListeners.remove(listener);
+    }
+
+    /**
+     * label changed listener
+     */
+    public interface LabelChangedListener {
+        void doLabelHasChanged(Node v);
+    }
+
+    /**
+     * how many objects are there?
+     *
+     * @return number of objects or -1
+     */
+    public int numberOfObjects() {
+        return graph.getNumberOfNodes();
+    }
+
+    /**
+     * how many selected objects are there?
+     *
+     * @return number of objects or -1
+     */
+    public int numberOfSelectedObjects() {
+        return viewer.getSelectedNodes().size();
+    }
+
+    @Override
+    public Collection<AbstractButton> getAdditionalButtons() {
+        return null;
+    }
+}
diff --git a/src/jloda/gui/find/SearchActions.java b/src/jloda/gui/find/SearchActions.java
new file mode 100644
index 0000000..811cef3
--- /dev/null
+++ b/src/jloda/gui/find/SearchActions.java
@@ -0,0 +1,445 @@
+/**
+ * SearchActions.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import jloda.gui.ChooseFileDialog;
+import jloda.util.*;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * actions for find and find/replace dialogs
+ * Daniel Huson, 7.2008
+ */
+public class SearchActions {
+    final static String CBOX = "cbox";
+    final static String CRITICAL = "critical";
+
+    final static String RADIOBUTTON = "rb";
+
+
+    private final SearchManager searchManager;
+    private final List<AbstractAction> all;
+
+    /**
+     * constructor
+     *
+     * @param searchManager
+     */
+    SearchActions(SearchManager searchManager) {
+        this.searchManager = searchManager;
+        this.all = new LinkedList<>();
+    }
+
+    /**
+     * enable and disable critical actions
+     *
+     * @param enable
+     */
+    public void setEnableCritical(boolean enable) {
+        for (AbstractAction action : all) {
+            action.setEnabled(enable);
+        }
+        if (enable)
+            updateEnableState();
+    }
+
+    /**
+     * update the enable state
+     */
+    public void updateEnableState() {
+
+        if (searchManager.getSearcher() instanceof EmptySearcher) {
+            for (AbstractAction action : all)
+                action.setEnabled(false);
+            return;
+        }
+
+        if (caseSensitiveOption != null)
+            ((JCheckBox) caseSensitiveOption.getValue(CBOX)).setSelected(searchManager.isCaseSensitiveOption());
+        if (wholeWordsOption != null)
+            ((JCheckBox) wholeWordsOption.getValue(CBOX)).setSelected(searchManager.isWholeWordsOnlyOption());
+        if (regularExpressionOption != null)
+            ((JCheckBox) regularExpressionOption.getValue(CBOX)).setSelected(searchManager.isRegularExpressionsOption());
+        if (forwardDirection != null)
+            ((JRadioButton) forwardDirection.getValue(RADIOBUTTON)).setSelected(searchManager.isForwardDirection());
+        if (backwardDirection != null)
+            ((JRadioButton) backwardDirection.getValue(RADIOBUTTON)).setSelected(!searchManager.isForwardDirection());
+        if (selectionScope != null) {
+            ((JRadioButton) selectionScope.getValue(RADIOBUTTON)).setEnabled(searchManager.getSearcher().isSelectionFindable());
+            if (!searchManager.getSearcher().isSelectionFindable())
+                searchManager.setGlobalScope(true);
+        }
+        if (globalScope != null)
+            ((JRadioButton) globalScope.getValue(RADIOBUTTON)).setSelected(searchManager.isGlobalScope());
+        if (selectionScope != null)
+            ((JRadioButton) selectionScope.getValue(RADIOBUTTON)).setSelected(!searchManager.isGlobalScope());
+
+        if (findAll != null)
+            findAll.setEnabled(searchManager.getSearcher().canFindAll());   // can find all in text
+
+        if (findFromFile != null)
+            findFromFile.setEnabled(searchManager.getSearcher().canFindAll());   // can find all in text
+
+        if (findAndReplace != null)
+            findAndReplace.setEnabled(searchManager.isAllowReplace());
+        if (replaceAll != null)
+            replaceAll.setEnabled(searchManager.isAllowReplace());
+        if (findAndReplace != null)
+            findAndReplace.setEnabled(searchManager.isAllowReplace());
+        if (findAndReplace != null)
+            findAndReplace.setEnabled(searchManager.isAllowReplace());
+    }
+
+    AbstractAction caseSensitiveOption;
+
+    public AbstractAction getCaseSensitiveOption(final JCheckBox cbox) {
+
+        AbstractAction action = caseSensitiveOption;
+        if (action != null) {
+            action.putValue(CBOX, cbox);
+            return action;
+        }
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                searchManager.setCaseSensitiveOption(cbox.isSelected());
+            }
+        };
+        action.putValue(CBOX, cbox);
+        action.putValue(AbstractAction.NAME, "Case sensitive");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Do not match upper and lower case letters");
+        all.add(action);
+        return caseSensitiveOption = action;
+    }
+
+    AbstractAction wholeWordsOption;
+
+    public AbstractAction getWholeWordsOption(final JCheckBox cbox) {
+        AbstractAction action = wholeWordsOption;
+        if (action != null) {
+            action.putValue(CBOX, cbox);
+            return action;
+        }
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                searchManager.setWholeWordsOnlyOption(cbox.isSelected());
+
+            }
+        };
+        action.putValue(CBOX, cbox);
+
+        action.putValue(AbstractAction.NAME, "Whole words only");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Match whole words only");
+        all.add(action);
+        return wholeWordsOption = action;
+    }
+
+    AbstractAction regularExpressionOption;
+
+    public AbstractAction getRegularExpressionOption(final JCheckBox cbox) {
+        AbstractAction action = regularExpressionOption;
+        if (action != null) {
+            action.putValue(CBOX, cbox);
+            return action;
+        }
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                searchManager.setRegularExpressionsOption(cbox.isSelected());
+
+            }
+        };
+        action.putValue(CBOX, cbox);
+
+        action.putValue(AbstractAction.NAME, "Regular expression");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Find using Java regular expression");
+
+        all.add(action);
+        return regularExpressionOption = action;
+    }
+
+    AbstractAction forwardDirection;
+
+    public AbstractAction getForwardDirection(final JRadioButton rb) {
+        AbstractAction action = forwardDirection;
+        if (action != null) {
+            action.putValue(RADIOBUTTON, rb);
+            return action;
+        }
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                searchManager.setForwardDirection(true);
+            }
+        };
+        action.putValue(RADIOBUTTON, rb);
+
+        action.putValue(AbstractAction.NAME, "Forward");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Search in forward direction");
+
+        all.add(action);
+        return forwardDirection = action;
+    }
+
+    AbstractAction backwardDirection;
+
+    public AbstractAction getBackwardDirection(final JRadioButton rb) {
+        AbstractAction action = backwardDirection;
+        if (action != null) {
+            action.putValue(RADIOBUTTON, rb);
+            return action;
+        }
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                searchManager.setForwardDirection(false);
+            }
+        };
+        action.putValue(RADIOBUTTON, rb);
+
+        action.putValue(AbstractAction.NAME, "Backward");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Search in backward direction");
+
+        all.add(action);
+        return backwardDirection = action;
+    }
+
+    AbstractAction globalScope;
+
+    public AbstractAction getGlobalScope(final JRadioButton rb) {
+        AbstractAction action = globalScope;
+        if (action != null) {
+            action.putValue(RADIOBUTTON, rb);
+            return action;
+        }
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                searchManager.setGlobalScope(true);
+            }
+        };
+        action.putValue(RADIOBUTTON, rb);
+        action.putValue(AbstractAction.NAME, "Global");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Search globally");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        all.add(action);
+        return globalScope = action;
+    }
+
+    AbstractAction selectionScope;
+
+    public AbstractAction getSelectionScope(final JRadioButton rb) {
+        AbstractAction action = selectionScope;
+        if (action != null) {
+            action.putValue(RADIOBUTTON, rb);
+            return action;
+        }
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                searchManager.setGlobalScope(false);
+            }
+        };
+        action.putValue(RADIOBUTTON, rb);
+        action.putValue(AbstractAction.NAME, "Selection");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Search only in selection");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        all.add(action);
+        return selectionScope = action;
+    }
+
+    private AbstractAction close;
+
+    public AbstractAction getClose() {
+        AbstractAction action = close;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                searchManager.getFrame().setVisible(false);
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Close");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Close the window");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("Close16.gif"));
+        all.add(action);
+        return close = action;
+    }
+
+    AbstractAction findFirst;
+
+    public AbstractAction getFindFirst() {
+        AbstractAction action = findFirst;
+        if (action != null) return action;
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                flushStrings();
+                searchManager.applyFindFirst();
+            }
+        };
+
+        action.putValue(AbstractAction.NAME, "First");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Find first occurrence");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        all.add(action);
+        return findFirst = action;
+    }
+
+    AbstractAction findNext;
+
+    public AbstractAction getFindNext() {
+        AbstractAction action = findNext;
+        if (action != null) return action;
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                flushStrings();
+                searchManager.applyFindNext();
+            }
+        };
+
+        action.putValue(AbstractAction.NAME, "Next");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Find next occurrence");
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_G,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+        action.putValue(CRITICAL, Boolean.TRUE);
+        all.add(action);
+        return findNext = action;
+    }
+
+    AbstractAction findAll;
+
+    public AbstractAction getFindAll() {
+        AbstractAction action = findAll;
+        if (action != null) return action;
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                flushStrings();
+                searchManager.applyFindAll();
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Find All");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Find all occurrences");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        all.add(action);
+        return findAll = action;
+    }
+
+
+    private AbstractAction findFromFile;
+
+    public AbstractAction getFindFromFile() {
+        AbstractAction action = findFromFile;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                File lastFile = ProgramProperties.getFile("FindFile");
+
+                File file = ChooseFileDialog.chooseFileToOpen(searchManager.findDialog.getFrame(), lastFile, new TextFileFilter(), new TextFileFilter(), event, "Open file containing search terms");
+                if (file != null) {
+                    try {
+                        searchManager.findFromFile(file);
+                    } catch (IOException e) {
+                        new Alert(searchManager.findDialog.getFrame(), "Find from file failed: " + e.getMessage());
+                        Basic.caught(e);
+                    }
+                    ProgramProperties.put("FindFile", file);
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "From File...");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Process each line of a file as a find query");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Open16.gif"));
+        all.add(action);
+        return findFromFile = action;
+    }
+
+
+    AbstractAction findAndReplace;
+
+    public AbstractAction getFindAndReplace() {
+        AbstractAction action = findAndReplace;
+        if (action != null) return action;
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                flushStrings();
+                searchManager.applyFindAndReplace();
+            }
+        };
+
+        action.putValue(AbstractAction.NAME, "Replace");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Find and replace next occurrence");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        all.add(action);
+        return findAndReplace = action;
+    }
+
+    AbstractAction replaceAll;
+
+    public AbstractAction getReplaceAll() {
+        AbstractAction action = replaceAll;
+        if (action != null) return action;
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                flushStrings();
+                searchManager.applyReplaceAll();
+            }
+        };
+
+        action.putValue(AbstractAction.NAME, "Replace All");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Replace all occurrence");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        all.add(action);
+        return replaceAll = action;
+    }
+
+    AbstractAction unselectAll;
+
+    public AbstractAction getUnselectAll() {
+        AbstractAction action = unselectAll;
+        if (action != null) return action;
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                searchManager.applyUnselectAll();
+            }
+        };
+
+        action.putValue(AbstractAction.NAME, "Unselect All");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Unselect all currently selected objects");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        all.add(action);
+        return unselectAll = action;
+    }
+
+
+    private void flushStrings() {
+        searchManager.setSearchText(searchManager.findDialog.getFindText());
+        searchManager.setReplaceText(searchManager.findDialog.getReplaceText());
+
+    }
+
+    public List<AbstractAction> getAll() {
+        return all;
+    }
+}
diff --git a/src/jloda/gui/find/SearchManager.java b/src/jloda/gui/find/SearchManager.java
new file mode 100644
index 0000000..057b774
--- /dev/null
+++ b/src/jloda/gui/find/SearchManager.java
@@ -0,0 +1,1204 @@
+/**
+ * SearchManager.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import jloda.gui.Message;
+import jloda.gui.ProgressDialog;
+import jloda.gui.commands.CommandManager;
+import jloda.gui.director.IDirectableViewer;
+import jloda.gui.director.IDirector;
+import jloda.gui.director.IViewerWithFindToolBar;
+import jloda.util.*;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * contains the logic to find and replace strings
+ * Daniel Huson, 7.2008
+ */
+public class SearchManager implements IDirectableViewer {
+    private IDirector dir;
+    private boolean caseSensitiveOption = false;
+    private boolean wholeWordsOnlyOption = false;
+    private boolean regularExpressionsOption = false;
+    private boolean forwardDirection = true;
+    private boolean globalScope = true;
+
+    private Thread worker = null;
+
+    private boolean equateUnderscoreWithSpace = false;
+
+    private boolean isLocked = false;
+
+    ISearcher[] targets;
+    private ISearcher searcher;
+
+    final IFindDialog findDialog;
+
+    final Set<String> disabledSearchers = new HashSet<>();
+
+    static SearchManager instance;
+
+    private boolean showReplace;
+    private boolean allowReplace = true;
+
+    /**
+     * constructor. This does not create a single instance.
+     *
+     * @param dir
+     * @param viewer
+     * @param target
+     * @param showReplace
+     * @param createToolBar
+     */
+    public SearchManager(IDirector dir, IViewerWithFindToolBar viewer, ISearcher target, boolean showReplace, boolean createToolBar) {
+        if (!createToolBar) {
+            if (getInstance() != null)
+                new Alert("Internal error, multiple instances of SearchManager");
+            else
+                setInstance(this);
+        }
+        this.dir = dir;
+        this.targets = new ISearcher[]{target};
+        this.showReplace = showReplace;
+        searcher = targets[0];
+        if (createToolBar)
+            findDialog = new FindToolBar(this, viewer, new SearchActions(this), showReplace, target.getAdditionalButtons());
+        else
+            findDialog = new FindWindow(searcher.getParent(), "", this, new SearchActions(this));
+    }
+
+    /**
+     * if constructor was instructed to create a tool bar, returns the tool bar, else returns null
+     *
+     * @return find toolbar or nul
+     */
+    public FindToolBar getFindDialogAsToolBar() {
+        if (findDialog instanceof FindToolBar)
+            return (FindToolBar) findDialog;
+        else
+            return null;
+    }
+
+
+    /**
+     * constructor
+     *
+     * @param dir
+     * @param title
+     * @param targets
+     * @param showReplace
+     */
+    public SearchManager(IDirector dir, String title, ISearcher[] targets, boolean showReplace) {
+        if (getInstance() != null)
+            new Alert("Internal error, multiple instances of SearchManager");
+        else
+            setInstance(this);
+        this.dir = dir;
+        this.targets = targets;
+        this.showReplace = showReplace;
+        searcher = targets[0];
+        findDialog = new FindWindow(searcher.getParent(), title, this, new SearchActions(this));
+    }
+
+    /**
+     * constructor
+     *
+     * @param title
+     * @param targets
+     * @param showReplace
+     */
+    public SearchManager(String title, ISearcher[] targets, boolean showReplace) {
+        if (getInstance() != null)
+            new Alert("Internal error, multiple instances of SearchManager");
+        else
+            setInstance(this);
+        this.dir = null;
+        this.targets = targets;
+        this.showReplace = showReplace;
+        searcher = targets[0];
+        findDialog = new FindWindow(searcher.getParent(), title, this, new SearchActions(this));
+    }
+
+    /**
+     * constructor for non-gui version. Doesn't set instance!
+     *
+     * @param targets
+     */
+    public SearchManager(ISearcher[] targets) {
+        this.dir = null;
+        this.targets = targets;
+        this.showReplace = false;
+        searcher = targets[0];
+        findDialog = null;
+    }
+
+    private String searchText = null;
+    private String replaceText = null;
+
+    public boolean isCaseSensitiveOption() {
+        return caseSensitiveOption;
+    }
+
+    public void setCaseSensitiveOption(boolean caseSensitiveOption) {
+        this.caseSensitiveOption = caseSensitiveOption;
+    }
+
+    public boolean isWholeWordsOnlyOption() {
+        return wholeWordsOnlyOption;
+    }
+
+    public void setWholeWordsOnlyOption(boolean wholeWordsOnlyOption) {
+        this.wholeWordsOnlyOption = wholeWordsOnlyOption;
+    }
+
+    public boolean isRegularExpressionsOption() {
+        return regularExpressionsOption;
+    }
+
+    public void setRegularExpressionsOption(boolean regularExpressionsOption) {
+        this.regularExpressionsOption = regularExpressionsOption;
+    }
+
+    public boolean isForwardDirection() {
+        return forwardDirection;
+    }
+
+    public void setForwardDirection(boolean forwardDirection) {
+        this.forwardDirection = forwardDirection;
+    }
+
+    public boolean isGlobalScope() {
+        return globalScope;
+    }
+
+    public void setGlobalScope(boolean globalScope) {
+        this.globalScope = globalScope;
+    }
+
+    public void setSearcher(ISearcher searcher) {
+        this.searcher = searcher;
+        findDialog.getActions().setEnableCritical(!disabledSearchers.contains(getSearcher().getName())); // turn off stuff is this search is disabled
+    }
+
+    public ISearcher getSearcher() {
+        return searcher;
+    }
+
+    /**
+     * replace current or next occurrence of the query string
+     */
+    public void applyFindAndReplace() {
+        if (isCommandLineMode()) {
+            final boolean found = doFindAndReplace(new ProgressSilent());
+            System.err.println(found ? "Replaced" : "No replacements");
+        } else {
+            findDialog.clearMessage();
+            if (worker == null || !worker.isAlive()) {
+                worker = new Thread(new Runnable() {
+                    public void run() {
+                        notifyLockUserInput();
+                        final boolean found = doFindAndReplace(new ProgressDialog("Search", "Find and replace", searcher.getParent()));
+                        SwingUtilities.invokeLater(new Runnable() {
+                            public void run() {
+                                findDialog.setMessage(found ? "Replaced" : "No replacements");
+                            }
+                        });
+                        notifyUnlockUserInput();
+                    }
+                });
+                worker.setPriority(Thread.currentThread().getPriority() - 1);
+                worker.start();
+            }
+        }
+    }
+
+    /**
+     * replace current or next occurrence of the query string
+     */
+    private boolean doFindAndReplace(ProgressListener progressListener) {
+        boolean changed = false;
+        try {
+            if (searcher instanceof IObjectSearcher) {
+                IObjectSearcher oSearcher = (IObjectSearcher) searcher;
+
+                progressListener.setMaximum(-1);
+
+                boolean ok = oSearcher.isCurrentSet();
+                if (!ok)
+                    ok = isForwardDirection() ? oSearcher.gotoFirst() : oSearcher.gotoLast();
+
+                final String regexp = prepareRegularExpression(equateUnderscoreWithSpace ? searchText.replaceAll("_", " ") : searchText);
+                final Pattern pattern = Pattern.compile(regexp);
+
+                try {
+                    while (ok) {
+                        if (isGlobalScope() || oSearcher.isCurrentSelected()) {
+                            String label = oSearcher.getCurrentLabel();
+                            if (equateUnderscoreWithSpace)
+                                label = label.replaceAll("_", " ");
+                            if (label == null)
+                                label = "";
+                            String replace = getReplacement(pattern, replaceText, label);
+                            if (replace != null && !label.equals(replace)) {
+                                oSearcher.setCurrentSelected(true);
+                                oSearcher.setCurrentLabel(replace);
+                                changed = true;
+                                break;
+                            }
+                        }
+
+                        ok = isForwardDirection() ? oSearcher.gotoNext() : oSearcher.gotoPrevious();
+                        progressListener.checkForCancel();
+
+                    }
+                } catch (CanceledException e) {
+                    System.err.println("Search canceled");
+                } finally {
+                    progressListener.close();
+                }
+            } else if (searcher instanceof ITextSearcher) {
+                ITextSearcher tSearcher = (ITextSearcher) searcher;
+                tSearcher.setGlobalScope(isGlobalScope());
+
+                final String regexp = prepareRegularExpression(equateUnderscoreWithSpace ? searchText.replaceAll("_", " ") : searchText);
+                changed = tSearcher.replaceNext(regexp, replaceText);
+            }
+        } catch (Exception ex) {
+            new Alert(findDialog.getFrame(), "Error: " + ex);
+        }
+        if (changed) {
+            searcher.updateView();
+        }
+        return changed;
+    }
+
+    /**
+     * erase the current selection
+     */
+    public void applyUnselectAll() {
+        findDialog.clearMessage();
+        searcher.selectAll(false);
+    }
+
+    /**
+     * replace all occurrences of the query string
+     */
+    public void applyReplaceAll() {
+        if (isCommandLineMode()) {
+            final int found = doReplaceAll();
+            System.err.println("Replacements: " + found);
+        } else {
+            findDialog.clearMessage();
+            if (worker == null || !worker.isAlive()) {
+                worker = new Thread(new Runnable() {
+                    public void run() {
+                        notifyLockUserInput();
+                        final int found = doReplaceAll();
+                        SwingUtilities.invokeLater(new Runnable() {
+                            public void run() {
+                                findDialog.setMessage("Replacements: " + found);
+                            }
+                        });
+                        notifyUnlockUserInput();
+                    }
+                });
+                worker.setPriority(Thread.currentThread().getPriority() - 1);
+                worker.start();
+            }
+        }
+    }
+
+    /**
+     * replace all occurrences of the query string
+     */
+    private int doReplaceAll() {
+        int count = 0;
+        boolean changed = false;
+
+        try {
+            if (searcher instanceof IObjectSearcher) {
+                IObjectSearcher oSearcher = (IObjectSearcher) searcher;
+                boolean ok = isForwardDirection() ? oSearcher.gotoFirst() : oSearcher.gotoLast();
+
+                ProgressListener progressListener = (searcher.getParent() != null ?
+                        (new ProgressDialog("Search", "Replace all", searcher.getParent())) : new ProgressSilent());
+                progressListener.setMaximum(oSearcher.numberOfObjects());
+
+                final String regexp = prepareRegularExpression(equateUnderscoreWithSpace ? searchText.replaceAll("_", " ") : searchText);
+                final Pattern pattern = Pattern.compile(regexp);
+
+                try {
+                    while (ok) {
+                        if (isGlobalScope() || oSearcher.isCurrentSelected()) {
+                            String label = oSearcher.getCurrentLabel();
+                            if (label == null)
+                                label = "";
+                            if (equateUnderscoreWithSpace)
+                                label = label.replaceAll("_", " ");
+                            String replace = getReplacement(pattern, replaceText, label);
+                            if (replace != null && !replace.equals(label)) {
+                                oSearcher.setCurrentSelected(true);
+                                oSearcher.setCurrentLabel(replace);
+                                changed = true;
+                                count++;
+                            }
+                        }
+                        ok = isForwardDirection() ? oSearcher.gotoNext() : oSearcher.gotoPrevious();
+                        progressListener.incrementProgress();
+                    }
+                } catch (CanceledException e) {
+                    System.err.println("Search canceled");
+                } finally {
+                    progressListener.close();
+                }
+            } else if (searcher instanceof ITextSearcher) {
+                ITextSearcher tSearcher = (ITextSearcher) searcher;
+                tSearcher.setGlobalScope(isGlobalScope());
+
+                final String regexp = prepareRegularExpression(equateUnderscoreWithSpace ? searchText.replaceAll("_", " ") : searchText);
+                count = tSearcher.replaceAll(regexp, replaceText, !isGlobalScope());
+                if (count > 0)
+                    changed = true;
+            }
+            if (changed) {
+                searcher.updateView();
+            }
+        } catch (Exception ex) {
+            new Alert(findDialog.getFrame(), "Error: " + ex);
+        }
+        return count;
+    }
+
+    /**
+     * find the first occurrence of the query
+     */
+    public void applyFindFirst() {
+        if (isCommandLineMode()) {
+            boolean found = doFindFirst();
+            System.err.println(found ? "found" : "no matches");
+        } else {
+            findDialog.clearMessage();
+            if (worker == null || !worker.isAlive()) {
+                worker = new Thread(new Runnable() {
+                    public void run() {
+                        notifyLockUserInput();
+                        final boolean found = doFindFirst();
+
+                        SwingUtilities.invokeLater(new Runnable() {
+                            public void run() {
+                                findDialog.setMessage(found ? "Found" : "No matches");
+                            }
+                        });
+                        notifyUnlockUserInput();
+                    }
+                });
+                worker.setPriority(Thread.currentThread().getPriority() - 1);
+                worker.start();
+            }
+        }
+    }
+
+    /**
+     * find the first occurrence of the query
+     */
+    private boolean doFindFirst() {
+        boolean changed = false;
+        try {
+            searcher.selectAll(false);
+            if (searcher instanceof IObjectSearcher) {
+                IObjectSearcher oSearcher = (IObjectSearcher) searcher;
+                boolean ok = isForwardDirection() ? oSearcher.gotoFirst() : oSearcher.gotoLast();
+
+                ProgressListener progressListener = (searcher.getParent() != null ?
+                        (new ProgressDialog("Search", "Find first", searcher.getParent())) : new ProgressSilent());
+                progressListener.setMaximum(oSearcher.numberOfObjects());
+
+                final String regexp = prepareRegularExpression(equateUnderscoreWithSpace ? searchText.replaceAll("_", " ") : searchText);
+                final Pattern pattern = Pattern.compile(regexp);
+
+                try {
+                    while (ok) {
+                        if (isGlobalScope() || oSearcher.isCurrentSelected()) {
+                            String label = oSearcher.getCurrentLabel();
+                            if (label == null)
+                                label = "";
+                            if (equateUnderscoreWithSpace)
+                                label = label.replaceAll("_", " ");
+                            if (matches(pattern, label)) {
+                                oSearcher.setCurrentSelected(true);
+                                changed = true;
+                                break;
+                            }
+                        }
+                        ok = isForwardDirection() ? oSearcher.gotoNext() : oSearcher.gotoPrevious();
+                        progressListener.incrementProgress();
+                    }
+                } catch (CanceledException e) {
+                    System.err.println("Search canceled");
+                } finally {
+                    progressListener.close();
+                }
+            } else if (searcher instanceof ITextSearcher) {
+                ITextSearcher tSearcher = (ITextSearcher) searcher;
+                tSearcher.setGlobalScope(isGlobalScope());
+
+                final String regexp = prepareRegularExpression(equateUnderscoreWithSpace ? searchText.replaceAll("_", " ") : searchText);
+                changed = tSearcher.findFirst(regexp);
+            }
+        } catch (Exception ex) {
+            new Alert(findDialog.getFrame(), "Error: " + ex);
+        }
+        if (changed)
+            searcher.updateView();
+        return changed;
+    }
+
+    /**
+     * find the next occurrence of the query
+     */
+    public void applyFindNext() {
+        if (isCommandLineMode()) {
+            final boolean found = doFindNext(new ProgressSilent());
+            System.err.println(found ? "found" : "no matches");
+        } else {
+            findDialog.clearMessage();
+            if (worker == null || !worker.isAlive()) {
+                worker = new Thread(new Runnable() {
+                    public void run() {
+                        notifyLockUserInput();
+                        final boolean found = doFindNext(new ProgressDialog("Search", "Find next", searcher.getParent()));
+
+                        SwingUtilities.invokeLater(new Runnable() {
+                            public void run() {
+                                findDialog.setMessage(found ? "Found" : "No matches");
+                            }
+                        });
+                        notifyUnlockUserInput();
+                    }
+                });
+                worker.setPriority(Thread.currentThread().getPriority() - 1);
+                worker.start();
+            }
+        }
+    }
+
+    /**
+     * find the next occurrence of the query
+     */
+    private boolean doFindNext(ProgressListener progressListener) {
+        boolean changed = false;
+        try {
+            if (searcher instanceof IObjectSearcher) {
+                IObjectSearcher oSearcher = (IObjectSearcher) searcher;
+                boolean ok = isForwardDirection() ? oSearcher.gotoNext() : oSearcher.gotoPrevious();
+
+                progressListener.setMaximum(-1);
+
+                final String regexp = prepareRegularExpression(equateUnderscoreWithSpace ? searchText.replaceAll("_", " ") : searchText);
+                final Pattern pattern = Pattern.compile(regexp);
+                try {
+                    while (ok) {
+                        if (isGlobalScope() || oSearcher.isCurrentSelected()) {
+                            String label = oSearcher.getCurrentLabel();
+                            if (label == null)
+                                label = "";
+                            if (equateUnderscoreWithSpace)
+                                label = label.replaceAll("_", " ");
+                            if (matches(pattern, label)) {
+                                oSearcher.setCurrentSelected(true);
+                                changed = true;
+                                break;
+                            }
+                        }
+                        ok = isForwardDirection() ? oSearcher.gotoNext() : oSearcher.gotoPrevious();
+                        progressListener.checkForCancel();
+                    }
+                } catch (CanceledException e) {
+                    System.err.println("Search canceled");
+                } finally {
+                    progressListener.close();
+                }
+            } else if (searcher instanceof ITextSearcher) {
+                ITextSearcher tSearcher = (ITextSearcher) searcher;
+                tSearcher.setGlobalScope(isGlobalScope());
+
+                final String regexp = prepareRegularExpression(equateUnderscoreWithSpace ? searchText.replaceAll("_", " ") : searchText);
+                if (isForwardDirection()) {
+                    changed = tSearcher.findNext(regexp);
+                } else
+                    changed = tSearcher.findPrevious(regexp);
+            }
+        } catch (Exception ex) {
+            new Alert(findDialog.getFrame(), "Error: " + ex);
+        }
+        if (changed)
+            searcher.updateView();
+        return changed;
+    }
+
+
+    /**
+     * select all occurrences of the query string
+     */
+    public void applyFindAll() {
+        if (isCommandLineMode()) {
+            final int found = Math.abs(doFindAll(new ProgressSilent()));
+            System.err.println("Found: " + found);
+        } else {
+            findDialog.clearMessage();
+            if (worker == null || !worker.isAlive()) {
+                worker = new Thread(new Runnable() {
+                    public void run() {
+                        notifyLockUserInput();
+                        ProgressListener progressListener = new ProgressDialog("Search", "Find all", searcher.getParent());
+                        int found = doFindAll(progressListener);
+                        if (found == Integer.MIN_VALUE)
+                            found = 0;
+                        final int finalFound = Math.abs(found);
+                        progressListener.close();
+                        SwingUtilities.invokeLater(new Runnable() {
+                            public void run() {
+                                findDialog.setMessage("Found: " + finalFound);
+                            }
+                        });
+                        notifyUnlockUserInput();
+                    }
+                });
+                worker.setPriority(Thread.currentThread().getPriority() - 1);
+                worker.start();
+            }
+        }
+    }
+
+    /**
+     * select all occurrences of the query string
+     */
+    private int doFindAll(ProgressListener progressListener) {
+        boolean changed = false;
+        int count = 0;
+        boolean canceled = false;
+        try {
+            if (searcher instanceof IObjectSearcher) {
+                IObjectSearcher oSearcher = (IObjectSearcher) searcher;
+                boolean ok = oSearcher.gotoFirst();
+
+                progressListener.setMaximum(oSearcher.numberOfObjects());
+
+                final String regexp = prepareRegularExpression(equateUnderscoreWithSpace ? searchText.replaceAll("_", " ") : searchText);
+                final Pattern pattern = Pattern.compile(regexp);
+                try {
+                    while (ok) {
+                        if (isGlobalScope() || oSearcher.isCurrentSelected()) {
+                            String label = oSearcher.getCurrentLabel();
+                            if (label == null)
+                                label = "";
+                            if (equateUnderscoreWithSpace)
+                                label = label.replaceAll("_", " ");
+                            boolean select = matches(pattern, label);
+                            if (select) {
+                                if (!oSearcher.isCurrentSelected()) {
+                                    changed = true;
+                                }
+                                oSearcher.setCurrentSelected(select);
+                                count++;
+                            }
+                        }
+                        ok = oSearcher.gotoNext();
+                        progressListener.incrementProgress();
+                    }
+                } catch (CanceledException e) {
+                    System.err.println("Search canceled");
+                    canceled = true;
+                }
+            } else if (searcher instanceof ITextSearcher) {
+                ITextSearcher tSearcher = (ITextSearcher) searcher;
+                tSearcher.setGlobalScope(isGlobalScope());
+
+                final String regexp = prepareRegularExpression(equateUnderscoreWithSpace ? searchText.replaceAll("_", " ") : searchText);
+                count = tSearcher.findAll(regexp);
+                if (count > 0)
+                    changed = true;
+            }
+        } catch (Exception ex) {
+            new Alert(findDialog.getFrame(), "Error: " + ex);
+        }
+        if (changed)
+            searcher.updateView();
+        if (canceled)
+            return count > 0 ? -count : Integer.MIN_VALUE; // negative count to indicate that this was canceled
+        else
+            return count;
+    }
+
+    /**
+     * find all strings present in the given file
+     *
+     * @param file
+     */
+    public void findFromFile(final File file) throws IOException {
+        if (isCommandLineMode()) {
+            {
+                int count = 0;
+                if (file != null && file.exists()) {
+                    BufferedReader r = new BufferedReader(new FileReader(file));
+                    String aLine;
+                    while ((aLine = r.readLine()) != null) {
+                        aLine = aLine.trim();
+                        if (aLine.length() > 0 && !aLine.startsWith("#")) {
+                            System.err.println("find and select: " + aLine);
+                            setSearchText(aLine);
+                            int found = doFindAll(new ProgressSilent());
+                            boolean canceled = (found < 0);
+                            count += Math.abs(found);
+                            if (canceled)
+                                break;
+                        }
+                    }
+                    if (count > 0)
+                        searcher.updateView();
+                }
+            }
+        } else {
+            findDialog.clearMessage();
+            if (worker == null || !worker.isAlive()) {
+                worker = new Thread(new Runnable() {
+                    public void run() {
+                        notifyLockUserInput();
+                        int count = 0;
+
+                        try {
+
+                            if (file != null && file.exists()) {
+                                BufferedReader r = new BufferedReader(new FileReader(file));
+
+                                ProgressListener progressListener = new ProgressDialog("Search", "Find all", searcher.getParent());
+                                String aLine;
+                                while ((aLine = r.readLine()) != null) {
+                                    aLine = aLine.trim();
+                                    if (aLine.length() > 0 && !aLine.startsWith("#")) {
+                                        System.err.println("find and select: " + aLine);
+                                        setSearchText(aLine);
+                                        int found = doFindAll(progressListener);
+                                        boolean canceled = (found < 0);
+                                        if (found != Integer.MIN_VALUE)
+                                            count += Math.abs(found);
+                                        if (canceled)
+                                            break;
+                                    }
+                                }
+                                progressListener.close();
+                            }
+                        } catch (Exception ex) {
+                            Basic.caught(ex);
+                        }
+                        final int finalCount = Math.abs(count);
+                        SwingUtilities.invokeLater(new Runnable() {
+                            public void run() {
+                                new Message(findDialog.getFrame(), "Matches: " + finalCount, 150, 100);
+                            }
+                        });
+                        notifyUnlockUserInput();
+                    }
+                });
+                worker.setPriority(Thread.currentThread().getPriority() - 1);
+                worker.start();
+            }
+        }
+    }
+
+
+    /**
+     * does label match pattern?
+     *
+     * @param pattern
+     * @param label
+     * @return true, if match
+     */
+    private boolean matches(Pattern pattern, String label) {
+        if (label == null)
+            label = "";
+        Matcher matcher = pattern.matcher(label);
+        return matcher.find();
+    }
+
+    /**
+     * determines whether pattern matches label.
+     *
+     * @param pattern
+     * @param replacement
+     * @param label
+     * @return result of replacing query by replace string in label
+     */
+    private String getReplacement(Pattern pattern, String replacement, String label) {
+        if (label == null)
+            label = "";
+        if (replacement == null)
+            replacement = "";
+
+        Matcher matcher = pattern.matcher(label);
+        return matcher.replaceAll(replacement);
+    }
+
+    /**
+     * prepares the regular expression that reflects the chosen find options
+     *
+     * @param query
+     * @return regular expression
+     */
+    private String prepareRegularExpression(String query) {
+        if (query == null)
+            query = "";
+
+        String regexp = "" + query; //Copy the search string over.
+
+        /* Reg expression or not? If not regular expression, we need to surround the above
+        with quote literals: \Q expression \E just in case there are some regexp characters
+        already there. Note - this will fail if string already contains \E or \Q !!!!!!! */
+        if (!isRegularExpressionsOption()) {
+            if (regexp.contains("\\E"))
+                throw new PatternSyntaxException("Illegal character ''\\'' in search string", query, -1);
+            // TODO: this doesn't seem to work here, perhaps needs 1.5?
+            regexp = '\\' + "Q" + regexp + '\\' + "E";
+        }
+
+        if (isWholeWordsOnlyOption())
+            regexp = "\\b" + regexp + "\\b";
+
+        /* Check if case insensitive - if it is, then append (?i) before string */
+        if (!isCaseSensitiveOption())
+            regexp = "(?i)" + regexp;
+
+        //System.err.println(regexp);
+        return regexp;
+    }
+
+    /**
+     * the current query string
+     *
+     * @return query
+     */
+    public String getSearchText() {
+        return searchText;
+    }
+
+    /**
+     * set the current query string
+     *
+     * @param searchText
+     */
+    public void setSearchText(String searchText) {
+        this.searchText = searchText;
+    }
+
+    /**
+     * get the current replacement string
+     *
+     * @return replacement
+     */
+    public String getReplaceText() {
+        return replaceText;
+    }
+
+    /**
+     * set the current replacement string
+     *
+     * @param replaceText
+     */
+    public void setReplaceText(String replaceText) {
+        this.replaceText = replaceText;
+    }
+
+    /**
+     * return the frame associated with the viewer
+     *
+     * @return frame
+     */
+    public JFrame getFrame() {
+        return findDialog.getFrame();
+    }
+
+    /**
+     * gets the title
+     *
+     * @return title
+     */
+    public String getTitle() {
+        return findDialog.getFrame().getTitle();
+    }
+
+    /**
+     * is viewer uptodate?
+     *
+     * @return uptodate
+     */
+    public boolean isUptoDate() {
+        return true;
+    }
+
+    /**
+     * ask view to destroy itself
+     */
+    public void destroyView() throws CanceledException {
+        // because the searchmanager is directed by all documents, don't want it to close when one document is closed
+        //searchWindow.getFrame().setVisible(false);
+    }
+
+    /**
+     * ask view to prevent user input
+     */
+    public void lockUserInput() {
+        isLocked = true;
+        if (findDialog != null)
+            findDialog.getActions().setEnableCritical(false);
+    }
+
+    public boolean isLocked() {
+        return isLocked;
+    }
+
+    /**
+     * set uptodate state
+     *
+     * @param flag
+     */
+    public void setUptoDate(boolean flag) {
+    }
+
+    /**
+     * ask view to allow user input
+     */
+    public void unlockUserInput() {
+        if (isLocked) {
+            isLocked = false;
+            if (findDialog != null)
+                findDialog.getActions().setEnableCritical(true);
+        }
+    }
+
+    /**
+     * ask view to update itself. This is method is wrapped into a runnable object
+     * and put in the swing event queue to avoid concurrent modifications.
+     *
+     * @param what what should be updated? Possible values: Director.ALL or Director.TITLE
+     */
+    public void updateView(String what) {
+        if (findDialog != null) {
+            findDialog.getActions().updateEnableState();
+            if (disabledSearchers.contains(getSearcher().getName()))
+                findDialog.getActions().setEnableCritical(false); // turn off stuff is this search is disabled
+        }
+    }
+
+    /**
+     * chooses the current searcher by name
+     *
+     * @param name
+     * @return true, if found
+     */
+    public boolean chooseSearcher(String name) {
+        if (findDialog != null)
+            return findDialog.selectTarget(name);
+        else  // need this in command-line mode:
+        {
+            for (ISearcher target : targets) {
+                if (target.getName().equalsIgnoreCase(name)) {
+                    searcher = target;
+                    updateView(IDirector.ALL);
+                    return true;
+                }
+            }
+            // if no exact match, use prefix
+            for (ISearcher target : targets) {
+                if (target.getName().toLowerCase().startsWith(name.toLowerCase())) {
+                    searcher = target;
+                    updateView(IDirector.ALL);
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * replace the set of searchers by a new set
+     *
+     * @param searchers
+     * @param showReplace
+     */
+    public void replaceSearchers(IDirector dir, ISearcher searchers[], boolean showReplace) {
+        this.dir = dir;
+        if (searchers != this.targets) {
+            this.targets = searchers;
+            searcher = searchers[0];
+            if (findDialog != null) {
+                findDialog.updateTargets();
+                updateView(IDirector.ALL);
+            }
+        }
+    }
+
+    /**
+     * enable or disable the named searcher
+     *
+     * @param searcherName
+     * @param enable
+     */
+    public void setEnabled(String searcherName, boolean enable) {
+        if (enable)
+            disabledSearchers.remove(searcherName);
+        else {
+            disabledSearchers.add(searcherName);
+            if (searcher.getName().equals(searcherName)) {
+                for (ISearcher target : targets) {
+                    if (!disabledSearchers.contains(target.getName())) {
+                        searcher = target;
+                        break;
+                    }
+                }
+            }
+        }
+        updateView(IDirector.ALL);
+    }
+
+    /**
+     * is named searcher currently enabled?
+     *
+     * @param searcherName
+     * @return true, if enabled
+     */
+    public boolean isEnabled(String searcherName) {
+        return !disabledSearchers.contains(searcherName);
+    }
+
+    /**
+     * is replace part of dialog showning?
+     *
+     * @return true, if replace showing
+     */
+    public boolean getShowReplace() {
+        return showReplace;
+    }
+
+    /**
+     * show or hide replace dialog
+     *
+     * @param showReplace
+     */
+    public void setShowReplace(boolean showReplace) {
+        if (showReplace != this.showReplace) {
+            this.showReplace = showReplace;
+            setAllowReplace(showReplace);
+            if (findDialog != null)
+                findDialog.updateTargets();
+        }
+    }
+
+    /**
+     * run a find. This is used in the command line version of a program
+     *
+     * @param searchText
+     * @param target
+     * @param all
+     * @param regularExpression
+     * @param wholeWord
+     * @param caseSensitive
+     */
+    public void runFind(String searchText, String target, boolean all, boolean regularExpression, boolean wholeWord, boolean caseSensitive) {
+        chooseSearcher(target);
+        setSearchText(searchText);
+        setRegularExpressionsOption(regularExpression);
+        setWholeWordsOnlyOption(wholeWord);
+        setCaseSensitiveOption(caseSensitive);
+        if (!all)
+            doFindNext(new ProgressSilent());
+        else
+            applyFindAll();
+    }
+
+    /**
+     * run a find. This is used in the command line version of a program
+     *
+     * @param searchFile
+     * @param target
+     * @param regularExpression
+     * @param wholeWord
+     * @param caseSensitive
+     */
+    public void runFindFromFile(File searchFile, String target, boolean regularExpression, boolean wholeWord, boolean caseSensitive) throws IOException {
+        chooseSearcher(target);
+        setRegularExpressionsOption(regularExpression);
+        setWholeWordsOnlyOption(wholeWord);
+        setCaseSensitiveOption(caseSensitive);
+        findFromFile(searchFile);
+    }
+
+    /**
+     * run a find and replace. This is used in the command line version of a program
+     *
+     * @param searchText
+     * @param replaceText
+     * @param target
+     * @param all
+     * @param regularExpression
+     * @param wholeWord
+     * @param caseSensitive
+     */
+    public boolean runFindReplace(String searchText, String replaceText, String target, boolean all, boolean regularExpression, boolean wholeWord, boolean caseSensitive) {
+        chooseSearcher(target);
+        setSearchText(searchText);
+        setReplaceText(replaceText);
+        setRegularExpressionsOption(regularExpression);
+        setWholeWordsOnlyOption(wholeWord);
+        setCaseSensitiveOption(caseSensitive);
+
+        if (!all) {
+            return doFindAndReplace(new ProgressSilent());
+        } else {
+            return doReplaceAll() > 0;
+        }
+    }
+
+    /**
+     * gets names of all targets
+     *
+     * @return names
+     */
+    public String[] getTargetNames() {
+        String[] names = new String[targets.length];
+
+        for (int i = 0; i < names.length; i++)
+            names[i] = targets[i].getName();
+        return names;
+    }
+
+    /**
+     * equate an underscore in a label with a space in the query?
+     *
+     * @return true, if set
+     */
+    public boolean isEquateUnderscoreWithSpace() {
+        return equateUnderscoreWithSpace;
+    }
+
+    /**
+     * equate an underscore in a label with a space in the query?
+     *
+     * @param equateUnderscoreWithSpace
+     */
+    public void setEquateUnderscoreWithSpace(boolean equateUnderscoreWithSpace) {
+        this.equateUnderscoreWithSpace = equateUnderscoreWithSpace;
+    }
+
+    /**
+     * gets the instance of the search manager.
+     * This will return null, if not yet set
+     *
+     * @return instance of search manager
+     */
+    public static SearchManager getInstance() {
+        return instance;
+    }
+
+    /**
+     * sets the instance of the search manager
+     *
+     * @param instance
+     */
+    public static void setInstance(SearchManager instance) {
+        SearchManager.instance = instance;
+    }
+
+
+    /**
+     * notifies the director to lock user input
+     */
+    private void notifyLockUserInput() {
+        try {
+            SwingUtilities.invokeAndWait(new Runnable() {
+                public void run() {
+                    if (dir != null)
+                        dir.notifyLockInput();
+                }
+            });
+        } catch (Exception e) {
+        }
+    }
+
+    /**
+     * notifies the director to unlock user input
+     */
+    private void notifyUnlockUserInput() {
+        try {
+            SwingUtilities.invokeAndWait(new Runnable() {
+                public void run() {
+                    if (dir != null)
+                        dir.notifyUnlockInput();
+                }
+            });
+        } catch (Exception e) {
+        }
+    }
+
+    private boolean isCommandLineMode() {
+        return findDialog == null;
+    }
+
+    public void chooseTargetForFrame(Component parent) {
+        if (!isLocked && findDialog != null)
+            findDialog.chooseTargetForFrame(parent);
+    }
+
+    public boolean isAllowReplace() {
+        return allowReplace;
+    }
+
+    public void setAllowReplace(boolean allowReplace) {
+        this.allowReplace = allowReplace;
+    }
+
+    public CommandManager getCommandManager() {
+        return null;
+    }
+
+    public IDirector getDir() {
+        return dir;
+    }
+
+    /**
+     * get the name of the class
+     *
+     * @return class name
+     */
+    @Override
+    public String getClassName() {
+        return "SearchManager";
+    }
+}
diff --git a/src/jloda/gui/find/TableSearcher.java b/src/jloda/gui/find/TableSearcher.java
new file mode 100644
index 0000000..cfe7bcb
--- /dev/null
+++ b/src/jloda/gui/find/TableSearcher.java
@@ -0,0 +1,244 @@
+/**
+ * TableSearcher.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Collection;
+
+/**
+ * table searcher
+ * Daniel Huson, 7.2008
+ */
+public class TableSearcher implements IObjectSearcher {
+    private final Component parent;
+    private final JTable table;
+    private final String name;
+
+    private int row = 0;
+    private int col = 0;
+
+
+    /**
+     * constructor
+     */
+    public TableSearcher(Component parent, String name, JTable table) {
+        this.parent = parent;
+        this.name = name;
+        this.table = table;
+    }
+
+    /**
+     * goto the first object
+     *
+     * @return true, if successful
+     */
+    public boolean gotoFirst() {
+        row = col = 0;
+        return true;
+    }
+
+    /**
+     * goto the next object
+     *
+     * @return true, if successful
+     */
+    public boolean gotoNext() {
+        if (col + 1 < table.getColumnCount()) {
+            col++;
+            return true;
+        } else if (row + 1 < table.getRowCount()) {
+            col = 0;
+            row++;
+            return true;
+        } else {
+            row = 0;
+            col = -1;
+            selectAll(false);
+            return false;
+        }
+    }
+
+    /**
+     * goto the last object
+     *
+     * @return true, if successful
+     */
+    public boolean gotoLast() {
+        if (table.getRowCount() > 0 && table.getColumnCount() > 0) {
+            row = table.getRowCount() - 1;
+            col = table.getColumnCount() - 1;
+            return true;
+        } else {
+            row = col = 0;
+            return false;
+        }
+    }
+
+    /**
+     * goto the previous object
+     *
+     * @return true, if successful
+     */
+    public boolean gotoPrevious() {
+        if (col > 0) {
+            col--;
+            return true;
+        } else if (row > 0) {
+            col = table.getColumnCount() - 1;
+            row--;
+            return true;
+        } else {
+            row = table.getRowCount() - 1;
+            col = table.getColumnCount();
+            selectAll(false);
+            return false;
+        }
+    }
+
+    /**
+     * is the current object set?
+     *
+     * @return true, if set
+     */
+    public boolean isCurrentSet() {
+        return row < table.getRowCount() && col < table.getColumnCount();
+    }
+
+    /**
+     * is the current object selected?
+     *
+     * @return true, if selected
+     */
+    public boolean isCurrentSelected() {
+        return isCurrentSet() && table.isCellSelected(row, col);
+    }
+
+    /**
+     * set selection state of current object
+     *
+     * @param select
+     */
+    public void setCurrentSelected(boolean select) {
+        if (isCurrentSet()) {
+            table.setRowSelectionInterval(row, row);
+            table.setColumnSelectionInterval(col, col);
+        }
+    }
+
+    /**
+     * get the label of the current object
+     *
+     * @return label
+     */
+    public String getCurrentLabel() {
+        if (isCurrentSet())
+            return table.getValueAt(row, col).toString();
+        else
+            return "";
+    }
+
+    /**
+     * set the label of the current object
+     *
+     * @param newLabel
+     */
+    public void setCurrentLabel(String newLabel) {
+        if (isCurrentSet())
+            table.setValueAt(newLabel, row, col);
+    }
+
+    /**
+     * how many objects are there?
+     *
+     * @return number of objects or -1
+     */
+    public int numberOfObjects() {
+        return table.getRowCount() * table.getColumnCount();
+    }
+
+    /**
+     * get the name for this type of search
+     *
+     * @return name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * is a global find possible?
+     *
+     * @return true, if there is at least one object
+     */
+    public boolean isGlobalFindable() {
+        return numberOfObjects() > 0;
+    }
+
+    /**
+     * is a selection find possible
+     *
+     * @return true, if at least one object is selected
+     */
+    public boolean isSelectionFindable() {
+        return table.getSelectedRowCount() > 0;
+    }
+
+    /**
+     * something has been changed or selected, update view
+     */
+    public void updateView() {
+    }
+
+    /**
+     * does this searcher support find all?
+     *
+     * @return true, if find all supported
+     */
+    public boolean canFindAll() {
+        return false;
+    }
+
+    /**
+     * set select state of all objects
+     *
+     * @param select
+     */
+    public void selectAll(boolean select) {
+        if (select)
+            table.selectAll();
+        else
+            table.clearSelection();
+    }
+
+    /**
+     * get the parent component
+     *
+     * @return parent
+     */
+    public Component getParent() {
+        return parent;
+    }
+
+    @Override
+    public Collection<AbstractButton> getAdditionalButtons() {
+        return null;
+    }
+}
diff --git a/src/jloda/gui/find/TextAreaSearcher.java b/src/jloda/gui/find/TextAreaSearcher.java
new file mode 100644
index 0000000..66933d0
--- /dev/null
+++ b/src/jloda/gui/find/TextAreaSearcher.java
@@ -0,0 +1,328 @@
+/**
+ * TextAreaSearcher.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.find;
+
+import jloda.util.Basic;
+
+import javax.swing.*;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Caret;
+import java.awt.*;
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * JTextArea searcher
+ * Daniel Huson, 7.2008
+ */
+public class TextAreaSearcher implements ITextSearcher {
+    final JTextArea textArea;
+
+    private final String name;
+
+    /**
+     * constructor
+     */
+    public TextAreaSearcher(String name, JTextArea textArea) {
+        this.name = name;
+        this.textArea = textArea;
+    }
+
+    /**
+     * get the parent component
+     *
+     * @return parent
+     */
+    public Component getParent() {
+        return textArea;
+    }
+
+    /**
+     * get the name for this type of search
+     *
+     * @return name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Find first instance
+     *
+     * @param regularExpression
+     * @return - returns boolean: true if text found, false otherwise
+     */
+    public boolean findFirst(String regularExpression) {
+        if (textArea == null) return false;
+        textArea.setCaretPosition(0);
+        return singleSearch(regularExpression, true);
+    }
+
+    /**
+     * Find next instance
+     *
+     * @param regularExpression
+     * @return - returns boolean: true if text found, false otherwise
+     */
+    public boolean findNext(String regularExpression) {
+        return textArea != null && singleSearch(regularExpression, true);
+    }
+
+    /**
+     * Find previous instance
+     *
+     * @param regularExpression
+     * @return - returns boolean: true if text found, false otherwise
+     */
+    public boolean findPrevious(String regularExpression) {
+        return textArea != null && singleSearch(regularExpression, false);
+    }
+
+    /**
+     * Replace selection with current. Does nothing if selection invalid.
+     *
+     * @param regularExpression
+     * @param replaceText
+     */
+    public boolean replaceNext(String regularExpression, String replaceText) {
+        if (textArea == null) return false;
+        if (findNext(regularExpression)) {
+            textArea.replaceSelection(replaceText);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Replace all occurrences of text in document, subject to options.
+     *
+     * @param regularExpression
+     * @param replaceText
+     * @return number of instances replaced
+     */
+    public int replaceAll(String regularExpression, String replaceText, boolean selectionOnly) {
+        if (textArea == null) return 0;
+        Pattern pattern = Pattern.compile(regularExpression);
+        String source;
+
+        //If we are doing replaceAll only in the selection, then we set the text appropriately.
+        //With Jave 1.5 this is done using regions, but for backward compatibility we will use
+        //a cruder version.
+        if (selectionOnly)
+            source = textArea.getSelectedText();
+        else
+            source = getText();
+
+        Matcher matcher = pattern.matcher(source);
+        //We do a manual count of the number of >>non-overlapping<< patterns match.
+        int count = 0;
+        int pos = 0;
+        while (matcher.find(pos)) {
+            count++;
+            pos = matcher.end();
+        }
+
+        //Now do the replaceAll
+        matcher.replaceAll(replaceText);
+
+        //Now put this back into the text ddocument
+
+        if (selectionOnly)
+            textArea.replaceSelection(matcher.replaceAll(replaceText));
+        else {
+            textArea.setText(matcher.replaceAll(replaceText));
+            textArea.setCaretPosition(0);
+        }
+
+        return count;
+    }
+
+    /**
+     * is a global find possible?
+     *
+     * @return true, if there is at least one object
+     */
+    public boolean isGlobalFindable() {
+        return textArea != null && getText().length() > 0;
+    }
+
+    /**
+     * is a selection find possible
+     *
+     * @return true, if at least one object is selected
+     */
+    public boolean isSelectionFindable() {
+        return textArea != null && textArea.getSelectedText() != null && textArea.getSelectedText().length() > 0;
+    }
+
+    /**
+     * Selects all occurrences of text in document, subject to options and constraints of document type
+     *
+     * @param pattern
+     */
+    public int findAll(String pattern) {
+        //Not implemented for text editors.... as we cannot select multiple chunks of text.
+        return 0;
+    }
+
+    /**
+     * something has been changed or selected, update view
+     */
+    public void updateView() {
+    }
+
+    /**
+     * does this searcher support find all?
+     *
+     * @return true, if find all supported
+     */
+    public boolean canFindAll() {
+        return false;
+    }
+
+    /**
+     * set select state of all objects
+     *
+     * @param select
+     */
+    public void selectAll(boolean select) {
+        if (textArea == null) return;
+        if (select) {
+            textArea.setCaretPosition(0);
+            textArea.moveCaretPosition(textArea.getText().length());
+        } else {
+            textArea.setCaretPosition(textArea.getCaretPosition());
+            textArea.moveCaretPosition(textArea.getCaretPosition());
+        }
+    }
+
+    /**
+     * gets the current text
+     *
+     * @return text
+     */
+    private String getText() {
+        if (textArea == null) return null;
+        int length = textArea.getDocument().getLength();
+        try {
+            return textArea.getText(0, length);
+        } catch (BadLocationException e) {
+            Basic.caught(e);
+            return null;
+        }
+    }
+
+
+    //We start the search at the end of the selection, which could be the dot or the mark.
+
+    private int getSearchStart() {
+        if (textArea == null) return 0;
+
+        Caret caret = textArea.getCaret();
+        int dot = caret.getDot();
+        int mark = caret.getMark();
+        return java.lang.Math.max(dot, mark);
+    }
+
+
+    private void selectMatched(Matcher matcher) {
+        if (textArea == null) return;
+
+        textArea.setCaretPosition(matcher.start());
+        textArea.moveCaretPosition(matcher.end());
+    }
+
+    private boolean singleSearch(String regularExpression, boolean forward) throws PatternSyntaxException {
+        if (textArea == null) return false;
+
+        //Do nothing if there is no text.
+        if (regularExpression.length() == 0)
+            return false;
+
+        //Search begins at the end of the currently selected portion of text.
+        int currentPoint = getSearchStart();
+
+
+        boolean found = false;
+
+        Pattern pattern = Pattern.compile(regularExpression);
+
+        String source = getText();
+        Matcher matcher = pattern.matcher(source);
+
+        if (forward)
+            found = matcher.find(currentPoint);
+        else {
+            //This is an inefficient algorithm to handle reverse search. It is a temporary
+            //stop gap until reverse searching is built into the API.
+            //TODO: Check every once and a while to see when matcher.previous() is implemented in the API.
+            //TODO: Consider use of GNU find/replace.
+            //TODO: use regions to make searching more efficient when we know the length of the search string to match.
+            int pos = 0;
+            int searchFrom = 0;
+            //System.err.println("Searching backwards before " + currentPoint);
+            while (matcher.find(searchFrom) && matcher.end() < currentPoint) {
+                pos = matcher.start();
+                searchFrom = matcher.end();
+                found = true;
+                //System.err.println("\tfound at [" + pos + "," + matcher.end() + "]" + " but still looking");
+            }
+            if (found)
+                matcher.find(pos);
+            //System.err.println("\tfound at [" + pos + "," + matcher.end() + "]");
+        }
+
+        if (!found && currentPoint != 0) {
+            matcher = pattern.matcher(source);
+            found = matcher.find();
+        }
+
+        if (!found)
+            return false;
+
+        //System.err.println("Pattern found between positions " + matcher.start() + " and " + matcher.end());
+        selectMatched(matcher);
+        return true;
+    }
+
+    /**
+     * set scope global rather than selected
+     *
+     * @param globalScope
+     */
+    public void setGlobalScope(boolean globalScope) {
+    }
+
+    /**
+     * get scope global rather than selected
+     *
+     * @return true, if search scope is global
+     */
+    public boolean isGlobalScope() {
+        return textArea != null;
+    }
+
+    @Override
+    public Collection<AbstractButton> getAdditionalButtons() {
+        return null;
+    }
+}
diff --git a/src/jloda/gui/format/Formatter.java b/src/jloda/gui/format/Formatter.java
new file mode 100644
index 0000000..8980047
--- /dev/null
+++ b/src/jloda/gui/format/Formatter.java
@@ -0,0 +1,804 @@
+/**
+ * Formatter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.format;
+
+/**
+ * format nodes and edges
+ * Daniel Huson, 2.2007
+ */
+
+import jloda.graph.EdgeSet;
+import jloda.graph.NodeSet;
+import jloda.graphview.*;
+import jloda.gui.ChooseColorDialog;
+import jloda.gui.WindowListenerAdapter;
+import jloda.gui.commands.CommandManager;
+import jloda.gui.director.IDirectableViewer;
+import jloda.gui.director.IDirector;
+import jloda.util.ProgramProperties;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.awt.event.*;
+import java.util.LinkedList;
+
+/**
+ * format nodes and edges
+ */
+public class Formatter implements IDirectableViewer {
+    public static final String CONFIGURATOR_GEOMETRY = "ConfiguratorGeometry";
+
+    private final java.util.List<IFormatterListener> formatterListeners = new LinkedList<>();
+
+    private boolean isLocked = false;
+
+    private boolean uptodate = false;
+    private IDirector dir;
+    private INodeEdgeFormatable viewer;
+    private final FormatterActions actions;
+    private final FormatterMenuBar menuBar;
+    private final JFrame frame;
+    static Cursor waitCursor = new Cursor(Cursor.WAIT_CURSOR);
+
+    private static Formatter instance = null;
+
+    private JComboBox nodeSize, fontName, fontSize, nodeShape, edgeShape, edgeWidth;
+    private JCheckBox boldFont, italicFont, labels, foregroundColor, backgroundColor, labelForegroundColor,
+            labelBackgroundColor;
+    private JButton rotateLabelsLeft, rotateLabelsRight;
+    private JColorChooser colorChooser;
+
+    private final JScrollBar alphaValueSBar = new JScrollBar(JScrollBar.HORIZONTAL, 255, 1, 0, 256);
+    private boolean noAlphaBounce = false;
+
+    /**
+     * constructor
+     *
+     * @param dir               the director
+     * @param viewer            the graph view
+     * @param showRotateButtons show label rotate buttons?
+     */
+    public Formatter(final IDirector dir, final INodeEdgeFormatable viewer, boolean showRotateButtons) {
+        this.viewer = viewer;
+        this.dir = dir;
+        actions = new FormatterActions(this, dir, viewer);
+        menuBar = new FormatterMenuBar(this, dir);
+        setUptoDate(true);
+
+        frame = new JFrame();
+        if (ProgramProperties.getProgramIcon() != null)
+            frame.setIconImage(ProgramProperties.getProgramIcon().getImage());
+        frame.setJMenuBar(menuBar);
+        frame.setLocationRelativeTo(viewer.getFrame());
+        final int[] geometry = ProgramProperties.get(CONFIGURATOR_GEOMETRY, new int[]{100, 100, 585, 475});
+        frame.setSize(geometry[2], geometry[3]);
+
+        //dir.setViewerLocation(this);
+        frame.setResizable(true);
+        setTitle(dir);
+
+        frame.getContentPane().add(getPanel(showRotateButtons));
+        frame.setVisible(true);
+
+        frame.addWindowListener(new WindowListenerAdapter() {
+            public void windowActivated(WindowEvent windowEvent) {
+                updateView("selection");
+            }
+        });
+        frame.addWindowListener(new WindowListenerAdapter() {
+            public void windowDeactivated(WindowEvent windowEvent) {
+                dir.notifyUpdateViewer(IDirector.ENABLE_STATE);
+            }
+        });
+        frame.addComponentListener(new ComponentAdapter() {
+            public void componentMoved(ComponentEvent e) {
+                componentResized(e);
+            }
+
+            public void componentResized(ComponentEvent event) {
+                if ((event.getID() == ComponentEvent.COMPONENT_RESIZED || event.getID() == ComponentEvent.COMPONENT_MOVED) &&
+                        (frame.getExtendedState() & JFrame.MAXIMIZED_HORIZ) == 0
+                        && (frame.getExtendedState() & JFrame.MAXIMIZED_VERT) == 0) {
+                    ProgramProperties.put(CONFIGURATOR_GEOMETRY, new int[]
+                            {frame.getLocation().x, frame.getLocation().y, frame.getSize().width,
+                                    frame.getSize().height});
+                }
+            }
+        });
+
+        final NodeActionListener nal = new NodeActionAdapter() {
+            public void doSelect(NodeSet nodes) {
+                // todo: update too expensive at present to call after change of selection
+                //updateView("selection");
+            }
+
+            public void doDeselect(NodeSet nodes) {
+                // updateView("selection");
+            }
+
+        };
+        viewer.addNodeActionListener(nal);
+        final EdgeActionListener eal = new EdgeActionAdapter() {
+            public void doSelect(EdgeSet edges) {
+                // updateView("selection");
+            }
+
+            public void doDeselect(EdgeSet edges) {
+                //updateView("selection");
+            }
+        };
+        viewer.addEdgeActionListener(eal);
+
+        frame.addWindowListener(new WindowListenerAdapter() {
+            public void windowClosing(WindowEvent event) {
+                viewer.removeNodeActionListener(nal);
+                viewer.removeEdgeActionListener(eal);
+                dir.removeViewer(Formatter.this);
+            }
+        });
+        updateView(IDirector.ENABLE_STATE);
+    }
+
+    /**
+     * set the viewer to a new viewer.
+     * If this is used, frame is set not to destroy itself
+     *
+     * @param dir
+     * @param viewer
+     */
+    public void setViewer(IDirector dir, INodeEdgeFormatable viewer) {
+        this.frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
+        this.viewer = viewer;
+        this.dir = dir;
+        actions.setViewer(dir, viewer);
+        menuBar.setViewer(dir);
+        setUptoDate(true);
+        setTitle(dir);
+    }
+
+    /**
+     * sets the title
+     *
+     * @param dir the director
+     */
+    public void setTitle(IDirector dir) {
+        String newTitle;
+
+        if (dir.getID() == 1)
+            newTitle = "Format - " + dir.getTitle() + " - " + ProgramProperties.getProgramName();
+        else
+            newTitle = "Format - " + dir.getTitle() + " [" + dir.getID() + "] - " + ProgramProperties.getProgramName();
+        if (!frame.getTitle().equals(newTitle))
+            frame.setTitle(newTitle);
+    }
+
+    /**
+     * returns the actions object associated with the window
+     *
+     * @return actions
+     */
+
+    public FormatterActions getActions() {
+        return actions;
+    }
+
+    /**
+     * is viewer uptodate?
+     *
+     * @return uptodate
+     */
+    public boolean isUptoDate() {
+        return uptodate;
+    }
+
+    /**
+     * ask view to update itself. This is method is wrapped into a runnable object
+     * and put in the swing event queue to avoid concurrent modifications.
+     *
+     * @param what is to be updated
+     */
+    public void updateView(String what) {
+        if (what.equals(IDirector.TITLE)) {
+            setTitle(dir);
+            return;
+        }
+
+        if (isLocked) {
+            return;
+        }
+
+        getActions().setIgnore(true); // only want to update stuff, ignore requests to perform events
+
+        uptodate = false;
+        getActions().setEnableCritical(true);
+        getActions().updateEnableState();
+        if (what.equals("selection") || what.equals(IDirector.ALL)) {
+
+            int nSize = viewer.getWidthSelectedNodes();
+            if (nSize == -1)
+                nodeSize.setSelectedIndex(-1);
+            else
+                nodeSize.setSelectedItem(Integer.toString(nSize));
+            int nShape = viewer.getShapeSelectedNodes();
+            if (nShape == -1)
+                nodeShape.setSelectedIndex(-1);
+            else
+                nodeShape.setSelectedIndex(nShape);
+
+            Color color = null;
+            int colorIsDefined = 0; // -1 over defined
+
+            if (colorIsDefined != -1 && ((JCheckBox) actions.getForegroundColorAction(null).getValue(FormatterActions.CHECKBOXITEM)).isSelected()) {
+                Color aColor = viewer.getColorSelectedNodes();
+                if (aColor != null) {
+                    if (colorIsDefined == 0) {
+                        color = aColor;
+                        colorIsDefined = 1;
+                    } else if (!aColor.equals(color))
+                        colorIsDefined = -1;
+                }
+                if (colorIsDefined != -1) {
+                    aColor = viewer.getColorSelectedEdges();
+                    if (aColor != null) {
+                        if (colorIsDefined == 0) {
+                            color = aColor;
+                            colorIsDefined = 1;
+                        } else if (!aColor.equals(color))
+                            colorIsDefined = -1;
+                    }
+                }
+            }
+            if (colorIsDefined != -1 && ((JCheckBox) actions.getBackgroundColorAction(null).getValue(FormatterActions.CHECKBOXITEM)).isSelected()) {
+                Color aColor = viewer.getBackgroundColorSelectedNodes();
+                if (aColor != null) {
+                    if (colorIsDefined == 0) {
+                        color = aColor;
+                        colorIsDefined = 1;
+                    } else if (!aColor.equals(color))
+                        colorIsDefined = -1;
+                }
+            }
+            if (colorIsDefined != -1 && ((JCheckBox) actions.getLabelForegroundColorAction(null).getValue(FormatterActions.CHECKBOXITEM)).isSelected()) {
+                Color aColor = viewer.getLabelColorSelectedNodes();
+                if (aColor != null) {
+                    if (colorIsDefined == 0) {
+                        color = aColor;
+                        colorIsDefined = 1;
+                    } else if (!aColor.equals(color))
+                        colorIsDefined = -1;
+                }
+                if (colorIsDefined != -1) {
+                    aColor = viewer.getLabelColorSelectedEdges();
+                    if (aColor != null) {
+                        if (colorIsDefined == 0) {
+                            color = aColor;
+                            colorIsDefined = 1;
+                        } else if (!aColor.equals(color))
+                            colorIsDefined = -1;
+                    }
+                }
+            }
+            if (colorIsDefined != -1 && ((JCheckBox) actions.getLabelBackgroundColorAction(null).getValue(FormatterActions.CHECKBOXITEM)).isSelected()) {
+                Color aColor = viewer.getLabelBackgroundColorSelectedNodes();
+                if (aColor != null) {
+                    if (colorIsDefined == 0) {
+                        color = aColor;
+                        colorIsDefined = 1;
+                    } else if (!aColor.equals(color))
+                        colorIsDefined = -1;
+                }
+                if (colorIsDefined != -1) {
+                    aColor = viewer.getLabelBackgroundColorSelectedEdges();
+                    if (aColor != null) {
+                        if (colorIsDefined == 0) {
+                            color = aColor;
+                            colorIsDefined = 1;
+                        } else if (!aColor.equals(color))
+                            colorIsDefined = -1;
+                    }
+                }
+            }
+            noAlphaBounce = true;
+            if (colorIsDefined == 1) {
+                //System.err.println("Selected color: " + color);
+                colorChooser.getSelectionModel().setSelectedColor(color);
+                alphaValueSBar.setValue(color.getAlpha());
+            } else
+                alphaValueSBar.setValue(255);
+            noAlphaBounce = false;
+
+            Font font = viewer.getFontSelected();
+            if (font == null) {
+                fontSize.setSelectedIndex(-1);
+                boldFont.setSelected(false);
+                italicFont.setSelected(false);
+                fontName.setSelectedIndex(-1);
+            } else {
+                fontSize.setSelectedItem(Integer.toString(font.getSize()));
+                if (font.getStyle() == Font.BOLD) {
+                    boldFont.setSelected(true);
+                    italicFont.setSelected(false);
+                }
+                if (font.getStyle() == Font.ITALIC) {
+                    boldFont.setSelected(false);
+                    italicFont.setSelected(true);
+                }
+                if (font.getStyle() == Font.ITALIC + Font.BOLD) {
+                    boldFont.setSelected(true);
+                    italicFont.setSelected(true);
+                }
+                if (font.getStyle() == Font.PLAIN) {
+                    boldFont.setSelected(false);
+                    italicFont.setSelected(false);
+                }
+                fontName.setSelectedItem(font.getName());
+            }
+
+            int eWidth = viewer.getLineWidthSelectedEdges();
+
+            if (eWidth == -1)
+                edgeWidth.setSelectedIndex(-1);
+            else
+                edgeWidth.setSelectedItem(Integer.toString(eWidth));
+
+            int eShape = viewer.getShapeSelectedEdges();
+            if (eShape == -1)
+                edgeShape.setSelectedIndex(-1);
+            else {
+                int i = 0;
+                if (eShape == EdgeView.STRAIGHT_EDGE)
+                    i = 1;
+                else if (eShape == EdgeView.QUAD_EDGE)
+                    i = 2;
+                edgeShape.setSelectedIndex(i);
+            }
+            labels.setSelected(viewer.hasLabelVisibleSelectedNodes() || viewer.hasLabelVisibleSelectedEdges());
+
+            getActions().getSaveDefaultFont().setEnabled(fontName.getSelectedIndex() != -1);
+            frame.repaint();
+            getActions().setIgnore(false); // ignore firing of events
+        }
+
+        colorChooser.setEnabled(viewer.hasSelectedNodes() || viewer.hasSelectedEdges());
+        uptodate = true;
+    }
+
+    /**
+     * ask view to prevent user input
+     */
+
+    public void lockUserInput() {
+        isLocked = true;
+        getActions().setEnableCritical(false);
+        frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+        colorChooser.setEnabled(false);
+        frame.getContentPane().setEnabled(false);
+    }
+
+    /**
+     * ask view to allow user input
+     */
+    public void unlockUserInput() {
+        colorChooser.setEnabled(true);
+        frame.setCursor(Cursor.getDefaultCursor());
+        isLocked = false;
+    }
+
+    /**
+     * ask view to destroy itself
+     */
+    public void destroyView() {
+        dir.removeViewer(this);
+        frame.dispose();
+    }
+
+    /**
+     * set uptodate state
+     *
+     * @param flag
+     */
+    public void setUptoDate(boolean flag) {
+        uptodate = flag;
+    }
+
+    /**
+     * returns the frame of the window
+     */
+    public JFrame getFrame() {
+        return frame;
+    }
+
+    private JPanel getPanel(boolean showRotateButtons) {
+        JPanel topPanel = new JPanel();
+        topPanel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
+        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
+        //topPanel.setLayout(new GridLayout(5,1));
+
+        JPanel fontPanel = new JPanel();
+        fontPanel.setLayout(new BoxLayout(fontPanel, BoxLayout.X_AXIS));
+        fontPanel.add(new JLabel("Font:"));
+        fontPanel.add(fontName = makeFont());
+        fontPanel.add(new JLabel("Size:"));
+        fontPanel.add(fontSize = makeFontSize());
+        fontPanel.add(boldFont = makeBold());
+        fontPanel.add(italicFont = makeItalic());
+        fontPanel.setPreferredSize(new Dimension(600, 30));
+        fontPanel.setMinimumSize(fontPanel.getPreferredSize());
+        fontPanel.setMaximumSize(fontPanel.getPreferredSize());
+        topPanel.add(fontPanel);
+
+        JPanel colorPanel0 = new JPanel();
+        colorPanel0.setLayout(new BoxLayout(colorPanel0, BoxLayout.Y_AXIS));
+        colorPanel0.setBorder(BorderFactory.createEtchedBorder());
+
+        JPanel colorPanel1 = new JPanel();
+        colorPanel1.setLayout(new BoxLayout(colorPanel1, BoxLayout.X_AXIS));
+        colorPanel1.add(colorChooser = makeColor());
+        JPanel colorSubPanel = new JPanel();
+        colorSubPanel.setBorder(BorderFactory.createEtchedBorder());
+        colorSubPanel.setLayout(new GridLayout(4, 2));
+        colorSubPanel.add(foregroundColor = makeForegroundColor());
+        foregroundColor.setText("Line Color");
+        foregroundColor.setSelected(true);
+        colorSubPanel.add(backgroundColor = makeBackgroundColor());
+        backgroundColor.setText("Fill Color");
+        colorSubPanel.add(labelForegroundColor = makeLabelForegroundColor());
+        labelForegroundColor.setText("Label Color");
+        colorSubPanel.add(labelBackgroundColor = makeLabelBackgroundColor());
+        labelBackgroundColor.setText("Label Fill Color");
+        colorPanel1.add(colorSubPanel);
+        colorPanel0.add(colorPanel1);
+
+        JPanel colorPanel2 = new JPanel();
+        colorPanel2.setLayout(new BoxLayout(colorPanel2, BoxLayout.X_AXIS));
+        colorPanel2.add(new JLabel("Alpha:"));
+        colorPanel2.add(alphaValueSBar);
+        alphaValueSBar.addAdjustmentListener(new AdjustmentListener() {
+            public void adjustmentValueChanged(AdjustmentEvent adjustmentEvent) {
+                if (!noAlphaBounce && !adjustmentEvent.getValueIsAdjusting()) {
+                    System.err.println("Changed");
+                    colorStateChanged();
+                }
+            }
+        });
+
+        colorPanel2.add(new JButton(actions.getRandomColorActionAction()));
+        colorPanel2.add(new JButton(actions.getNoColorActionAction()));
+        colorPanel2.add(new JButton(actions.getApplyColorAction()));
+        colorPanel0.add(colorPanel2);
+
+        topPanel.add(colorPanel0);
+
+        JPanel nodePanel = new JPanel();
+        nodePanel.setLayout(new BoxLayout(nodePanel, BoxLayout.X_AXIS));
+        nodePanel.add(new JLabel("Node size: "));
+        nodePanel.add(nodeSize = makeNodeSize());
+        nodePanel.add(new JLabel("Node shape:"));
+        nodePanel.add(nodeShape = makeNodeShape());
+        topPanel.add(nodePanel);
+
+        JPanel edgePanel = new JPanel();
+        edgePanel.setLayout(new BoxLayout(edgePanel, BoxLayout.X_AXIS));
+        edgePanel.add(new JLabel("Edge width:"));
+        edgePanel.add(edgeWidth = makeEdgeWidth());
+        edgePanel.add(new JLabel("Edge Style:"));
+        edgePanel.add(edgeShape = makeEdgeShape());
+        topPanel.add(edgePanel);
+
+        JPanel labelPanel = new JPanel();
+        labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.X_AXIS));
+        labelPanel.add(new JLabel("Show Labels:"));
+        labelPanel.add(labels = makeLabels());
+        // labels.setText("Show Labels");
+        if (showRotateButtons) {
+            labelPanel.add(new JLabel("   Rotate Node Labels: "));
+            labelPanel.add(rotateLabelsLeft = new JButton(actions.getRotateLabelsLeft()));
+            labelPanel.add(rotateLabelsRight = new JButton(actions.getRotateLabelsRight()));
+        }
+        topPanel.add(labelPanel);
+
+        JPanel bottomPanel = new JPanel();
+        bottomPanel.setLayout(new BorderLayout());
+        bottomPanel.setBorder(BorderFactory.createEtchedBorder());
+        bottomPanel.add(new JButton(actions.getClose()), BorderLayout.EAST);
+
+        JPanel panel = new JPanel();
+        panel.setLayout(new BorderLayout());
+        panel.add(topPanel, BorderLayout.NORTH);
+        panel.add(bottomPanel, BorderLayout.SOUTH);
+        return panel;
+    }
+
+    /**
+     * @return Returns the viewer.
+     */
+    public INodeEdgeFormatable getViewer() {
+        return viewer;
+    }
+
+    private JComboBox makeNodeSize() {
+        Object[] possibleValues = {"1", "2", "3", "4", "5", "6", "7", "8", "10"};
+        JComboBox box = new JComboBox(possibleValues);
+        box.setEditable(true);
+        box.setMinimumSize(box.getPreferredSize());
+        box.setAction(actions.getNodeSize());
+        return box;
+    }
+
+    private JComboBox makeNodeShape() {
+        Object[] possibleValues = {"none", "square", "circle", "triangle", "diamond"};
+        JComboBox box = new JComboBox(possibleValues);
+        box.setMinimumSize(box.getPreferredSize());
+        box.setAction(actions.getNodeShape());
+        return box;
+    }
+
+
+    private JComboBox makeEdgeShape() {
+        Object[] possibleValues = {"angular", "straight", "curved"};
+        JComboBox box = new JComboBox(possibleValues);
+        box.setMinimumSize(box.getPreferredSize());
+        box.setAction(actions.getEdgeShape());
+        return box;
+    }
+
+    private JComboBox makeFont() {
+        JComboBox box = new JComboBox(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames());
+        box.setAction(actions.getFont());
+        box.setMinimumSize(box.getPreferredSize());
+        return box;
+    }
+
+    private JComboBox makeFontSize() {
+        Object[] possibleValues = {"8", "10", "12", "14", "16", "18", "20", "22", "24", "26", "28", "32", "36", "40", "44"};
+        JComboBox box = new JComboBox(possibleValues);
+        box.setEditable(true);
+        box.setAction(actions.getFontSize());
+        box.setMinimumSize(box.getPreferredSize());
+        return box;
+    }
+
+    private JCheckBox makeBold() {
+        JCheckBox box = new JCheckBox("Bold");
+        box.setAction(actions.getNodeFontBold());
+        return box;
+    }
+
+    private JCheckBox makeItalic() {
+        JCheckBox box = new JCheckBox("Italic");
+        box.setAction(actions.getNodeFontItalic());
+        return box;
+    }
+
+
+    private JCheckBox makeLabels() {
+        JCheckBox box = new JCheckBox();
+        box.setAction(actions.getShowLabels(box));
+        return box;
+    }
+
+    private JCheckBox makeForegroundColor() {
+        JCheckBox cbox = new JCheckBox();
+        cbox.setAction(actions.getForegroundColorAction(cbox));
+        return cbox;
+    }
+
+    private JCheckBox makeBackgroundColor() {
+        JCheckBox cbox = new JCheckBox();
+        cbox.setAction(actions.getBackgroundColorAction(cbox));
+        return cbox;
+    }
+
+    private JCheckBox makeLabelForegroundColor() {
+        JCheckBox cbox = new JCheckBox();
+        cbox.setAction(actions.getLabelForegroundColorAction(cbox));
+        return cbox;
+    }
+
+    private JCheckBox makeLabelBackgroundColor() {
+        JCheckBox cbox = new JCheckBox();
+        cbox.setAction(actions.getLabelBackgroundColorAction(cbox));
+        return cbox;
+    }
+
+    private JColorChooser makeColor() {
+        final JColorChooser chooser = ChooseColorDialog.colorChooser;
+
+        chooser.setPreviewPanel(new JPanel());
+
+        chooser.getSelectionModel().addChangeListener(new ChangeListener() {
+            public void stateChanged(ChangeEvent ev) {
+                colorStateChanged();
+            }
+        });
+        return chooser;
+    }
+
+    private void colorStateChanged() {
+        boolean changed = false;
+        Color color = getColor();
+        if (viewer.hasSelectedNodes()) {
+            if (foregroundColor.isSelected()) {
+                if (viewer.setColorSelectedNodes(color))
+                    changed = true;
+            }
+            if (backgroundColor.isSelected()) {
+                if (viewer.setBackgroundColorSelectedNodes(color)) changed = true;
+            }
+            if (labelForegroundColor.isSelected()) {
+                if (viewer.setLabelColorSelectedNodes(color)) changed = true;
+            }
+            if (labelBackgroundColor.isSelected()) {
+                if (viewer.setLabelBackgroundColorSelectedNodes(color)) changed = true;
+            }
+            if (changed)
+                fireNodeFormatChanged(viewer.getSelectedNodes());
+        }
+        if (viewer.hasSelectedEdges()) {
+            if (foregroundColor.isSelected()) {
+                if (viewer.setColorSelectedEdges(color)) changed = true;
+            }
+            if (labelForegroundColor.isSelected()) {
+                if (viewer.setLabelColorSelectedEdges(color)) changed = true;
+            }
+            if (labelBackgroundColor.isSelected()) {
+                if (viewer.setLabelBackgroundColorSelectedEdges(color)) changed = true;
+            }
+            if (changed)
+                fireEdgeFormatChanged(viewer.getSelectedEdges());
+        }
+        if (changed) {
+            dir.setDirty(true);
+            viewer.repaint();
+        }
+    }
+
+    private JComboBox makeEdgeWidth() {
+        Object[] possibleValues = {"1", "2", "3", "4", "5", "6", "7", "8", "10"};
+        JComboBox box = new JComboBox(possibleValues);
+        box.setEditable(true);
+        box.setMinimumSize(box.getPreferredSize());
+        box.setAction(actions.getEdgeWidth());
+        return box;
+    }
+
+    /**
+     * gets the title of this viewer
+     *
+     * @return title
+     */
+    public String getTitle() {
+        return frame.getTitle();
+    }
+
+    /**
+     * fire node format changed
+     *
+     * @param nodes
+     */
+    void fireNodeFormatChanged(NodeSet nodes) {
+        if (nodes != null && nodes.size() > 0) {
+            for (Object formatterListener : formatterListeners) {
+                IFormatterListener listener = (IFormatterListener) formatterListener;
+                listener.nodeFormatChanged(nodes);
+            }
+        }
+    }
+
+    /**
+     * fire edge format changed
+     *
+     * @param edges
+     */
+    void fireEdgeFormatChanged(EdgeSet edges) {
+        if (edges != null && edges.size() > 0) {
+            for (Object formatterListener : formatterListeners) {
+                IFormatterListener listener = (IFormatterListener) formatterListener;
+                listener.edgeFormatChanged(edges);
+            }
+        }
+    }
+
+    /**
+     * add a formatter listener
+     *
+     * @param listener
+     */
+    public void addFormatterListener(IFormatterListener listener) {
+        formatterListeners.add(listener);
+    }
+
+    /**
+     * remove a formatter listener
+     *
+     * @param listener
+     */
+    public void removeFormatterListener(IFormatterListener listener) {
+        formatterListeners.remove(listener);
+    }
+
+    public void saveFontAsDefault() {
+        try {
+            String family = fontName.getSelectedItem().toString();
+            int size = Integer.parseInt(fontSize.getSelectedItem().toString());
+            if (size > 0) {
+                boolean bold = boldFont.isSelected();
+                boolean italics = italicFont.isSelected();
+                int style = 0;
+                if (bold)
+                    style += Font.BOLD;
+                if (italics)
+                    style += Font.ITALIC;
+                ProgramProperties.put(ProgramProperties.DEFAULT_FONT, family, style, size);
+            }
+        } catch (Exception ex) {
+        }
+    }
+
+    public JColorChooser getColorChooser() {
+        return colorChooser;
+    }
+
+    public static Formatter getInstance() {
+        return instance;
+    }
+
+    public static void setInstance(Formatter instance) {
+        Formatter.instance = instance;
+    }
+
+    public IDirector getDir() {
+        return dir;
+    }
+
+    public CommandManager getCommandManager() {
+        return null;
+    }
+
+    /**
+     * is viewer currently locked?
+     *
+     * @return true, if locked
+     */
+    public boolean isLocked() {
+        return isLocked;
+    }
+
+    protected Color getColor() {
+        if (alphaValueSBar.getValue() == 255)
+            return colorChooser.getColor();
+        else {
+            Color color = colorChooser.getColor();
+            return new Color(color.getRed(), color.getGreen(), color.getBlue(), alphaValueSBar.getValue());
+        }
+    }
+
+    /**
+     * get the name of the class
+     *
+     * @return class name
+     */
+    @Override
+    public String getClassName() {
+        return "Formatter";
+    }
+}
diff --git a/src/jloda/gui/format/FormatterActions.java b/src/jloda/gui/format/FormatterActions.java
new file mode 100644
index 0000000..cf5831c
--- /dev/null
+++ b/src/jloda/gui/format/FormatterActions.java
@@ -0,0 +1,871 @@
+/**
+ * FormatterActions.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.format;
+
+import jloda.export.TransferableGraphic;
+import jloda.graphview.EdgeView;
+import jloda.graphview.INodeEdgeFormatable;
+import jloda.graphview.NodeView;
+import jloda.gui.director.IDirector;
+import jloda.util.Alert;
+import jloda.util.ResourceManager;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * actions associated with a node-edge-configurator window
+ */
+public class FormatterActions {
+    public final static String CHECKBOXITEM = "CheckBox";
+    public final static String DEPENDS_ON_NODESELECTION = "NSEL";
+    public final static String DEPENDS_ON_ONE_NODE_OR_EDGE = "ONORE";
+    public final static String DEPENDS_ON_NODE_OR_EDGE = "NORE";
+    public final static String DEPENDS_ON_XYLOCKED = "LOCK";
+    public final static String DEPENDS_ON_NOT_XYLOCKED = "NLOCK";
+    public final static String DEPENDS_ON_EDGESELECTION = "ESEL";
+    public final static String TEXTAREA = "TA"; // text area object
+    public final static String CRITICAL = "Critical"; // is action critical? bool
+
+    private final Formatter formatter;
+    private IDirector dir;
+    private final List<AbstractAction> all = new LinkedList<>();
+    private INodeEdgeFormatable viewer;
+
+    private boolean ignore = false; // ignore firing when in update only of controls
+
+    /**
+     * constructor
+     *
+     * @param formatter
+     * @param dir
+     */
+    FormatterActions(Formatter formatter, IDirector dir, INodeEdgeFormatable viewer) {
+        this.formatter = formatter;
+        this.dir = dir;
+        this.viewer = viewer;
+    }
+
+    public void setViewer(IDirector dir, INodeEdgeFormatable viewer) {
+        this.viewer = viewer;
+        this.dir = dir;
+    }
+
+    /**
+     * enable or disable critical actions
+     *
+     * @param on show or hide?
+     */
+    public void setEnableCritical(boolean on) {
+        if (viewer == null)
+            on = false;
+        for (Action action : all) {
+            if (viewer == null || action.getValue(CRITICAL) != null
+                    && (((Boolean) action.getValue(CRITICAL))).equals(Boolean.TRUE))
+                action.setEnabled(on);
+        }
+        if (on)
+            updateEnableState();
+    }
+
+    /**
+     * This is where we update the enable state of all actions!
+     */
+    public void updateEnableState() {
+        for (AbstractAction action : all) {
+            Boolean dependsOnNodeSelection = (Boolean) action.getValue(DEPENDS_ON_NODESELECTION);
+            Boolean dependsOnEdgeSelection = (Boolean) action.getValue(DEPENDS_ON_EDGESELECTION);
+            Boolean dependsOnOneNodeOrEdge = (Boolean) action.getValue(DEPENDS_ON_ONE_NODE_OR_EDGE);
+            Boolean dependsOnNodeOrEdge = (Boolean) action.getValue(DEPENDS_ON_NODE_OR_EDGE);
+            Boolean dependsOnXYLocked = (Boolean) action.getValue(DEPENDS_ON_XYLOCKED);
+
+            action.setEnabled(true);
+            if (dependsOnNodeSelection != null && dependsOnNodeSelection) {
+                boolean enable = (viewer.hasSelectedNodes());
+                action.setEnabled(enable);
+            }
+            if (dependsOnEdgeSelection != null && dependsOnEdgeSelection) {
+                boolean enable = (viewer.hasSelectedEdges());
+                action.setEnabled(enable);
+            }
+            if (dependsOnXYLocked != null && dependsOnXYLocked) {
+                action.setEnabled(viewer.getLockXYScale());
+            }
+            if (dependsOnNodeOrEdge != null && dependsOnNodeOrEdge) {
+                boolean enable = (viewer.hasSelectedNodes()) || (viewer.hasSelectedEdges());
+                action.setEnabled(enable);
+            }
+        }
+    }
+
+    /**
+     * returns all actions
+     *
+     * @return actions
+     */
+    public List getAll() {
+        return all;
+    }
+
+    // here we define the configurator window specific actions:
+
+    private AbstractAction close;
+
+    /**
+     * close this viewer
+     *
+     * @return close action
+     */
+    public AbstractAction getClose() {
+        AbstractAction action = close;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                dir.removeViewer(formatter);
+                formatter.getFrame().setVisible(false);
+                formatter.getFrame().dispose();
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Close");
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_W,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+        action.putValue(AbstractAction.MNEMONIC_KEY, new Integer('C'));
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Close this window");
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("Close16.gif"));
+        // close is critical because we can't easily kill the worker thread
+
+        all.add(action);
+        return close = action;
+    }
+
+    private AbstractAction edgeWidth;
+
+    public AbstractAction getEdgeWidth() {
+        AbstractAction action = edgeWidth;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                if (!ignore) {
+                    Object selectedValue = ((JComboBox) event.getSource()).getSelectedItem();
+                    if (selectedValue != null) {
+                        byte size = 1;
+                        try {
+                            size = Byte.parseByte((String) selectedValue);
+                        } catch (Exception ex) {
+                        }
+                        viewer.setLineWidthSelectedEdges(size);
+                        dir.setDirty(true);
+                        formatter.fireEdgeFormatChanged(viewer.getSelectedEdges());
+
+                    }
+                    viewer.repaint();
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Edge Width");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Set edge width");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(DEPENDS_ON_EDGESELECTION, Boolean.TRUE);
+        all.add(action);
+        return edgeWidth = action;
+    }
+
+    private AbstractAction showLabels;
+
+    public AbstractAction getShowLabels(final JCheckBox cbox) {
+        AbstractAction action = showLabels;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                if (!ignore) {
+                    viewer.setLabelVisibleSelectedNodes(cbox.isSelected());
+                    formatter.fireNodeFormatChanged(viewer.getSelectedNodes());
+                    viewer.setLabelVisibleSelectedEdges(cbox.isSelected());
+                    formatter.fireEdgeFormatChanged(viewer.getSelectedEdges());
+                    viewer.repaint();
+                    dir.setDirty(true);
+                }
+            }
+        };
+        //action.putValue(AbstractAction.NAME, "Labels");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Show labels");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(DEPENDS_ON_NODE_OR_EDGE, Boolean.TRUE);
+        all.add(action);
+        return showLabels = action;
+    }
+
+    private AbstractAction font;
+
+    public AbstractAction getFont() {
+        AbstractAction action = font;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                if (!ignore) {
+                    Object selectedValue = ((JComboBox) event.getSource()).getSelectedItem();
+                    if (selectedValue != null) {
+                        String family = selectedValue.toString();
+                        boolean changed = false;
+                        if (setNodeFont(family, -1, -1, -1))
+                            changed = true;
+                        if (setEdgeFont(family, -1, -1, -1))
+                            changed = true;
+                        if (changed) {
+                            viewer.repaint();
+                            dir.setDirty(true);
+                        }
+                    }
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Font");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Set label font");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(DEPENDS_ON_NODE_OR_EDGE, Boolean.TRUE);
+        all.add(action);
+        return font = action;
+    }
+
+    private AbstractAction fontSize;
+
+    public Action getFontSize() {
+        AbstractAction action = fontSize;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                if (!ignore && event != null && (event.getActionCommand() == null || event.getActionCommand().equals("comboBoxChanged"))) {
+                    Object source = event.getSource();
+                    if (source != null && source instanceof JComboBox) {
+                        Object selectedValue = ((JComboBox) event.getSource()).getSelectedItem();
+                        if (selectedValue != null) {
+                            int size;
+                            try {
+                                size = Integer.parseInt((String) selectedValue);
+                            } catch (NumberFormatException e) {
+                                new Alert(formatter.getFrame(), "Font Size must be an integer! Size set to 10.");
+                                size = 10;
+                                ((JComboBox) event.getSource()).setSelectedItem("10");
+                            }
+                            boolean changed = false;
+                            if (setNodeFont(null, -1, -1, size))
+                                changed = true;
+                            if (setEdgeFont(null, -1, -1, size))
+                                changed = true;
+                            if (changed) {
+                                viewer.repaint();
+                                dir.setDirty(true);
+                            }
+                        }
+                    }
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Font Size");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Set label font size");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(DEPENDS_ON_NODE_OR_EDGE, Boolean.TRUE);
+        all.add(action);
+        return fontSize = action;
+    }
+
+    private AbstractAction bold;
+
+    public Action getNodeFontBold() {
+        AbstractAction action = bold;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                if (!ignore) {
+                    int state = ((JCheckBox) event.getSource()).isSelected() ? 1 : 0;
+                    boolean changed = false;
+                    if (setNodeFont(null, state, -1, -1))
+                        changed = true;
+                    if (setEdgeFont(null, state, -1, -1))
+                        changed = true;
+                    if (changed) {
+                        viewer.repaint();
+                        dir.setDirty(true);
+                    }
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Bold");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Set label font bold");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(DEPENDS_ON_NODE_OR_EDGE, Boolean.TRUE);
+        all.add(action);
+        return bold = action;
+    }
+
+    private AbstractAction italic;
+
+    public Action getNodeFontItalic() {
+        AbstractAction action = italic;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                if (!ignore) {
+                    int state = ((JCheckBox) event.getSource()).isSelected() ? 1 : 0;
+                    boolean changed = false;
+                    if (setNodeFont(null, -1, state, -1))
+                        changed = true;
+                    if (setEdgeFont(null, -1, state, -1))
+                        changed = true;
+                    if (changed) {
+                        viewer.repaint();
+                        dir.setDirty(true);
+                    }
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Italic");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Set label font italic");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(DEPENDS_ON_NODE_OR_EDGE, Boolean.TRUE);
+        all.add(action);
+        return italic = action;
+    }
+
+    private AbstractAction nodeSize;
+
+    public AbstractAction getNodeSize() {
+        AbstractAction action = nodeSize;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                if (!ignore) {
+                    Object selectedValue = ((JComboBox) event.getSource()).getSelectedItem();
+                    if (selectedValue != null) {
+                        Byte size = 1;
+                        try {
+                            size = Byte.parseByte((String) selectedValue);
+                        } catch (Exception ex) {
+                        }
+                        viewer.setWidthSelectedNodes(size);
+                        viewer.setHeightSelectedNodes(size);
+                        formatter.fireNodeFormatChanged(viewer.getSelectedNodes());
+                    }
+                    viewer.repaint();
+                    dir.setDirty(true);
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Node Size");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Set node size");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(DEPENDS_ON_NODESELECTION, Boolean.TRUE);
+        all.add(action);
+        return nodeSize = action;
+    }
+
+    private AbstractAction nodeShape;
+
+    public Action getNodeShape() {
+
+        AbstractAction action = nodeShape;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                if (!ignore) {
+                    Object selectedValue = ((JComboBox) event.getSource()).getSelectedItem();
+                    if (selectedValue != null) {
+                        byte shape = -1;
+                        if (selectedValue == "none") shape = 0;
+                        if (selectedValue == "square") shape = NodeView.RECT_NODE;
+                        if (selectedValue == "circle") shape = NodeView.OVAL_NODE;
+                        if (selectedValue == "triangle") shape = NodeView.TRIANGLE_NODE;
+                        if (selectedValue == "diamond") shape = NodeView.DIAMOND_NODE;
+                        viewer.setShapeSelectedNodes(shape);
+                        formatter.fireNodeFormatChanged(viewer.getSelectedNodes());
+                    }
+                    viewer.repaint();
+                    dir.setDirty(true);
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Node Shape");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Set node shape");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(DEPENDS_ON_NODESELECTION, Boolean.TRUE);
+        all.add(action);
+        return nodeShape = action;
+    }
+
+    private AbstractAction edgeShape;
+
+    public Action getEdgeShape() {
+
+        AbstractAction action = edgeShape;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                if (!ignore) {
+                    Object selectedValue = ((JComboBox) event.getSource()).getSelectedItem();
+                    if (selectedValue != null) {
+                        // todo: this kind of operation should only apply to uncollapsed nodes
+                        byte shape = -1;
+                        if (selectedValue == "angular") shape = EdgeView.POLY_EDGE;
+                        if (selectedValue == "straight") shape = EdgeView.STRAIGHT_EDGE;
+                        if (selectedValue == "curved") shape = EdgeView.QUAD_EDGE;
+                        viewer.setShapeSelectedEdges(shape);
+                        formatter.fireEdgeFormatChanged(viewer.getSelectedEdges());
+                    }
+                    viewer.repaint();
+                    dir.setDirty(true);
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Edge Shape");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Set edge shape");
+        action.putValue(DEPENDS_ON_EDGESELECTION, Boolean.TRUE);
+        all.add(action);
+        return edgeShape = action;
+    }
+
+    private AbstractAction rotateLabelsLeft = getRotateLabelsLeft();
+
+    public AbstractAction getRotateLabelsLeft() {
+        AbstractAction action = rotateLabelsLeft;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                viewer.rotateLabelsSelectedNodes(-1);
+                formatter.fireNodeFormatChanged(viewer.getSelectedNodes());
+                viewer.rotateLabelsSelectedEdges(-1);
+                formatter.fireEdgeFormatChanged(viewer.getSelectedEdges());
+                dir.setDirty(true);
+                viewer.repaint();
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Left");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Rotate labels left");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("RotateLeft16.gif"));
+        action.putValue(DEPENDS_ON_NODESELECTION, Boolean.TRUE);
+        all.add(action);
+        return rotateLabelsLeft = action;
+    }
+
+    private AbstractAction rotateLabelsRight = getRotateLabelsRight();
+
+    public AbstractAction getRotateLabelsRight() {
+        AbstractAction action = rotateLabelsRight;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                viewer.rotateLabelsSelectedNodes(1);
+                formatter.fireNodeFormatChanged(viewer.getSelectedNodes());
+                viewer.rotateLabelsSelectedEdges(1);
+                formatter.fireEdgeFormatChanged(viewer.getSelectedEdges());
+
+                dir.setDirty(true);
+                viewer.repaint();
+
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Right");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Rotate labels right");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("RotateRight16.gif"));
+        action.putValue(DEPENDS_ON_NODESELECTION, Boolean.TRUE);
+        all.add(action);
+        return rotateLabelsRight = action;
+    }
+
+    /**
+     * get ignore firing of events
+     *
+     * @return true, if we are currently ignoring firing of events
+     */
+    public boolean getIgnore() {
+        return ignore;
+    }
+
+    /**
+     * set ignore firing of events
+     *
+     * @param ignore
+     */
+    public void setIgnore(boolean ignore) {
+        this.ignore = ignore;
+    }
+
+    /**
+     * set the edge font
+     *
+     * @param family
+     * @param bold
+     * @param italics
+     * @param size
+     * @return true, if anything changed
+     */
+    public boolean setEdgeFont(String family, int bold, int italics, int size) {
+        if (viewer.setFontSelectedEdges(family, bold, italics, size)) {
+            formatter.fireEdgeFormatChanged(viewer.getSelectedEdges());
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * set the node font
+     *
+     * @param family
+     * @param bold
+     * @param italics
+     * @param size
+     * @return true, if anything changed
+     */
+    public boolean setNodeFont(String family, int bold, int italics, final int size) {
+        if (viewer.setFontSelectedNodes(family, bold, italics, size)) {
+            formatter.fireNodeFormatChanged(viewer.getSelectedNodes());
+            return true;
+        }
+        return false;
+    }
+
+    private AbstractAction cut = getCut();
+
+    public AbstractAction getCut() {
+        AbstractAction action = cut;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                TransferableGraphic tg = new TransferableGraphic(viewer.getPanel());
+                Toolkit.getDefaultToolkit().getSystemClipboard().setContents(tg, tg);
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Cut");
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_X,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() /*| java.awt.event.InputEvent.SHIFT_MASK*/));
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Cut");
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Cut16.gif"));
+        action.putValue(CRITICAL, Boolean.TRUE);
+        all.add(action);
+        return cut = action;
+    }
+
+    private AbstractAction copy = getCopy();
+
+    public AbstractAction getCopy() {
+        AbstractAction action = copy;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+
+                TransferableGraphic tg = new TransferableGraphic(viewer.getPanel(), viewer.getScrollPane());
+                Toolkit.getDefaultToolkit().getSystemClipboard().setContents(tg, tg);
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Copy");
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_C,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() /*| java.awt.event.InputEvent.SHIFT_MASK*/));
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Copy graph to clipboard");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Copy16.gif"));
+        all.add(action);
+        return copy = action;
+    }
+
+
+    private AbstractAction paste = getPaste();
+
+    public AbstractAction getPaste() {
+        AbstractAction action = paste;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                TransferableGraphic tg = new TransferableGraphic(viewer.getPanel());
+                Toolkit.getDefaultToolkit().getSystemClipboard().setContents(tg, tg);
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Paste");
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_V,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Paste");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Paste16.gif"));
+        all.add(action);
+        return paste = action;
+    }
+
+    private AbstractAction saveDefaultFont = getSaveDefaultFont();
+
+    public AbstractAction getSaveDefaultFont() {
+        AbstractAction action = saveDefaultFont;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                formatter.saveFontAsDefault();
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Set Font as Default");
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_F,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Set current font as default");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("Empty16.gif"));
+        all.add(action);
+        return saveDefaultFont = action;
+    }
+
+    private AbstractAction foregroundColorAction;
+
+    public AbstractAction getForegroundColorAction(final JCheckBox cbox) {
+        AbstractAction action = foregroundColorAction;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+            }
+        };
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Color");
+        action.putValue(DEPENDS_ON_NODE_OR_EDGE, Boolean.TRUE);
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(CHECKBOXITEM, cbox);
+        all.add(action);
+        return foregroundColorAction = action;
+    }
+
+    private AbstractAction backgroundColorAction;
+
+    public AbstractAction getBackgroundColorAction(final JCheckBox cbox) {
+        AbstractAction action = backgroundColorAction;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+            }
+        };
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Back Color");
+        action.putValue(CHECKBOXITEM, cbox);
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(DEPENDS_ON_NODE_OR_EDGE, Boolean.TRUE);
+        action.putValue(CHECKBOXITEM, cbox);
+        all.add(action);
+        return backgroundColorAction = action;
+    }
+
+    private AbstractAction labelForegroundColorAction;
+
+    public AbstractAction getLabelForegroundColorAction(final JCheckBox cbox) {
+        AbstractAction action = labelForegroundColorAction;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+            }
+        };
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Label Color");
+        action.putValue(CHECKBOXITEM, cbox);
+        action.putValue(DEPENDS_ON_NODE_OR_EDGE, Boolean.TRUE);
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(CHECKBOXITEM, cbox);
+        all.add(action);
+        return labelForegroundColorAction = action;
+    }
+
+    private AbstractAction labelBackgroundColorAction;
+
+    public AbstractAction getLabelBackgroundColorAction(final JCheckBox cbox) {
+        AbstractAction action = labelBackgroundColorAction;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+            }
+        };
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Label Back Color");
+        action.putValue(DEPENDS_ON_NODE_OR_EDGE, Boolean.TRUE);
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(CHECKBOXITEM, cbox);
+        all.add(action);
+        return labelBackgroundColorAction = action;
+    }
+
+    private AbstractAction randomColorActionAction;
+
+    public AbstractAction getRandomColorActionAction() {
+        AbstractAction action = randomColorActionAction;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                boolean changed = false;
+                boolean fore = ((JCheckBox) foregroundColorAction.getValue(CHECKBOXITEM)).isSelected();
+                boolean back = ((JCheckBox) backgroundColorAction.getValue(CHECKBOXITEM)).isSelected();
+                boolean labelfore = ((JCheckBox) labelForegroundColorAction.getValue(CHECKBOXITEM)).isSelected();
+                boolean labelback = ((JCheckBox) labelBackgroundColorAction.getValue(CHECKBOXITEM)).isSelected();
+
+                if (viewer.hasSelectedNodes()) {
+                    viewer.setRandomColorsSelectedNodes(fore, back, labelfore, labelback);
+                    formatter.fireNodeFormatChanged(viewer.getSelectedNodes());
+                    changed = true;
+                }
+
+                if (viewer.hasSelectedEdges()) {
+                    viewer.setRandomColorsSelectedEdges(fore, labelfore, labelback);
+                    formatter.fireEdgeFormatChanged(viewer.getSelectedEdges());
+                    changed = true;
+                }
+
+                if (changed) {
+                    dir.setDirty(true);
+                    viewer.repaint();
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Random Colors");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Randomly color nodes, edges and labels");
+        action.putValue(DEPENDS_ON_NODE_OR_EDGE, Boolean.TRUE);
+        all.add(action);
+        return randomColorActionAction = action;
+    }
+
+    private AbstractAction noColorActionAction;
+
+    public AbstractAction getNoColorActionAction() {
+        AbstractAction action = noColorActionAction;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                boolean changed = false;
+                boolean fore = ((JCheckBox) foregroundColorAction.getValue(CHECKBOXITEM)).isSelected();
+                boolean back = ((JCheckBox) backgroundColorAction.getValue(CHECKBOXITEM)).isSelected();
+                boolean labelfore = ((JCheckBox) labelForegroundColorAction.getValue(CHECKBOXITEM)).isSelected();
+                boolean labelback = ((JCheckBox) labelBackgroundColorAction.getValue(CHECKBOXITEM)).isSelected();
+
+                if (viewer.hasSelectedNodes()) {
+                    if (fore)
+                        viewer.setColorSelectedNodes(null);
+                    if (back)
+                        viewer.setBackgroundColorSelectedNodes(null);
+                    if (labelfore)
+                        viewer.setLabelColorSelectedNodes(null);
+                    if (labelback)
+                        viewer.setLabelBackgroundColorSelectedNodes(null);
+                    changed = true;
+                    formatter.fireNodeFormatChanged(viewer.getSelectedNodes());
+                    viewer.selectAllNodes(false);
+                }
+                if (viewer.hasSelectedEdges()) {
+                    if (fore)
+                        viewer.setColorSelectedEdges(null);
+                    if (labelfore)
+                        viewer.setLabelColorSelectedEdges(null);
+                    if (labelback)
+                        viewer.setLabelBackgroundColorSelectedEdges(null);
+                    changed = true;
+                    formatter.fireEdgeFormatChanged(viewer.getSelectedEdges());
+                    viewer.selectAllEdges(false);
+                }
+
+                if (changed) {
+                    dir.setDirty(true);
+                    viewer.repaint();
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Invisible");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Set color to invisible");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(DEPENDS_ON_NODE_OR_EDGE, Boolean.TRUE);
+        all.add(action);
+        return noColorActionAction = action;
+    }
+
+
+    private AbstractAction applyColorAction;
+
+    public AbstractAction getApplyColorAction() {
+        AbstractAction action = applyColorAction;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                Color color = formatter.getColor();
+                boolean changed = false;
+                boolean fore = ((JCheckBox) foregroundColorAction.getValue(CHECKBOXITEM)).isSelected();
+                boolean back = ((JCheckBox) backgroundColorAction.getValue(CHECKBOXITEM)).isSelected();
+                boolean labelfore = ((JCheckBox) labelForegroundColorAction.getValue(CHECKBOXITEM)).isSelected();
+                boolean labelback = ((JCheckBox) labelBackgroundColorAction.getValue(CHECKBOXITEM)).isSelected();
+
+                if (viewer.hasSelectedNodes()) {
+                    if (fore)
+                        viewer.setColorSelectedNodes(color);
+                    if (back)
+                        viewer.setBackgroundColorSelectedNodes(color);
+                    if (labelfore)
+                        viewer.setLabelColorSelectedNodes(color);
+                    if (labelback)
+                        viewer.setLabelBackgroundColorSelectedNodes(color);
+                    changed = true;
+                    formatter.fireNodeFormatChanged(viewer.getSelectedNodes());
+                }
+                if (viewer.hasSelectedEdges()) {
+                    if (fore)
+                        viewer.setColorSelectedEdges(color);
+                    if (labelfore)
+                        viewer.setLabelColorSelectedEdges(color);
+                    if (labelback)
+                        viewer.setLabelBackgroundColorSelectedEdges(color);
+                    changed = true;
+                    formatter.fireEdgeFormatChanged(viewer.getSelectedEdges());
+                }
+
+                if (changed) {
+                    dir.setDirty(true);
+                    viewer.repaint();
+                }
+
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Again");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Apply the last chosen color again");
+        action.putValue(CRITICAL, Boolean.TRUE);
+        action.putValue(DEPENDS_ON_NODE_OR_EDGE, Boolean.TRUE);
+        all.add(action);
+        return applyColorAction = action;
+    }
+}
diff --git a/src/jloda/gui/format/FormatterMenuBar.java b/src/jloda/gui/format/FormatterMenuBar.java
new file mode 100644
index 0000000..fd7154d
--- /dev/null
+++ b/src/jloda/gui/format/FormatterMenuBar.java
@@ -0,0 +1,113 @@
+/**
+ * FormatterMenuBar.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.format;
+
+/**
+ * Formatter menu bar
+ * Daniel Huson, 7.2010
+ *
+ */
+
+import jloda.gui.director.IDirector;
+import jloda.util.MenuMnemonics;
+import jloda.util.ProgramProperties;
+
+import javax.swing.*;
+
+
+/**
+ * menubar for node/edge configurator
+ */
+public class FormatterMenuBar extends JMenuBar {
+    private final Formatter conf;
+    private IDirector dir;
+
+    /**
+     * construtor
+     *
+     * @param conf
+     * @param dir
+     */
+    public FormatterMenuBar(Formatter conf, IDirector dir) {
+        super();
+
+        this.conf = conf;
+        this.dir = dir;
+
+        addFileMenu();
+        addEditMenu();
+        addOptionsMenu();
+    }
+
+    public void setViewer(IDirector dir) {
+        this.dir = dir;
+    }
+
+    /**
+     * returns the tool bar for this simple viewer
+     */
+    private void addFileMenu() {
+        JMenu menu = new JMenu("File");
+
+        // viewer version opens new browser, dir version doesn't
+        // menu.add(viewer.getActions().getOpenFile());
+
+        //menu.addSeparator();
+
+        menu.add(conf.getActions().getClose());
+        if (!ProgramProperties.isMacOS()) {
+            menu.addSeparator();
+            menu.add(dir.getMainViewer().getQuit());
+        }
+        MenuMnemonics.setMnemonics(menu);
+        add(menu);
+    }
+
+    private void addEditMenu() {
+        JMenu menu = new JMenu("Edit");
+
+        //menu.addSeparator();
+        JMenuItem menuItem = new JMenuItem(conf.getActions().getCut());
+        menuItem.setText("Cut");
+        menu.add(menuItem);
+        menuItem = new JMenuItem(conf.getActions().getCopy());
+        menuItem.setText("Copy");
+        menu.add(menuItem);
+        menuItem = new JMenuItem(conf.getActions().getPaste());
+        menuItem.setText("Paste");
+        menu.add(menuItem);
+        menu.addSeparator();
+        //menuItem = new JMenuItem(viewer.getActions().getSelectAll());
+        //menuItem.setText("Select All");
+        menu.add(menuItem);
+        MenuMnemonics.setMnemonics(menu);
+        add(menu);
+    }
+
+    private void addOptionsMenu() {
+        JMenu menu = new JMenu("Options");
+        menu.add(conf.getActions().getSaveDefaultFont());
+        MenuMnemonics.setMnemonics(menu);
+        add(menu);
+    }
+}
+
+
+
diff --git a/src/jloda/gui/format/IFormatterListener.java b/src/jloda/gui/format/IFormatterListener.java
new file mode 100644
index 0000000..c3b3948
--- /dev/null
+++ b/src/jloda/gui/format/IFormatterListener.java
@@ -0,0 +1,33 @@
+/**
+ * IFormatterListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.format;
+
+import jloda.graph.EdgeSet;
+import jloda.graph.NodeSet;
+
+/**
+ * listens for formatter events
+ * Daniel Huson, 3.2007
+ */
+public interface IFormatterListener {
+    void nodeFormatChanged(NodeSet nodes);
+
+    void edgeFormatChanged(EdgeSet edges);
+}
diff --git a/src/jloda/gui/message/MessageWindow.java b/src/jloda/gui/message/MessageWindow.java
new file mode 100644
index 0000000..6a4e148
--- /dev/null
+++ b/src/jloda/gui/message/MessageWindow.java
@@ -0,0 +1,476 @@
+/**
+ * MessageWindow.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.message;
+
+
+import jloda.gui.find.SearchManager;
+import jloda.util.Alert;
+import jloda.util.Basic;
+import jloda.util.ProgramProperties;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.PrintStream;
+
+/**
+ * message window
+ *
+ * @author huson & Franz
+ *         17.2.2004
+ */
+public class MessageWindow {
+    private final MessageWindowActions actions;
+    private final JFrame frame;
+    private boolean toConsoleWhenHidden = true;
+    private static MessageWindow instance;
+    final public static String SEARCHER_NAME = "Messages";
+
+    public JTextArea textArea = null;
+
+    /**
+     * sets up the message window
+     *
+     * @param icon
+     * @param title
+     * @param parent
+     */
+    public MessageWindow(ImageIcon icon, String title, Component parent) {
+        this(icon, title, parent, true);
+    }
+
+    /**
+     * sets up the message window
+     *
+     * @param icon
+     * @param title
+     * @param parent
+     * @param visible
+     */
+    public MessageWindow(ImageIcon icon, String title, Component parent, boolean visible) {
+        if (getInstance() != null)
+            new Alert("Internal error, multiple instances of MessageWindow");
+        else
+            setInstance(this);
+
+        actions = new MessageWindowActions(this);
+        MessageWindowMenuBar menuBar = new MessageWindowMenuBar(this);
+
+        final JPopupMenu popupMenu = new JPopupMenu();
+        popupMenu.add(actions.getClear());
+        popupMenu.add(actions.getCopy());
+
+        frame = new JFrame();
+        if (icon != null)
+            frame.setIconImage(icon.getImage());
+        frame.setJMenuBar(menuBar);
+        frame.setSize(400, 200);
+        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+
+        setTitle(title);
+
+        frame.getContentPane().add(getPanel());
+        try {
+            if (parent != null) {
+                Point location = parent.getLocation();
+                frame.setSize(new Dimension((int) Math.min(600.0, parent.getSize().getWidth()), 200));
+                frame.setLocation(location.x, location.y + Math.min(600, (int) parent.getSize().getHeight()));
+            }
+        } catch (Exception ex) {
+            frame.setLocationRelativeTo(parent);
+        }
+
+        if (visible) {
+            frame.setVisible(true);
+            startCapturingOutput();
+        }
+
+        frame.addWindowListener(new WindowAdapter() {
+            public void windowClosing(WindowEvent event) {
+                if (toConsoleWhenHidden)
+                    stopCapturingOutput();
+                frame.setVisible(false);
+            }
+
+            public void windowActivated(WindowEvent windowEvent) {
+                startCapturingOutput();
+                SearchManager searchManager = SearchManager.getInstance();
+                if (searchManager != null)
+                    searchManager.chooseTargetForFrame(getTextArea());
+            }
+
+            public void windowOpened(WindowEvent event) {
+                startCapturingOutput();
+            }
+        });
+
+        frame.addMouseListener(new MouseAdapter() {
+            /**
+             * Invoked when a mouse button has been pressed on a component.
+             */
+            public void mousePressed(MouseEvent e) {
+                if (e.isPopupTrigger())
+                    popupMenu.show(frame, e.getX(), e.getY());
+            }
+
+            /**
+             * Invoked when a mouse button has been released on a component.
+             */
+            public void mouseReleased(MouseEvent e) {
+                if (e.isPopupTrigger())
+                    popupMenu.show(frame, e.getX(), e.getY());
+            }
+        });
+
+    }
+
+    /**
+     * sets the title
+     *
+     * @param title
+     */
+    public void setTitle(String title) {
+        frame.setTitle(title);
+    }
+
+    /**
+     * returns the actions object associated with the window
+     *
+     * @return actions
+     */
+    public MessageWindowActions getActions() {
+        return actions;
+    }
+
+
+    /**
+     * returns the frame of the window
+     */
+    public JFrame getFrame() {
+        return frame;
+    }
+
+    private JPanel panel = null;
+
+    /**
+     * gets the content pane
+     *
+     * @return the content pane
+     */
+    private JPanel getPanel() {
+        if (panel != null)
+            return panel;
+        panel = new JPanel();
+        panel.setLayout(new GridBagLayout());
+        GridBagConstraints constraints = new GridBagConstraints();
+        //Insets insets = new Insets(1, 5, 1, 5);
+        //constraints.insets = insets;
+
+        constraints.fill = GridBagConstraints.BOTH;
+        constraints.weightx = 1;
+        constraints.weighty = 1;
+        constraints.gridx = 0;
+        constraints.gridy = 0;
+        constraints.gridwidth = 1;
+        constraints.gridheight = 1;
+
+        textArea = new JTextArea();
+        textArea.setFont(new Font("Courier", Font.PLAIN, 12));
+        textArea.setSelectionColor(ProgramProperties.SELECTION_COLOR);
+
+        AbstractAction action = getActions().getInput();
+        action.putValue(MessageWindowActions.JTEXTAREA, textArea);
+        textArea.setToolTipText((String) action.getValue(AbstractAction.SHORT_DESCRIPTION));
+
+        JScrollPane scrollP = new JScrollPane(textArea);
+
+        // ((JTextArea) comp).addPropertyChangeListener(action);
+        // textArea.setToolTipText((String) action.getValue(AbstractAction.SHORT_DESCRIPTION));
+        panel.add(scrollP, constraints);
+
+        return panel;
+    }
+
+    final PrintStream systemOut = System.out;
+    final PrintStream systemErr = System.err;
+    PrintStream printStream = null;
+
+    /**
+     * start capturing all output to standard out and err
+     */
+    public void startCapturingOutput() {
+        if (printStream == null) {
+            //this is the trick: overload everything in PrintStream
+            //and redirect anything sent to this to the text box
+            printStream = new PrintStream(System.out) {
+                public void println(String x) {
+                    textArea.append(x + "\n");
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void print(String x) {
+                    textArea.append(x);
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void println(Object x) {
+                    textArea.append(x + "\n");
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void print(Object x) {
+                    textArea.append("" + x);
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void println(boolean x) {
+                    textArea.append(x + "\n");
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void print(boolean x) {
+                    textArea.append("" + x);
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void println(int x) {
+                    textArea.append(x + "\n");
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void print(int x) {
+                    textArea.append("" + x);
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void println(float x) {
+                    textArea.append(x + "\n");
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void print(float x) {
+                    textArea.append("" + x);
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void println(char x) {
+                    textArea.append(x + "\n");
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void print(char x) {
+                    textArea.append("" + x);
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void println(double x) {
+                    textArea.append(x + "\n");
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void print(double x) {
+                    textArea.append("" + x);
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void println(char[] x) {
+                    textArea.append(Basic.toString(x) + "\n");
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void print(char[] x) {
+                    textArea.append(Basic.toString(x));
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void println(long x) {
+                    textArea.append(x + "\n");
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void print(long x) {
+                    textArea.append("" + x);
+                    textArea.setCaretPosition(textArea.getText().length());
+                }
+
+                public void write(byte[] buf, int off, int len) {
+                    for (int i = 0; i < len; i++)
+                        write(buf[off + i]);
+                }
+
+                public void write(byte b) {
+                    print((char) b);
+                }
+
+                public void setError() {
+                }
+
+                public boolean checkError() {
+                    return false;
+                }
+
+                public void flush() {
+                }
+            };
+        }
+
+        String collected = Basic.stopCollectingStdErr();
+        if (collected.length() > 0)
+            printStream.print(collected);
+        System.setOut(printStream);
+        System.setErr(printStream);
+    }
+
+    /**
+     * stop capturing output to standard out and err
+     */
+    public void stopCapturingOutput() {
+        System.setOut(systemOut);
+        System.setErr(systemErr);
+    }
+
+    /**
+     * gets the title of this viewer
+     *
+     * @return title
+     */
+    public String getTitle() {
+        return frame.getTitle();
+    }
+
+    /**
+     * show or hide message window
+     *
+     * @param visible
+     */
+    public void setVisible(boolean visible) {
+        if (visible) {
+            frame.setVisible(true);
+            startCapturingOutput();
+        } else {
+            stopCapturingOutput();
+            frame.setVisible(false);
+            if (frame.getDefaultCloseOperation() == WindowConstants.EXIT_ON_CLOSE)
+                System.exit(0);
+        }
+    }
+
+    /**
+     * is visible?
+     */
+    public boolean isVisible() {
+        return frame.isVisible();
+    }
+
+    /**
+     * gets the text area
+     *
+     * @return text area
+     */
+    public JTextArea getTextArea() {
+        return textArea;
+    }
+
+    /**
+     * send all messages to console when window hidden?
+     *
+     * @return true, if messages should go to console, when hidden
+     */
+    public boolean isToConsoleWhenHidden() {
+        return toConsoleWhenHidden;
+    }
+
+    /**
+     * send all messages to console when window is hidden?
+     *
+     * @param toConsoleWhenHidden
+     */
+    public void setToConsoleWhenHidden(boolean toConsoleWhenHidden) {
+        this.toConsoleWhenHidden = toConsoleWhenHidden;
+        if (!frame.isVisible()) {
+            if (toConsoleWhenHidden)
+                stopCapturingOutput();
+            else
+                startCapturingOutput();
+        }
+    }
+
+    /**
+     * gets the instance of the message window, if it already exists.
+     * WIll return nul, if  not yet set
+     *
+     * @return message window or null
+     */
+    public static MessageWindow getInstance() {
+        return instance;
+    }
+
+    /**
+     * sets the instance
+     *
+     * @param instance
+     */
+    public static void setInstance(MessageWindow instance) {
+        MessageWindow.instance = instance;
+    }
+
+    /**
+     * add an item to a menu
+     *
+     * @param action
+     */
+    public void addToMenu(String menuName, AbstractAction action) {
+        if (action != null) {
+            JMenuBar bar = frame.getJMenuBar();
+            for (int i = 0; i < bar.getMenuCount(); i++) {
+                if (bar.getMenu(i).getText() != null && bar.getMenu(i).getText().startsWith(menuName)) {
+                    JMenu menu = bar.getMenu(i);
+                    if (menu.getItemCount() > 0 && menu.getItem(menu.getItemCount() - 1).getText().length() > 0)
+                        menu.addSeparator();
+                    menu.add(action);
+                    return;
+                }
+            }
+        }
+    }
+
+    /**
+     * add an button to a menu
+     *
+     * @param item
+     */
+    public void addToMenu(String menuName, JMenuItem item) {
+        if (item != null) {
+            JMenuBar bar = frame.getJMenuBar();
+            for (int i = 0; i < bar.getMenuCount(); i++) {
+                if (bar.getMenu(i).getText() != null && bar.getMenu(i).getText().startsWith(menuName)) {
+                    JMenu menu = bar.getMenu(i);
+                    if (menu.getItemCount() > 0 && menu.getItem(menu.getItemCount() - 1).getText().length() > 0)
+                        menu.addSeparator();
+                    menu.add(item);
+                    return;
+                }
+            }
+        }
+    }
+}
diff --git a/src/jloda/gui/message/MessageWindowActions.java b/src/jloda/gui/message/MessageWindowActions.java
new file mode 100644
index 0000000..242b2b8
--- /dev/null
+++ b/src/jloda/gui/message/MessageWindowActions.java
@@ -0,0 +1,422 @@
+/**
+ * MessageWindowActions.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.message;
+
+import jloda.util.ResourceManager;
+import jloda.util.TextPrinter;
+
+import javax.swing.*;
+import javax.swing.text.DefaultEditorKit;
+import javax.swing.undo.CannotRedoException;
+import javax.swing.undo.CannotUndoException;
+import javax.swing.undo.UndoManager;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.print.PrinterJob;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.Writer;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * actions associated with a message window
+ *
+ * @author huson
+ *         Date: 17.2.2004
+ */
+public class MessageWindowActions {
+    private final MessageWindow viewer;
+    private final List all = new LinkedList();
+    public static final String JCHECKBOX = "JCHECKBOX";
+    public static final String JTEXTAREA = "JTEXTAREA";
+    public static final String CRITICAL = "CRITICAL";
+
+    public MessageWindowActions(MessageWindow viewer) {
+        this.viewer = viewer;
+    }
+
+    /**
+     * enable or disable critical actions
+     *
+     * @param flag show or hide?
+     */
+    public void setEnableCritical(boolean flag) {
+        // because we don't want to duplicate that code
+    }
+
+    /**
+     * This is where we update the enable state of all actions!
+     */
+    public void updateEnableState() {
+    }
+
+    /**
+     * returns all actions
+     *
+     * @return actions
+     */
+    public List getAll() {
+        return all;
+    }
+
+    // here we define the algorithms window specific actions:
+
+    private AbstractAction printIt;
+
+    /**
+     * print action
+     *
+     * @return print action
+     */
+    public AbstractAction getPrintIt() {
+        AbstractAction action = printIt;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+
+                PrinterJob job = PrinterJob.getPrinterJob();
+
+                TextPrinter printer = new TextPrinter(viewer.textArea.getText(), viewer.textArea.getFont());
+                job.setPrintable(printer);
+                // Put up the dialog box
+                if (job.printDialog()) {
+                    // Print the job if the user didn't cancel printing
+                    try {
+                        job.print();
+                    } catch (Exception ex) {
+                        System.err.println("Print failed: " + ex);
+                    }
+                }
+
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Print...");
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_P,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Print the content");
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Print16.gif"));
+
+        all.add(action);
+        return printIt = action;
+    }
+
+    private AbstractAction close;
+
+    /**
+     * close this viewer
+     *
+     * @return close action
+     */
+    public AbstractAction getClose() {
+        AbstractAction action = close;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                viewer.setVisible(false);
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Close");
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_W,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+        action.putValue(AbstractAction.MNEMONIC_KEY, new Integer('C'));
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Close this viewer");
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("Close16.gif"));
+        // close is critical because we can't easily kill the worker thread
+
+        all.add(action);
+        return close = action;
+    }
+
+    static File lastSaveFile = new File(System.getProperty("user.dir"));
+    private AbstractAction saveFile;
+
+    public AbstractAction getSaveFile() {
+        AbstractAction action = saveFile;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                JFileChooser chooser = new JFileChooser(lastSaveFile);
+                if (chooser.showSaveDialog(viewer.getFrame())
+                        == JFileChooser.APPROVE_OPTION) {
+                    File file = chooser.getSelectedFile();
+
+                    if (file.exists() &&
+                            JOptionPane.showConfirmDialog(null,
+                                    "This file already exists. " +
+                                            "Would you like to overwrite the existing file?",
+                                    "Save File",
+                                    JOptionPane.YES_NO_OPTION) == 1)
+                        return; // overwrite canceled
+
+                    try {
+                        Writer w = new FileWriter(file);
+                        String text = viewer.textArea.getText();
+                        w.write(text);
+                        w.close();
+                    } catch (Exception ex) {
+                        System.err.println("Save failed: " + ex);
+                    }
+                    lastSaveFile = file;
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Save As...");
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke('S',
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Save16.gif"));
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Save as text file");
+
+        all.add(action);
+        return saveFile = action;
+    }
+
+    AbstractAction input;
+
+    AbstractAction getInput() {
+        if (input != null)
+            return input;
+        AbstractAction action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+            }
+        };
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Messages");
+        all.add(action);
+        return input = action;
+    }
+
+    AbstractAction clear;
+
+    public AbstractAction getClear() {
+        if (clear != null)
+            return clear;
+        AbstractAction action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                viewer.textArea.setText("");
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Clear");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Clear the messages");
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+        all.add(action);
+        return clear = action;
+    }
+
+    ////// basic textComponent Actions
+    private AbstractAction undo;
+
+    /**
+     * undo action
+     */
+    public AbstractAction getUndo(final UndoManager undoManager) {
+        AbstractAction action = undo;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                try {
+                    undoManager.undo();
+                } catch (CannotUndoException ex) {
+                }
+                updateUndoRedo(undoManager);
+            }
+        };
+        action.setEnabled(false);
+
+        action.putValue(AbstractAction.NAME, "Undo");
+        action.putValue(AbstractAction.MNEMONIC_KEY, new Integer('U'));
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Z,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+        // quit.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("quit"));
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Undo");
+
+        action.putValue(CRITICAL, Boolean.TRUE);
+
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Undo16.gif"));
+
+        //all.add(action);
+        return undo = action;
+    }//End of getUndo
+
+    /**
+     * updates the undo action
+     */
+    public void updateUndoRedo(UndoManager undoManager) {
+        if (undoManager.canUndo()) {
+            undo.setEnabled(true);
+        } else {
+            undo.setEnabled(false);
+        }
+        if (undoManager.canRedo()) {
+            redo.setEnabled(true);
+        } else {
+            redo.setEnabled(false);
+        }
+    }
+
+    private AbstractAction redo;
+
+    /**
+     * redo action
+     */
+    public AbstractAction getRedo(final UndoManager undoManager) {
+        AbstractAction action = redo;
+        if (action != null) return action;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                try {
+                    undoManager.redo();
+                } catch (CannotRedoException ex) {
+                }
+                updateUndoRedo(undoManager);
+            }
+        };
+        action.setEnabled(false);
+
+        action.putValue(AbstractAction.NAME, "Redo");
+        action.putValue(AbstractAction.MNEMONIC_KEY, new Integer('R'));
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Z,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | java.awt.event.InputEvent.SHIFT_MASK));
+        // quit.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("quit"));
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Redo");
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Redo16.gif"));
+
+        action.putValue(CRITICAL, Boolean.TRUE);
+        //all.add(action);
+        return redo = action;
+
+    }//End of getRedo
+
+    // need an instance to get default textComponent Actions
+    private DefaultEditorKit kit;
+
+
+    private Action cut;
+
+    public Action getCut() {
+        Action action = cut;
+        if (action != null) return action;
+
+        if (kit == null) kit = new DefaultEditorKit();
+
+        Action[] defActions = kit.getActions();
+        for (Action defAction : defActions) {
+            if (defAction.getValue(Action.NAME) == DefaultEditorKit.cutAction) {
+                action = defAction;
+            }
+        }
+        action.putValue(AbstractAction.MNEMONIC_KEY, (int) 'T');
+        action.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_X,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+
+        action.putValue(Action.SHORT_DESCRIPTION, "Cut");
+
+        action.putValue(CRITICAL, Boolean.TRUE);
+
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Cut16.gif"));
+
+        all.add(action);
+        return cut = action;
+    }
+
+    private Action copy;
+
+    public Action getCopy() {
+        Action action = copy;
+        if (action != null) return action;
+
+        if (kit == null) kit = new DefaultEditorKit();
+
+        Action[] defActions = kit.getActions();
+        for (Action defAction : defActions) {
+
+            if ((defAction.getValue(Action.NAME)).equals(DefaultEditorKit.copyAction)) {
+                action = defAction;
+            }
+        }
+        action.putValue(AbstractAction.MNEMONIC_KEY, (int) 'C');
+        action.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_C,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+        action.putValue(Action.SHORT_DESCRIPTION, "Copy");
+        action.putValue(CRITICAL, Boolean.TRUE);
+
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Copy16.gif"));
+
+        all.add(action);
+        return copy = action;
+    }
+
+    private Action paste;
+
+    public Action getPaste() {
+        Action action = paste;
+        if (action != null) return action;
+
+        if (kit == null) kit = new DefaultEditorKit();
+
+        Action[] defActions = kit.getActions();
+        for (Action defAction : defActions) {
+            if (defAction.getValue(Action.NAME) == DefaultEditorKit.pasteAction) {
+                action = defAction;
+            }
+        }
+        action.putValue(AbstractAction.MNEMONIC_KEY, (int) 'P');
+        action.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_V,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+        action.putValue(Action.SHORT_DESCRIPTION, "Paste");
+        action.putValue(CRITICAL, Boolean.TRUE);
+
+        action.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("sun/toolbarButtonGraphics/general/Paste16.gif"));
+
+        all.add(action);
+        return paste = action;
+    }
+
+    private Action selectAll;
+
+    public Action getSelectAll() {
+        Action action = selectAll;
+        if (action != null) return action;
+
+        if (kit == null) kit = new DefaultEditorKit();
+
+        Action[] defActions = kit.getActions();
+        for (Action defAction : defActions) {
+            if (defAction.getValue(Action.NAME) == DefaultEditorKit.selectAllAction) {
+                action = defAction;
+            }
+        }
+        action.putValue(AbstractAction.MNEMONIC_KEY, (int) 'A');
+        action.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_A,
+                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+        action.putValue(Action.SHORT_DESCRIPTION, "Select All");
+
+        action.putValue(CRITICAL, Boolean.TRUE);
+        all.add(action);
+        return selectAll = action;
+    }
+
+}
diff --git a/src/jloda/gui/message/MessageWindowMenuBar.java b/src/jloda/gui/message/MessageWindowMenuBar.java
new file mode 100644
index 0000000..1a0e57c
--- /dev/null
+++ b/src/jloda/gui/message/MessageWindowMenuBar.java
@@ -0,0 +1,97 @@
+/**
+ * MessageWindowMenuBar.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.gui.message;
+
+
+import jloda.util.MenuMnemonics;
+import jloda.util.ProgramProperties;
+
+import javax.swing.*;
+
+
+/**
+ * the editor window menu bar
+ *
+ * @author huson
+ *         Date: 17.2.04
+ */
+public class MessageWindowMenuBar extends JMenuBar {
+    private final MessageWindow viewer;
+
+    public MessageWindowMenuBar(MessageWindow viewer) {
+        super();
+        this.viewer = viewer;
+
+        if (!ProgramProperties.isMacOS()) {
+            addFileMenu();
+            addEditMenu();
+
+            for (int i = 0; i < this.getMenuCount(); i++)
+                MenuMnemonics.setMnemonics(this.getMenu(i));
+        } else {
+            //ToDo: should be copy of other menus, with things dimmed out.
+            addFileMenu();
+            addEditMenu();
+            for (int i = 0; i < this.getMenuCount(); i++)
+                MenuMnemonics.setMnemonics(this.getMenu(i));
+        }
+    }
+
+    /**
+     * returns the tool bar for this simple viewer
+     */
+    private void addFileMenu() {
+        JMenu menu = new JMenu("File");
+
+        menu.add(viewer.getActions().getSaveFile());
+        menu.addSeparator();
+        menu.add(viewer.getActions().getPrintIt());
+        menu.addSeparator();
+        menu.add(viewer.getActions().getClose());
+
+        add(menu);
+    }
+
+    private void addEditMenu() {
+        JMenu menu = new JMenu("Edit");
+
+        //menu.add(viewer.getActions().getUndo());
+        //menu.addSeparator();
+        JMenuItem menuItem = new JMenuItem(viewer.getActions().getCut());
+        menuItem.setText("Cut");
+        menu.add(menuItem);
+        menuItem = new JMenuItem(viewer.getActions().getCopy());
+        menuItem.setText("Copy");
+        menu.add(menuItem);
+        menuItem = new JMenuItem(viewer.getActions().getPaste());
+        menuItem.setText("Paste");
+        menu.add(menuItem);
+        menu.addSeparator();
+        menuItem = new JMenuItem(viewer.getActions().getSelectAll());
+        menuItem.setText("Select All");
+        menu.add(menuItem);
+        menu.addSeparator();
+
+        menu.add(viewer.getActions().getClear());
+        // menu.add(viewer.getActions().gedendroscopet());
+
+        add(menu);
+    }
+}
diff --git a/src/jloda/phylo/HomoplasyScore.java b/src/jloda/phylo/HomoplasyScore.java
new file mode 100644
index 0000000..fb4a8b2
--- /dev/null
+++ b/src/jloda/phylo/HomoplasyScore.java
@@ -0,0 +1,181 @@
+/**
+ * HomoplasyScore.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.phylo;
+
+import jloda.graph.Edge;
+import jloda.graph.Node;
+import jloda.graph.NodeIntegerArray;
+
+import java.io.IOException;
+import java.util.BitSet;
+import java.util.List;
+
+/**
+ * Compute the distortion score on a tree
+ * Daniel Huson, 2.2006
+ */
+public class HomoplasyScore {
+    static public int computeBestHomoplasyScoreForSplit(PhyloTree tree, BitSet A, BitSet B) throws IOException {
+        return computeBestHomoplasyScoreForSplit(tree, A, B, null);
+    }
+
+    /**
+     * given a phylogentic tree, multifurcations, multiple and internal labels ok, computes
+     * the best possible homplays score achievable for a given split, that is, the best
+     * score over all possible refinements of the tree.
+     * See Huson, Steel and Witfield, in preparation.
+     *
+     * @param tree
+     * @param A    one side of split
+     * @param B    other side of split
+     * @param root the root to use, this should not make any difference, but is here for testing
+     *             purposes
+     * @return homoplasy score for split
+     * @throws IOException
+     */
+    static public int computeBestHomoplasyScoreForSplit(PhyloTree tree, BitSet A, BitSet B, Node root) throws IOException {
+        if (tree.getNumberOfNodes() < 2 || A.cardinality() <= 1 || B.cardinality() <= 1)
+            return 0;
+        BitSet treeTaxa = new BitSet();
+        // setup scoring map:
+        NodeIntegerArray scoreA = new NodeIntegerArray(tree); // optimal score for subtree labeled A at root
+        NodeIntegerArray scoreB = new NodeIntegerArray(tree); // optimal score for subtree labeled B at root
+        for (Node v = tree.getFirstNode(); v != null; v = v.getNext()) {
+            List vTaxa = tree.getNode2Taxa(v);
+            boolean hasA = false;
+            boolean hasB = false;
+            for (Object aVTaxa : vTaxa) {
+                int t = (Integer) aVTaxa;
+                if (A.get(t))
+                    hasA = true;
+                else if (B.get(t))
+                    hasB = true;
+                else
+                    throw new IOException("Taxon t=" + t + ": not present in split");
+                treeTaxa.set(t);
+            }
+            int aValue = 0;
+            int bValue = 0;
+            if (hasA && !hasB)
+                bValue = Integer.MAX_VALUE;
+            else if (!hasA && hasB)
+                aValue = Integer.MAX_VALUE;
+            else if (hasA && hasB)
+                aValue = bValue = 1; // TODO: is this really correct?
+            scoreA.set(v, aValue);
+            scoreB.set(v, bValue);
+        }
+        // if only 0 or 1 of either side of the split occurs in T, then score is 0:
+        BitSet intersection = (BitSet) treeTaxa.clone();
+        intersection.and(A);
+        if (intersection.cardinality() <= 1)
+            return 0;
+        intersection = (BitSet) treeTaxa.clone();
+        intersection.and(B);
+        if (intersection.cardinality() <= 1)
+            return 0;
+
+        // recursively compute score:
+        //Node root = null;
+        if (root == null)
+            root = tree.getFirstNode();
+
+        /*
+        for (root = tree.getFirstNode(); root != null; root = root.getNext())
+           if (tree.getNode2Taxa(root) == null || tree.getNode2Taxa(root).size() == 0)
+                break;
+        System.err.println("root "+root);
+        if (root == null)
+            throw new JlodaException("No unlabeled node available as root");
+        */
+        //System.out.println("initially:");
+        //printScores(tree,scoreA,scoreB);
+        computeScoreRec(root, null, scoreA, scoreB);
+        return Math.min(scoreA.getValue(root), scoreB.getValue(root)) - 1;
+    }
+
+    /**
+     * recursively does the work
+     *
+     * @param v
+     * @param e
+     * @param scoreA
+     * @param scoreB
+     */
+    private static void computeScoreRec(Node v, Edge e, NodeIntegerArray scoreA,
+                                        NodeIntegerArray scoreB) {
+        //System.out.println("Entering with v="+v);
+        //printScores(tree,scoreA,scoreB);
+        boolean hasAMuchBetterThanB = false;
+        boolean hasBMuchBetterThanA = false;
+        int countA = 0;
+        int countB = 0;
+        // first visit all children to compute their scores:
+        for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f)) {
+            if (f != e) {
+                Node w = f.getOpposite(v);
+                if (w.getDegree() > 1)
+                    computeScoreRec(w, f, scoreA, scoreB);
+                if (scoreA.getValue(w) <= scoreB.getValue(w) - 1) {
+                    hasAMuchBetterThanB = true;
+                    countB += scoreA.getValue(w);
+                } else {
+                    countB += scoreB.getValue(w);
+                }
+                if (scoreB.getValue(w) <= scoreA.getValue(w) - 1) {
+                    hasBMuchBetterThanA = true;
+                    countA += scoreB.getValue(w);
+                } else {
+                    countA += scoreA.getValue(w);
+                }
+            }
+        }
+        // this might be a labeled internal node, treat it as an additional leaf node:
+        if (scoreA.getValue(v) <= scoreB.getValue(v) - 1) {
+            hasAMuchBetterThanB = true;
+            countB += scoreA.getValue(v);
+        } else {
+            countB += scoreB.getValue(v);
+        }
+        if (scoreB.getValue(v) <= scoreA.getValue(v) - 1) {
+            hasBMuchBetterThanA = true;
+            countA += scoreB.getValue(v);
+        } else {
+            countA += scoreA.getValue(v);
+        }
+        // add 1 for change, if necessary:
+        if (hasAMuchBetterThanB)
+            countB += 1;
+        if (hasBMuchBetterThanA)
+            countA += 1;
+        // set value for node
+        scoreA.set(v, countA);
+        scoreB.set(v, countB);
+
+        //System.out.println("Exiting with v="+v);
+        //printScores(tree,scoreA,scoreB);
+    }
+
+    static void printScores(PhyloTree tree, NodeIntegerArray scoreA, NodeIntegerArray scoreB) {
+        for (Node v = tree.getFirstNode(); v != null; v = v.getNext()) {
+            System.out.println("v=" + v + " scoreA=" + scoreA.getValue(v) + " scoreB=" + scoreB.getValue(v));
+        }
+    }
+}
diff --git a/src/jloda/phylo/PhyloGraph.java b/src/jloda/phylo/PhyloGraph.java
new file mode 100644
index 0000000..d4b9107
--- /dev/null
+++ b/src/jloda/phylo/PhyloGraph.java
@@ -0,0 +1,1188 @@
+/**
+ * PhyloGraph.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.phylo;
+
+/**
+ *
+ * Phylogenetic graph
+ *
+ * @author Daniel Huson, 2005
+ */
+
+import jloda.graph.*;
+import jloda.util.Basic;
+import jloda.util.Geometry;
+import jloda.util.NotOwnerException;
+import jloda.util.Pair;
+
+import java.awt.geom.Point2D;
+import java.util.*;
+
+public class PhyloGraph extends Graph {
+    final NodeArray<String> nodeLabels;
+    final EdgeDoubleArray edgeWeights;
+    final EdgeDoubleArray edgeAngles;
+    final EdgeArray<String> edgeLabels;
+    final EdgeArray<Double> edgeConfidences;
+    final EdgeIntegerArray splits;
+    final Vector<Node> taxon2node;
+    final Vector<Integer> taxon2cycle;
+    final NodeArray<List<Integer>> node2taxa;
+
+    public boolean edgeConfidencesSet = false; // use this to decide whether to output edge confidences
+
+    // if you add anything here, make sure it gets added to copy, too!
+
+    /**
+     * Construct a new empty phylogenetic graph. Also registers a listener that will update the taxon2node
+     * array if nodes are deleted.
+     */
+    public PhyloGraph() {
+        super();
+        nodeLabels = new NodeArray<>(this);
+        edgeWeights = new EdgeDoubleArray(this);
+        edgeLabels = new EdgeArray<>(this);
+        edgeConfidences = new EdgeDoubleArray(this);
+
+        edgeAngles = new EdgeDoubleArray(this);
+        splits = new EdgeIntegerArray(this);
+        taxon2node = new Vector<>();
+        taxon2cycle = new Vector<>();
+        node2taxa = new NodeArray<>(this);
+
+        addGraphUpdateListener(new GraphUpdateAdapter() {
+            public void deleteNode(Node v) {
+                List<Integer> list = node2taxa.get(v);
+                if (list != null) {
+                    for (Integer t : list) {
+                        taxon2node.set(t - 1, null);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Clears the graph.  All auxiliary arrays are cleared.
+     */
+    public void clear() {
+        deleteAllNodes();
+        nodeLabels.clear();
+        edgeWeights.clear();
+        edgeLabels.clear();
+        edgeConfidences.clear();
+        splits.clear();
+        taxon2node.clear();
+        taxon2cycle.clear();
+        node2taxa.clear();
+    }
+
+    /**
+     * copies one phylo graph to another
+     *
+     * @param src the source graph
+     * @return old node to new node mapping
+     */
+    public NodeArray<Node> copy(PhyloGraph src) {
+        clear();
+        NodeArray<Node> oldNode2NewNode = new NodeArray<>(src);
+        copy(src, oldNode2NewNode, new EdgeArray<Edge>(src));
+        return oldNode2NewNode;
+    }
+
+    /**
+     * copies one phylo graph to another
+     *
+     * @param src             the source graph
+     * @param oldNode2NewNode
+     * @param oldEdge2NewEdge
+     */
+    public NodeArray<Node> copy(PhyloGraph src, NodeArray<Node> oldNode2NewNode, EdgeArray<Edge> oldEdge2NewEdge) {
+        clear();
+        if (oldNode2NewNode == null)
+            oldNode2NewNode = new NodeArray<>(src);
+        if (oldEdge2NewEdge == null)
+            oldEdge2NewEdge = new EdgeArray<>(src);
+
+        super.copy(src, oldNode2NewNode, oldEdge2NewEdge);
+        edgeConfidencesSet = src.edgeConfidencesSet;
+
+        for (Node v = src.getFirstNode(); v != null; v = src.getNextNode(v)) {
+            Node w = (oldNode2NewNode.get(v));
+            nodeLabels.set(w, src.nodeLabels.get(v));
+            node2taxa.set(w, src.node2taxa.get(v));
+        }
+        for (Edge e = src.getFirstEdge(); e != null; e = src.getNextEdge(e)) {
+            Edge f = (oldEdge2NewEdge.get(e));
+            edgeWeights.set(f, src.edgeWeights.get(e));
+            edgeLabels.set(f, src.edgeLabels.get(e));
+            edgeConfidences.set(f, src.edgeConfidences.get(e));
+            edgeAngles.set(f, src.edgeAngles.get(e));
+            splits.set(f, src.splits.get(e));
+        }
+        for (int i = 0; i < src.taxon2node.size(); i++) {
+            Node v = src.getTaxon2Node(i + 1);
+            if (v != null)
+                setTaxon2Node(i + 1, oldNode2NewNode.get(v));
+        }
+        for (int i = 0; i < src.taxon2cycle.size(); i++) {
+
+            int c = src.getTaxon2Cycle(i + 1);
+            setTaxon2Cycle(i + 1, c);
+        }
+        return oldNode2NewNode;
+    }
+
+    /**
+     * Gets an enumeration of all node labels.
+     */
+    public Set<String> getNodeLabels() {
+        Set<String> set = new HashSet<>();
+
+        for (Node v = getFirstNode(); v != null; v = getNextNode(v))
+            if (getLabel(v) != null && getLabel(v).length() > 0)
+                set.add(getLabel(v));
+
+        return set;
+    }
+
+    /**
+     * Returns the number of nodes that have a label.
+     *
+     * @return count int
+     */
+    public int computeNumLabeledNodes() {
+        int count = 0;
+        for (Node v = getFirstNode(); v != null; v = getNextNode(v))
+            if (nodeLabels.get(v) != null)
+                count++;
+        return count;
+    }
+
+    /**
+     * returns the number of splits.
+     * number of splits: amount of different split-id's in the graph.
+     *
+     * @return number of splits
+     */
+    public Integer[] getSplitIds() {
+        int count = 0;
+        ArrayList<Integer> ids = new ArrayList<>();
+        for (Edge e = getFirstEdge(); e != null; e = getNextEdge(e)) {
+            if (!ids.contains(splits.get(e))) {
+                ids.add(splits.get(e));
+                count++;
+            }
+        }
+        return ids.toArray(new Integer[count]);
+    }
+
+
+    /**
+     * Sets the angle of an edge.
+     *
+     * @param e Edge
+     * @param d angle
+     */
+    public void setAngle(Edge e, double d) {
+        edgeAngles.set(e, d);
+    }
+
+    /**
+     * Gets the angle of an edge.
+     *
+     * @param e Edge
+     * @return angle
+     */
+    public double getAngle(Edge e) {
+        if (edgeAngles.get(e) == null)
+            return 0;
+        else
+            return edgeAngles.getValue(e);
+    }
+
+    /**
+     * Sets the weight of an edge.
+     *
+     * @param e Edge
+     * @param d double
+     */
+    public void setWeight(Edge e, double d) {
+        edgeWeights.set(e, d);
+    }
+
+    /**
+     * Gets the weight of an edge.
+     *
+     * @param e Edge
+     * @return edgeWeights double
+     */
+    public double getWeight(Edge e) {
+        if (edgeWeights.get(e) == null)
+            return 1;
+        else
+            return edgeWeights.get(e);
+    }
+
+
+    /**
+     * Sets the label of an edge.
+     *
+     * @param e   Edge
+     * @param lab String
+     */
+    public void setLabel(Edge e, String lab) {
+        edgeLabels.set(e, lab);
+    }
+
+    /**
+     * Gets the label of an edge.
+     *
+     * @param e Edge
+     * @return edgeLabels String
+     */
+    public String getLabel(Edge e) {
+        return edgeLabels.get(e);
+    }
+
+    /**
+     * Sets the confidence of an edge.
+     *
+     * @param e Edge
+     * @param d double
+     */
+    public void setConfidence(Edge e, double d) {
+        edgeConfidencesSet = true;
+        edgeConfidences.set(e, d);
+    }
+
+    /**
+     * Gets the confidence of an edge.
+     *
+     * @param e Edge
+     * @return confidence
+     */
+    public double getConfidence(Edge e) {
+        if (edgeConfidences.get(e) == null)
+            return 1;
+        else
+            return edgeConfidences.get(e);
+    }
+
+
+    /**
+     * Sets the taxon label of a node.
+     *
+     * @param v   Node
+     * @param str String
+     */
+    public void setLabel(Node v, String str) {
+        nodeLabels.set(v, str);
+    }
+
+
+    /**
+     * Sets the label of a node to a list of taxon names
+     *
+     * @param v      node
+     * @param labels list of labels
+     */
+    public void setLabels(Node v, List labels) {
+        if (labels != null) {
+            String str = "";
+            Iterator it = labels.iterator();
+
+            while (it.hasNext()) {
+                String label = (String) it.next();
+                str += label;
+                if (it.hasNext())
+                    str += ", ";
+            }
+            setLabel(v, str);
+        }
+    }
+
+    /**
+     * Gets the taxon label of a node.
+     *
+     * @param v Node
+     * @return nodeLabels String
+     */
+    public String getLabel(Node v) {
+        return nodeLabels.get(v);
+    }
+
+    /**
+     * sets the split-id of an edge
+     *
+     * @param e  the edge
+     * @param id the id
+     */
+    public void setSplit(Edge e, int id) {
+        splits.set(e, id);
+    }
+
+    /**
+     * gets the split-id of an edge
+     *
+     * @param e the edge
+     * @return the split-id of the given edge
+     */
+    public int getSplit(Edge e) {
+        if (splits.get(e) == null)
+            return 0;
+        return splits.get(e);
+    }
+
+    /**
+     * find the corresponding node for a given taxon-id.
+     *
+     * @param taxId the taxon-id
+     * @return the Node representing the taxon with id <code>taxId</code>.
+     */
+    public Node getTaxon2Node(int taxId) {
+        if (taxId <= taxon2node.size())
+            return taxon2node.get(taxId - 1);
+        else {
+            //  System.err.println("getTaxon2Node: no Node set for taxId " + taxId + " (taxa2Nodes.size(): " + taxon2node.size() + ")");
+            return null;
+        }
+    }
+
+    /**
+     * find the position of a taxon in the cyclic ordering.
+     *
+     * @param taxId the taxon-id.
+     * @return the index of taxon with id <code>taxId</code> in the cyclic ordering.
+     */
+    public int getTaxon2Cycle(int taxId) {
+        if (taxId <= taxon2cycle.size())
+            return taxon2cycle.get(taxId - 1);
+        else {
+            System.err.println("getTaxon2Cycle: no cycle-index set for taxId " + taxId + " (taxon2cycle.size(): " + taxon2cycle.size() + ")");
+            return -1;
+        }
+    }
+
+    /**
+     * returns the number of taxa
+     *
+     * @return number of taxa
+     */
+    public int getNumberOfTaxa() {
+        return taxon2node.size();
+    }
+
+    /**
+     * gets the cycle of taxa
+     *
+     * @return cyclic ordering of taxa
+     */
+    public int[] getCycle() {
+        int[] cycle = new int[taxon2node.size() + 1];
+        for (int t = 1; t <= taxon2node.size(); t++)
+            cycle[getTaxon2Cycle(t)] = t;
+        return cycle;
+    }
+
+    /**
+     * set the position of a taxon in the cyclic ordering.
+     *
+     * @param taxId      the taxon-id.
+     * @param cycleIndex the index of taxon with id <code>taxId</code> in the cyclic ordering.
+     */
+    public void setTaxon2Cycle(int taxId, int cycleIndex) {
+        if (taxId <= taxon2cycle.size()) {
+            taxon2cycle.setElementAt(cycleIndex, taxId - 1);
+        } else {
+            taxon2cycle.setSize(taxId);
+            taxon2cycle.setElementAt(cycleIndex, taxId - 1);
+        }
+    }
+
+    /**
+     * set which Node represents the taxon with id <code>taxId</code>.
+     *
+     * @param taxId   the taxon-id.
+     * @param taxNode the Node representing the taxon with id <code>taxId</code>.
+     * @throws NotOwnerException
+     */
+    public void setTaxon2Node(int taxId, Node taxNode) {
+        this.checkOwner(taxNode);
+        if (taxId <= taxon2node.size()) {
+            taxon2node.setElementAt(taxNode, taxId - 1);
+        } else {
+            taxon2node.setSize(taxId);
+            taxon2node.setElementAt(taxNode, taxId - 1);
+        }
+    }
+
+    /**
+     * add a taxon to be represented by the specified node
+     *
+     * @param v     the node.
+     * @param taxon the id of the taxon to be added
+     */
+    public void setNode2Taxa(Node v, int taxon) {
+        getNode2Taxa(v).add(taxon);
+    }
+
+    /**
+     * Gets a list of all taxa represented by this node
+     *
+     * @param v the node
+     * @return list containing ids of taxa associated with that node
+     */
+    public List<Integer> getNode2Taxa(Node v) {
+        if (node2taxa.get(v) == null)
+            node2taxa.set(v, new LinkedList<Integer>()); // lazy initialization
+        return node2taxa.get(v);
+    }
+
+    /**
+     * Clears the taxon 2 node3 map
+     */
+    public void clearTaxon2Node() {
+        taxon2node.clear();
+    }
+
+    /**
+     * Clears the taxon 2 node3 map
+     */
+    public void clearNode2Taxa() {
+        node2taxa.clear();
+    }
+
+    /**
+     * Clears the taxa entries for the specified node
+     *
+     * @param node the node
+     */
+    public void clearNode2Taxa(Node node) {
+        node2taxa.set(node, null);
+    }
+
+    /**
+     * Embeds the graph using the given cyclic ordering.
+     *
+     * @param ordering   the cyclic ordering.
+     * @param useWeights scale edges by their weights?
+     * @param noise      alter split-angles randomly by a small amount to prevent occlusion of edges.
+     * @return node array of coordinates
+     */
+    public NodeArray embed(int[] ordering, boolean useWeights, boolean noise) {
+        int ntax = ordering.length - 1;
+
+        Node[] ordering_n = new Node[ntax];
+
+        for (int i = 1; i <= ntax; i++) {
+            ordering_n[getTaxon2Cycle(i) - 1] = getTaxon2Node(i);
+        }
+
+        // get splits
+        HashMap<Integer, ArrayList<Node>> splits = getSplits(ordering_n);
+        for (Integer key : splits.keySet()) sortSplit(ordering_n, splits.get(key));
+
+        /** get unit-vectors in split-direction */
+        HashMap<Integer, Double> dirs = getDirectionVectors(splits, ordering_n, noise);
+
+        /** compute coords */
+        return computeCoords(dirs, ordering_n, useWeights);
+    }
+
+
+    /**
+     * get splits:
+     * depth search / cross each split just once
+     * add taxa to currently crossed splits.
+     *
+     * @param ordering the cyclic ordering
+     */
+    private HashMap<Integer, ArrayList<Node>> getSplits(Node[] ordering) {
+
+        /** the splits */
+        HashMap<Integer, ArrayList<Node>> splits = new HashMap<>();
+
+        /** stack for nodes which still have to be visited */
+        Stack<Node> toVisit = new Stack<>();
+        /** Boolean-stack to determine whether current Node is backtracking-node */
+        Stack<Boolean> backtrack = new Stack<>();
+        /** Edge-stack to determine enter-edge */
+        Stack<Edge> edges = new Stack<>();
+        /** collect already seen nodes */
+        ArrayList<Node> seen = new ArrayList<>();
+        /** collect currently crossed split-ids */
+        ArrayList<Integer> crossedSplits = new ArrayList<>();
+
+        // init..
+        toVisit.push(ordering[0]);
+        backtrack.push(false);
+        Edge enter = null;
+
+        // start traversal
+        while (!toVisit.empty()) {
+            // current Node
+            Node u = toVisit.pop();
+            // enter-edge
+            if (!edges.isEmpty()) enter = edges.pop();
+            // are we backtracking?
+            boolean backtracking = backtrack.pop();
+
+            /** first visit (not backtracking) */
+            if (!backtracking) {   //  && !seen.contains(u)
+                if (enter != null) {
+                    // current split-id
+                    Integer cId = this.getSplit(enter);
+                    crossedSplits.add(cId);
+                    if (!splits.containsKey(cId))
+                        splits.put(cId, new ArrayList<Node>());
+                }
+                seen.add(u);
+
+                /** if the current Node is a taxa-node, add it to currently crossed splits */
+                if (this.getNode2Taxa(u).size() != 0) {
+                    for (Integer crossedSplit : crossedSplits) {
+                        ArrayList<Node> s = splits.get(crossedSplit);
+                        s.add(u);
+                    }
+                }
+
+                /**
+                 * push adjacent nodes (if not already seen)
+                 * and current node (backtrack)
+                 */
+                Iterator e = this.getAdjacentEdges(u);
+                while (e.hasNext()) {
+                    Edge edge = (Edge) e.next();
+                    Integer sId = this.getSplit(edge);
+                    Node v = this.getOpposite(u, edge);
+                    if (!seen.contains(v) && !crossedSplits.contains(sId)) {
+                        toVisit.push(u);
+                        backtrack.push(true);
+                        toVisit.push(v);
+                        backtrack.push(false);
+                        // push edge twice (visit & backtrack)
+                        edges.push(edge);
+                        edges.push(edge);
+                    }
+                }
+
+                /** backtrack */
+            } else {
+                // backtracking -> remove crossed split
+                if (enter != null) {
+                    Integer cId = this.getSplit(enter);
+                    crossedSplits.remove(cId);
+                }
+
+            }
+        } // end while
+        return splits;
+    }
+
+    /**
+     * sort a split according to the cyclic ordering.
+     *
+     * @param ordering the cyclic ordering
+     * @param split    the split which has to be sorted
+     */
+    private void sortSplit(Node[] ordering, ArrayList<Node> split) {
+
+        // convert Node[] to List in order to use List.indexOf(..)
+        List<Node> orderingList = Arrays.asList(ordering);
+        ArrayList<Node> t1 = new ArrayList<>(split.size());
+        ArrayList<Node> t2 = new ArrayList<>(split.size());
+        for (int i = 0; i < split.size(); i++) {
+            int index = orderingList.indexOf(split.get(i)) - 1;
+            if (index == -1) index = ordering.length - 1;
+            // split doesn't contain previous taxa in the cyclic ordering
+            // => the following (split.cardinality) taxa in the cyclic ordering
+            //      give the sorted split.
+            if (!(split.contains(ordering[index]))) {
+                int j = 1;
+                // get both sides of the split
+                for (; j < split.size() + 1; j++) {
+                    t1.add(ordering[(index + j) % ordering.length]);
+                }
+                for (int k = j; k < j + (ordering.length - split.size()); k++) {
+                    t2.add(ordering[(index + k) % ordering.length]);
+                }
+                break;
+            }
+        }
+        // chose the split that doesn't contain the first taxon in the
+        // cyclic ordering, because coordinates are computed starting there.
+        split.clear();
+        if (t2.contains(ordering[0]))
+            split.addAll(t1);
+        else
+            split.addAll(t2);
+    }
+
+
+    /**
+     * determine the direction vectors for each split.
+     * angle: ((leftSplitBoundary + rightSplitBoundary)/amountOfTaxa)*Pi
+     *
+     * @param splits   the sorted splits
+     * @param ordering the cyclic ordering
+     * @param noise    alter split-angles randomly by a small amount to prevent occlusion of edges
+     * @return direction vectors for each split
+     */
+    private HashMap<Integer, Double> getDirectionVectors(HashMap<Integer, ArrayList<Node>> splits, Node[] ordering, boolean noise) {
+        final Random rand = new Random(666);    // add noise, if necessary
+        final HashMap<Integer, Double> dirs = new HashMap<>(splits.size());
+        final List<Node> orderingList = Arrays.asList(ordering);
+
+        Edge currentEdge = this.getFirstEdge();
+        int currentSplit;
+
+        for (int j = 0; j < this.getNumberOfEdges(); j++) {
+            //We do a loop on the edges to keep the angles of the splits which have already been computed
+            double angle;
+            currentSplit = this.getSplit(currentEdge);
+            Integer splitId = currentSplit;
+            if (!dirs.containsKey(splitId)) {
+                if (this.getAngle(currentEdge) > 0.00000000001) {
+                    //This is an old edge, we affect its angle to its split
+                    angle = this.getAngle(currentEdge);
+                    dirs.put(currentSplit, angle);
+                } else {
+                    //This is a new edge, so we affect it an angle according to the equal angle algorithm
+                    final ArrayList<Node> split = splits.get(splitId);
+                    int xp = 0;
+                    int xq = 0;
+
+                    if (split.size() > 0) {
+                        xp = orderingList.indexOf(split.get(0));
+                        xq = orderingList.indexOf(split.get(split.size() - 1));
+                    }
+
+                    angle = ((((double) xp + (double) xq) / (double) ordering.length) * Math.PI);
+                    if (noise && split.size() > 1) {
+                        angle = 0.02 * rand.nextFloat() + angle;
+                    }
+                    dirs.put(splitId, angle);
+                }
+            } else {
+                angle = dirs.get(currentSplit);
+            }
+
+            this.setAngle(currentEdge, angle);
+            currentEdge = this.getNextEdge(currentEdge);
+        }
+        return dirs;
+    }
+
+
+    /**
+     * compute coords for each node.
+     * depth first traversal / cross each split just once before backtracking
+     *
+     * @param dirs       the direction vectors for each split
+     * @param ordering   the cyclic ordering
+     * @param useWeights scale edges by edge weights?
+     * @return node array of coordinates
+     */
+    public NodeArray computeCoords(HashMap<Integer, Double> dirs, Node[] ordering, boolean useWeights) {
+        NodeArray<Point2D> coords = new NodeArray<>(this);
+
+        /** stack for nodes which still have to be visited */
+        Stack<Node> toVisit = new Stack<>();
+        /** Boolean-stack to determine wether current Node is backtracking-node */
+        Stack<Boolean> backtrack = new Stack<>();
+        /** Edge-stack to determine enter-edge */
+        Stack<Edge> edges = new Stack<>();
+        /** collect already seen nodes */
+        ArrayList<Node> seen = new ArrayList<>();
+        /** collect already computed nodes to check equal locations */
+        HashMap<Node, Point2D> locations = new HashMap<>();
+        /** collect currently crossed split-ids */
+        ArrayList<Integer> crossedSplits = new ArrayList<>();
+        /** current node-location */
+        Point2D.Double currentPoint = new Point2D.Double();
+        currentPoint.setLocation(0.0, 0.0);
+
+        // init..
+        toVisit.push(ordering[0]);
+        backtrack.push(false);
+        Edge enter = null;
+
+        // start traversal
+        while (!toVisit.empty()) {
+            // current Node
+            Node u = toVisit.pop();
+            // enter-edge
+            if (!edges.isEmpty()) enter = edges.pop();
+
+            // are we backtracking?
+            boolean backtracking = backtrack.pop();
+
+            /** visit */
+            if (!backtracking) {
+                if (enter != null) {
+                    // current split-id
+                    Integer cId = getSplit(enter);
+                    double w = (useWeights ? this.getWeight(enter) : 1.0);
+                    crossedSplits.add(cId);
+
+                    double angle = dirs.get(cId);
+                    currentPoint = (Point2D.Double) Geometry.translateByAngle(currentPoint, angle, w);
+                }
+
+                // set location, check equal locations
+                Point2D loc = new Point2D.Double(currentPoint.getX(), currentPoint.getY());
+                // equal locations: append labels
+                if (locations.containsValue(loc)) {
+                    Node twinNode;
+                    String tLabel = this.getLabel(u);
+
+                    for (Node v : locations.keySet()) {
+                        if (locations.get(v).equals(loc)) {
+                            twinNode = v;
+                            if (this.getLabel(twinNode) != null)
+                                tLabel = (tLabel != null) ? tLabel + ", " + this.getLabel(twinNode)
+                                        : this.getLabel(twinNode);
+                            this.setLabel(twinNode, null);
+                            this.setLabel(u, tLabel);
+                        }
+                    }
+                }
+                coords.set(u, loc);
+                locations.put(u, loc);
+
+                seen.add(u);
+
+                /**
+                 * push adjacent nodes (if not already seen)
+                 * and current node (backtrack)
+                 */
+                Iterator e = this.getAdjacentEdges(u);
+                while (e.hasNext()) {
+                    Edge edge = (Edge) e.next();
+                    Integer sId = getSplit(edge);
+                    Node v = this.getOpposite(u, edge);
+                    if (!seen.contains(v) && !crossedSplits.contains(sId)) {
+                        toVisit.push(u);
+                        backtrack.push(true);
+                        toVisit.push(v);
+                        backtrack.push(false);
+                        // push edge twice (visit & backtrack)
+                        edges.push(edge);
+                        edges.push(edge);
+                    }
+                }
+
+                /** backtrack */
+            } else {
+                if (enter != null) {
+                    Integer cId = getSplit(enter);
+                    crossedSplits.remove(cId);
+                    double w = (useWeights ? this.getWeight(enter) : 1.0);
+                    double angle = dirs.get(cId);
+                    currentPoint = (Point2D.Double) Geometry.translateByAngle(currentPoint, angle, -w);
+                }
+            }
+        } // end while
+        return coords;
+    }
+
+    /**
+     * returns the number of splits mentioned in this graph
+     *
+     * @return number of splits
+     */
+    public int getNumberOfSplits() {
+        BitSet seen = new BitSet();
+        for (Edge e = getFirstEdge(); e != null; e = getNextEdge(e))
+            seen.set(getSplit(e));
+        return seen.cardinality();
+    }
+
+    /**
+     * returns the highest split id mentioned in this graph
+     *
+     * @return highest split id mentioned
+     */
+    public int getMaxSplitId() {
+        int max = 0;
+        for (Edge e = getFirstEdge(); e != null; e = getNextEdge(e)) {
+            if (getSplit(e) > max)
+                max = getSplit(e);
+        }
+        return max;
+    }
+
+    /**
+     * gives a String-representation of this PhyloGraph, containing
+     * node-labels, edge-labels and edge-weights.
+     *
+     * @return the String representation of this PhyloGraph
+     */
+    public String toString() {
+
+        StringBuilder buf = new StringBuilder();
+
+        buf.append("\nNodes: ").append(getNumberOfNodes()).append("\n");
+        buf.append("id\t\tlabel\n");
+        buf.append("-----------------\n");
+        for (Node v = getFirstNode(); v != null; v = getNextNode(v)) {
+            buf.append(v.toString());
+            if (getLabel(v) != null)
+                buf.append("\t\t").append(getLabel(v)).append("\n");
+            else
+                buf.append("\n");
+        }
+        buf.append("\nEdges: ").append(getNumberOfEdges()).append("\n");
+        buf.append("id\t\tsplitlabel\tweight\n");
+        buf.append("----------------------\n");
+        for (Edge e = getFirstEdge(); e != null; e = getNextEdge(e)) {
+            buf.append(super.getId(e)).append("\t\t").append(splits.get(e)).append("\t\t").append(edgeWeights.get(e)).append("\n");
+        }
+        return buf.toString();
+    }
+
+    /**
+     * produces a clone of this graph
+     *
+     * @return a clone of this graph
+     */
+    public Object clone() {
+        super.clone();
+        PhyloGraph result = new PhyloGraph();
+        result.copy(this);
+        return result;
+    }
+
+    /**
+     * removes a taxon from the graph, but leaves the corresponding node label, if any
+     *
+     * @param id
+     */
+    public void removeTaxon(int id) {
+        taxon2node.set(id - 1, null);
+        taxon2cycle.remove(id - 1);
+        for (Node v = getFirstNode(); v != null; v = getNextNode(v)) {
+            List list = getNode2Taxa(v);
+            int which = list.indexOf(id);
+            if (which != -1) {
+                list.remove(which);
+                break; // should only be one mention of this taxon
+            }
+        }
+    }
+
+    /**
+     * removes a split from the graph by contracting all edges associated with the split
+     *
+     * @param splitId
+     */
+    public void removeSplit(int splitId) {
+        Node one = getTaxon2Node(1);
+
+        if (one != null) {
+            // determine all nodes and edges that separate S(1) from X-S(1)
+            List<Pair<Node, Edge>> separators = new LinkedList<>(); // each is a pair consisting of a node and edge
+            NodeSet seen = new NodeSet(this);
+
+            getAllSeparators(splitId, one, null, seen, separators);
+
+            // determine all nodes on opposite end of separating edges
+            NodeSet opposites = new NodeSet(this);
+            Iterator it = separators.iterator();
+            while (it.hasNext()) {
+                Pair pair = (Pair) it.next();
+                opposites.add(getOpposite((Node) pair.getFirst(), (Edge) pair.getSecond()));
+            }
+
+            // reconnect edges that are adjacent to opposite ends of separators:
+
+            it = separators.iterator();
+            while (it.hasNext()) {
+                Pair pair = (Pair) it.next();
+                Node v = (Node) pair.getFirst();
+                Edge e = (Edge) pair.getSecond();
+                Node w = getOpposite(v, e);
+
+                for (Edge f = getFirstAdjacentEdge(w); f != null; f = getNextAdjacentEdge(f, w)) {
+                    if (f != e) {
+                        Node u = getOpposite(w, f);
+                        if (u != v && !opposites.contains(u)) {
+                            Edge g = null;
+                            try {
+                                g = newEdge(u, v);
+                            } catch (IllegalSelfEdgeException e1) {
+                                Basic.caught(e1);
+                            }
+                            setSplit(g, getSplit(f));
+                            setWeight(g, getWeight(f));
+                            setAngle(g, getAngle(f));
+                        }
+                    }
+                }
+
+                if (getLabel(w) != null && getLabel(w).length() > 0) {
+                    if (getLabel(v) == null)
+                        setLabel(v, getLabel(w));
+                    else
+                        setLabel(v, getLabel(v) + ", " + getLabel(w));
+                }
+
+                if (getNode2Taxa(w) != null) // node is labeled by taxa, move labels to v
+                {
+                    for (Integer t : getNode2Taxa(w)) {
+                        setTaxon2Node(t, v);
+                        setNode2Taxa(v, t);
+                    }
+                    getNode2Taxa(w).clear();
+                }
+                // delete old node w.
+                deleteNode(w);
+            }
+        }
+    }
+
+
+    /**
+     * recursively finds all edges representing the named split.
+     *
+     * @param splitId
+     * @param v
+     * @param e
+     * @param seen
+     * @param separators adds the resulting pair of (node,edge) into this list
+     * @throws NotOwnerException
+     */
+    public void getAllSeparators(int splitId, Node v, Edge e, NodeSet seen, List<Pair<Node, Edge>> separators) {
+        if (!seen.contains(v)) {
+            seen.add(v);
+            for (Edge f = getFirstAdjacentEdge(v); f != null; f = getNextAdjacentEdge(f, v)) {
+                if (f != e) {
+                    if (getSplit(f) == splitId) {
+                        separators.add(new Pair<>(v, f));
+                    } else
+                        getAllSeparators(splitId, getOpposite(v, f), f, seen, separators);
+                }
+            }
+        }
+    }
+
+    /**
+     * finds an edge with the given split id that separates 1 from rest of graph
+     *
+     * @param splitId
+     * @param v
+     * @param e
+     * @param seen
+     * @return Pair consisting of node and edge
+     * @throws NotOwnerException
+     */
+    public Pair<Node, Edge> getSeparator(int splitId, Node v, Edge e, NodeSet seen) {
+        if (!seen.contains(v)) {
+            seen.add(v);
+            for (Edge f = getFirstAdjacentEdge(v); f != null; f = getNextAdjacentEdge(f, v)) {
+                if (f != e) {
+                    if (getSplit(f) == splitId)
+                        return new Pair<>(v, f);
+                    else {
+                        Pair<Node, Edge> pair = getSeparator(splitId, getOpposite(v, f), f, seen);
+                        if (pair != null)
+                            return pair;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * returns a labeling of all nodes by the sets of characters in state 1
+     *
+     * @param split2chars
+     * @param firstChars
+     * @return labeling of all nodes by 01 strings
+     */
+    public NodeArray labelNodesBySequences(Map split2chars, char[] firstChars) {
+        final NodeArray labels = new NodeArray(this);
+        System.err.println("base-line= " + (new String(firstChars)));
+        Node v = getTaxon2Node(1);
+        BitSet used = new BitSet(); // set of splits used in current path
+
+        labelNodesBySequencesRec(v, used, split2chars, firstChars, labels);
+        return labels;
+    }
+
+    /**
+     * recursively do the work
+     *
+     * @param v
+     * @param used
+     * @param split2chars
+     * @param firstChars
+     * @param labels
+     */
+    private void labelNodesBySequencesRec(Node v, BitSet used, Map split2chars, char[] firstChars, NodeArray<String> labels) {
+        if (labels.get(v) == null) {
+            BitSet flips = new BitSet();
+            for (int s = used.nextSetBit(1); s >= 0; s = used.nextSetBit(s + 1)) {
+                if (s > 0)
+                    flips.or((BitSet) split2chars.get(s));
+                // s=0 happens in rooted graph
+            }
+            StringBuilder label = new StringBuilder();
+            for (int c = 1; c < firstChars.length; c++) {
+                if (flips.get(c) == (firstChars[c] == '1'))
+                    label.append("0");
+                else
+                    label.append("1");
+            }
+            labels.set(v, label.toString());
+            for (Edge e = v.getFirstAdjacentEdge(); e != null; e = v.getNextAdjacentEdge(e)) {
+                int s = getSplit(e);
+                if (!used.get(s)) {
+                    used.set(s);
+                    labelNodesBySequencesRec(v.getOpposite(e), used, split2chars, firstChars, labels);
+                    used.set(s, false);
+                }
+            }
+        }
+    }
+
+    /**
+     * changes the node labels of the tree using the mapping old-to-new
+     *
+     * @param old2new
+     */
+    public void changeLabels(Map old2new) {
+        for (Node v = getFirstNode(); v != null; v = getNextNode(v)) {
+            String label = getLabel(v);
+            if (label != null && old2new.containsKey(label))
+                setLabel(v, (String) old2new.get(label));
+        }
+    }
+
+    /**
+     * add the nodes and edges of another graph to this graph. Doesn't make the graph connected, though!
+     *
+     * @param graph
+     */
+    public void add(PhyloGraph graph) {
+        NodeArray<Node> old2new = new NodeArray<>(graph);
+        for (Node v = graph.getFirstNode(); v != null; v = graph.getNextNode(v)) {
+            Node w = newNode();
+            old2new.set(v, w);
+            setLabel(w, graph.getLabel(v));
+
+        }
+        for (Edge e = graph.getFirstEdge(); e != null; e = graph.getNextEdge(e)) {
+            Edge f = null;
+            try {
+                f = newEdge(old2new.get(graph.getSource(e)), old2new.get(graph.getTarget(e)));
+            } catch (IllegalSelfEdgeException e1) {
+                Basic.caught(e1);
+            }
+            setLabel(f, graph.getLabel(e));
+            setWeight(f, graph.getWeight(e));
+            if (graph.edgeConfidences.get(e) != null)
+                setConfidence(f, graph.getConfidence(e));
+        }
+    }
+
+    /**
+     * scales all edge weights by the given factor
+     *
+     * @param factor
+     */
+    public void scaleEdgeWeights(float factor) {
+        for (Edge e = getFirstEdge(); e != null; e = getNextEdge(e)) {
+            setWeight(e, factor * getWeight(e));
+        }
+    }
+
+    /**
+     * compute the current set of all leaves
+     * @return all leaves
+     */
+    public NodeSet computeSetOfLeaves() {
+        NodeSet nodes = new NodeSet(this);
+        for (Node v = getFirstNode(); v != null; v = getNextNode(v))
+            if (v.getOutDegree() == 0)
+                nodes.add(v);
+        return nodes;
+    }
+
+    /**
+     * compute the max id of any node
+     *
+     * @return max id
+     */
+    public int computeMaxId() {
+        int max = 0;
+        for (Node v = getFirstNode(); v != null; v = getNextNode(v)) {
+            if (max < v.getId())
+                max = v.getId();
+        }
+        return max;
+    }
+
+    /**
+     * gets the average distance from this node to a leaf.
+     *
+     * @param v
+     * @return average distance to a leaf
+     */
+    public double computeAverageDistanceToALeaf(Node v) {
+        // assumes that all edges are oriented away from the root
+        NodeSet seen = new NodeSet(this);
+        Pair<Double, Integer> pair = new Pair<>(0.0, 0);
+        computeAverageDistanceToLeafRec(v, null, 0, seen, pair);
+        double sum = pair.getFirstDouble();
+        int leaves = pair.getSecondInt();
+        if (leaves > 0)
+            return sum / leaves;
+        else
+            return 0;
+    }
+
+    /**
+     * recursively does the work
+     *
+     * @param v
+     * @param distance from root
+     * @param seen
+     * @param pair
+     */
+    private void computeAverageDistanceToLeafRec(Node v, Edge e, double distance, NodeSet seen, Pair<Double, Integer> pair) {
+        if (!seen.contains(v)) {
+            seen.add(v);
+
+            if (v.getOutDegree() > 0) {
+                for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f)) {
+                    if (f != e) {
+                        computeAverageDistanceToLeafRec(f.getOpposite(v), f, distance + getWeight(f), seen, pair);
+                    }
+                }
+            } else {
+                pair.setFirst(pair.getFirst() + distance);
+                pair.setSecond(pair.getSecond() + 1);
+            }
+        }
+    }
+}
diff --git a/src/jloda/phylo/PhyloGraphView.java b/src/jloda/phylo/PhyloGraphView.java
new file mode 100644
index 0000000..40e2a4d
--- /dev/null
+++ b/src/jloda/phylo/PhyloGraphView.java
@@ -0,0 +1,649 @@
+/**
+ * PhyloGraphView.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.phylo;
+
+import jloda.graph.*;
+import jloda.graphview.EdgeActionAdapter;
+import jloda.graphview.EdgeView;
+import jloda.graphview.GraphView;
+import jloda.graphview.NodeView;
+import jloda.util.Basic;
+import jloda.util.Geometry;
+import jloda.util.NotOwnerException;
+import jloda.util.Pair;
+
+import java.awt.*;
+import java.awt.geom.Point2D;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.*;
+import java.util.List;
+
+/**
+ * PhyloGraph view
+ * Daniel Huson, 2002
+ */
+
+public class PhyloGraphView extends GraphView {
+    private boolean useSplitSelectionModel = true;
+    private boolean inEdgeClickSelection = false;
+
+    /**
+     * Constructs a view of a phylogenetic graph, setting
+     * window width and height to 400.
+     *
+     * @param phyloGraph the PhyloGraph
+     */
+    public PhyloGraphView(PhyloGraph phyloGraph) {
+        this(phyloGraph, 400, 400);
+    }
+
+    /**
+     * construcs a phylogenetic graphview initialized to an empty graph
+     */
+    public PhyloGraphView() {
+        this(new PhyloGraph(), 400, 400);
+    }
+
+    /**
+     * Constructs a view of a phylogentic G.
+     *
+     * @param phyloGraph the PhyloGraph
+     * @param w          the width
+     * @param h          the height
+     */
+    public PhyloGraphView(PhyloGraph phyloGraph, int w, int h) {
+        super(phyloGraph, w, h);
+
+        setDefaultNodeLocation(0, 0);
+        setDefaultNodeBackgroundColor(Color.BLACK);
+        setDefaultNodeColor(Color.BLACK);
+        setDefaultEdgeDirection(EdgeView.UNDIRECTED);
+        setDefaultNodeLabelLayout(NodeView.LAYOUT);
+
+        setMaintainEdgeLengths(true);
+        setAllowEditEdgeLabelsOnDoubleClick(true);
+        setAllowEditEdgeLabelsOnDoubleClick(true);
+        setAllowEditNodeLabelsOnDoubleClick(true);
+        setAllowEditNodeLabelsOnDoubleClick(true);
+        // setAllowRubberbandEdges(false);
+
+        resetViews();
+
+        // this takes care of the split selection mode: whenever an edge is clicked on,
+        // we select all edges of the same split and also all nodes on one side of the split
+        addEdgeActionListener(new EdgeActionAdapter() {
+            public void doClick(EdgeSet edges, int numClicks) {
+                inEdgeClickSelection = true;
+            }
+
+            public void doRelease(EdgeSet edges) {
+                inEdgeClickSelection = false;
+            }
+
+            public void doSelect(EdgeSet edges) {
+                if (inEdgeClickSelection && getUseSplitSelectionModel() && edges.size() == 1) // exactly one edge selected, select split side
+                {
+                    Edge e = edges.getFirstElement();
+                    try {
+                        int splitId = getPhyloGraph().getSplit(e);
+                        if (splitId == 0)
+                            return;
+                        selectAllNodes(false);
+                        selectAllEdges(false);
+                        // todo: for this to work for reticulate networks, reticulate edges
+                        // must be oriented toward the reticulation node
+                        selectGraphComponent(getGraph().getTarget(e), splitId);
+                        if (2 * getSelectedNodes().size() > getGraph().getNumberOfNodes()) {
+                            // invert selection of nodes:
+                            for (Node v = getGraph().getFirstNode(); v != null; v = getGraph().getNextNode(v)) {
+                                if (getSelected(v))
+                                    selectedNodes.remove(v);
+                                else
+                                    selectedNodes.add(v);
+                            }
+                        }
+
+                    } catch (NotOwnerException ex) {
+                        jloda.util.Basic.caught(ex);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Constructs a view of a phylogentic tree.
+     *
+     * @param tree PhyloTree
+     */
+    public PhyloGraphView(PhyloTree tree) {
+        this(tree, false);
+    }
+
+    /**
+     * Constructs a view of a phylogentic tree.
+     *
+     * @param tree PhyloTree
+     */
+    public PhyloGraphView(PhyloTree tree, boolean computeEmbedding) {
+        this(tree, 400, 400);
+        setDefaultNodeLocation(0, 0);
+        setMaintainEdgeLengths(true);
+
+        try {
+            for (Node v = tree.getFirstNode(); v != null; v = tree.getNextNode(v))
+                setLabel(v, tree.getLabel(v));
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+        //System.err.print("embedding:");
+        if (computeEmbedding)
+            embed();
+        //System.err.println("done");
+    }
+
+
+    /**
+     * selects all nodes and edges on the smaller part of the split
+     *
+     * @param v  the start node
+     * @param id the split id
+     */
+    private void selectGraphComponent(Node v, int id) {
+        try {
+            if (!getSelected(v)) {
+                selectedNodes.add(v);  // don't use setSelected, infinite loop!
+
+                for (Edge e = getGraph().getFirstAdjacentEdge(v); e != null; e = getGraph().getNextAdjacentEdge(e, v)) {
+                    if (!getSelected(e)) {
+                        if (getPhyloGraph().getSplit(e) == id)
+                            selectedEdges.add(e);
+                        else {
+                            Node w = getGraph().getOpposite(v, e);
+                            selectGraphComponent(w, id);
+                        }
+                    }
+                }
+            }
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * update view of nodes and edges
+     */
+    public void resetViews() {
+        PhyloGraph G = (PhyloGraph) getGraph();
+
+        for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+            setLabel(v, G.getLabel(v));
+            //setShape(v, NodeView.NONE_NODE);
+            /*
+        if (G.getLabel(v) != null && G.getLabel(v).equals("") == false)
+            setShape(v, NodeView.OVAL_NODE);
+        else
+            setShape(v, NodeView.NONE_NODE);
+            */
+
+        }
+        for (Edge e = G.getFirstEdge(); e != null; e = G.getNextEdge(e)) {
+            setLabel(e, G.getLabel(e));
+            //setDirection(e, EdgeView.UNDIRECTED);
+        }
+    }
+
+    /**
+     * returns the phylograph  associated with this phylographview
+     *
+     * @return the phylograph
+     */
+    public PhyloGraph getPhyloGraph() {
+        return (PhyloGraph) super.getGraph();
+    }
+
+    /**
+     * Select all nodes labeled
+     */
+    public void selectAllLabeledNodes() {
+        selectedNodes.clear();
+        for (Node v = getGraph().getFirstNode(); v != null; v = v.getNext()) {
+            if (getPhyloGraph().getNode2Taxa(v) != null && getLabel(v) != null && getLabel(v).length() > 0)
+                selectedNodes.add(v);
+        }
+    }
+
+    /**
+     * select all nodes labeled by any of the given taxa set
+     *
+     * @param taxaSet collection of taxa
+     */
+    public void selectNodesLabeledByTaxa(BitSet taxaSet) {
+        selectedNodes.clear();
+        if (taxaSet.cardinality() == 0)
+            return;
+        int count = 0;
+        Iterator it = getPhyloGraph().nodeIterator();
+        boolean allFound = false;
+        while (it.hasNext() && !allFound) {
+            Node v = (Node) it.next();
+            List L = getPhyloGraph().getNode2Taxa(v);
+
+            if (L == null)
+                continue;
+
+            for (Object aL : L) {
+                if (taxaSet.get((Integer) aL)) {
+                    selectedNodes.add(v);
+                    if (++count == taxaSet.cardinality())
+                        allFound = true;
+                    break;
+                }
+            }
+        }
+        fireDoSelect(selectedNodes);
+    }
+
+    /**
+     * are we using the split selection model?
+     *
+     * @return boolean
+     */
+    public boolean getUseSplitSelectionModel() {
+        return useSplitSelectionModel;
+    }
+
+    /**
+     * set or unset use of split selection model
+     *
+     * @param useSplitSelectionModel
+     */
+    public void setUseSplitSelectionModel(boolean useSplitSelectionModel) {
+        this.useSplitSelectionModel = useSplitSelectionModel;
+    }
+
+    /**
+     * removes the given split from the graph by contracting all edges representing
+     * the split
+     *
+     * @param splitId
+     */
+    public void removeSplit(int splitId, boolean updateNodePositions) {
+        PhyloGraph graph = getPhyloGraph();
+        try {
+            Node one = graph.getTaxon2Node(1);
+            if (one != null) {
+                List<Pair<Node, Edge>> separators = new LinkedList<>();
+                graph.getAllSeparators(splitId, one, null, new NodeSet(graph), separators);
+                if (updateNodePositions) {
+                        // move all the nodes on one side of the split:
+                        Pair separator = (Pair) separators.get(0);
+                        Node v = (Node) separator.getFirst();
+                        Edge e = (Edge) separator.getSecond();
+                        Node w = graph.getOpposite(v, e);
+                        Point2D offset = Geometry.diff(getLocation(w), getLocation(v));
+                        Point2D oneSideOffset = new Point2D.Double(0.5 * offset.getX(), 0.5 * offset.getY());
+                        moveNodes(splitId, oneSideOffset, v, null, new NodeSet(graph));
+                        Point2D otherSideOffset = new Point2D.Double(-0.5 * offset.getX(), -0.5 * offset.getY());
+                        moveNodes(splitId, otherSideOffset, w, null, new NodeSet(graph));
+                    }
+
+                    // remove the split in the graph
+                    graph.removeSplit(splitId);
+
+                    // update labels:
+
+                for (Pair<Node, Edge> separator : separators) {
+                    Node u = separator.getFirst();
+                        setLabel(u, graph.getLabel(u));
+                    }
+            }
+        } catch (NotOwnerException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * moves all nodes that are reachable without using an edge of the given splitId.
+     *
+     * @param splitId forbidden splitId
+     * @param offset  move nodes by this amount
+     * @param v       current node
+     * @param e       current edge
+     * @param seen    set of nodes already visited
+     * @throws NotOwnerException
+     */
+    private void moveNodes(int splitId, Point2D offset, Node v, Edge e, NodeSet seen) throws NotOwnerException {
+        if (!seen.contains(v)) {
+            seen.add(v);
+            Point2D origLocation = getLocation(v);
+            Point2D newLocation = new Point2D.Double(origLocation.getX() + offset.getX(), origLocation.getY() + offset.getY());
+            setLocation(v, newLocation);
+            PhyloGraph graph = getPhyloGraph();
+            for (Edge f = graph.getFirstAdjacentEdge(v); f != null; f = graph.getNextAdjacentEdge(f, v)) {
+                if (f != e) {
+                    if (graph.getSplit(f) != splitId)
+                        moveNodes(splitId, offset, graph.getOpposite(v, f), f, seen);
+                }
+            }
+        }
+    }
+
+    /**
+     * Writes a tree in bracket notation
+     *
+     * @param wgts write edge weights or not
+     */
+    public String getNewick(boolean wgts) {
+        if (getPhyloGraph().getNumberOfEdges() != getPhyloGraph().getNumberOfNodes() - 1
+                || getPhyloGraph().getNumberConnectedComponents() != 1)
+            return null; // graph is not a tree
+
+        StringWriter out = new StringWriter();
+        if (getPhyloGraph().getNumberOfEdges() > 0) {
+            try {
+                boolean ok;
+
+                NodeSet seen = new NodeSet(getGraph());
+
+                Edge e = getPhyloGraph().getFirstEdge();
+                Node v = getPhyloGraph().getSource(e);
+                Node u = getPhyloGraph().getTarget(e);
+                out.write("(");
+                ok = writeRec(seen, out, v, e, wgts);
+                if (wgts) {
+                    double weight = getPhyloGraph().getWeight(e);
+                    try {
+                        weight = Double.parseDouble(this.getLabel(e));
+                    } catch (Exception ex) {
+                    }
+                    out.write(":" + (float) (weight / 2.0));
+                }
+                out.write(",");
+                if (ok)
+                    ok = writeRec(seen, out, u, e, wgts);
+                if (wgts) {
+                    double weight = getPhyloGraph().getWeight(e);
+                    try {
+                        weight = Double.parseDouble(this.getLabel(e));
+                    } catch (Exception ex) {
+                    }
+
+                    out.write(":" + (float) (weight / 2.0) + "):0;");
+                } else
+                    out.write(");");
+                if (!ok || seen.size() != getPhyloGraph().getNumberOfNodes())
+                    return null;
+            } catch (IOException ex) {
+                Basic.caught(ex);
+                return null;
+            }
+        } else if (getPhyloGraph().getNumberOfNodes() == 1) {
+            out.write("(" + this.getLabel(getPhyloGraph().getFirstNode()) + ");");
+        } else
+            out.write("();");
+        return out.toString();
+    }
+
+    /**
+     * Recursively writes a tree in bracket notation
+     *
+     * @param out  Writer
+     * @param r    Node
+     * @param e    Edge
+     * @param wgts boolean
+     */
+    private boolean writeRec(NodeSet seen, Writer out, Node r, Edge e, boolean wgts)
+            throws IOException {
+        if (seen.contains(r))
+            return false;
+        seen.add(r);
+
+        if (getPhyloGraph().getDegree(r) == 1) {
+            out.write(this.getLabel(r));
+        } else // degree >=2
+        {
+            if (this.getLabel(r) != null && this.getLabel(r).length() > 0)
+                out.write(this.getLabel(r) + ",");
+            boolean first = true;
+            out.write("(");
+            Iterator edges = getPhyloGraph().getAdjacentEdges(r);
+            while (edges.hasNext()) {
+                Edge f = (Edge) edges.next();
+                if (f != e) {
+                    if (first)
+                        first = false;
+                    else
+                        out.write(",");
+
+                    Node v = getPhyloGraph().getOpposite(r, f);
+                    if (!writeRec(seen, out, v, f, wgts))
+                        return false;
+                    if (wgts) {
+                        double weight = getPhyloGraph().getWeight(f);
+                        try {
+                            weight = Double.parseDouble(this.getLabel(f));
+                        } catch (Exception ex) {
+                        }
+                        out.write(":" + (float) (weight));
+                    }
+                }
+            }
+            out.write(")");
+        }
+        return true;
+    }
+
+
+    /**
+     * Embeds the tree in linear time.
+     */
+    public void embed() {
+        Graph G = getGraph();
+        if (G.getNumberOfNodes() == 0)
+            return;
+
+        {
+            Node root = G.getFirstNode();
+            NodeSet leaves = new NodeSet(G);
+
+            try {
+                for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                    if (G.getDegree(v) == 1)
+                        leaves.add(v);
+                    if (G.getDegree(v) > G.getDegree(root))
+                        root = v;
+                }
+
+                // recursively visit all nodes in the tree and determine the
+                // angle 0-2PI of each edge. nodes are placed around the unit
+                // circle at position
+                // n=1,2,3,... and then an edge along which we visited nodes
+                // k,k+1,...j-1,j is directed towards positions k,k+1,...,j
+
+                EdgeDoubleArray angle = new EdgeDoubleArray(G); // angle of edge
+                Random rand = new Random();
+                rand.setSeed(1);
+                int seen = setAnglesRec(0, root, null, leaves, angle, rand);
+
+                // rotate all edges so that taxon number 1 appears on the right:
+                Node v = getPhyloGraph().getTaxon2Node(1);
+                if (v != null) {
+                    Edge e = v.getFirstAdjacentEdge();
+                    if (e != null) {
+                        double alpha = angle.getValue(e);
+                        for (Edge f = getGraph().getFirstEdge(); f != null; f = f.getNext()) {
+                            angle.set(f, angle.getValue(f) - alpha);
+                        }
+                    }
+                }
+
+                if (seen != leaves.size())
+                    System.err.println("Warning: Number of nodes seen: " + seen +
+                            " != Number of leaves: " + leaves.size());
+
+                // recursively compute node coordinates from edge angles:
+                setCoordsRec(root, null, angle);
+            } catch (NotOwnerException ex) {
+                Basic.caught(ex);
+            }
+        }
+    }
+
+    /**
+     * Recursively determines the angle of every tree edge.
+     *
+     * @param num    int
+     * @param root   Node
+     * @param entry  Edge
+     * @param leaves NodeSet
+     * @param angle  EdgeDoubleArray
+     * @param rand   Random
+     * @return b int
+     */
+
+    private int setAnglesRec(int num, Node root, Edge entry, NodeSet leaves, EdgeDoubleArray angle, Random rand) throws NotOwnerException {
+        Graph G = getGraph();
+
+        if (leaves.contains(root))
+            return num + 1;
+        else {
+            Iterator edges = G.getAdjacentEdges(root);
+            // edges.permute(); // look at children in random order
+
+            int a = num; // is number of nodes seen so far
+            int b = 0;     // number of nodes after visiting subtree
+
+            while (edges.hasNext()) {
+                Edge e = (Edge) edges.next();
+                if (e != entry) {
+                    b = setAnglesRec(a, G.getOpposite(root, e), e, leaves, angle, rand);
+
+                    // point towards the segment of the unit circle a...b:
+                    angle.set(e, Math.PI * (a + b) / leaves.size());
+
+                    a = b;
+                }
+            }
+            if (b == 0)
+                System.err.println("Warning: setAnglesRec: recursion failed");
+            return b;
+        }
+    }
+
+    /**
+     * recursively compute node coordinates from edge angles:
+     *
+     * @param root  Node
+     * @param entry Edge
+     * @param angle EdgeDouble
+     */
+
+    private void setCoordsRec(Node root, Edge entry, EdgeDoubleArray angle)
+            throws NotOwnerException {
+        Graph G = getGraph();
+
+        Iterator edges = G.getAdjacentEdges(root);
+
+        while (edges.hasNext()) {
+            Edge e = (Edge) edges.next();
+
+            if (e != entry) {
+                Node v = G.getOpposite(root, e);
+
+                // translate in the computed direction by the given amount
+                setLocation(v,
+                        Geometry.translateByAngle(getLocation(root), angle.getValue(e),
+                                ((PhyloTree) G).getWeight(e)));
+
+                setCoordsRec(v, e, angle);
+            }
+        }
+    }
+
+    /**
+     * gets the set of selected node labels
+     *
+     * @return selected node labels
+     */
+    public Set<String> getSelectedNodeLabels() {
+        Set<String> selectedLabels = new HashSet<>();
+        for (Node v = getSelectedNodes().getFirstElement(); v != null; v = getSelectedNodes().getNextElement(v))
+            if (getPhyloGraph().getLabel(v) != null)
+                selectedLabels.add(getPhyloGraph().getLabel(v));
+        return selectedLabels;
+
+    }
+
+    /**
+     * contract all given edges
+     *
+     * @param edges
+     * @return number of edges successfully removed
+     */
+    public boolean contractAll(Set<Edge> edges) {
+        boolean result = false;
+        final PhyloGraph graph = getPhyloGraph();
+        final Set<Node> diVertices = new HashSet<>();
+        while (edges.size() > 0) {
+            final Edge e = edges.iterator().next();
+            edges.remove(e);
+            if (!graph.isSpecial(e) && e.getTarget().getOutDegree() > 0) {
+                final Node v = e.getSource();
+                final Node w = e.getTarget();
+                if (w.getOutDegree() == 0) {
+                    if (graph.getLabel(w) != null && graph.getLabel(w).length() > 0) {
+                        if (graph.getLabel(v) == null || graph.getLabel(v).length() == 0)
+                            graph.setLabel(v, graph.getLabel(w));
+                        else {
+                            graph.setLabel(v, graph.getLabel(v) + "+" + graph.getLabel(w));
+                        }
+                    }
+                    graph.deleteEdge(e);
+                } else {
+                    for (Edge f = w.getFirstOutEdge(); f != null; f = w.getNextOutEdge(f)) {
+                        final Edge h = graph.newEdge(v, f.getTarget());
+                        graph.setWeight(h, graph.getWeight(f));
+                        graph.setConfidence(h, graph.getConfidence(f));
+                        if (edges.remove(f))
+                            edges.add(h);
+                        result = true;
+                    }
+                    diVertices.remove(w);
+                    graph.deleteNode(w);
+                    if (v.getInDegree() == 1 && v.getOutDegree() == 1)
+                        diVertices.add(v);
+                }
+            }
+        }
+
+        for (Node v : diVertices)
+        {
+            Edge f = graph.newEdge(v.getFirstInEdge().getSource(), v.getFirstOutEdge().getTarget());
+            graph.setWeight(f, graph.getWeight(v.getFirstInEdge()) + graph.getWeight(v.getFirstOutEdge()));
+            graph.setConfidence(f, 0.5 * (graph.getConfidence(v.getFirstInEdge()) + graph.getConfidence(v.getFirstOutEdge())));
+        }
+        return result;
+    }
+}
+
+// EOF
diff --git a/src/jloda/phylo/PhyloTree.java b/src/jloda/phylo/PhyloTree.java
new file mode 100644
index 0000000..840c1be
--- /dev/null
+++ b/src/jloda/phylo/PhyloTree.java
@@ -0,0 +1,1271 @@
+/**
+ * PhyloTree.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.phylo;
+
+/**
+ * @version $Id: PhyloTree.java,v 1.87 2010-05-01 09:37:58 huson Exp $
+ *
+ * Phylogenetic tree
+ *
+ * @author Daniel Huson
+ */
+
+import jloda.graph.*;
+import jloda.util.Basic;
+import jloda.util.NotOwnerException;
+import jloda.util.Pair;
+import jloda.util.ProgramProperties;
+
+import java.io.*;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public class PhyloTree extends PhyloGraph {
+    public static final boolean ALLOW_WRITE_RETICULATE = true;
+    public static final boolean ALLOW_READ_RETICULATE = true;
+    public static final boolean ALLOW_READ_WRITE_EDGE_LABELS = true;
+
+    public boolean allowMultiLabeledNodes = true;
+
+    Node root = null; // can be a node or edge
+    boolean inputHasMultiLabels = false;
+    static boolean warnMultiLabeled = true;
+    private boolean hideCollapsedSubTreeOnWrite = false;
+    public static final String COLLAPSED_NODE_SUFFIX = "{+}";
+
+    private final boolean cleanLabelsOnWrite;
+
+    private String name = null;
+
+    protected final NodeArray<List<Node>> node2GuideTreeChildren; // keep track of children in LSA tree in network
+
+    /**
+     * Construct a new empty phylogenetic tree.
+     */
+    public PhyloTree() {
+        super();
+        cleanLabelsOnWrite = ProgramProperties.get("cleanTreeLabelsOnWrite", false);
+        node2GuideTreeChildren = new NodeArray<>(this);
+    }
+
+    /**
+     * Clears the tree.
+     */
+    public void clear() {
+        super.clear();
+        setRoot((Node) null);
+    }
+
+    /**
+     * copies a phylogenetic tree
+     *
+     * @param src original tree
+     * @return mapping of old nodes to new nodes
+     */
+    public NodeArray<Node> copy(PhyloTree src) {
+        NodeArray<Node> oldNode2NewNode = super.copy(src);
+
+        if (src.getRoot() != null) {
+            Node root = src.getRoot();
+            setRoot(oldNode2NewNode.get(root));
+        }
+        for (Node v = src.getFirstNode(); v != null; v = v.getNext()) {
+            List<Node> children = src.getNode2GuideTreeChildren().get(v);
+            if (children != null) {
+                List<Node> newChildren = new LinkedList<>();
+                for (Node w : children) {
+                    newChildren.add(oldNode2NewNode.get(w));
+                }
+                getNode2GuideTreeChildren().set(oldNode2NewNode.get(v), newChildren);
+            }
+        }
+
+        return oldNode2NewNode;
+    }
+
+    /**
+     * copies a phylogenetic tree
+     *
+     * @param src
+     * @param oldNode2NewNode
+     * @param oldEdge2NewEdge
+     */
+    public void copy(PhyloTree src, NodeArray<Node> oldNode2NewNode, EdgeArray<Edge> oldEdge2NewEdge) {
+        oldNode2NewNode = super.copy(src, oldNode2NewNode, oldEdge2NewEdge);
+        super.copy(src, oldNode2NewNode, oldEdge2NewEdge);
+        if (src.getRoot() != null) {
+            Node root = src.getRoot();
+            setRoot(oldNode2NewNode.get(root));
+        }
+        for (Node v = src.getFirstNode(); v != null; v = v.getNext()) {
+            List<Node> children = src.getNode2GuideTreeChildren().get(v);
+            if (children != null) {
+                List<Node> newChildren = new LinkedList<>();
+                for (Node w : children) {
+                    newChildren.add(oldNode2NewNode.get(w));
+                }
+                getNode2GuideTreeChildren().set(oldNode2NewNode.get(v), newChildren);
+            }
+        }
+    }
+
+    /**
+     * clones the current tree
+     *
+     * @return a clone of the current tree
+     */
+    public Object clone() {
+        PhyloTree tree = new PhyloTree();
+        tree.copy(this);
+        return tree;
+    }
+
+    /**
+     * Sets the label of an edge.
+     *
+     * @param e Edge
+     * @param a String
+     */
+    public void setLabel(Edge e, String a) throws NotOwnerException {
+        edgeLabels.set(e, a);
+    }
+
+    /**
+     * Produces a string representation of the tree in bracket notation.
+     *
+     * @return a string representation of the tree in bracket notation
+     */
+    public String toBracketString() {
+        StringWriter sw = new StringWriter();
+        try {
+            write(sw, true);
+        } catch (Exception ex) {
+            Basic.caught(ex);
+            return "();";
+        }
+        return sw.toString();
+    }
+
+    /**
+     * Produces a string representation of the tree in bracket notation.
+     *
+     * @return a string representation of the tree in bracket notation
+     */
+    public String toBracketString(boolean showWeights) {
+        StringWriter sw = new StringWriter();
+        try {
+            write(sw, showWeights);
+        } catch (Exception ex) {
+            Basic.caught(ex);
+            return "();";
+        }
+        return sw.toString();
+    }
+
+    /**
+     * gets the string representation of this tree
+     *
+     * @return tree
+     */
+    public String toString() {
+        return toBracketString();
+    }
+
+    /**
+     * Produces a string representation of the tree in bracket notation.
+     *
+     * @return a string representation of the tree in bracket notation
+     */
+    public String toString(Map translate) {
+        StringWriter sw = new StringWriter();
+        try {
+            if (translate == null || translate.size() == 0) {
+                this.write(sw, true);
+
+            } else {
+                PhyloTree tmpTree = new PhyloTree();
+                tmpTree.copy(this);
+                for (Node v = tmpTree.getFirstNode(); v != null; v = v.getNext()) {
+                    String key = tmpTree.getLabel(v);
+                    if (key != null) {
+                        String value = (String) translate.get(key);
+                        if (value != null)
+                            tmpTree.setLabel(v, value);
+                    }
+                }
+                tmpTree.write(sw, true);
+            }
+        } catch (Exception ex) {
+            Basic.caught(ex);
+            return "()";
+        }
+        return sw.toString();
+    }
+
+    /**
+     * Given a string representation of a tree, returns the tree.
+     *
+     * @param str      String
+     * @param keepRoot Boolean. Set the root as the top level node and remove deg 2 nodes
+     * @return tree PhyloTree
+     */
+    static public PhyloTree valueOf(String str, boolean keepRoot) throws IOException {
+        final PhyloTree tree = new PhyloTree();
+        tree.parseBracketNotation(str, keepRoot);
+        return tree;
+    }
+
+    /**
+     * Read a tree in newick notation as unrooted tree
+     *
+     * @param r the reader
+     */
+    public void read(Reader r) throws IOException {
+        read(r, false);
+
+    }
+
+    /**
+     * Read a tree in Newick notation, as rooted tree, if desired
+     *
+     * @param r      the reader
+     * @param rooted read as rooted tree
+     */
+    public void read(final Reader r, final boolean rooted) throws IOException {
+        final BufferedReader br;
+        if (r instanceof BufferedReader)
+            br = (BufferedReader) r;
+        else
+            br = new BufferedReader(r);
+        parseBracketNotation(br.readLine(), rooted);
+    }
+
+    /**
+     * parse a tree in Newick format, discarding the root
+     *
+     * @param str
+     * @throws IOException
+     */
+    public void parseBracketNotation(String str) throws IOException {
+        parseBracketNotation(str, false);
+    }
+
+    boolean hasWeights = false;
+
+    /**
+     * parse a tree in newick format, as a rooted tree, if desired.
+     *
+     * @param str
+     * @param rooted maintain root, even if it has degree 2
+     * @throws IOException
+     */
+    public void parseBracketNotation(String str, boolean rooted) throws IOException {
+        parseBracketNotation(str, rooted, true);
+    }
+
+
+    /**
+     * parse a tree in newick format, as a rooted tree, if desired.
+     *
+     * @param str
+     * @param rooted   maintain root, even if it has degree 2
+     * @param doClear: erase the existing tree?
+     * @throws IOException
+     */
+    public void parseBracketNotation(String str, boolean rooted, boolean doClear) throws IOException {
+        if (doClear)
+            clear();
+        setInputHasMultiLabels(false);
+        Map<String, Node> seen = new HashMap<>();
+
+        hasWeights = false;
+        try {
+            parseBracketNotationRecursively(seen, 0, null, 0, str);
+        } catch (IOException ex) {
+            System.err.println(str);
+            throw ex;
+        }
+        Node v = getFirstNode();
+        if (v != null) {
+            if (rooted) {
+                setRoot(v);
+                if (!hasWeights && isUnlabeledDiVertex(v)) {
+                    setWeight(v.getFirstAdjacentEdge(), 0.5);
+                    setWeight(v.getLastAdjacentEdge(), 0.5);
+                }
+            } else {
+                setRoot((Node) null);
+                if (isUnlabeledDiVertex(v))
+                    delDivertex(v);
+            }
+        }
+        if (ALLOW_READ_RETICULATE)
+            postProcessReticulate();
+
+        // System.err.println("Bootstrap values detected:    " + getInputHasBootstrapValuesOnNodes());
+        // System.err.println("Multi-labeled nodes detected: " + getInputHasMultiLabels());
+    }
+
+    /**
+     * is v an unlabeled node of degree 2?
+     *
+     * @param v
+     * @return true, if v is an unlabeled node of degree 2
+     */
+    private boolean isUnlabeledDiVertex(Node v) {
+        return v.getDegree() == 2 && (getLabel(v) == null || getLabel(v).length() == 0);
+    }
+
+    private static final String punctuationCharacters = "),;:";
+    private static final String startOfNumber = "-.0123456789";
+
+
+    /**
+     * recursively do the work
+     *
+     * @param seen  set of seen labels
+     * @param depth distance from root
+     * @param v     parent node
+     * @param i     current position in string
+     * @param str   string
+     * @return new current position
+     * @throws IOException
+     */
+    private int parseBracketNotationRecursively(Map<String, Node> seen, int depth, Node v, int i, String str) throws IOException {
+        try {
+            for (i = Basic.skipSpaces(str, i); i < str.length(); i = Basic.skipSpaces(str, i + 1)) {
+                Node w = newNode();
+                String label = null;
+                if (str.charAt(i) == '(') {
+                    i = parseBracketNotationRecursively(seen, depth + 1, w, i + 1, str);
+                    if (str.charAt(i) != ')')
+                        throw new IOException("Expected ')' at position " + i);
+                    i = Basic.skipSpaces(str, i + 1);
+                    while (i < str.length() && punctuationCharacters.indexOf(str.charAt(i)) == -1) {
+                        int i0 = i;
+                        StringBuilder buf = new StringBuilder();
+                        boolean inQuotes = false;
+                        while (i < str.length() && (inQuotes || punctuationCharacters.indexOf(str.charAt(i)) == -1)) {
+                            if (str.charAt(i) == '\'')
+                                inQuotes = !inQuotes;
+                            else
+                                buf.append(str.charAt(i));
+                            i++;
+                        }
+                        label = buf.toString().trim();
+
+                        if (label.length() > 0) {
+                            if (!getAllowMultiLabeledNodes() && seen.containsKey(label) && PhyloTreeUtils.findReticulateLabel(label) == null)
+                            // if label already used, make unique, unless this is a reticulate node
+                            {
+                                if (label.startsWith("'") && label.endsWith("'") && label.length() > 1)
+                                    label = label.substring(1, label.length() - 1);
+                                // give first occurence of this label the suffix .1
+                                final Node old = seen.get(label);
+                                if (old != null) // change label of node
+                                {
+                                    setLabel(old, label + ".1");
+                                    seen.put(label, null); // keep label in, but null indicates has changed
+                                    seen.put(label + ".1", old);
+                                    setInputHasMultiLabels(true);
+                                }
+
+                                int t = 1;
+                                String labelt;
+                                do {
+                                    labelt = label + "." + (++t);
+                                } while (seen.containsKey(labelt));
+                                label = labelt;
+                            }
+                            seen.put(label, w);
+                        }
+                        setLabel(w, label);
+                        if (label.length() == 0)
+                            throw new IOException("Expected label at position " + i0);
+                    }
+                } else // everything to next ) : or , is considered a label:
+                {
+                    if (getNumberOfNodes() == 1)
+                        throw new IOException("Expected '(' at position " + i);
+                    int i0 = i;
+                    final StringBuilder buf = new StringBuilder();
+                    boolean inQuotes = false;
+                    while (i < str.length() && (inQuotes || punctuationCharacters.indexOf(str.charAt(i)) == -1)) {
+                        if (str.charAt(i) == '\'')
+                            inQuotes = !inQuotes;
+                        else
+                            buf.append(str.charAt(i));
+                        i++;
+                    }
+                    label = buf.toString().trim();
+
+                    if (label.startsWith("'") && label.endsWith("'") && label.length() > 1)
+                        label = label.substring(1, label.length() - 1).trim();
+
+
+                    if (label.length() > 0) {
+                        if (!getAllowMultiLabeledNodes() && seen.containsKey(label) && PhyloTreeUtils.findReticulateLabel(label) == null) {
+                            // give first occurrence of this label the suffix .1
+                            Node old = seen.get(label);
+                            if (old != null) // change label of node
+                            {
+                                setLabel(old, label + ".1");
+                                seen.put(label, null); // keep label in, but null indicates has changed
+                                seen.put(label + ".1", old);
+                                setInputHasMultiLabels(true);
+                                if (getWarnMultiLabeled())
+                                    System.err.println("multi-label: " + label);
+                            }
+
+                            int t = 1;
+                            String labelt;
+                            do {
+                                labelt = label + "." + (++t);
+                            } while (seen.containsKey(labelt));
+                            label = labelt;
+                        }
+                        seen.put(label, w);
+                    }
+                    setLabel(w, label);
+                    if (label.length() == 0)
+                        throw new IOException("Expected label at position " + i0);
+                }
+                Edge e = null;
+                if (v != null)
+                    e = newEdge(v, w);
+
+                // detect and read embedded bootstrap values:
+                i = Basic.skipSpaces(str, i);
+
+                // read edge weights
+
+                if (i < str.length() && str.charAt(i) == ':') // edge weight is following
+                {
+                    i = Basic.skipSpaces(str, i + 1);
+                    int i0 = i;
+                    StringBuilder buf = new StringBuilder();
+                    while (i < str.length() && (punctuationCharacters.indexOf(str.charAt(i)) == -1 && str.charAt(i) != '['))
+                        buf.append(str.charAt(i++));
+                    String number = buf.toString().trim();
+                    try {
+                        double weight = Math.max(0, Double.parseDouble(number));
+                        if (e != null)
+                            setWeight(e, weight);
+                        if (!hasWeights)
+                            hasWeights = true;
+                    } catch (Exception ex) {
+                        throw new IOException("Expected number at position " + i0 + " (got: '" + number + "')");
+                    }
+                }
+
+                // adjust edge weights for reticulate edges
+                if (e != null) {
+                    try {
+                        if (label != null && PhyloTreeUtils.isReticulateNode(label)) {
+                            // if an instance of a reticulate node is marked ##, then we will set the weight of the edge to the node to a number >0
+                            // to indicate that edge should be drawn as a tree node
+                            if (PhyloTreeUtils.isReticulateAcceptorEdge(label)) {
+                                if (getWeight(e) <= 0)
+                                    setWeight(e, 0.000001);
+                            } else {
+                                if (getWeight(e) > 0)
+                                    setWeight(e, 0);
+                            }
+                        }
+                    } catch (IllegalSelfEdgeException e1) {
+                        Basic.caught(e1);
+                    }
+                }
+
+                // now i should be pointing to a ',', a ')' or '[' (for a label)
+                if (i >= str.length()) {
+                    if (depth == 0)
+                        return i; // finished parsing tree
+                    else
+                        throw new IOException("Unexpected end of line");
+                }
+                if (str.charAt(i) == '[') // edge label
+                {
+                    int x = str.indexOf('[', i + 1);
+                    int j = str.indexOf(']', i + 1);
+                    if (j == -1 || (x != -1 && x < j))
+                        throw new IOException("Error in edge label at position: " + i);
+                    setLabel(e, str.substring(i + 1, j));
+                    i = j + 1;
+                }
+                if (str.charAt(i) == ';' && depth == 0)
+
+                    return i; // finished parsing tree
+                else if (str.charAt(i) == ')')
+                    return i;
+                else if (str.charAt(i) != ',')
+                    throw new IOException("Unexpected '" + str.charAt(i) + "' at position " + i);
+            }
+        } catch (NotOwnerException ex) {
+            throw new IOException(ex);
+        }
+        return -1;
+    }
+
+    /**
+     * deletes artificial divertex
+     *
+     * @param v Node
+     * @return the new edge
+     */
+    public Edge delDivertex(Node v) {
+        if (v.getDegree() != 2)
+            throw new RuntimeException("v not divertex, degree is: " + v.getDegree());
+
+        Edge e = getFirstAdjacentEdge(v);
+        Edge f = getLastAdjacentEdge(v);
+
+        Node x = getOpposite(v, e);
+        Node y = getOpposite(v, f);
+
+        Edge g = null;
+        try {
+            if (x == e.getSource())
+                g = newEdge(x, y);
+            else
+                g = newEdge(y, x);
+        } catch (IllegalSelfEdgeException e1) {
+            Basic.caught(e1);
+        }
+        if (edgeWeights.get(e) != null && edgeWeights.get(f) != null)
+            setWeight(g, getWeight(e) + getWeight(f));
+        if (root == v)
+            root = null;
+        deleteNode(v);
+        return g;
+    }
+
+    /**
+     * post processes a tree that really describes a reticulate network
+     *
+     * @return number of reticulate nodes
+     */
+    public int postProcessReticulate() {
+        int count = 0;
+
+        // determine all the groups of reticulate ndoes
+        Map<String, List<Node>> reticulateNumber2Nodes = new HashMap<>(); // maps each reticulate-node prefix to the set of all nodes that have it
+
+        for (Node v = getFirstNode(); v != null; v = v.getNext()) {
+            String label = getLabel(v);
+            if (label != null && label.length() > 0) {
+                String reticulateLabel = PhyloTreeUtils.findReticulateLabel(label);
+                if (reticulateLabel != null) {
+                    setLabel(v, PhyloTreeUtils.removeReticulateNodeSuffix(label));
+                    List<Node> list = reticulateNumber2Nodes.get(reticulateLabel);
+                    if (list == null) {
+                        list = new LinkedList<>();
+                        reticulateNumber2Nodes.put(reticulateLabel, list);
+                    }
+                    list.add(v);
+                }
+            }
+        }
+
+        // collapse all instances of a reticulate node into one node
+        for (String reticulateNumber : reticulateNumber2Nodes.keySet()) {
+            List<Node> list = reticulateNumber2Nodes.get(reticulateNumber);
+            if (list.size() > 0) {
+                count++;
+                Node u = newNode();  // all edges leading to a reticulate node will be redirected to this node
+                for (Node v : list) {
+                    if (getLabel(v) != null) {
+                        if (getLabel(u) == null)
+                            setLabel(u, getLabel(v));
+                        else if (!getLabel(u).equals(getLabel(v)))
+                            setLabel(u, getLabel(u) + "," + getLabel(v));
+                    }
+
+                    for (Edge e = v.getFirstAdjacentEdge(); e != null; e = v.getNextAdjacentEdge(e)) {
+                        Node w = v.getOpposite(e);
+                        Edge f;
+                        if (w == e.getSource()) {
+                            f = newEdge(w, u);
+                            setSpecial(f, true);
+                        } else {
+                            f = newEdge(u, w);
+                        }
+                        setSplit(f, getSplit(e));
+                        setWeight(f, getWeight(e));
+                        setLabel(f, getLabel(e));
+                    }
+                    deleteNode(v);
+                }
+                boolean hasReticulateAcceptorEdge = false;
+                for (Edge e = u.getFirstInEdge(); e != null; e = u.getNextInEdge(e)) {
+                    if (getWeight(e) > 0)
+                        if (!hasReticulateAcceptorEdge)
+                            hasReticulateAcceptorEdge = true;
+                        else {
+                            setWeight(e, 0);
+                            System.err.println("Warning: node has more than one reticulate-acceptor edge, will only use first");
+                        }
+                }
+                if (hasReticulateAcceptorEdge) {
+                    for (Edge e = u.getFirstInEdge(); e != null; e = u.getNextInEdge(e)) {
+                        if (getWeight(e) == 0)
+                            setWeight(e, -1);
+                    }
+                }
+            }
+        }
+        return count;
+    }
+
+    /**
+     * Writes a tree in bracket notation
+     *
+     * @param outs         the writer
+     * @param writeWeights write edge weights or not
+     */
+    public void write(Writer outs, boolean writeWeights) throws IOException {
+        write(outs, writeWeights, false, null, null);
+    }
+
+    /**
+     * Writes a tree in bracket notation
+     *
+     * @param outs         the writer
+     * @param writeWeights write edge weights or not
+     */
+    public void write(Writer outs, boolean writeWeights, boolean writeEdgeLabels) throws IOException {
+        write(outs, writeWeights, writeEdgeLabels, null, null);
+    }
+
+    private int nodeNumber = 0;
+    private int edgeNumber = 0;
+    private NodeIntegerArray node2reticulateNumber;  // global number of the reticulate node
+    private int reticulateNodeNumber;
+
+    /**
+     * Writes a tree in bracket notation. Uses extended bracket notation to write reticulate network
+     *
+     * @param w                the writer
+     * @param writeEdgeWeights write edge weights or not
+     * @param nodeId2Number    if non-null, will contain node-id to number mapping after call
+     * @param edgeId2Number    if non-null, will contain edge-id to number mapping after call
+     */
+    public void write(Writer w, boolean writeEdgeWeights, boolean writeEdgeLabels, Map<Integer, Integer> nodeId2Number, Map<Integer, Integer> edgeId2Number) throws IOException {
+        nodeNumber = 0;
+        edgeNumber = 0;
+        if (ALLOW_WRITE_RETICULATE) {
+            // following two lines enable us to write cluster networks and reticulate networks in Newick format
+            node2reticulateNumber = new NodeIntegerArray(this);
+            reticulateNodeNumber = 0;
+        }
+
+        if (getNumberOfEdges() > 0) {
+            if (getRoot() == null) {
+                root = getFirstNode();
+                for (Node v = root; v != null; v = v.getNext()) {
+                    if (v.getDegree() > root.getDegree())
+                        root = v;
+                }
+            }
+            writeRec(w, root, null, writeEdgeWeights, writeEdgeLabels, nodeId2Number, edgeId2Number, getLabelForWriting(root));
+        } else if (getNumberOfNodes() == 1) {
+            w.write("(" + getLabelForWriting(getFirstNode()) + ");");
+            if (nodeId2Number != null)
+                nodeId2Number.put(getFirstNode().getId(), 1);
+        } else
+            w.write("();");
+    }
+
+    /**
+     * Recursively writes a tree in bracket notation
+     *
+     * @param outs
+     * @param v
+     * @param e
+     * @param writeEdgeWeights
+     * @param writeEdgeLabels
+     * @param nodeId2Number
+     * @param edgeId2Number
+     * @param nodeLabel
+     * @throws IOException
+     */
+    private void writeRec(Writer outs, Node v, Edge e, boolean writeEdgeWeights, boolean writeEdgeLabels, Map<Integer, Integer> nodeId2Number, Map<Integer, Integer> edgeId2Number, String nodeLabel)
+            throws IOException {
+        if (nodeId2Number != null)
+            nodeId2Number.put(v.getId(), ++nodeNumber);
+        // todo: need to change all code so that trees are always directed away from the root.
+        // todo: must do this in splitstree first!
+
+        int outDegree = 0;
+        if (e == null)
+            outDegree = getDegree(v);
+        else if (isSpecial(e)) {
+            for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f))
+                if (!isSpecial(f))
+                    outDegree++;
+        } else
+            outDegree = getDegree(v) - 1;
+        if ((outDegree > 0 || e == null) && (!isHideCollapsedSubTreeOnWrite() || getLabel(v) == null || !getLabel(v).endsWith(PhyloTree.COLLAPSED_NODE_SUFFIX))) {
+            outs.write("(");
+            boolean first = true;
+            for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f)) {
+                if (f != e) {
+                    if (node2reticulateNumber.getInt(v) > 0 && isSpecial(f))
+                        continue; // don't climb back up a special edge
+
+                    if (edgeId2Number != null)
+                        edgeId2Number.put(f.getId(), ++edgeNumber);
+
+                    if (first)
+                        first = false;
+                    else
+                        outs.write(",");
+
+                    Node w = v.getOpposite(f);
+                    boolean inEdgeHasWeight = (getWeight(f) > 0);
+
+
+                    if (isSpecial(f)) {
+                        if (node2reticulateNumber.getInt(w) == 0) {
+                            node2reticulateNumber.set(w, ++reticulateNodeNumber);
+                            String label;
+                            if (getLabel(w) != null)
+                                label = getLabelForWriting(w) + PhyloTreeUtils.makeReticulateNodeLabel(inEdgeHasWeight, node2reticulateNumber.getInt(w));
+                            else
+                                label = PhyloTreeUtils.makeReticulateNodeLabel(inEdgeHasWeight, node2reticulateNumber.getInt(w));
+
+                            writeRec(outs, w, f, writeEdgeWeights, writeEdgeLabels, nodeId2Number, edgeId2Number, label);
+                        } else {
+                            String label;
+                            if (getLabel(w) != null)
+                                label = getLabelForWriting(w) + PhyloTreeUtils.makeReticulateNodeLabel(inEdgeHasWeight, node2reticulateNumber.getInt(w));
+                            else
+                                label = PhyloTreeUtils.makeReticulateNodeLabel(inEdgeHasWeight, node2reticulateNumber.getInt(w));
+                            outs.write(label);
+                            if (writeEdgeWeights) {
+                                if (getWeight(f) >= 0)
+                                    outs.write(":" + (float) (getWeight(f)));
+                                if (writeEdgeLabels && getLabel(f) != null) {
+                                    outs.write("[" + getLabelForWriting(f) + "]");
+                                }
+                            }
+                        }
+                    } else
+                        writeRec(outs, w, f, writeEdgeWeights, writeEdgeLabels, nodeId2Number, edgeId2Number,
+                                getLabelForWriting(w));
+                }
+            }
+            outs.write(")");
+        }
+        if (nodeLabel != null && nodeLabel.length() > 0)
+            outs.write(nodeLabel);
+        else if (outDegree == 0)
+            outs.write("?");
+        if (writeEdgeWeights && e != null) {
+            if (getWeight(e) >= 0)
+                outs.write(":" + (float) (getWeight(e)));
+            if (writeEdgeLabels && getLabel(e) != null) {
+                outs.write("[" + getLabelForWriting(e) + "]");
+            }
+        }
+    }
+
+    /**
+     * get the label to be used for writing. Will have single quotes, if label contains punctuation character or white space
+     *
+     * @param v
+     * @return
+     */
+    public String getLabelForWriting(Node v) {
+        String label = cleanLabelsOnWrite ? getCleanLabel(v) : getLabel(v);
+        if (label != null) {
+            for (int i = 0; i < label.length(); i++) {
+                if (punctuationCharacters.indexOf(label.charAt(i)) != -1 || Character.isWhitespace(label.charAt(i)))
+                    return "'" + label + "'";
+            }
+        }
+        return label;
+    }
+
+    /**
+     * get the label to be used for writing. Will have single quotes, if label contains punctuation character or white space
+     *
+     * @param e
+     * @return
+     */
+    public String getLabelForWriting(Edge e) {
+        String label = cleanLabelsOnWrite ? getCleanLabel(e) : getLabel(e);
+        if (label != null) {
+            for (int i = 0; i < label.length(); i++) {
+                if (punctuationCharacters.indexOf(label.charAt(i)) != -1 || Character.isWhitespace(label.charAt(i)))
+                    return "'" + label + "'";
+            }
+        }
+        return label;
+    }
+
+    /**
+     * gets a clean version of the label. This is a label that can be printed in a Newick string
+     *
+     * @param v
+     * @return clean label
+     */
+    private String getCleanLabel(Node v) {
+        String label = getLabel(v);
+        if (label == null)
+            return null;
+        else {
+            label = getLabel(v).trim();
+            label = label.replaceAll("[ \\[\\]\\(\\),:;]+", "_");
+            if (label.length() > 0)
+                return label;
+            else
+                return "_";
+        }
+    }
+
+    /**
+     * computes mapping of node ids to numbers 1..numberOfNodes and of edge ids to numbers 1..numberOfEdges.
+     * Proceeds recursively from the root of the tree
+     *
+     * @param nodeId2Number
+     * @param edgeId2Number
+     */
+    public void setId2NumberMaps(Map<Integer, Integer> nodeId2Number, Map<Integer, Integer> edgeId2Number) {
+        if (getRoot() != null)
+            setId2NumberMapsRec(getRoot(), null, new Pair<>(0, 0), nodeId2Number, edgeId2Number);
+    }
+
+    /**
+     * recursively does the work
+     *
+     * @param v
+     * @param e
+     * @param nodeNumberEdgeNumber
+     * @param nodeId2Number
+     * @param edgeId2Number
+     */
+    private void setId2NumberMapsRec(Node v, Edge e, Pair<Integer, Integer> nodeNumberEdgeNumber, Map<Integer, Integer> nodeId2Number, Map<Integer, Integer> edgeId2Number) {
+        int nodes = nodeNumberEdgeNumber.getFirstInt() + 1;
+        nodeNumberEdgeNumber.setFirst(nodes);
+        nodeId2Number.put(v.getId(), nodes);
+        for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f))
+            if (f != e) {
+                int edges = nodeNumberEdgeNumber.getSecondInt() + 1;
+                nodeNumberEdgeNumber.setSecond(edges);
+                edgeId2Number.put(v.getId(), edges);
+                if (PhyloTreeUtils.okToDescendDownThisEdge(this, f, v))
+                    setId2NumberMapsRec(f.getOpposite(v), f, nodeNumberEdgeNumber, nodeId2Number, edgeId2Number);
+            }
+    }
+
+
+    /**
+     * sets the number 2 node and number 2 edge maps
+     *
+     * @param num2node
+     * @param num2edge
+     */
+    public void setNum2NodeEdgeArray(Num2NodeArray num2node, Num2EdgeArray num2edge) {
+        num2node.clear(getNumberOfNodes());
+        num2edge.clear(getNumberOfEdges());
+        setNum2NodeEdgeArrayRec(getRoot(), null, new Pair<>(0, 0), num2node, num2edge);
+    }
+
+    /**
+     * recursively do the work
+     *
+     * @param v
+     * @param e
+     * @param nodeNumberEdgeNumber
+     * @param num2node
+     * @param num2edge
+     */
+    private void setNum2NodeEdgeArrayRec(Node v, Edge e, Pair<Integer, Integer> nodeNumberEdgeNumber, Num2NodeArray num2node, Num2EdgeArray num2edge) {
+        int nodes = nodeNumberEdgeNumber.getFirstInt() + 1;
+        nodeNumberEdgeNumber.setFirst(nodes);
+        num2node.put(nodes, v);
+        for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f))
+            if (f != e) {
+                int edges = nodeNumberEdgeNumber.getSecondInt() + 1;
+                nodeNumberEdgeNumber.setSecond(edges);
+                num2edge.put(edges, f);
+                if (PhyloTreeUtils.okToDescendDownThisEdge(this, f, v))
+                    setNum2NodeEdgeArrayRec(f.getOpposite(v), f, nodeNumberEdgeNumber, num2node, num2edge);
+            }
+    }
+
+    /**
+     * gets the root node if set, or null
+     *
+     * @return root or null
+     */
+    public Node getRoot() {
+        return root;
+    }
+
+    /**
+     * sets the root node
+     *
+     * @param root
+     */
+    public void setRoot(Node root) {
+        this.root = root;
+    }
+
+    /**
+     * sets the root node in the middle of this edge
+     *
+     * @param e
+     */
+    public void setRoot(Edge e, EdgeArray<String> edgeLabels) {
+        setRoot(e, getWeight(e) * 0.5, getWeight(e) * 0.5, edgeLabels);
+    }
+
+    /**
+     * sets the root node in the middle of this edge
+     *
+     * @param e
+     * @param weightToSource weight for new edge adjacent to source of e
+     * @param weightToTarget weight for new adjacent to target of e
+     */
+    public void setRoot(Edge e, double weightToSource, double weightToTarget, EdgeArray<String> edgeLabels) {
+        final Node root = getRoot();
+        if (root != null && root.getDegree() == 2 && (getNode2Taxa(root) == null || getNode2Taxa(root).size() == 0)) {
+            if (root == e.getSource()) {
+                Edge f = (root.getFirstAdjacentEdge() != e ? root.getFirstAdjacentEdge() : root.getLastAdjacentEdge());
+                setWeight(e, weightToSource);
+                setWeight(f, weightToTarget);
+                return; // root stays root
+            } else if (root == e.getTarget()) {
+                Edge f = (root.getFirstAdjacentEdge() != e ? root.getFirstAdjacentEdge() : root.getLastAdjacentEdge());
+                setWeight(e, weightToTarget);
+                setWeight(f, weightToSource);
+                return; // root stays root
+            }
+            eraseRoot(edgeLabels);
+        }
+        Node v = e.getSource();
+        Node w = e.getTarget();
+        Node u = newNode();
+        Edge vu = newEdge(v, u);
+        Edge uw = newEdge(u, w);
+        setWeight(vu, weightToSource);
+        setWeight(uw, weightToTarget);
+        if (edgeLabels != null) {
+            edgeLabels.set(vu, edgeLabels.get(e));
+            edgeLabels.set(uw, edgeLabels.get(e));
+        }
+
+        deleteEdge(e);
+        setRoot(u);
+    }
+
+    /**
+     * erase the current root. If it has out-degree two and is not node-labeled, then two out edges will be replaced by single ege
+     *
+     * @param edgeLabels if non-null and root has two out edges, will try to copy one of the edge labels to the new edge
+     */
+    public void eraseRoot(EdgeArray<String> edgeLabels) {
+        final Node oldRoot = getRoot();
+        setRoot((Node) null);
+        if (oldRoot != null) {
+            if (getOutDegree(oldRoot) == 2 && getLabel(oldRoot) == null) {
+                if (edgeLabels != null) {
+                    String label = null;
+                    for (Edge e = oldRoot.getFirstOutEdge(); e != null; e = oldRoot.getNextOutEdge(e)) {
+                        if (label == null && edgeLabels.get(e) != null)
+                            label = edgeLabels.get(e);
+                        edgeLabels.set(e, null);
+                    }
+                    final Edge e = delDivertex(oldRoot);
+                    edgeLabels.set(e, label);
+                } else
+                    delDivertex(oldRoot);
+            }
+        }
+    }
+
+    /**
+     * prints a tree
+     *
+     * @param out  the print stream
+     * @param wgts show weights?
+     * @throws Exception
+     */
+    public void print(PrintStream out, boolean wgts) throws Exception {
+        StringWriter st = new StringWriter();
+        write(st, wgts);
+        out.println(st.toString());
+    }
+
+
+    /**
+     * was last read tree multi-labeled? If so, the parser replaces instances of the same label
+     * by label.1, label.2 ...
+     *
+     * @return true, if input was multi labeled
+     */
+    public boolean getInputHasMultiLabels() {
+        return inputHasMultiLabels;
+    }
+
+    private void setInputHasMultiLabels(boolean inputHasMultiLabels) {
+        this.inputHasMultiLabels = inputHasMultiLabels;
+    }
+
+
+    /**
+     * returns true if string contains a bootstrap value
+     *
+     * @param label
+     * @return true, if label contains a non-negative float
+     */
+    public static boolean isBootstrapValue(String label) {
+        try {
+            return Float.parseFloat(label) >= 0;
+        } catch (Exception ex) {
+            return false;
+        }
+    }
+
+    /**
+     * allow different nodes to have the same names
+     *
+     * @return
+     */
+    public boolean getAllowMultiLabeledNodes() {
+        return allowMultiLabeledNodes;
+    }
+
+    /**
+     * allow different nodes to have the same names
+     *
+     * @param allowMultiLabeledNodes
+     */
+    public void setAllowMultiLabeledNodes(boolean allowMultiLabeledNodes) {
+        this.allowMultiLabeledNodes = allowMultiLabeledNodes;
+    }
+
+    /**
+     * compute the cycle for this tree and then return it
+     *
+     * @return cycle for this tree
+     */
+    public int[] getCycle(Node v) {
+        computeCycleRec(v, null, 0);
+
+        return super.getCycle();
+    }
+
+    /**
+     * recursively compute a cycle
+     *
+     * @param v
+     * @param e
+     * @param pos
+     */
+    private int computeCycleRec(Node v, Edge e, int pos) {
+        final List<Integer> taxa = node2taxa.get(v);
+        if (taxa != null) {
+            for (Integer t : taxa) {
+                setTaxon2Cycle(t, ++pos);
+            }
+        }
+        for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f)) {
+            if (f != e && PhyloTreeUtils.okToDescendDownThisEdge(this, f, v))
+                pos = computeCycleRec(f.getOpposite(v), f, pos);
+        }
+        return pos;
+    }
+
+    /**
+     * returns tree, if all nodes have degree <=3
+     *
+     * @return true, if binary
+     */
+    public boolean isBifurcating() {
+        for (Node v = getFirstNode(); v != null; v = v.getNext())
+            if (v.getDegree() > 3)
+                return false;
+        return true;
+    }
+
+    /**
+     * warn about multi-labeled trees in input?
+     *
+     * @return true, if warnings are given
+     */
+    static public boolean getWarnMultiLabeled() {
+        return warnMultiLabeled;
+    }
+
+    /**
+     * warn about multi-labeled trees in input?
+     *
+     * @param warnMultiLabeled
+     */
+    static public void setWarnMultiLabeled(boolean warnMultiLabeled) {
+        PhyloTree.warnMultiLabeled = warnMultiLabeled;
+    }
+
+    /**
+     * given a rooted tree and a set of collapsed nodes, returns a tree that contains
+     * only the uncollapsed part of the tree
+     *
+     * @param src
+     * @param collapsedNodes
+     */
+    public void extractTree(PhyloTree src, NodeSet collapsedNodes) {
+        clear();
+        if (src.getRoot() != null) {
+            NodeArray<Node> oldNode2newNode = super.copy(src);
+
+            if (getRoot() != null && oldNode2newNode != null) {
+                setRoot(oldNode2newNode.get(src.getRoot()));
+            }
+
+            NodeSet toDelete = new NodeSet(this);
+            toDelete.addAll();
+            extractTreeRec(src.getRoot(), null, collapsedNodes, oldNode2newNode, toDelete);
+            while (!toDelete.isEmpty()) {
+                Node v = toDelete.getFirstElement();
+                toDelete.remove(v);
+                deleteNode(v);
+            }
+        }
+    }
+
+    /**
+     * recursively does the work
+     *
+     * @param v
+     * @param e
+     * @param collapsedNodes
+     * @param oldNode2newNode
+     * @param toDelete
+     */
+    private void extractTreeRec(Node v, Edge e, NodeSet collapsedNodes, NodeArray<Node> oldNode2newNode, NodeSet toDelete) {
+        toDelete.remove(oldNode2newNode.get(v));
+        if (!collapsedNodes.contains(v)) {
+            for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f)) {
+                if (f != e && PhyloTreeUtils.okToDescendDownThisEdge(this, f, v)) {
+                    extractTreeRec(f.getOpposite(v), f, collapsedNodes, oldNode2newNode, toDelete);
+                }
+            }
+        }
+    }
+
+    /**
+     * hide collapsed subtrees on write?
+     *
+     * @return true, if hidden
+     */
+    public boolean isHideCollapsedSubTreeOnWrite() {
+        return hideCollapsedSubTreeOnWrite;
+    }
+
+    /**
+     * hide collapsed subtrees on write?
+     *
+     * @param hideCollapsedSubTreeOnWrite
+     */
+    public void setHideCollapsedSubTreeOnWrite(boolean hideCollapsedSubTreeOnWrite) {
+        this.hideCollapsedSubTreeOnWrite = hideCollapsedSubTreeOnWrite;
+    }
+
+    /**
+     * redirect edges away from root. Assumes that special edges already point away from root
+     */
+    public void redirectEdgesAwayFromRoot() {
+        redirectEdgesAwayFromRootRec(getRoot(), null);
+
+    }
+
+    /**
+     * recursively does the work
+     *
+     * @param v
+     * @param e
+     */
+    private void redirectEdgesAwayFromRootRec(Node v, Edge e) {
+        if (e != null && v != e.getTarget() && !isSpecial(e))
+            e.reverse();
+        for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f)) {
+            if (f != e && PhyloTreeUtils.okToDescendDownThisEdge(this, f, v))
+                redirectEdgesAwayFromRootRec(f.getOpposite(v), f);
+        }
+    }
+
+    /**
+     * gets a clean version of the label. This is a label that can be printed in a Newick string
+     *
+     * @param v
+     * @return clean label
+     */
+    private String getCleanLabel(Edge v) {
+        String label = getLabel(v);
+        if (label == null)
+            return null;
+        else {
+            label = getLabel(v).trim();
+            label = label.replaceAll("[ \\[\\]\\(\\),:]+", "_");
+            if (label.length() > 0)
+                return label;
+            else
+                return "_";
+        }
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * returns the number of nodes with outdegree 0
+     *
+     * @return number of out-degree 0 nodes
+     */
+    public int getNumberOfLeaves() {
+        int count = 0;
+        for (Node v = getFirstNode(); v != null; v = v.getNext())
+            if (v.getOutDegree() == 0)
+                count++;
+        return count;
+    }
+
+
+    /**
+     * gets the node-2-guide-tree-children array
+     *
+     * @return array
+     */
+    public NodeArray<List<Node>> getNode2GuideTreeChildren() {
+        return node2GuideTreeChildren;
+    }
+}
+
+// EOF
diff --git a/src/jloda/phylo/PhyloTreeUtils.java b/src/jloda/phylo/PhyloTreeUtils.java
new file mode 100644
index 0000000..ec7e00d
--- /dev/null
+++ b/src/jloda/phylo/PhyloTreeUtils.java
@@ -0,0 +1,326 @@
+/**
+ * PhyloTreeUtils.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package jloda.phylo;
+
+import jloda.graph.Edge;
+import jloda.graph.Node;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * some utilities used in phylotree to write and parse reticulate networks
+ * Daniel Huson, 8.2007
+ */
+public class PhyloTreeUtils {
+	private final static boolean CAN_READ_OLD_HnmH_SYNTAX = true;
+
+	/**
+	 * looks for a suffix of the label that starts with '#'
+	 *
+	 * @param label
+	 * @return label or null
+	 */
+	public static String findReticulateLabel(String label) {
+
+		if (CAN_READ_OLD_HnmH_SYNTAX) {
+			int[] hnmh = findOldHnmHLabel(label);
+			if (hnmh != null) {
+				String string = label.substring(hnmh[0], hnmh[1]);
+				StringBuilder buf = new StringBuilder();
+				int state = 0;
+				for (int pos = 0; pos > string.length() && state < 2; pos++) {
+					char ch = string.charAt(pos);
+					switch (state) {
+					case 0: // before number
+						if (Character.isDigit(ch)) {
+							buf.append(ch);
+							state = 1;
+						}
+						break;
+					case 1:
+						if (Character.isDigit(ch)) {
+							buf.append(ch);
+						} else
+							state = 2;
+						break;
+					}
+				}
+				if (state == 2)
+					return buf.toString();
+				else
+					return null;
+			}
+		}
+		int pos = label.lastIndexOf("#"); // look for last instance of '#',
+											// followed by H or L
+		if (pos >= 0 && pos < label.length() - 1 && "HLhl".indexOf(label.charAt(pos + 1)) != -1)
+			return label.substring(pos + 1, label.length());
+		else
+			return null;
+	}
+
+	/**
+	 * determines whether this a reticulate node
+	 *
+	 * @param label
+	 * @return true, if label contains # followed by H L h or l
+	 */
+	public static boolean isReticulateNode(String label) {
+		int pos = label.lastIndexOf("#"); // look for last instance of '#'
+											// followed by H or L
+		return (pos >= 0 && pos < label.length() - 1 && "HLhl".indexOf(label.charAt(pos + 1)) != -1);
+	}
+
+	/**
+	 * determines whether the edge leading to this instance of a reticulate node
+	 * should be treated as an acceptor edge, i.e. as a tree edge that is the
+	 * target of of HGT edge At most one such edge per reticulate node is
+	 * allowed
+	 *
+	 * @param label
+	 * @return true, if label contains ## followed by H L h or l
+	 */
+	public static boolean isReticulateAcceptorEdge(String label) {
+		int pos = label.lastIndexOf("##"); // look for last instance of '##'
+											// followed by H or L
+		return (pos >= 0 && pos < label.length() - 2 && "HLhl".indexOf(label.charAt(pos + 2)) != -1);
+	}
+
+	/**
+	 * removes the reticulate node string from the node label
+	 *
+	 * @param label
+	 * @return string without label or null, if string only consisted of
+	 *         substring
+	 */
+	public static String removeReticulateNodeSuffix(String label) {
+		if (CAN_READ_OLD_HnmH_SYNTAX) {
+			int[] hnmh = findOldHnmHLabel(label);
+			if (hnmh != null) {
+				StringBuilder buf = new StringBuilder();
+				if (hnmh[0] > 0)
+					buf.append(label.substring(0, hnmh[0]));
+				if (hnmh[1] < label.length())
+					buf.append(label.substring(hnmh[1], label.length()));
+				return buf.toString();
+			}
+		}
+
+		int pos = label.indexOf("#");
+		if (pos == -1)
+			return label;
+		else if (pos == 0)
+			return null;
+		else
+			return label.substring(0, pos);
+	}
+
+	/**
+	 * for backward compatibility, finds a label of the form Hn.mH
+	 *
+	 * @param label
+	 * @return first and last+1 position of Hn.mH or null
+	 */
+	private static int[] findOldHnmHLabel(String label) {
+		int state = 0;
+		int start = 0;
+		int finish = 0;
+		for (int i = 0; i < label.length(); i++) {
+			char ch = label.charAt(i);
+			switch (state) {
+			case 0: // outside possible label, looking for H
+				if (ch == 'H') {
+					state = 1;
+					start = i;
+				}
+				break;
+			case 1: // looking for first number
+				if (Character.isDigit(ch))
+					state = 2;
+				else
+					state = 0;
+				break;
+			case 2: // looking for more numbers or dot
+				if (Character.isDigit(ch))
+					state = 2;
+				else if (ch == '.')
+					state = 3;
+				else
+					state = 0;
+				break;
+			case 3: // looking for second number
+				if (Character.isDigit(ch))
+					state = 4;
+				else
+					state = 0;
+				break;
+			case 4: // looking for more numbers or H
+				if (Character.isDigit(ch))
+					state = 4;
+				else if (ch == 'H') {
+					state = 5;
+					finish = i;
+				} else
+					state = 0;
+				break;
+			}
+			if (state == 5)
+				return new int[] { start, finish + 1 };
+		}
+		return null;
+	}
+
+	/**
+	 * makes the node label for a reticulate node
+	 *
+	 * @param number
+	 * @return label
+	 */
+	static String makeReticulateNodeLabel(boolean asAcceptorEdgeTarget, int number) {
+		if (asAcceptorEdgeTarget)
+			return "##H" + number;
+		else
+			return "#H" + number;
+	}
+
+	/**
+	 * determines whether it is ok to descend down an edge in a recursive
+	 * traverse of a tree. It is ok if the edge is not a reticulate edge or is
+	 * the first reticulate edge that enters f
+	 *
+	 * @param tree
+	 * @param e
+	 * @param v
+	 * @return true, if we should descend this edge, false else
+	 */
+	static public boolean okToDescendDownThisEdge(PhyloTree tree, Edge e, Node v) {
+		if (!tree.isSpecial(e))
+			return true;
+		else {
+			if (v != e.getSource())
+				return false; // only go DOWN special edges.
+			Node w = e.getTarget();
+			for (Edge f = w.getFirstInEdge(); f != null; f = w.getNextInEdge(f)) {
+				if (tree.isSpecial(f)) {
+					return f == e; // e must be first in-coming special edge
+				}
+			}
+		}
+		return true; // can't happen
+	}
+
+	/**
+	 * determines whether a set of trees only contains single-labeled trees (not
+	 * necessarily sharing the same set of taxa)
+	 *
+	 * @param trees
+	 * @return true, if ok
+	 */
+	public static boolean areSingleLabeledTrees(PhyloTree[] trees) {
+
+		for (PhyloTree t : trees) {
+			Set<String> taxa = new HashSet<>();
+			for (Node v = t.getFirstNode(); v != null; v = v.getNext()) {
+				if (v.getOutDegree() == 0) {
+					if (taxa.contains(t.getLabel(v)))
+						return false; // not single labeled
+					else
+						taxa.add(t.getLabel(v));
+				}
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * determines whether a tree is single-labeled (not necessarily sharing the
+	 * same set of taxa)
+	 *
+	 * @param trees
+	 * @return true, if ok
+	 */
+	public static boolean areSingleLabeledTrees(PhyloTree trees) {
+
+		Set<String> taxa = new HashSet<>();
+		for (Node v = trees.getFirstNode(); v != null; v = v.getNext()) {
+			if (v.getOutDegree() == 0) {
+				if (taxa.contains(trees.getLabel(v)))
+					return false; // not single labeled
+				else
+					taxa.add(trees.getLabel(v));
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * determines whether the two trees are single-labeled trees on the same
+	 * taxon sets
+	 *
+	 * @param tree1
+	 * @param tree2
+	 * @return true, if ok
+	 */
+	public static boolean areSingleLabeledTreesWithSameTaxa(PhyloTree tree1, PhyloTree tree2) {
+		Set<String> labels1 = new HashSet<>();
+
+		if (tree1.getSpecialEdges().size() > 0 || tree2.getSpecialEdges().size() > 0)
+			return false;
+
+		for (Node v = tree1.getFirstNode(); v != null; v = v.getNext()) {
+			if (v.getOutDegree() == 0) {
+				if (labels1.contains(tree1.getLabel(v)))
+					return false; // not single labeled
+				else
+					labels1.add(tree1.getLabel(v));
+			}
+		}
+
+		Set<String> labels2 = new HashSet<>();
+		for (Node v = tree2.getFirstNode(); v != null; v = v.getNext()) {
+			if (v.getOutDegree() == 0) {
+				if (!labels1.contains(tree2.getLabel(v)))
+					return false; // not present in first tree
+				if (labels2.contains(tree2.getLabel(v)))
+					return false; // not single labeled
+				else
+					labels2.add(tree2.getLabel(v));
+			}
+		}
+		return labels1.size() == labels2.size();
+	}
+
+	/**
+	 * is given phyloTree a bifurcating tree?
+	 *
+	 * @param phyloTree
+	 * @return true, if bifurcating tree
+	 */
+	public static boolean isBifurcatingTree(PhyloTree phyloTree) {
+		for (Node v = phyloTree.getFirstNode(); v != null; v = v.getNext()) {
+			if (v.getInDegree() > 1 || (v.getOutDegree() != 0 && v.getOutDegree() != 2))
+				return false;
+		}
+		return true;
+	}
+}
diff --git a/src/jloda/phylo/PhyloTreeView.java b/src/jloda/phylo/PhyloTreeView.java
new file mode 100644
index 0000000..876e9b1
--- /dev/null
+++ b/src/jloda/phylo/PhyloTreeView.java
@@ -0,0 +1,402 @@
+/**
+ * PhyloTreeView.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.phylo;
+
+import jloda.graph.*;
+import jloda.graphview.EdgeView;
+import jloda.graphview.GraphView;
+import jloda.graphview.NodeView;
+import jloda.util.Geometry;
+import jloda.util.NotOwnerException;
+import jloda.util.Pair;
+
+import java.util.*;
+
+/**
+ *  tree viewer
+ *  Daniel Huson, 2000
+ */
+
+public class PhyloTreeView extends GraphView {
+
+    /**
+     * Constructs a view of a phylogentic tree.
+     *
+     * @param tree PhyloTree
+     */
+    public PhyloTreeView(PhyloTree tree) {
+        this(tree, 400, 400);
+    }
+
+    /**
+     * Constructs a view of a phylogentic tree.
+     *
+     * @param tree        PhyloTree
+     * @param doEmbedding compute an embedding of the tree?
+     */
+    public PhyloTreeView(PhyloTree tree, boolean doEmbedding) {
+        this(tree, 400, 400, doEmbedding);
+    }
+
+    /**
+     * Constructs a view of a phylogentic tree. Computes an embedding of the tree.
+     *
+     * @param tree PhyloTree
+     * @param w    int
+     * @param h    int
+     */
+    public PhyloTreeView(PhyloTree tree, int w, int h) {
+        this(tree, w, h, true);
+
+    }
+
+    /**
+     * Constructs a view of a phylogentic tree. Optinally computes an embedding of the tree.
+     *
+     * @param tree        PhyloTree
+     * @param w           int
+     * @param h           int
+     * @param doEmbedding
+     */
+    public PhyloTreeView(PhyloTree tree, int w, int h, boolean doEmbedding) {
+        super(tree, w, h);
+        setDefaultNodeLocation(0, 0);
+        setMaintainEdgeLengths(true);
+
+        resetViews();
+
+        if (getGraph().getNumberOfNodes() != 0 && doEmbedding) {
+            System.err.print("embedding:");
+            embed();
+            System.err.println("done");
+        }
+    }
+
+    /**
+     * Embeds the tree in linear time.
+     */
+    public void embed() {
+        Graph G = getGraph();
+        if (G.getNumberOfNodes() == 0)
+            return;
+
+        // synchronized(G)
+        {
+            Node root = G.getFirstNode();
+            NodeSet leaves = new NodeSet(G);
+
+            for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+                if (G.getDegree(v) == 1)
+                    leaves.add(v);
+                if (G.getDegree(v) > G.getDegree(root))
+                    root = v;
+            }
+
+            // recursively visit all nodes in the tree and determine the
+            // angle 0-2PI of each edge. nodes are placed around the unit
+            // circle at position
+            // n=1,2,3,... and then an edge along which we visited nodes
+            // k,k+1,...j-1,j is directed towards positions k,k+1,...,j
+
+            EdgeDoubleArray angle = new EdgeDoubleArray(G); // angle of edge
+            Random rand = new Random();
+            rand.setSeed(1);
+            int seen = setAnglesRec(0, root, null, leaves, angle, rand);
+
+            if (seen != leaves.size())
+                System.err.println("Warning: Number of nodes seen: " + seen +
+                        " != Number of leaves: " + leaves.size());
+
+            // recursively compute node coordinates from edge angles:
+            setCoordsRec(root, null, angle);
+        }
+    }
+
+    /**
+     * Recursively determines the angle of every tree edge.
+     *
+     * @param num    int
+     * @param root   Node
+     * @param entry  Edge
+     * @param leaves NodeSet
+     * @param angle  EdgeDoubleArray
+     * @param rand   Random
+     * @return b int
+     */
+
+    private int setAnglesRec(int num, Node root, Edge entry, NodeSet leaves, EdgeDoubleArray angle, Random rand) throws NotOwnerException {
+        Graph G = getGraph();
+
+        if (leaves.contains(root))
+            return num + 1;
+        else {
+            Iterator edges = G.getAdjacentEdges(root);
+
+            // edges.permute(); // look at children in random order
+
+            int a = num; // is number of nodes seen so far
+            int b = 0;     // number of nodes after visiting subtree
+
+            while (edges.hasNext()) {
+                Edge e = (Edge) edges.next();
+                if (e != entry) {
+                    b = setAnglesRec(a, G.getOpposite(root, e), e, leaves, angle, rand);
+
+                    // point towards the segment of the unit circle a...b:
+                    angle.set(e, Math.PI * (a + b) / leaves.size());
+
+                    a = b;
+                }
+            }
+            if (b == 0)
+                System.err.println("Warning: setAnglesRec: recursion failed");
+            return b;
+        }
+    }
+
+    /**
+     * recursively compute node coordinates from edge angles:
+     *
+     * @param root  Node
+     * @param entry Edge
+     * @param angle EdgeDouble
+     */
+
+    private void setCoordsRec(Node root, Edge entry, EdgeDoubleArray angle)
+            throws NotOwnerException {
+        Graph G = getGraph();
+
+        Iterator<Edge> edges = G.getAdjacentEdges(root);
+
+        while (edges.hasNext()) {
+            Edge e = edges.next();
+
+            if (e != entry) {
+                Node v = G.getOpposite(root, e);
+
+                // translate in the computed direction by the given amount
+                setLocation(v,
+                        Geometry.translateByAngle(getLocation(root), angle.getValue(e),
+                                ((PhyloTree) G).getWeight(e)));
+
+                setCoordsRec(v, e, angle);
+            }
+        }
+    }
+
+    /**
+     * show or hide labels of set of nodes
+     *
+     * @param nodes
+     * @param show
+     */
+    public void showLabels(NodeSet nodes, boolean show) {
+        for (Node v = nodes.getFirstElement(); v != null; v = nodes.getNextElement(v)) {
+            setLabelVisible(v, show);
+        }
+    }
+
+    /**
+     * update view of nodes and edges
+     */
+    public void resetViews() {
+        PhyloTree G = (PhyloTree) getGraph();
+
+        for (Node v = G.getFirstNode(); v != null; v = G.getNextNode(v)) {
+            setLabel(v, G.getLabel(v));
+            //setShape(v, NodeView.NONE_NODE);
+
+            if (G.getLabel(v) != null && !G.getLabel(v).equals("")) {
+                setShape(v, NodeView.OVAL_NODE);
+                setLabelLayout(v, NodeView.LAYOUT);
+                setWidth(v, 1);
+                setHeight(v, 1);
+            } else
+                setShape(v, NodeView.NONE_NODE);
+
+        }
+        for (Edge e = G.getFirstEdge(); e != null; e = G.getNextEdge(e)) {
+            setLabel(e, G.getLabel(e));
+            setDirection(e, EdgeView.UNDIRECTED);
+        }
+    }
+
+    /**
+     * get the tree induced by the given selection of nodes
+     *
+     * @return induced tree or null
+     * @selected
+     */
+    public PhyloTree getInducedTree(Map<Integer, String> id2name, NodeSet selected) {
+        if (getNumberSelectedNodes() > 0) {
+            PhyloTree tarTree = new PhyloTree();
+            Node root = getInducedTreeRec(id2name, selected, getPhyloTree().getRoot(), tarTree);
+            if (root != null) {
+                tarTree.setRoot(root);
+
+                while (false && root != null && root.getOutDegree() == 1)  // delete path from original root down to first branching node
+                {
+                    root = root.getFirstOutEdge().getTarget();
+                    tarTree.deleteNode(tarTree.getRoot());
+                    tarTree.setRoot(root);
+                }
+                return tarTree;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * recursively does the work
+     *
+     * @param srcV
+     * @param tarTree
+     * @return node if any selected nodes here
+     */
+    private Node getInducedTreeRec(Map<Integer, String> id2name, NodeSet selected, Node srcV, PhyloTree tarTree) {
+        LinkedList<Node> below = new LinkedList<>();
+        for (Edge e = srcV.getFirstOutEdge(); e != null; e = srcV.getNextOutEdge(e)) {
+            Node srcW = e.getTarget();
+            Node tarW = getInducedTreeRec(id2name, selected, srcW, tarTree);
+            if (tarW != null)
+                below.add(tarW);
+        }
+        boolean hasNodeData = (srcV.getData() != null && srcV.getData() instanceof NodeData);  // if this has node data, don't use counts of things not present
+
+        if (below.size() == 0) {
+            if (selected.contains(srcV)) {
+                Node tarV = tarTree.newNode();
+                tarV.setInfo(srcV.getInfo());
+                if (hasNodeData) {
+                    NodeData srcND = (NodeData) srcV.getData();
+                    NodeData tarND = new NodeData(srcND.getSummarized(), srcND.getSummarized());
+                    tarV.setData(tarND);
+                } else
+                    tarV.setData(srcV.getData());
+                tarTree.setLabel(tarV, id2name.get(srcV.getInfo()));
+                return tarV;
+            } else
+                return null;
+        } else if (below.size() == 1 && !selected.contains(srcV)) {
+            return below.getFirst();
+        } else {  // has at least two children
+            Node tarV = tarTree.newNode();
+            if (selected.contains(srcV))
+                tarV.setInfo(srcV.getInfo());
+            Set<Node> toDelete = new HashSet<>();
+            Set<Node> toAdd = new HashSet<>();
+            for (Node u : below) {
+                if (u.getInfo() != null)
+                    tarTree.newEdge(tarV, u);
+                else {  // child is not selected, connect all its children directly
+                    for (Edge f = u.getFirstOutEdge(); f != null; f = u.getNextOutEdge(f)) {
+                        Node z = f.getTarget();
+                        tarTree.newEdge(tarV, z);
+                        toAdd.add(z);
+                    }
+                    tarTree.deleteNode(u);
+                    toDelete.add(u);
+                }
+            }
+            below.removeAll(toDelete);
+            below.addAll(toAdd);
+
+            if (hasNodeData) {   // recompute summarized
+                NodeData srcND = (NodeData) srcV.getData();
+                int[] summarized = Arrays.copyOf(srcND.getAssigned(), srcND.getAssigned().length);
+                for (Node u : below) {
+                    for (int i = 0; i < summarized.length; i++) {
+                        final int[] uSummarized = ((NodeData) u.getData()).getSummarized();
+                        final int value = (i < uSummarized.length ? uSummarized[i] : 0);
+                        summarized[i] += value;
+                    }
+                }
+                tarV.setData(new NodeData(srcND.getAssigned(), summarized));
+            } else
+                tarV.setData(srcV.getData());
+            tarTree.setLabel(tarV, id2name.get(srcV.getInfo()));
+            return tarV;
+        }
+    }
+
+    /**
+     * get the associated phyloTree
+     *
+     * @return phyloTree
+     */
+    public PhyloTree getPhyloTree() {
+        return (PhyloTree) getGraph();
+    }
+
+    /**
+     * rotate the tree so that node labels are alphabetically sorted
+     * Note that this is a topological operation that does not modify coordinates
+     */
+    public void topologicallySortTreeLexicographically() {
+        if (getPhyloTree().getRoot() != null)
+            sortTreeAlphabeticallyRec(getPhyloTree().getRoot());
+    }
+
+    /**
+     * rotates tree so as to sort leaves alphabetically
+     *
+     * @param v
+     * @return lexicographic smallest leaf label below
+     */
+    private String sortTreeAlphabeticallyRec(Node v) {
+        if (v.getOutDegree() == 0)
+            return getLabel(v);
+        else { // out degree must be >0
+            final ArrayList<Pair<String, Edge>> list = new ArrayList<>(v.getOutDegree());
+            for (Edge e = v.getFirstOutEdge(); e != null; e = v.getNextOutEdge(e)) {
+                String first = sortTreeAlphabeticallyRec(e.getTarget());
+                list.add(new Pair<>(first, e));
+            }
+            list.sort(new Comparator<Pair<String, Edge>>() {
+                @Override
+                public int compare(Pair<String, Edge> a, Pair<String, Edge> b) {
+                    int compare = a.getFirst().compareTo(b.getFirst());
+                    if (compare != 0)
+                        return compare;
+                    else if (a.getSecond().getId() < b.getSecond().getId())
+                        return -1;
+                    else if (a.getSecond().getId() > b.getSecond().getId())
+                        return 1;
+                    else
+                        return 0;
+                }
+            });
+            final ArrayList<Edge> edges = new ArrayList<>(v.getDegree());
+            for (Pair<String, Edge> pair : list) {
+                edges.add(pair.getSecond());
+            }
+            if (v.getInDegree() > 0)
+                edges.add(v.getFirstInEdge());
+            v.rearrangeAdjacentEdges(edges);
+
+            if (getLabel(v) != null && getLabel(v).compareTo(list.get(0).getFirst()) == -1)
+                return getLabel(v);
+            else
+                return list.get(0).getFirst();
+        }
+    }
+}
+
+// EOF
diff --git a/src/jloda/phylo/TreeDrawerAngled.java b/src/jloda/phylo/TreeDrawerAngled.java
new file mode 100644
index 0000000..368fd60
--- /dev/null
+++ b/src/jloda/phylo/TreeDrawerAngled.java
@@ -0,0 +1,236 @@
+/**
+ * TreeDrawerAngled.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.phylo;
+
+import jloda.graph.Edge;
+import jloda.graph.EdgeSet;
+import jloda.graph.Node;
+import jloda.graph.NodeSet;
+import jloda.graphview.*;
+
+import java.awt.*;
+
+/**
+ * draws a tree using parallel edges
+ * Daniel Huson, 1.2007
+ */
+public class TreeDrawerAngled extends DefaultGraphDrawer implements IGraphDrawer {
+    private final PhyloGraphView treeView;
+    private final PhyloTree tree;
+
+    final static public String DESCRIPTION = "Draw (rooted) trees using angled lines";
+
+    /**
+     * constructor
+     *
+     * @param graphView
+     * @param graph
+     */
+    public TreeDrawerAngled(PhyloGraphView graphView, PhyloTree graph) {
+        super(graphView);
+        this.treeView = graphView;
+        this.tree = graph;
+        setupGraphView(graphView);
+    }
+
+    /**
+     * setdup the graphview
+     *
+     * @param graphView
+     */
+    public void setupGraphView(GraphView graphView) {
+        graphView.setAllowInternalEdgePoints(false);
+        graphView.setMaintainEdgeLengths(true);
+        graphView.setAllowMoveNodes(true);
+        graphView.setAllowMoveInternalEdgePoints(false);
+        graphView.setKeepAspectRatio(false);
+        graphView.setAllowRotationArbitraryAngle(false);
+        graphView.trans.setAngle(0);
+    }
+
+    /**
+     * paint the graph. If rect is non-null, only need to cover rect
+     *
+     * @param graphics
+     * @param rect
+     */
+    public void paint(Graphics graphics, Rectangle rect) {
+        super.paint(graphics, rect);
+    }
+
+    /**
+     * compute an embedding of the graph
+     *
+     * @param toScale if true, build to-scale embedding
+     * @return true, if embedding was computed
+     */
+    public boolean computeEmbedding(boolean toScale) {
+        if (tree.getNumberOfNodes() == 0)
+            return true;
+
+        treeView.removeAllInternalPoints();
+
+        Node root = tree.getRoot();
+        if (root == null)
+            root = tree.getFirstNode();
+
+        computeEmbeddingRec(root, null, 0, 0, toScale);
+
+        return true;
+    }
+
+    /**
+     * recursively compute the embedding
+     *
+     * @param v
+     * @param e
+     * @param hDistToRoot horizontal distance from node to root
+     * @param leafNumber  rank of leaf in vertical ordering
+     * @param toScale
+     * @return index of last leaf
+     */
+    private int computeEmbeddingRec(Node v, Edge e, double hDistToRoot, int leafNumber, boolean toScale) {
+        if (v.getDegree() == 1 && e != null)  // hit a leaf
+        {
+            treeView.setLocation(v, toScale ? hDistToRoot : 0, ++leafNumber);
+        } else {
+            int old = leafNumber + 1;
+            for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f)) {
+                if (f != e) {
+                    Node w = f.getOpposite(v);
+                    leafNumber = computeEmbeddingRec(w, f, hDistToRoot + tree.getWeight(f), leafNumber, toScale);
+                }
+            }
+            double x;
+            if (toScale)
+                x = hDistToRoot;
+            else
+                x = -0.5 * (leafNumber - old);
+            double y = 0.5 * (leafNumber + old);
+            treeView.setLocation(v, x, y);
+        }
+        return leafNumber;
+    }
+
+    /**
+     * get all nodes hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return nodes hit
+     */
+    public NodeSet getHitNodes(int x, int y) {
+        return super.getHitNodes(x, y);
+    }
+
+    /**
+     * get all nodes hit by mouse at (x,y) with tolerance of d pixels
+     *
+     * @param x
+     * @param y
+     * @param d tolerance
+     * @return nodes hit
+     */
+    public NodeSet getHitNodes(int x, int y, int d) {
+        return super.getHitNodes(x, y, d);
+    }
+
+    public NodeSet getHitNodeLabels(int x, int y) {
+        return super.getHitNodeLabels(x, y);
+    }
+
+    /**
+     * get all nodes contained in rect
+     *
+     * @param rect
+     * @return nodes contained in rect
+     */
+    public NodeSet getHitNodes(Rectangle rect) {
+        return super.getHitNodes(rect);
+    }
+
+    /**
+     * get all node labels contained in rect
+     *
+     * @param rect
+     * @return node labels contained in rect
+     */
+    public NodeSet getHitNodeLabels(Rectangle rect) {
+        return null;
+    }
+
+    /**
+     * get all edges hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return edges hits
+     */
+    public EdgeSet getHitEdges(int x, int y) {
+        return super.getHitEdges(x, y);
+
+    }
+
+    /**
+     * get all edge labels hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return edge labels
+     */
+    public EdgeSet getHitEdgeLabels(int x, int y) {
+        return super.getHitEdgeLabels(x, y);
+    }
+
+    /**
+     * get all edges contained in rect
+     *
+     * @param rect
+     * @return edges contained in rect
+     */
+    public EdgeSet getHitEdges(Rectangle rect) {
+        return super.getHitEdges(rect);
+    }
+
+    /**
+     * get all edge labels contained in rect
+     *
+     * @param rect
+     * @return edges contained in rect
+     */
+    public EdgeSet getHitEdgeLabels(Rectangle rect) {
+        return super.getHitEdgeLabels(rect);
+    }
+
+    /**
+     * set the default label positions for nodes and edges
+     *
+     * @param resetAll
+     */
+    public void resetLabelPositions(boolean resetAll) {
+        for (Node v = tree.getFirstNode(); v != null; v = v.getNext())
+            treeView.setLabelLayout(v, NodeView.EAST);
+        if (tree.getRoot() != null)
+            treeView.setLabelLayout(tree.getRoot(), NodeView.WEST);
+        for (Edge e = tree.getFirstEdge(); e != null; e = e.getNext())
+            treeView.setLabelLayout(e, EdgeView.CENTRAL);
+
+    }
+}
diff --git a/src/jloda/phylo/TreeDrawerCircular.java b/src/jloda/phylo/TreeDrawerCircular.java
new file mode 100644
index 0000000..d6b9caa
--- /dev/null
+++ b/src/jloda/phylo/TreeDrawerCircular.java
@@ -0,0 +1,371 @@
+/**
+ * TreeDrawerCircular.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.phylo;
+
+import jloda.graph.*;
+import jloda.graphview.*;
+import jloda.util.Geometry;
+
+import java.awt.*;
+import java.awt.geom.Point2D;
+import java.util.LinkedList;
+import java.util.Stack;
+
+/**
+ * draws a tree using circle arc edges
+ * Daniel Huson, 1.2007
+ */
+public class TreeDrawerCircular extends DefaultGraphDrawer implements IGraphDrawer {
+    private final PhyloGraphView viewer;
+    private final PhyloTree tree;
+    private NodeSet flipNodes;
+
+    final static public String DESCRIPTION = "Draw (rooted) tree using circle segments";
+
+    /**
+     * constructor
+     *
+     * @param graphView
+     * @param graph
+     */
+    public TreeDrawerCircular(PhyloGraphView graphView, PhyloTree graph) {
+        super(graphView);
+        this.viewer = graphView;
+        flipNodes = new NodeSet(graph);
+        this.tree = graph;
+        setupGraphView(graphView);
+    }
+
+    /**
+     * setd up the graphview
+     *
+     * @param graphView
+     */
+    public void setupGraphView(GraphView graphView) {
+        graphView.setAllowInternalEdgePoints(true);
+        graphView.setMaintainEdgeLengths(true);
+        graphView.setAllowMoveNodes(true);
+        graphView.setAllowMoveInternalEdgePoints(false);
+        graphView.setKeepAspectRatio(true);
+        graphView.setAllowRotationArbitraryAngle(true);
+    }
+
+    /**
+     * paint the graph. If rect is non-null, only need to cover rect
+     *
+     * @param graphics
+     * @param rect
+     */
+    public void paint(Graphics graphics, Rectangle rect) {
+        if (tree.getRoot() == null)
+            return;
+        super.paint(graphics, rect);
+    }
+
+    /**
+     * compute an embedding of the graph
+     *
+     * @param toScale if true, build to-scale embedding
+     * @return true, if embedding was computed
+     */
+    public boolean computeEmbedding(boolean toScale) {
+        if (tree.getNumberOfNodes() == 0)
+            return true;
+        viewer.removeAllInternalPoints();
+
+        Node root = tree.getRoot();
+        if (root == null) {
+            tree.setRoot(tree.getFirstNode());
+            root = tree.getRoot();
+        }
+        NodeSet leaves = new NodeSet(tree);
+
+        for (Node v = tree.getFirstNode(); v != null; v = tree.getNextNode(v)) {
+            if (tree.getDegree(v) == 1)
+                leaves.add(v);
+        }
+
+        // recursively visit all nodes in the tree and determine the
+        // angle 0-2PI of each edge. nodes are placed around the unit
+        // circle at position
+        // n=1,2,3,... and then an edge along which we visited nodes
+        // k,k+1,...j-1,j is directed towards positions k,k+1,...,j
+
+        EdgeDoubleArray angle = new EdgeDoubleArray(tree); // angle of edge
+        setAnglesRec(0, root, null, leaves, angle);
+
+        // recursively compute node coordinates from edge angles:
+        setCoords(root, angle);
+        return true;
+    }
+
+    /**
+     * Recursively determines the angle of every tree edge.
+     *
+     * @param num    int
+     * @param v      Node
+     * @param e      Edge
+     * @param leaves NodeSet
+     * @param angle  EdgeDoubleArray
+     * @return b int
+     */
+
+    private int setAnglesRec(int num, Node v, Edge e, NodeSet leaves, EdgeDoubleArray angle) {
+        if (leaves.contains(v))
+            return num + 1;
+        else {
+            int a = num; // is number of nodes seen so far
+            int b = 0;     // number of nodes after visiting subtree
+            final boolean reverse = flipNodes.contains(v);
+            for (Edge f = (reverse ? v.getLastAdjacentEdge() : v.getFirstAdjacentEdge()); f != null;
+                 f = (reverse ? v.getPrevAdjacentEdge(f) : v.getNextAdjacentEdge(f))) {
+                if (f != e) {
+                    b = setAnglesRec(a, tree.getOpposite(v, f), f, leaves, angle);
+
+                    // point towards the segment of the unit circle a...b:
+                    angle.set(f, Math.PI * (a + 1 + b) / leaves.size());
+                    a = b;
+                }
+            }
+            if (b == 0)
+                System.err.println("Warning: setAnglesRec: recursion failed");
+            return b;
+        }
+    }
+
+    /**
+     * set the coordinates for all nodes and interior edge points
+     *
+     * @param root   root of tree
+     * @param angles assignment of angles to edges
+     */
+    private void setCoords(Node root, EdgeDoubleArray angles) {
+        viewer.setLocation(root, new Point(0, 0));
+        for (Edge f = root.getFirstAdjacentEdge(); f != null; f = root.getNextAdjacentEdge(f)) {
+            Node w = f.getOpposite(root);
+            viewer.setLocation(w, Geometry.translateByAngle(viewer.getLocation(root), angles.getValue(f),
+                    tree.getWeight(f)));
+
+                setCoordsRec(viewer.getLocation(root), w, f, angles);
+                addInternalPoints(angles);
+        }
+    }
+
+    /**
+     * recursively compute node coordinates from edge angles:
+     *
+     * @param origin location of origin
+     * @param v      Node
+     * @param e      Edge
+     * @param angles EdgeDouble
+     */
+    private void setCoordsRec(Point2D origin, Node v, Edge e, EdgeDoubleArray angles) {
+        Point2D vp = viewer.getLocation(v);
+        for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f)) {
+            if (f != e) {
+                Node w = f.getOpposite(v);
+                Point2D b = Geometry.rotateAbout(vp, angles.getValue(f) - angles.getValue(e), origin);
+                Point2D c = Geometry.translateByAngle(b, angles.getValue(f), tree.getWeight(f));
+                viewer.setLocation(w, c);
+                setCoordsRec(origin, w, f, angles);
+            }
+        }
+    }
+
+    /**
+     * setup arc edges
+     */
+    protected void addInternalPoints(EdgeDoubleArray angles) {
+        final Stack<Node> stack = new Stack<>();
+        stack.push(tree.getRoot());
+
+        Point2D originPt = new Point2D.Double(0, 0);
+
+        while (stack.size() > 0) {
+            final Node v = stack.pop();
+            final Point2D vPt = viewer.getLocation(v);
+            // add internal points to edges
+            final double vAngle = (v.getInDegree() == 1 ? angles.get(v.getFirstInEdge()) : 0);
+            for (Edge f = v.getFirstOutEdge(); f != null; f = v.getNextOutEdge(f)) {
+                Node w = f.getTarget();
+                if (!tree.isSpecial(f) || tree.getWeight(f) == 1) {
+                    viewer.getEV(f).setShape(EdgeView.ARC_LINE_EDGE);
+                    double wAngle = (w.getInDegree() == 1 ? angles.get(w.getFirstInEdge()) : 0);
+                    java.util.List<Point2D> list = new LinkedList<>();
+                    list.add(originPt);
+                    Point2D aPt = Geometry.rotate(vPt, wAngle - vAngle);
+                    list.add(aPt);
+                    viewer.setInternalPoints(f, list);
+                } else if (tree.isSpecial(f)) {
+                    viewer.getEV(f).setShape(EdgeView.QUAD_EDGE);
+                    double wAngle = (w.getInDegree() == 1 ? angles.get(w.getFirstInEdge()) : 0);
+                    java.util.List<Point2D> list = new LinkedList<>();
+                    Point2D aPt = Geometry.rotate(vPt, wAngle - vAngle);
+                    list.add(aPt);
+                    viewer.setInternalPoints(f, list);
+                }
+                if (PhyloTreeUtils.okToDescendDownThisEdge(tree, f, v)) {
+                    stack.push(f.getTarget());
+                }
+            }
+        }
+    }
+
+    /**
+     * get all nodes hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return nodes hit
+     */
+    public NodeSet getHitNodes(int x, int y) {
+        return super.getHitNodes(x, y);
+    }
+
+    /**
+     * get all nodes hit by mouse at (x,y) with tolerance of d pixels
+     *
+     * @param x
+     * @param y
+     * @param d tolerance
+     * @return nodes hit
+     */
+    public NodeSet getHitNodes(int x, int y, int d) {
+        return super.getHitNodes(x, y, d);
+    }
+
+    public NodeSet getHitNodeLabels(int x, int y) {
+        return super.getHitNodeLabels(x, y);
+    }
+
+    /**
+     * get all nodes contained in rect
+     *
+     * @param rect
+     * @return nodes contained in rect
+     */
+    public NodeSet getHitNodes(Rectangle rect) {
+        return super.getHitNodes(rect);
+    }
+
+    /**
+     * get all node labels contained in rect
+     *
+     * @param rect
+     * @return node labels contained in rect
+     */
+    public NodeSet getHitNodeLabels(Rectangle rect) {
+        return super.getHitNodeLabels(rect);
+    }
+
+    /**
+     * get all edges hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return edges hits
+     */
+    public EdgeSet getHitEdges(int x, int y) {
+        return super.getHitEdges(x, y);
+
+    }
+
+    /**
+     * get all edge labels hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return edge labels
+     */
+    public EdgeSet getHitEdgeLabels(int x, int y) {
+        return super.getHitEdgeLabels(x, y);
+    }
+
+    /**
+     * get all edges contained in rect
+     *
+     * @param rect
+     * @return edges contained in rect
+     */
+    public EdgeSet getHitEdges(Rectangle rect) {
+        return super.getHitEdges(rect);
+    }
+
+    /**
+     * get all edge labels contained in rect
+     *
+     * @param rect
+     * @return edges contained in rect
+     */
+    public EdgeSet getHitEdgeLabels(Rectangle rect) {
+        return super.getHitEdgeLabels(rect);
+    }
+
+    /**
+     * set the default label positions for nodes and edges
+     *
+     * @param resetAll
+     */
+    public void resetLabelPositions(boolean resetAll) {
+        for (Node v = tree.getFirstNode(); v != null; v = v.getNext()) {
+            NodeView nv = viewer.getNV(v);
+            if (nv.getLabelVisible() && nv.getLabel() != null && nv.getLabel().length() > 0) {
+                if (v.getDegree() == 1) {
+                    final Edge e = v.getFirstAdjacentEdge();
+                    final Node w = e.getOpposite(v);
+                    final Point pV = trans.w2d(nv.getLocation());
+                    Point2D nextToV = viewer.getNV(w).getLocation();
+                    if (viewer.getInternalPoints(e) != null &&
+                            viewer.getInternalPoints(e).size() != 0) {
+                        if (v == e.getSource())
+                            nextToV = viewer.getInternalPoints(e).get(0);
+                        else
+                            nextToV = viewer.getInternalPoints(e).get(
+                                    viewer.getInternalPoints(e).size() - 1);
+                    }
+                    Point pW = trans.w2d(nextToV);
+
+                    double angle = Geometry.moduloTwoPI(Geometry.computeAngle(Geometry.diff(pW, pV)));
+                    if (angle > 1.75 * Math.PI)
+                        viewer.getNV(v).setLabelLayout(NodeView.WEST);
+                    else if (angle > 1.25 * Math.PI)
+                        viewer.getNV(v).setLabelLayout(NodeView.SOUTH);
+                    else if (angle > 0.75 * Math.PI)
+                        viewer.getNV(v).setLabelLayout(NodeView.EAST);
+                    else if (angle > 0.25 * Math.PI)
+                        viewer.getNV(v).setLabelLayout(NodeView.NORTH);
+                    else
+                        viewer.getNV(v).setLabelLayout(NodeView.WEST);
+                } else
+                    viewer.getNV(v).setLabelLayout(NodeView.NORTHEAST);
+            }
+        }
+        for (Edge e = tree.getFirstEdge(); e != null; e = e.getNext())
+            viewer.setLabelLayout(e, EdgeView.CENTRAL);
+    }
+
+    public NodeSet getFlipNodes() {
+        return flipNodes;
+    }
+
+    public void setFlipNodes(NodeSet flipNodes) {
+        this.flipNodes = flipNodes;
+    }
+}
diff --git a/src/jloda/phylo/TreeDrawerParallel.java b/src/jloda/phylo/TreeDrawerParallel.java
new file mode 100644
index 0000000..e86def8
--- /dev/null
+++ b/src/jloda/phylo/TreeDrawerParallel.java
@@ -0,0 +1,242 @@
+/**
+ * TreeDrawerParallel.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.phylo;
+
+import jloda.graph.Edge;
+import jloda.graph.EdgeSet;
+import jloda.graph.Node;
+import jloda.graph.NodeSet;
+import jloda.graphview.DefaultGraphDrawer;
+import jloda.graphview.GraphView;
+import jloda.graphview.IGraphDrawer;
+
+import java.awt.*;
+import java.awt.geom.Point2D;
+import java.util.LinkedList;
+
+/**
+ * draws a tree using parallel edges
+ * Daniel Huson, 1.2007
+ */
+public class TreeDrawerParallel extends DefaultGraphDrawer implements IGraphDrawer {
+    private final PhyloGraphView treeView;
+    private final PhyloTree tree;
+
+    final static public String DESCRIPTION = "Draw (rooted) trees using parallel lines";
+
+    /**
+     * constructor
+     *
+     * @param graphView
+     * @param graph
+     */
+    public TreeDrawerParallel(PhyloGraphView graphView, PhyloTree graph) {
+        super(graphView);
+        this.treeView = graphView;
+        this.tree = graph;
+        setupGraphView(graphView);
+    }
+
+    /**
+     * setd up the graphview
+     *
+     * @param graphView
+     */
+    public void setupGraphView(GraphView graphView) {
+        graphView.setAllowInternalEdgePoints(false);
+        graphView.setMaintainEdgeLengths(true);
+        graphView.setAllowMoveNodes(true);
+        graphView.setAllowMoveInternalEdgePoints(false);
+        graphView.setKeepAspectRatio(false);
+        graphView.setAllowRotationArbitraryAngle(false);
+        graphView.trans.setAngle(0);
+    }
+
+    /**
+     * paint the graph. If rect is non-null, only need to cover rect
+     *
+     * @param graphics
+     * @param rect
+     */
+    public void paint(Graphics graphics, Rectangle rect) {
+        super.paint(graphics, rect);
+    }
+
+    /**
+     * compute an embedding of the graph
+     *
+     * @param toScale if true, build to-scale embedding
+     * @return true, if embedding was computed
+     */
+    public boolean computeEmbedding(boolean toScale) {
+        treeView.removeAllInternalPoints();
+        Node root = tree.getRoot();
+        if (root == null) {
+            // compute root
+            root = tree.getFirstNode();
+        }
+        computeEmbeddingRec(root, null, 0, 0, toScale);
+
+        return false;
+    }
+
+    /**
+     * recursively compute the embedding
+     *
+     * @param v
+     * @param e
+     * @param hDistToRoot horizontal distance from node to root
+     * @param leafNumber  rank of leaf in vertical ordering
+     * @param toScale
+     * @return index of last leaf
+     */
+    private int computeEmbeddingRec(Node v, Edge e, double hDistToRoot, int leafNumber, boolean toScale) {
+        if (v.getDegree() == 1 && e != null)  // hit a leaf
+        {
+            treeView.setLocation(v, toScale ? hDistToRoot : 0, ++leafNumber);
+        } else {
+            Point2D first = null;
+            Point2D last = null;
+            double minX = Double.MAX_VALUE;
+            for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f)) {
+                if (f != e) {
+                    Node w = f.getOpposite(v);
+                    leafNumber = computeEmbeddingRec(w, f, hDistToRoot + tree.getWeight(f), leafNumber, toScale);
+                    if (first == null)
+                        first = treeView.getLocation(w);
+                    last = treeView.getLocation(w);
+                    if (last.getX() < minX)
+                        minX = last.getX();
+                }
+            }
+            if (first != null) // always true
+            {
+                double x;
+                if (toScale)
+                    x = hDistToRoot;
+                else
+                    x = minX - 1;
+                double y = 0.5 * (last.getY() + first.getY());
+                treeView.setLocation(v, x, y);
+
+                for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f)) {
+                    if (f != e) {
+                        Node w = f.getOpposite(v);
+                        java.util.List<Point2D> list = new LinkedList<>();
+                        Point2D p = new Point2D.Double(x, treeView.getLocation(w).getY());
+                        list.add(p);
+                        treeView.setInternalPoints(f, list);
+                    }
+                }
+            }
+        }
+        return leafNumber;
+    }
+
+    /**
+     * get all nodes hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return nodes hit
+     */
+    public NodeSet getHitNodes(int x, int y) {
+        return super.getHitNodes(x, y);
+    }
+
+    /**
+     * get all nodes hit by mouse at (x,y) with tolerance of d pixels
+     *
+     * @param x
+     * @param y
+     * @param d tolerance
+     * @return nodes hit
+     */
+    public NodeSet getHitNodes(int x, int y, int d) {
+        return super.getHitNodes(x, y, d);
+    }
+
+    public NodeSet getHitNodeLabels(int x, int y) {
+        return super.getHitNodeLabels(x, y);
+    }
+
+    /**
+     * get all nodes contained in rect
+     *
+     * @param rect
+     * @return nodes contained in rect
+     */
+    public NodeSet getHitNodes(Rectangle rect) {
+        return super.getHitNodes(rect);
+    }
+
+    /**
+     * get all node labels contained in rect
+     *
+     * @param rect
+     * @return node labels contained in rect
+     */
+    public NodeSet getHitNodeLabels(Rectangle rect) {
+        return null;
+    }
+
+    /**
+     * get all edges hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return edges hits
+     */
+    public EdgeSet getHitEdges(int x, int y) {
+        return super.getHitEdges(x, y);
+
+    }
+
+    /**
+     * get all edge labels hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return edge labels
+     */
+    public EdgeSet getHitEdgeLabels(int x, int y) {
+        return super.getHitEdgeLabels(x, y);
+    }
+
+    /**
+     * get all edges contained in rect
+     *
+     * @param rect
+     * @return edges contained in rect
+     */
+    public EdgeSet getHitEdges(Rectangle rect) {
+        return super.getHitEdges(rect);
+    }
+
+    /**
+     * get all edge labels contained in rect
+     *
+     * @param rect
+     * @return edges contained in rect
+     */
+    public EdgeSet getHitEdgeLabels(Rectangle rect) {
+        return super.getHitEdgeLabels(rect);
+    }
+}
diff --git a/src/jloda/phylo/TreeDrawerRadial.java b/src/jloda/phylo/TreeDrawerRadial.java
new file mode 100644
index 0000000..61cb367
--- /dev/null
+++ b/src/jloda/phylo/TreeDrawerRadial.java
@@ -0,0 +1,332 @@
+/**
+ * TreeDrawerRadial.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.phylo;
+
+import jloda.graph.*;
+import jloda.graphview.*;
+import jloda.util.Geometry;
+
+import java.awt.*;
+import java.awt.geom.Point2D;
+import java.util.Iterator;
+import java.util.Random;
+
+/**
+ * draws a tree using parallel edges
+ * Daniel Huson, 1.2007
+ */
+public class TreeDrawerRadial extends DefaultGraphDrawer implements IGraphDrawer {
+    private final PhyloGraphView treeView;
+    private final PhyloTree tree;
+
+    final static public String DESCRIPTION = "Draw (rooted) trees using angled lines";
+
+    /**
+     * constructor
+     *
+     * @param graphView
+     * @param graph
+     */
+    public TreeDrawerRadial(PhyloGraphView graphView, PhyloTree graph) {
+        super(graphView);
+        this.treeView = graphView;
+        this.tree = graph;
+        setupGraphView(graphView);
+    }
+
+    /**
+     * setd up the graphview
+     *
+     * @param graphView
+     */
+    public void setupGraphView(GraphView graphView) {
+        graphView.setAllowInternalEdgePoints(true);
+        graphView.setMaintainEdgeLengths(true);
+        graphView.setAllowMoveNodes(true);
+        graphView.setAllowMoveInternalEdgePoints(false);
+        graphView.setKeepAspectRatio(true);
+        graphView.setAllowRotationArbitraryAngle(true);
+    }
+
+    /**
+     * paint the graph. If rect is non-null, only need to cover rect
+     *
+     * @param graphics
+     * @param rect
+     */
+    public void paint(Graphics graphics, Rectangle rect) {
+        Node root = tree.getFirstNode();
+        for (Node v = tree.getFirstNode(); v != null; v = tree.getNextNode(v)) {
+            if (tree.getDegree(v) > tree.getDegree(root))
+                root = v;
+        }
+
+        super.paint(graphics, rect);
+    }
+
+    /**
+     * compute an embedding of the graph
+     *
+     * @param toScale if true, build to-scale embedding
+     * @return true, if embedding was computed
+     */
+    public boolean computeEmbedding(boolean toScale) {
+        if (tree.getNumberOfNodes() == 0)
+            return true;
+        treeView.removeAllInternalPoints();
+
+        // don't use setRoot to remember root
+        Node root = tree.getFirstNode();
+        NodeSet leaves = new NodeSet(tree);
+
+        for (Node v = tree.getFirstNode(); v != null; v = tree.getNextNode(v)) {
+            if (tree.getDegree(v) == 1)
+                leaves.add(v);
+            if (tree.getDegree(v) > tree.getDegree(root))
+                root = v;
+        }
+
+        // recursively visit all nodes in the tree and determine the
+        // angle 0-2PI of each edge. nodes are placed around the unit
+        // circle at position
+        // n=1,2,3,... and then an edge along which we visited nodes
+        // k,k+1,...j-1,j is directed towards positions k,k+1,...,j
+
+        EdgeDoubleArray angle = new EdgeDoubleArray(tree); // angle of edge
+        Random rand = new Random();
+        rand.setSeed(1);
+        int seen = setAnglesRec(0, root, null, leaves, angle, rand);
+
+        // rotate all edges so that taxon number 1 appears on the right:
+        Node v = tree.getTaxon2Node(1);
+        if (v != null) {
+            Edge e = v.getFirstAdjacentEdge();
+            if (e != null) {
+                double alpha = angle.getValue(e);
+                for (Edge f = tree.getFirstEdge(); f != null; f = f.getNext()) {
+                    angle.set(f, angle.getValue(f) - alpha);
+                }
+            }
+        }
+
+        if (seen != leaves.size())
+            System.err.println("Warning: Number of nodes seen: " + seen +
+                    " != Number of leaves: " + leaves.size());
+
+        // recursively compute node coordinates from edge angles:
+        setCoordsRec(root, null, angle);
+        return true;
+    }
+
+    /**
+     * Recursively determines the angle of every tree edge.
+     *
+     * @param num    int
+     * @param root   Node
+     * @param entry  Edge
+     * @param leaves NodeSet
+     * @param angle  EdgeDoubleArray
+     * @param rand   Random
+     * @return b int
+     */
+
+    private int setAnglesRec(int num, Node root, Edge entry, NodeSet leaves,
+                             EdgeDoubleArray angle, Random rand) {
+        if (leaves.contains(root))
+            return num + 1;
+        else {
+            Iterator edges = tree.getAdjacentEdges(root);
+            // edges.permute(); // look at children in random order
+            int a = num; // is number of nodes seen so far
+            int b = 0;     // number of nodes after visiting subtree
+            while (edges.hasNext()) {
+                Edge e = (Edge) edges.next();
+                if (e != entry) {
+                    b = setAnglesRec(a, tree.getOpposite(root, e), e, leaves, angle, rand);
+
+                    // point towards the segment of the unit circle a...b:
+                    angle.set(e, Math.PI * (a + 1 + b) / leaves.size());
+                    a = b;
+                }
+            }
+            if (b == 0)
+                System.err.println("Warning: setAnglesRec: recursion failed");
+            return b;
+        }
+    }
+
+    /**
+     * recursively compute node coordinates from edge angles:
+     *
+     * @param root  Node
+     * @param entry Edge
+     * @param angle EdgeDouble
+     */
+    private void setCoordsRec(Node root, Edge entry, EdgeDoubleArray angle) {
+        Iterator edges = tree.getAdjacentEdges(root);
+
+        while (edges.hasNext()) {
+            Edge e = (Edge) edges.next();
+
+            if (e != entry) {
+                Node v = tree.getOpposite(root, e);
+
+                // translate in the computed direction by the given amount
+                treeView.setLocation(v,
+                        Geometry.translateByAngle(treeView.getLocation(root), angle.getValue(e),
+                                tree.getWeight(e)));
+                setCoordsRec(v, e, angle);
+            }
+        }
+    }
+
+    /**
+     * get all nodes hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return nodes hit
+     */
+    public NodeSet getHitNodes(int x, int y) {
+        return super.getHitNodes(x, y);
+    }
+
+    /**
+     * get all nodes hit by mouse at (x,y) with tolerance of d pixels
+     *
+     * @param x
+     * @param y
+     * @param d tolerance
+     * @return nodes hit
+     */
+    public NodeSet getHitNodes(int x, int y, int d) {
+        return super.getHitNodes(x, y, d);
+    }
+
+    public NodeSet getHitNodeLabels(int x, int y) {
+        return super.getHitNodeLabels(x, y);
+    }
+
+    /**
+     * get all nodes contained in rect
+     *
+     * @param rect
+     * @return nodes contained in rect
+     */
+    public NodeSet getHitNodes(Rectangle rect) {
+        return super.getHitNodes(rect);
+    }
+
+    /**
+     * get all node labels contained in rect
+     *
+     * @param rect
+     * @return node labels contained in rect
+     */
+    public NodeSet getHitNodeLabels(Rectangle rect) {
+        return super.getHitNodeLabels(rect);
+    }
+
+    /**
+     * get all edges hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return edges hits
+     */
+    public EdgeSet getHitEdges(int x, int y) {
+        return super.getHitEdges(x, y);
+
+    }
+
+    /**
+     * get all edge labels hit by mouse at (x,y)
+     *
+     * @param x
+     * @param y
+     * @return edge labels
+     */
+    public EdgeSet getHitEdgeLabels(int x, int y) {
+        return super.getHitEdgeLabels(x, y);
+    }
+
+    /**
+     * get all edges contained in rect
+     *
+     * @param rect
+     * @return edges contained in rect
+     */
+    public EdgeSet getHitEdges(Rectangle rect) {
+        return super.getHitEdges(rect);
+    }
+
+    /**
+     * get all edge labels contained in rect
+     *
+     * @param rect
+     * @return edges contained in rect
+     */
+    public EdgeSet getHitEdgeLabels(Rectangle rect) {
+        return super.getHitEdgeLabels(rect);
+    }
+
+    /**
+     * set the default label positions for nodes and edges
+     *
+     * @param resetAll
+     */
+    public void resetLabelPositions(boolean resetAll) {
+        for (Node v = tree.getFirstNode(); v != null; v = v.getNext()) {
+            NodeView nv = treeView.getNV(v);
+            if (nv.getLabelVisible() && nv.getLabel() != null && nv.getLabel().length() > 0) {
+                if (v.getDegree() == 1) {
+                    final Edge e = v.getFirstAdjacentEdge();
+                    final Node w = e.getOpposite(v);
+                    final Point pV = trans.w2d(nv.getLocation());
+                    Point2D nextToV = treeView.getNV(w).getLocation();
+                    if (treeView.getInternalPoints(e) != null &&
+                            treeView.getInternalPoints(e).size() != 0) {
+                        if (v == e.getSource())
+                            nextToV = treeView.getInternalPoints(e).get(0);
+                        else
+                            nextToV = treeView.getInternalPoints(e).get(
+                                    treeView.getInternalPoints(e).size() - 1);
+                    }
+                    Point pW = trans.w2d(nextToV);
+
+                    double angle = Geometry.moduloTwoPI(Geometry.computeAngle(Geometry.diff(pW, pV)));
+                    if (angle > 1.75 * Math.PI)
+                        treeView.getNV(v).setLabelLayout(NodeView.WEST);
+                    else if (angle > 1.25 * Math.PI)
+                        treeView.getNV(v).setLabelLayout(NodeView.SOUTH);
+                    else if (angle > 0.75 * Math.PI)
+                        treeView.getNV(v).setLabelLayout(NodeView.EAST);
+                    else if (angle > 0.25 * Math.PI)
+                        treeView.getNV(v).setLabelLayout(NodeView.NORTH);
+                    else
+                        treeView.getNV(v).setLabelLayout(NodeView.WEST);
+                } else
+                    treeView.getNV(v).setLabelLayout(NodeView.NORTHEAST);
+            }
+        }
+        for (Edge e = tree.getFirstEdge(); e != null; e = e.getNext())
+            treeView.setLabelLayout(e, EdgeView.CENTRAL);
+    }
+}
diff --git a/src/jloda/phylo/TreeParseException.java b/src/jloda/phylo/TreeParseException.java
new file mode 100644
index 0000000..d7eda78
--- /dev/null
+++ b/src/jloda/phylo/TreeParseException.java
@@ -0,0 +1,44 @@
+/**
+ * TreeParseException.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.phylo;
+/**
+ * @version $Id: TreeParseException.java,v 1.4 2007-01-03 06:32:45 huson Exp $
+ *
+ * Expections for all of jloda
+ *
+ * @author Daniel Huson
+ */
+
+
+/**
+ * Error parsing phylogenetic tree in bracket format.
+ */
+public class TreeParseException extends Exception {
+    /**
+     * Constructor of TreeParseexception
+     *
+     * @param str String
+     */
+    public TreeParseException(String str) {
+        super(str);
+    }
+}
+// EOF
+
diff --git a/src/jloda/progs/ApproximateBinaryExpansion.java b/src/jloda/progs/ApproximateBinaryExpansion.java
new file mode 100644
index 0000000..67748ea
--- /dev/null
+++ b/src/jloda/progs/ApproximateBinaryExpansion.java
@@ -0,0 +1,67 @@
+/**
+ * ApproximateBinaryExpansion.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * approximate binary expansion of number between 0 and 1
+ * Daniel Huson, 12.2011
+ */
+public class ApproximateBinaryExpansion {
+    /**
+     * approximate square root of two
+     *
+     * @param args
+     * @throws java.io.IOException
+     */
+    public static void main(String[] args) throws IOException {
+        // get parameters:
+        System.out.println("Approximate binary expansion of x between 0 and 1");
+        BufferedReader r = (new BufferedReader(new InputStreamReader(System.in)));
+        System.out.print("Enter x: ");
+        System.out.flush();
+        double x = Double.parseDouble(r.readLine());
+        if (x < 0 || x > 1)
+            throw new IOException("Out of range: " + x);
+        System.out.print("Enter n: ");
+        System.out.flush();
+        int n = Integer.parseInt(r.readLine());
+
+        // run algorithm:
+        System.out.print("Binary expansion: 0.");
+
+        double a = 0, b = 1;
+        for (int i = 0; i < n; i++) {
+            double c = (a + b) / 2;
+            if (c < x) {
+                System.out.print("1");
+                a = c;
+            } else {
+                System.out.print("0");
+                b = c;
+            }
+        }
+
+        System.out.println();
+    }
+}
diff --git a/src/jloda/progs/ApproximateSquareRootOf2.java b/src/jloda/progs/ApproximateSquareRootOf2.java
new file mode 100644
index 0000000..43b34b5
--- /dev/null
+++ b/src/jloda/progs/ApproximateSquareRootOf2.java
@@ -0,0 +1,61 @@
+/**
+ * ApproximateSquareRootOf2.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * approximate square root of 2
+ * Daniel Huson, 12.2011
+ */
+public class ApproximateSquareRootOf2 {
+    /**
+     * approximate square root of two
+     *
+     * @param args
+     * @throws IOException
+     */
+    public static void main(String[] args) throws IOException {
+        // print prompt:
+        System.out.println("Approximation of square root of 2");
+        System.out.println("Using a=0 and b=2");
+        System.out.print("Enter max error: ");
+        System.out.flush();
+        // get parameters:
+        double maxError = Double.parseDouble((new BufferedReader(new InputStreamReader(System.in))).readLine());
+
+        // run algorithm:
+        double a = 0, b = 2;
+        while (b - a > maxError) {
+            double c = (a + b) / 2;
+
+            System.out.println(String.format("a=%1.12g b=%1.12g   c=%1.12g   b-a=%g", a, b, c, b - a));
+
+            if (c * c < 2)
+                a = c;
+            else
+                b = c;
+        }
+        // output:
+        System.out.println("Approximation: " + a);
+    }
+}
diff --git a/src/jloda/progs/CoverDigraph.java b/src/jloda/progs/CoverDigraph.java
new file mode 100644
index 0000000..8332b72
--- /dev/null
+++ b/src/jloda/progs/CoverDigraph.java
@@ -0,0 +1,315 @@
+/**
+ * CoverDigraph.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * Cover digraph construction
+ * @version $Id: CoverDigraph.java,v 1.6 2009-09-25 13:47:12 huson Exp $
+ * @author daniel Huson
+ * 7.03
+ */
+package jloda.progs;
+
+import jloda.graph.Edge;
+import jloda.graph.Node;
+import jloda.graph.NodeIntegerArray;
+import jloda.graphview.GraphViewListener;
+import jloda.phylo.PhyloGraph;
+import jloda.phylo.PhyloGraphView;
+import jloda.util.Basic;
+import jloda.util.CommandLineOptions;
+import jloda.util.NotOwnerException;
+import jloda.util.PhylipUtils;
+
+import javax.swing.*;
+import java.io.*;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Comparator;
+
+/**
+ * Cover digraph construction
+ */
+public class CoverDigraph {
+    private int ntax;
+    private GeneOccurrences[] genes;
+    private PhyloGraph graph;
+    private String[] tax2name;
+
+    /**
+     * read the gene sets from a stream.
+     * Format:
+     * ntax ngenes
+     * label-gene1 taxon taxon ....
+     * label-gene2 taxon taxon ...
+     * ....
+     *
+     * @param r the reader
+     */
+    public void readGenes(Reader r) throws IOException {
+        String[][] data = new String[2][];
+
+        PhylipUtils.read(data, r);
+        ntax = Array.getLength(data[0]) - 1;
+        int ngenes = data[1][1].length();
+
+        tax2name = new String[ntax + 1];
+        genes = new GeneOccurrences[ngenes];
+        for (int c = 0; c < genes.length; c++) {
+
+            genes[c] = new GeneOccurrences();
+            genes[c].label = "g" + c;
+            genes[c].taxa = new BitSet();
+        }
+
+        for (int i = 1; i <= ntax; i++) {
+            tax2name[i] = data[0][i];
+            String seq = data[1][i];
+            System.err.println("taxa " + tax2name[i] + ": " + seq);
+
+            for (int c = 0; c < seq.length(); c++) {
+                if (seq.charAt(c) == '1')
+                    genes[c].taxa.set(i);
+            }
+        }
+    }
+
+    /**
+     * write the input data
+     *
+     * @param w writer
+     */
+    public void writeGenes(Writer w) throws IOException {
+        for (GeneOccurrences gene : genes) {
+            w.write(gene.label + " ");
+            for (int t = 1; t <= ntax; t++)
+                if (gene.taxa.get(t))
+                    w.write(" " + t);
+            w.write("\n");
+        }
+    }
+
+    /**
+     * write the whole thing to a string
+     *
+     * @return a string
+     */
+    public String toString() {
+        Writer sw = new StringWriter();
+        try {
+            writeGenes(sw);
+        } catch (IOException ex) {
+            Basic.caught(ex);
+        }
+        return sw.toString();
+    }
+
+    /**
+     * computes the cover digraph
+     */
+    public void computeGraph() throws NotOwnerException {
+        /* order sets of genes by their size: */
+        Arrays.sort(genes, new BitSetComparator());
+        graph = new PhyloGraph();
+        Node[] gene2node = new Node[genes.length];
+        NodeIntegerArray node2covered = new NodeIntegerArray(graph);
+
+
+        for (int i = 0; i < genes.length; i++) {
+            // prepare label
+            String label = "" + genes[i].label + ":";
+            for (int t = 1; t <= ntax; t++)
+                if (genes[i].taxa.get(t))
+                    label += " " + t;
+
+            // check whether gene has same profile as a previous one:
+            boolean found = false;
+            for (int j = i - 1; !found && j >= 0; j--) {
+                if (genes[i].taxa.cardinality() < genes[j].taxa.cardinality())
+                    break; // because genes are ordered by increasing size
+                if (genes[i].taxa.equals(genes[j].taxa)) {   // genes have same profile
+                    Node v = gene2node[j];
+                    gene2node[i] = v;
+                    graph.setLabel(v, graph.getLabel(v) + ", " + label);
+                    // add label of this gene
+                    found = true;
+                }
+            }
+            if (!found) {
+
+                Node v = graph.newNode(genes[i].taxa);
+
+                graph.setLabel(v, label);
+                gene2node[i] = v;
+                for (int j = i - 1; j >= 0; j--) {
+                    Node w = gene2node[j];
+
+                    if (node2covered.getValue(w) < i + 1) // doesn't cover a node between v and w
+                    {
+                        if (BitSetComparator.isSubset(genes[i].taxa, genes[j].taxa)) {
+                            graph.newEdge(w, v);
+                            markAllCoveringNodesRec(i + 1, w, node2covered);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * mark all nodes above this one as covered
+     *
+     * @param id
+     * @param v
+     * @param node2covered
+     */
+    private void markAllCoveringNodesRec(int id, Node v, NodeIntegerArray node2covered)
+            throws NotOwnerException {
+        if (node2covered.getValue(v) >= id)
+            return;
+
+        node2covered.set(v, id);
+
+        for (Edge e = graph.getFirstAdjacentEdge(v); e != null; e = graph.getNextAdjacentEdge(e, v))
+            if (graph.getTarget(e) == v) {
+                Node w = graph.getOpposite(v, e);
+                if (node2covered.getValue(w) < id) {
+                    markAllCoveringNodesRec(id, w, node2covered);
+                }
+            }
+    }
+
+
+    /**
+     * displays the graph
+     */
+    public void showGraph() {
+
+        JFrame F = new JFrame("CoveredDigraph");
+        PhyloGraphView view = new PhyloGraphView(graph);
+        view.setAutoLayoutLabels(true);
+        F.getContentPane().add(view);
+        F.setSize(view.getSize());
+        // F.setResizable(false);
+        F.addKeyListener(new GraphViewListener(view));
+
+
+        // set node locations:
+        try {
+            int[] v2h = new int[ntax + 1];
+
+            for (Node v = graph.getFirstNode(); v != null; v = graph.getNextNode(v)) {
+                BitSet taxa = (BitSet) graph.getInfo(v);
+                int vert = taxa.cardinality();
+                int hor = (v2h[vert]++);
+                view.setLocation(v, hor, -vert);
+            }
+        } catch (NotOwnerException e) {
+            Basic.caught(e);
+        }
+        F.setVisible(true);
+        view.fitGraphToWindow();
+        view.setMaintainEdgeLengths(false);
+    }
+
+    /**
+     * run the program
+     */
+    public static void main(String args[]) throws Exception {
+        CommandLineOptions options = new CommandLineOptions(args);
+        options.setDescription("CoverDigraph" +
+                "- compute cover digraph from gene content");
+        String infile = options.getOption("-i", "Input file", "");
+        options.done();
+
+        FileReader r = new FileReader(infile);
+
+        CoverDigraph cd = new CoverDigraph();
+
+        cd.readGenes(r);
+        System.err.println("got:\n" + cd);
+
+        cd.computeGraph();
+
+
+        cd.showGraph();
+
+        System.err.println("ordered:\n" + cd);
+    }
+
+}
+
+class BitSetComparator implements Comparator {
+    /**
+     * Compares its two sets for order.   First by size, then lexicographically
+     *
+     * @param o1 the first object to be compared.
+     * @param o2 the second object to be compared.
+     * @return a negative integer, zero, or a positive integer as the
+     *         first argument is less than, equal to, or greater than the
+     *         second.
+     * @throws ClassCastException if the arguments' types prevent them from
+     *                            being compared by this Comparator.
+     */
+    public int compare(Object o1, Object o2) {
+        BitSet bs1 = ((GeneOccurrences) o1).taxa;
+        BitSet bs2 = ((GeneOccurrences) o2).taxa;
+
+        if (bs1.cardinality() < bs2.cardinality())
+            return 1;
+        else if (bs1.cardinality() > bs2.cardinality())
+            return -1;
+
+
+        int top = Math.max(bs1.length(), bs2.length()) + 1;
+
+        for (int t = 1; t <= top; t++) {
+            if (bs1.get(t) && !bs2.get(t))
+                return 1;
+            else if (!bs1.get(t) && bs2.get(t))
+                return -1;
+        }
+        return 0;
+    }
+
+    /**
+     * is bs1 subset of bs2?
+     *
+     * @param bs1 first bit set
+     * @param bs2 second bit set
+     * @return true, if bs1 subset of bs2
+     */
+    public static boolean isSubset(BitSet bs1, BitSet bs2) {
+        int top = bs1.length() + 1;
+
+        for (int t = 1; t <= top; t++) {
+            if (bs1.get(t) && !bs2.get(t))
+                return false;
+        }
+        return true;
+    }
+}
+
+/**
+ * a gene with occurrences
+ */
+class GeneOccurrences {
+    BitSet taxa;
+    String label;
+}
diff --git a/src/jloda/progs/Date2Number.java b/src/jloda/progs/Date2Number.java
new file mode 100644
index 0000000..44b8556
--- /dev/null
+++ b/src/jloda/progs/Date2Number.java
@@ -0,0 +1,57 @@
+/**
+ * Date2Number.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.util.Basic;
+import jloda.util.UsageException;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * converts a date into a long
+ * Daniel Huson Jun 8, 2006
+ */
+public class Date2Number {
+    public static void main(String[] args) throws Exception {
+        if (args.length != 0)
+            throw new UsageException("Date2Number");
+        Date date = new Date();
+        System.out.println("Current date=" + date + "=" + date.getTime());
+
+        BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
+        String aLine;
+        while ((aLine = r.readLine()) != null) {
+            if (aLine.equals("q"))
+                break;
+            if (aLine.length() > 0) {
+                try {
+                    date = DateFormat.getDateInstance().parse(aLine);
+                    System.out.print("" + aLine + "=" + date + "=" + date.getTime() + "L");
+                } catch (Exception ex) {
+                    Basic.caught(ex);
+                }
+            }
+        }
+    }
+
+}
diff --git a/src/jloda/progs/GeneEvolutionSimulator.java b/src/jloda/progs/GeneEvolutionSimulator.java
new file mode 100644
index 0000000..9a2f97b
--- /dev/null
+++ b/src/jloda/progs/GeneEvolutionSimulator.java
@@ -0,0 +1,289 @@
+/**
+ * GeneEvolutionSimulator.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * simulates gene evolution along a tree
+ * @version $Id: GeneEvolutionSimulator.java,v 1.12 2007-01-14 02:55:44 huson Exp $
+ * @author Daniel Huson
+ * 8.2003
+ */
+package jloda.progs;
+
+import jloda.graph.Edge;
+import jloda.graph.Node;
+import jloda.graphview.EdgeView;
+import jloda.graphview.GraphViewListener;
+import jloda.phylo.PhyloTree;
+import jloda.phylo.PhyloTreeView;
+import jloda.util.*;
+
+import java.awt.*;
+import java.awt.geom.Point2D;
+import java.io.File;
+import java.io.FileReader;
+import java.io.PrintStream;
+import java.util.BitSet;
+import java.util.Random;
+
+/**
+ * Simulates gene evolution along a tree
+ */
+public class GeneEvolutionSimulator {
+    private static final Random rand = new Random();
+    private static boolean verbose = false;
+
+    /**
+     * run the program
+     */
+    public static void main(String args[]) throws Exception {
+        CommandLineOptions options = new CommandLineOptions(args);
+        options.setDescription("GeneEvolutionSimulator" +
+                "- simulate birth and death of genes along a tree");
+
+        int initNumber = options.getOption("-n", "Initial number of genes", 100);
+        boolean gaussianInitialNumber = options.getOption("-a", "Gaussian distribution of initial number", true, false);
+        float probGain = (float) options.getOption("-g", "Prob of gain in a time step", 0.1);
+        float probLoss = (float) options.getOption("-l", "Prob of loss in a time step", 0.1);
+        float factor = (float) options.getOption("-f", "Multipler for all edge lengths", 1.0);
+        long seed = options.getOption("-s", "Set seed from random number generator", -1);
+        boolean phylipFormat = options.getOption("+p", "PhylipSequences format output", false, true);
+        boolean display = options.getOption("-d", "Display tree", true, false);
+        verbose = options.getOption("-v", "Verbose mode", true, false);
+        String fileName = options.getMandatoryOption("-i", "Input tree file", "");
+        options.done();
+
+        if (seed >= 0) {
+            rand.setSeed(seed);
+        }
+
+        /** Gaussian initial number */
+        if (gaussianInitialNumber) {
+            RandomGaussian randGaussian = new RandomGaussian(initNumber, Math.sqrt(initNumber));
+            if (seed >= 0)
+                randGaussian.setSeed(seed);
+            initNumber = randGaussian.nextInt();
+            System.err.println("# Initial length changed to: " + initNumber);
+        }
+
+        FileReader r = new FileReader(new File(fileName));
+        PhyloTree tree = new PhyloTree();
+        tree.read(r, true);
+
+
+        if (verbose) {
+            System.err.println("# tree: " + tree);
+            System.err.println("# Factor: " + factor + " N: " + initNumber + " pG; " + probGain + " pL: " + probLoss);
+        }
+        if (factor != 1.0)
+            tree.scaleEdgeWeights(factor);
+
+        simulate(initNumber, probGain, probLoss, tree);
+
+        if (phylipFormat)
+            printGenesPhylip(tree, System.out);
+        if (verbose)
+            printGenes(tree, System.err);
+
+        if (display)
+            show(tree);
+    }
+
+
+    /**
+     * simulate gene birth and death along the tree
+     *
+     * @param initNumber initial number of genes
+     * @param probGain   probability of gene being born in time step
+     * @param probLoss   probability of gene being lost in time step
+     * @param tree       the rooted tree
+     */
+    static public void simulate(int initNumber, float probGain, float probLoss,
+                                PhyloTree tree) throws Exception {
+        BitSet initialGenes = new BitSet();
+        for (int g = 1; g <= initNumber; g++)
+            initialGenes.set(g);
+        int firstNewGene = initNumber + 1;
+
+        // setup genes at root:
+        tree.setInfo(tree.getRoot(), initialGenes);
+        tree.setLabel(tree.getRoot(), "root");
+
+        simulateRec(tree.getRoot(), null, probGain / 10.0, probLoss / 10.0, firstNewGene, tree);
+    }
+
+    /**
+     * recursively does the work
+     *
+     * @param v            current node
+     * @param e            entry edge
+     * @param probGain10   prob gain/10
+     * @param probLoss10   prob loss/10
+     * @param firstNewGene first new gene name available
+     * @param tree
+     */
+    static private int simulateRec(Node v, Edge e, double probGain10, double probLoss10,
+                                   int firstNewGene, PhyloTree tree) throws Exception {
+        int nGained = 0;
+        int nLost = 0;
+        for (Edge f = tree.getFirstAdjacentEdge(v); f != null; f = tree.getNextAdjacentEdge(f, v)) {
+            if (f != e) {
+                Node w = tree.getOpposite(v, f); // child node
+                if (tree.getInfo(w) != null)
+                    throw new Exception("Reccurent node: " + w);
+                BitSet genesW = new BitSet();
+
+                int ticks = (int) (10.0 * tree.getWeight(f));
+
+                // determine which genes survive from v to w:
+                BitSet genesV = (BitSet) tree.getInfo(v);
+                for (int i = genesV.nextSetBit(0); i >= 0; i = genesV.nextSetBit(i + 1)) {
+                    boolean ok = true;
+
+                    for (int t = 1; ok && t <= ticks; t++) {
+                        if (flipCoin(probLoss10))
+                            ok = false;
+                    }
+                    if (ok)
+                        genesW.set(i);
+                    else
+                        nLost++;
+                }
+                // add new genes
+                for (int t = 1; t <= 10.0 * tree.getWeight(f); t++) {
+                    if (flipCoin(probGain10)) {
+                        genesW.set(firstNewGene++);
+                        nGained++;
+                    }
+                }
+
+                System.err.println("Weight: " + tree.getWeight(f) + " Gained: " + nGained + " Lost: " + nLost);
+                tree.setInfo(w, genesW);
+
+                firstNewGene = simulateRec(w, f, probGain10, probLoss10, firstNewGene, tree);
+            }
+        }
+        return firstNewGene;
+    }
+
+    /**
+     * show the tree
+     *
+     * @param tree
+     */
+    static public void show(PhyloTree tree) throws NotOwnerException {
+        for (Node v = tree.getFirstNode(); v != null; v = tree.getNextNode(v)) {
+            BitSet genesV = (BitSet) tree.getInfo(v);
+            tree.setLabel(v, tree.getLabel(v) + ":" + Basic.toString(genesV));
+        }
+
+        PhyloTreeView TV = new PhyloTreeView(tree, 600, 600);
+
+        for (Node v = tree.getFirstNode(); v != null; v = tree.getNextNode(v)) {
+            Point2D apt = TV.getLocation(v);
+            TV.setLocation(v, apt.getX() + 200, apt.getY() + 200);
+        }
+        for (Edge e = tree.getFirstEdge(); e != null; e = tree.getNextEdge(e)) {
+            TV.setDirection(e, EdgeView.UNDIRECTED);
+            TV.setLabelVisible(e, true);
+        }
+        Frame F = new Frame("TreeView");
+        F.setSize(TV.getSize());
+        // F.setResizable(false);
+        F.add(TV);
+        F.addKeyListener(new GraphViewListener(TV));
+        F.setVisible(true);
+        TV.fitGraphToWindow();
+
+    }
+
+    /**
+     * flip a coin
+     *
+     * @param prob of heads
+     * @return true, if heads
+     */
+    static public boolean flipCoin(double prob) {
+        return rand.nextDouble() <= prob;
+    }
+
+    /**
+     * prints the evolved genes in phylip format
+     *
+     * @param tree
+     * @param out
+     * @throws NotOwnerException
+     */
+    private static void printGenesPhylip(PhyloTree tree, PrintStream out)
+            throws NotOwnerException {
+        // determine the set of all mentioned genes:
+        int ntax = 0;
+        BitSet genes = new BitSet();
+        for (Node v = tree.getFirstNode(); v != null; v = tree.getNextNode(v)) {
+            if (tree.getLabel(v) != null && !tree.getLabel(v).equals("root")) {
+                ntax++;
+                genes.or((BitSet) tree.getInfo(v));
+            }
+        }
+        out.println(ntax + " " + genes.cardinality());
+
+        for (Node v = tree.getFirstNode(); v != null; v = tree.getNextNode(v)) {
+            if (tree.getLabel(v) != null && !tree.getLabel(v).equals("root")) {
+                // int count=0;
+                out.print(PhylipUtils.padLabel(tree.getLabel(v), 10) + " ");
+                BitSet genesV = (BitSet) tree.getInfo(v);
+                for (int i = genes.nextSetBit(0); i >= 0; i = genes.nextSetBit(i + 1)) {
+                    /*
+                    if(count==50)
+                    {
+                        out.print("\n");
+                        count=0;
+                    }
+                    else
+                        count++;
+                    */
+                    if (genesV.get(i))
+                        out.print("1");
+                    else
+                        out.print("0");
+                }
+                out.print("\n");
+
+            }
+        }
+    }
+
+
+    /**
+     * prints the generated data
+     *
+     * @param tree
+     * @param ps   print stream
+     */
+    private static void printGenes(PhyloTree tree, PrintStream ps)
+            throws NotOwnerException {
+        for (Node v = tree.getFirstNode(); v != null; v = tree.getNextNode(v)) {
+            if (tree.getLabel(v) != null) {
+                if (tree.getLabel(v).equals("root"))
+                    ps.print("# ");
+                ps.println(tree.getLabel(v) + ": " + tree.getInfo(v));
+            }
+        }
+    }
+
+}
diff --git a/src/jloda/progs/GraphPather.java b/src/jloda/progs/GraphPather.java
new file mode 100644
index 0000000..3f58bb3
--- /dev/null
+++ b/src/jloda/progs/GraphPather.java
@@ -0,0 +1,209 @@
+/**
+ * GraphPather.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.graph.*;
+import jloda.util.CommandLineOptions;
+import jloda.util.UsageException;
+
+import java.io.*;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * greedily finds paths in a distance graph
+ * Daniel Huson, 5.2011
+ */
+public class GraphPather {
+    static public void main(String[] args) throws UsageException, IOException {
+        CommandLineOptions options = new CommandLineOptions(args);
+        options.setDescription("GraphPather - finds paths in the graph of a distance matrix");
+
+        String infileName = options.getMandatoryOption("-i", "Input file", "");
+        String outFileName = options.getOption("-o", "Output file (or stdout)", "");
+        double threshold = options.getOption("-t", "max distance threshold", Float.MAX_VALUE);
+        options.done();
+
+        BufferedReader r = new BufferedReader(new FileReader(infileName));
+        int n = 0;
+        int lineNumber = 0;
+
+
+        BufferedWriter out;
+        if (outFileName.length() > 0)
+            out = new BufferedWriter(new FileWriter(outFileName));
+        else
+            out = new BufferedWriter(new OutputStreamWriter(System.out));
+
+
+        final Graph graph = new Graph();
+        Node[] id2node = null;
+
+        int a = 0;
+
+        System.err.println("Parsing distances:");
+        String aLine;
+        while ((aLine = r.readLine()) != null) {
+            lineNumber++;
+            aLine = aLine.trim();
+            if (aLine.length() > 0 && !aLine.startsWith("#")) {
+                String[] tokens = aLine.split(" ");
+
+                if (n == 0) {
+                    n = tokens.length;
+                    id2node = new Node[n];
+                    for (int b = 0; b < n; b++) {
+                        id2node[b] = graph.newNode();
+                        id2node[b].setInfo(b);
+                    }
+                } else if (tokens.length != n) {
+                    throw new IOException("Line " + lineNumber + ": wrong number of tokens: " + tokens.length);
+                }
+                for (int b = 0; b < n; b++) {
+                    if (a != b) {
+                        float value = Float.parseFloat(tokens[b]);
+                        if (value <= threshold) {
+                            graph.newEdge(id2node[a], id2node[b], value);
+                        }
+                    }
+                }
+                a++;
+            }
+            if (a > n)
+                throw new IOException("Line " + lineNumber + ": too many lines");
+
+        }
+        if (a < n)
+            throw new IOException("Line " + lineNumber + ": too few lines");
+
+        System.err.println("done (" + n + " x " + n + ")");
+
+        System.err.println("Sorting edges:");
+
+        Edge[] edges = new Edge[graph.getNumberOfEdges()];
+
+        int count = 0;
+        for (Edge e = graph.getFirstEdge(); e != null; e = e.getNext()) {
+            edges[count++] = e;
+        }
+
+        Arrays.sort(edges, new Comparator<Edge>() {
+            public int compare(Edge edge1, Edge edge2) {
+                if ((Float) edge1.getInfo() < (Float) edge2.getInfo())
+                    return -1;
+                else if ((Float) edge1.getInfo() > (Float) edge2.getInfo())
+                    return 1;
+                else if (edge1.getId() < edge2.getId())
+                    return -1;
+                else if (edge1.getId() > edge2.getId())
+                    return 1;
+                else
+                    return 0;
+            }
+        });
+        System.err.println("done (" + edges.length + ")");
+
+        System.err.println("Selecting edges:");
+
+        NodeArray<Node> other = new NodeArray<>(graph);
+        NodeArray<Integer> degree = new NodeArray<>(graph, 0);
+
+        EdgeSet selected = new EdgeSet(graph);
+
+        for (Edge e : edges) {
+            Node v = e.getSource();
+            Node w = e.getTarget();
+
+            if (degree.get(v) == 0 && degree.get(w) == 0) {
+                selected.add(e);
+                degree.set(v, 1);
+                degree.set(w, 1);
+                other.set(v, w);
+                other.set(w, v);
+            } else if (degree.get(v) == 0 && degree.get(w) == 1) {
+                selected.add(e);
+                degree.set(v, 1);
+                degree.set(w, 2);
+                Node u = other.get(w);
+                other.set(u, v);
+                other.set(v, u);
+            } else if (degree.get(v) == 1 && degree.get(w) == 0) {
+                selected.add(e);
+                degree.set(v, 2);
+                degree.set(w, 1);
+                Node u = other.get(v);
+                other.set(u, w);
+                other.set(w, u);
+            } else if (degree.get(v) == 1 && degree.get(w) == 1 && other.get(v) != w) {
+                selected.add(e);
+                degree.set(v, 2);
+                degree.set(w, 2);
+                Node uv = other.get(v);
+                Node uw = other.get(w);
+                other.set(uv, uw);
+                other.set(uw, uv);
+            }
+        }
+        List<Edge> toDelete = new LinkedList<>();
+        for (Edge e = graph.getFirstEdge(); e != null; e = e.getNext()) {
+            if (!selected.contains(e))
+                toDelete.add(e);
+        }
+        for (Edge e : toDelete) {
+            graph.deleteEdge(e);
+        }
+
+        System.err.println("done (" + selected.size() + ")");
+
+
+        System.err.println("Building paths:");
+
+        NodeSet used = new NodeSet(graph);
+
+        int countPaths = 0;
+
+        for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+            if (v.getDegree() == 1 && !used.contains(v)) {
+                countPaths++;
+                Edge e = null;
+                do {
+                    out.write(" " + ((Integer) v.getInfo() + 1));
+                    Edge f = v.getFirstAdjacentEdge();
+                    if (f == e) {
+                        f = v.getLastAdjacentEdge();
+                    }
+                    if (f != e) {
+                        v = f.getOpposite(v);
+                        e = f;
+                    } else
+                        e = null;
+                    used.add(v);
+                }
+                while (e != null);
+                out.write("\n");
+            }
+        }
+        out.close();
+        System.err.println("done (" + countPaths + ")");
+
+    }
+}
diff --git a/src/jloda/progs/GraphViewDemo.java b/src/jloda/progs/GraphViewDemo.java
new file mode 100644
index 0000000..c9b2711
--- /dev/null
+++ b/src/jloda/progs/GraphViewDemo.java
@@ -0,0 +1,124 @@
+/**
+ * GraphViewDemo.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.graph.*;
+import jloda.graphview.EdgeView;
+import jloda.graphview.GraphView;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.geom.Point2D;
+import java.util.Random;
+
+/**
+ * Demo of using GraphView class
+ * Daniel Huson, 6.2006
+ */
+public class GraphViewDemo {
+
+    public static void main(String[] args) {
+        // setup small graph:
+        Graph graph = new Graph();
+        final GraphView graphView = new GraphView(graph);
+
+        graphView.getScrollPane().getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
+        graphView.getScrollPane().setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
+        graphView.getScrollPane().setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
+        graphView.getScrollPane().addKeyListener(graphView.getGraphViewListener());
+
+        graphView.setSize(800, 800);
+        graphView.setAllowMoveNodes(true);
+        graphView.setAllowRubberbandNodes(true);
+        graphView.setAutoLayoutLabels(true);
+        graphView.setFixedNodeSize(true);
+        graphView.setMaintainEdgeLengths(false);
+        graphView.setAllowEdit(false);
+        graphView.setCanvasColor(Color.WHITE);
+
+        graphView.getScrollPane().addComponentListener(new ComponentAdapter() {
+            public void componentResized(ComponentEvent event) {
+                final Dimension ps = graphView.trans.getPreferredSize();
+                int x = Math.max(ps.width, graphView.getScrollPane().getWidth() - 20);
+                int y = Math.max(ps.height, graphView.getScrollPane().getHeight() - 20);
+                ps.setSize(x, y);
+                graphView.setPreferredSize(ps);
+                graphView.getScrollPane().getViewport().setViewSize(new Dimension(x, y));
+                graphView.repaint();
+            }
+        });
+
+        Random rand = new Random(666);
+        Node[] nodes = new Node[500];
+
+        for (int i = 0; i < nodes.length; i++) {
+            nodes[i] = graph.newNode();
+            graphView.setLocation(nodes[i], rand.nextInt(100), rand.nextInt(100));
+            for (int j = 0; j < i; j++) {
+                if (rand.nextDouble() < 0.003)
+                    graph.newEdge(nodes[i], nodes[j]);
+            }
+        }
+
+        // add labels to nodes:
+        for (int i = 0; i < nodes.length; i++)
+            graphView.setLabel(nodes[i], "Node " + i);
+
+        // draw all edges directed edges
+        for (Edge e = nodes[0].getFirstAdjacentEdge(); e != null; e = nodes[0].getNextAdjacentEdge(e)) {
+            graphView.setDirection(e, EdgeView.DIRECTED);
+        }
+
+        // compute simple layout:
+        FruchtermanReingoldLayout fruchtermanReingoldLayout = new FruchtermanReingoldLayout(graph, null);
+        NodeArray<Point2D> coordinates = new NodeArray<>(graph);
+        fruchtermanReingoldLayout.apply(1000, coordinates);
+        for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+            graphView.setLocation(v, coordinates.get(v));
+            graphView.setHeight(v, 10);
+            graphView.setWidth(v, 10);
+        }
+        for (Edge e = graph.getFirstEdge(); e != null; e = e.getNext()) {
+            graphView.setDirection(e, EdgeView.UNDIRECTED);
+        }
+
+        // setup jframe with graphView and quit button:
+        final JFrame frame = new JFrame("GraphViewDemo");
+        frame.setSize(graphView.getSize());
+        frame.addKeyListener(graphView.getGraphViewListener());
+
+        frame.getContentPane().setLayout(new BorderLayout());
+        frame.getContentPane().add(graphView.getScrollPane(), BorderLayout.CENTER);
+        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+
+
+        // show the frame:
+        frame.setVisible(true);
+
+        graphView.setSize(400, 400);
+
+        graphView.fitGraphToWindow();
+
+
+    }
+
+}
diff --git a/src/jloda/progs/Gunzip.java b/src/jloda/progs/Gunzip.java
new file mode 100644
index 0000000..7782081
--- /dev/null
+++ b/src/jloda/progs/Gunzip.java
@@ -0,0 +1,46 @@
+/**
+ * Gunzip.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.util.UsageException;
+
+import java.io.*;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * gunzip a file
+ * Daniel Huson, 10.2008
+ */
+public class Gunzip {
+    public static void main(String[] args) throws UsageException, IOException {
+        if (args.length != 2)
+            throw new UsageException("gunzip infile outfile");
+
+        BufferedReader r = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(args[0]))));
+
+        BufferedWriter w = new BufferedWriter(new FileWriter(new File(args[1])));
+        String aLine;
+        while ((aLine = r.readLine()) != null) {
+            w.write(aLine + "\n");
+        }
+        r.close();
+        w.close();
+    }
+}
diff --git a/src/jloda/progs/ImageProcessor.java b/src/jloda/progs/ImageProcessor.java
new file mode 100644
index 0000000..4fc032b
--- /dev/null
+++ b/src/jloda/progs/ImageProcessor.java
@@ -0,0 +1,154 @@
+/**
+ * ImageProcessor.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+/**
+ * a simple image processing program
+ * Daniel Huson, 3.2008
+ */
+
+import jloda.util.CommandLineOptions;
+import jloda.util.UsageException;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.Hashtable;
+
+/**
+ * a simple image processor
+ * Daniel Huson, 3.2008
+ */
+public class ImageProcessor extends Component {
+    public static void main(String[] args) throws Exception {
+        ImageProcessor traitMapper = new ImageProcessor();
+
+        traitMapper.run(args);
+    }
+
+    /**
+     * run the trait mapper
+     *
+     * @param args
+     * @throws UsageException
+     */
+    public void run(String[] args) throws Exception {
+        CommandLineOptions options = new CommandLineOptions(args);
+        options.setDescription(ImageProcessor.class.getName() + "- simple image processor: replaces all non-green pixels by black");
+
+        String inputFile = options.getMandatoryOption("-i", "Image file", "");
+        int patchSize = options.getOption("-p", "pixel patch size", 3);
+        String outputFormat = options.getOption("-f", "output format", ImageIO.getWriterFormatNames(), "png");
+        String outputFile = options.getOption("-o", "Output file", "out");
+        options.done();
+
+        BufferedImage inputImage = readImage(inputFile);
+        BufferedImage outputImage = filterGreen(inputImage, patchSize);
+
+        writeImage(outputFile, outputFormat, outputImage);
+    }
+
+    /**
+     * computes a new image from the input image replacing everything that is not green
+     *
+     * @param originalImage
+     * @return new image
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    public BufferedImage filterGreen(BufferedImage originalImage, int patchSize) throws IOException, InterruptedException {
+        int width = originalImage.getWidth(this);
+        int height = originalImage.getHeight(this);
+
+        // BufferedImage newImage = new BufferedImage(width, height,originalImage.getType());
+        Hashtable properties = new Hashtable();
+        String[] names = originalImage.getPropertyNames();
+        if (names != null) {
+            for (String name : names) properties.put(name, originalImage.getProperty(name));
+        }
+
+        // todo: I don't know whether this really produces a new image. Perhaps one needs to some cloning somewhere?
+        BufferedImage newImage = new BufferedImage(originalImage.getColorModel(), originalImage.getRaster(), originalImage.isAlphaPremultiplied(), properties);
+
+        newImage.setData(originalImage.getRaster());
+        for (int x = 0; x < width; x++)
+            for (int y = 0; y < height; y++) {
+                if (x <= patchSize || y <= patchSize || x >= width - patchSize || y >= height - patchSize || !isGreen(originalImage, x, y, patchSize))
+                    newImage.setRGB(x, y, Color.BLACK.getRGB());
+            }
+        return newImage;
+    }
+
+    /**
+     * is this pixel green?
+     *
+     * @param originalImage
+     * @param x
+     * @param y
+     * @param delta
+     * @return true, if green
+     */
+    private boolean isGreen(BufferedImage originalImage, int x, int y, int delta) {
+        int max = (2 * delta + 1) * (2 * delta + 1);
+
+        int count = 0;
+        for (int dx = -delta; dx <= delta; dx++) {
+            for (int dy = -delta; dy <= delta; dy++) {
+                Color originalColor = new Color(originalImage.getRGB(x + dx, y + dy));
+                if (originalColor.getGreen() > 120 && (originalColor.getRed() < originalColor.getGreen() - 15 || originalColor.getBlue() < originalColor.getGreen() - 15))
+                    count++;
+            }
+        }
+        return count > 0.5 * max;
+    }
+
+    /**
+     * read a buffered image from a file
+     *
+     * @param fileName
+     * @return image
+     * @throws IOException
+     */
+    public static BufferedImage readImage(String fileName) throws IOException {
+        System.err.print("Reading image file '" + fileName + "':");
+        if (!(new File(fileName)).canRead())
+            throw new IOException("Can't read file '" + fileName + "'");
+        BufferedImage image = ImageIO.read(new File(fileName));
+        System.err.println(" done");
+        return image;
+    }
+
+    /**
+     * write a buffered image to a file
+     *
+     * @param fileName
+     * @param formatName
+     * @param bufferedImage
+     * @throws IOException
+     */
+    public static void writeImage(String fileName, String formatName, BufferedImage bufferedImage) throws IOException {
+        System.err.print("Writing image file '" + fileName + "." + formatName + "':");
+        File file = new File(fileName + "." + formatName);
+        ImageIO.write(bufferedImage, formatName, file);
+        System.err.println(" done");
+    }
+}
diff --git a/src/jloda/progs/JTableWithRowHeaders.java b/src/jloda/progs/JTableWithRowHeaders.java
new file mode 100644
index 0000000..bab8612
--- /dev/null
+++ b/src/jloda/progs/JTableWithRowHeaders.java
@@ -0,0 +1,106 @@
+/**
+ * JTableWithRowHeaders.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import javax.swing.*;
+import javax.swing.table.*;
+import java.awt.*;
+
+public class JTableWithRowHeaders extends JFrame {
+
+    public JTableWithRowHeaders() {
+        super("Row Header Test");
+        setSize(300, 200);
+        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+
+        TableModel tm = new AbstractTableModel() {
+            final String[] data = {"", "a", "b", "c", "d", "e"};
+
+            final String[] headers = {"Row #", "Column 1", "Column 2", "Column 3", "Column 4", "Column 5"};
+
+            public int getColumnCount() {
+                return data.length;
+            }
+
+            public int getRowCount() {
+                return 1000;
+            }
+
+            public String getColumnName(int col) {
+                return headers[col];
+            }
+
+            public Object getValueAt(int row, int col) {
+                return data[col] + row;
+            }
+        };
+
+        TableColumnModel cm = new DefaultTableColumnModel() {
+            boolean first = true;
+
+            public void addColumn(TableColumn tc) {
+                if (first) {
+                    first = false;
+                    return;
+                }
+                tc.setMinWidth(150);
+                super.addColumn(tc);
+            }
+        };
+
+        TableColumnModel rowHeaderModel = new DefaultTableColumnModel() {
+            boolean first = true;
+
+            public void addColumn(TableColumn tc) {
+                if (first) {
+                    tc.setMaxWidth(tc.getPreferredWidth());
+                    super.addColumn(tc);
+                    first = false;
+                }
+            }
+        };
+
+        JTable jt = new JTable(tm, cm);
+        JTable headerColumn = new JTable(tm, rowHeaderModel);
+        jt.createDefaultColumnsFromModel();
+        headerColumn.createDefaultColumnsFromModel();
+
+        jt.setSelectionModel(headerColumn.getSelectionModel());
+
+        headerColumn.setBackground(Color.lightGray);
+        headerColumn.setColumnSelectionAllowed(false);
+        headerColumn.setCellSelectionEnabled(false);
+
+        JViewport jv = new JViewport();
+        jv.setView(headerColumn);
+        jv.setPreferredSize(headerColumn.getMaximumSize());
+
+        jt.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+
+        JScrollPane jsp = new JScrollPane(jt);
+        jsp.setRowHeader(jv);
+        jsp.setCorner(ScrollPaneConstants.UPPER_LEFT_CORNER, headerColumn.getTableHeader());
+        getContentPane().add(jsp, BorderLayout.CENTER);
+    }
+
+    public static void main(String args[]) {
+        new JTableWithRowHeaders().setVisible(true);
+    }
+}
diff --git a/src/jloda/progs/Lines2FastA.java b/src/jloda/progs/Lines2FastA.java
new file mode 100644
index 0000000..117bfa2
--- /dev/null
+++ b/src/jloda/progs/Lines2FastA.java
@@ -0,0 +1,82 @@
+/**
+ * Lines2FastA.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.util.CommandLineOptions;
+import jloda.util.UsageException;
+
+import java.io.*;
+
+/**
+ * converts lines of sequences into fastA format
+ * Daniel Huson, 10.2010
+ */
+public class Lines2FastA {
+    /**
+     * run the tool
+     *
+     * @param args
+     * @throws UsageException
+     * @throws IOException
+     */
+    public static void main(String[] args) throws UsageException, IOException {
+        if (args.length == 0)
+            args = new String[]{"-i", "/Users/huson/SecondExpt_Batch1Samples_Peptides_in_in-house_DB.txt",
+                    "-o", "/Users/huson/data/megan/sludge-peptides/SecondExpt_Batch1Samples_Peptides_in_in-house_DB.fasta",
+                    "-p", "peptide"
+            };
+
+        CommandLineOptions options = new CommandLineOptions(args);
+        options.setDescription("Convert lines of sequence to FastA");
+
+
+        String infile = options.getOption("-i", "Input file", "");
+        String outfile = options.getOption("-o", "Output file", "");
+        int firstId = options.getOption("-n", "Numbering start", 1);
+        String prefix = options.getOption("-p", "Read name prefix", "r");
+        options.done();
+
+        BufferedReader r;
+        BufferedWriter w;
+        if (infile.length() == 0) {
+            r = (new BufferedReader(new InputStreamReader(System.in)));
+        } else {
+            r = (new BufferedReader(new FileReader(infile)));
+        }
+        if (outfile.length() == 0) {
+            w = (new BufferedWriter(new OutputStreamWriter(System.out)));
+        } else {
+            w = (new BufferedWriter(new FileWriter(outfile)));
+        }
+
+        String aLine;
+        int count = 0;
+        while ((aLine = r.readLine()) != null) {
+            aLine = aLine.trim();
+            if (aLine.length() == 0 || aLine.startsWith("#"))
+                continue;
+            w.write(">" + prefix + (firstId + count) + "\n" + aLine + "\n");
+            count++;
+        }
+        w.close();
+        r.close();
+        System.err.println("Done (" + count + ")");
+    }
+}
diff --git a/src/jloda/progs/MABlocker.java b/src/jloda/progs/MABlocker.java
new file mode 100644
index 0000000..de391e2
--- /dev/null
+++ b/src/jloda/progs/MABlocker.java
@@ -0,0 +1,617 @@
+/**
+ * MABlocker.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.graph.Graph;
+import jloda.graph.MaxClique;
+import jloda.graph.Node;
+import jloda.graph.NodeIntegerArray;
+import jloda.util.Basic;
+import jloda.util.CommandLineOptions;
+import jloda.util.FastA;
+import jloda.util.Pair;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.text.NumberFormat;
+import java.util.*;
+
+/**
+ * Given a multi-alignment of individual reads, produces blocks of strongly aligned stuff
+ *
+ * @author huson
+ *         Date: 10-Aug-2004
+ */
+public class MABlocker {
+    static public void main(String[] args) throws Exception {
+        CommandLineOptions options = new CommandLineOptions(args);
+        String cname = options.getMandatoryOption("-i", "input file", "");
+        int minLength = options.getOption("-l", "min length of block", 100);
+        boolean generateFastA = options.getOption("+f", "create FastASequences files", false, true);
+        boolean generateCGViz = options.getOption("-c", "create CGViz files", true, false);
+        boolean usePercentOfPrevious = options.getOption("-p", "use precent of previous", true, false);
+        boolean useGrowingAlgorithm = options.getOption("-g", "use growing algorithm", true, false);
+        int minSequences = options.getOption("-s", "min number of sequences in a block", 4);
+        int minOverlap = options.getOption("-m", "min length of pairwise overlap", 100);
+        int minPercent = options.getOption("-d", "min percent of non-gap positions in a sequence", 80);
+        options.done();
+
+        System.err.println("# Options: " + options);
+
+        FastA fasta = new FastA();
+        fasta.read(new FileReader(new File(cname)));
+        int numSequences = fasta.getSize();
+
+        int length = 0;
+        for (int i = 0; i < fasta.getSize(); i++) {
+            if (i == 0)
+                length = fasta.getSequence(i).length();
+            else if (fasta.getSequence(i).length() != length)
+                throw new Exception("Sequence 0 and " + i + ": lengths differ: " + length
+                        + " and " + fasta.getSequence(i).length());
+        }
+        System.err.println("# Got " + numSequences + " sequences of length " + length);
+
+        int[] starts = new int[numSequences];
+        int[] stops = new int[numSequences];
+        stopsStarts(fasta, length, starts, stops);
+
+        int[] ordering = new int[numSequences];
+        int[] inverse = new int[numSequences];
+        computeEliminationScheme(numSequences, starts, ordering, inverse);
+        int[][] matrix = computeCoverageMatrix(numSequences, starts, stops, minOverlap, ordering, usePercentOfPrevious);
+
+        List blocks = new LinkedList();
+        if (useGrowingAlgorithm) {
+            BitSet positions = getGapPositions(fasta, length);
+            for (int i = positions.nextSetBit(0); i != -1; i = positions.nextSetBit(i + 1)) {
+                int j = positions.nextSetBit(i + 1);
+                if (j != -1 && j - i + 1 >= 25) {
+                    Block block = computeBlock(false, fasta, length, i, j, positions, starts, stops, minSequences, minPercent);
+                    if (block != null && block.stopPos - block.startPos + 1 >= minLength) {
+                        blocks.add(block);
+                    }
+                    block = computeBlock(true, fasta, length, i, j, positions, starts, stops, minSequences, minPercent);
+                    if (block != null && block.stopPos - block.startPos + 1 >= minLength) {
+                        blocks.add(block);
+                    }
+                }
+            }
+            makeBlocksUnique(blocks);
+            System.err.println("# Number of blocks: " + blocks.size());
+        } else {
+            List maxCliques = MaxClique.computeAll(matrix, ordering);
+            System.err.println("# MaxCliques: " + maxCliques.size());
+
+            for (Object maxClique : maxCliques) {
+                BitSet clique = (BitSet) maxClique;
+                System.err.println("# MaxClique: " + clique);
+                if (clique.cardinality() >= minSequences) {
+                    Block block = buildBlock(clique, starts, stops);
+                    if (block.stopPos - block.startPos + 1 >= minLength)
+                        blocks.add(block);
+                }
+            }
+        }
+
+        if (generateFastA)
+            writeFastAFiles(cname, fasta, blocks);
+        if (generateCGViz)
+            writeCGVizFiles(cname, fasta, blocks);
+
+        {
+            FileWriter w = new FileWriter(new File(cname + ".fa"));
+            for (int t = 0; t < numSequences; t++) {
+                w.write("> t" + t + "_" + fasta.getHeader(t) + "\n");
+                w.write(Basic.wraparound(fasta.getSequence(t), 65));
+            }
+            w.close();
+        }
+
+        {
+            FileWriter w = new FileWriter(new File("nexus.header"));
+            {
+                w.write("#nexus\n");
+                w.write("begin taxa;\ndimensions ntax=" + numSequences + ";\ntaxlabels\n");
+                for (int t = 0; t < numSequences; t++) {
+                    w.write("t" + t + "_" + fasta.getHeader(t) + "\n");
+                }
+                w.write(";\nend;\n;");
+                w.write("begin trees;\n");
+            }
+            w.close();
+        }
+
+        {
+            FileWriter w = new FileWriter(new File("overview.cgv"));
+            w.write("{DATA overview\n");
+            w.write("[__GLOBAL__] dimension=2\n");
+            for (int i = 0; i < numSequences; i++) {
+                int v = ordering[i];
+                w.write("seq=" + v + " start= " + starts[v] + " stop= " + stops[v] +
+                        " len= " + (stops[v] - starts[v] + 1) + ": " + starts[v] + " " + i
+                        + " " + stops[v] + " " + i + "\n");
+            }
+            w.write("}\n");
+            w.write("{DATA blocks\n");
+            Iterator it = blocks.iterator();
+            int blockNumber = 0;
+            while (it.hasNext()) {
+                blockNumber++;
+                Block block = (Block) it.next();
+                int minOrder = numSequences + 1;
+                int maxOrder = -1;
+                for (int t = block.taxa.nextSetBit(0); t >= 0; t = block.taxa.nextSetBit(t + 1)) {
+                    if (inverse[t] < minOrder)
+                        minOrder = inverse[t];
+                    if (inverse[t] > maxOrder)
+                        maxOrder = inverse[t];
+                }
+                w.write("num=" + blockNumber + " start=" + block.startPos + " stop=" + block.stopPos
+                        + " minOrder=" + minOrder + " maxOrder=" + maxOrder + " numSequences=" +
+                        block.taxa.cardinality() + ": " + block.startPos + " " + (minOrder) +
+                        " " + block.stopPos + " " + (maxOrder) + "\n");
+            }
+            w.write("}\n");
+            w.close();
+        }
+    }
+
+    /**
+     * make blocks sufficiently unique
+     *
+     * @param blocks
+     */
+    private static void makeBlocksUnique(List blocks) {
+        Block[] blocksArray = (Block[]) blocks.toArray(new Block[blocks.size()]);
+        blocks.clear();
+
+        Graph containmentGraph = new Graph();
+        Node[] id2node = new Node[blocksArray.length];
+        NodeIntegerArray node2id = new NodeIntegerArray(containmentGraph);
+
+        for (int i = 0; i < blocksArray.length; i++) {
+            id2node[i] = containmentGraph.newNode();
+            node2id.set(id2node[i], i);
+        }
+        for (int i = 0; i < blocksArray.length; i++) {
+            for (int j = 0; j < blocksArray.length; j++) {
+                if (i != j) {
+                    // System.err.println("Block "+blocksArray[i]+" contains block "+blocksArray[j]+": "+contains(blocksArray[i], blocksArray[j]));
+
+                    if (contains(blocksArray[i], blocksArray[j])
+                            && (i < j || !contains(blocksArray[j], blocksArray[i])))
+                        containmentGraph.newEdge(id2node[i], id2node[j]);
+                }
+            }
+        }
+        // System.err.println("Graph: "+containmentGraph.toString());
+
+        for (Node v = containmentGraph.getFirstNode(); v != null; v = v.getNext()) {
+            if (v.getInDegree() == 0) {
+                Block block = blocksArray[node2id.getValue(v)];
+                blocks.add(block);
+                /*
+                 {
+                System.err.print("Block "+block+" contains:");
+                Stack stack=new Stack();
+                stack.add(v);
+                while(stack.size()>0) {
+                    Node w=(Node)stack.pop();
+                    for(Edge e=w.getFirstOutEdge();e!=null;e=w.getNextOutEdge(e))
+                    {
+                        Node u=e.getTarget();
+                        System.err.print(" "+blocksArray[node2id.getValue(u)]);
+                        stack.add(u);
+                    }
+                }
+                System.err.println();
+                }
+                */
+            }
+        }
+    }
+
+    /**
+     * does block a contain block b?
+     *
+     * @param a
+     * @param b
+     * @return true, if a contains b
+     */
+    private static boolean contains(Block a, Block b) {
+        BitSet intersection = (BitSet) a.taxa.clone();
+        intersection.and(b.taxa);
+        if (intersection.cardinality() != b.taxa.cardinality())
+            return false;
+
+        for (int s = intersection.nextSetBit(0); s != -1; s = intersection.nextSetBit(s + 1)) {
+            if (a.startPos > b.startPos + 5 || a.stopPos < b.stopPos - 5)
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * for a given seed segment (a,b), attempts to compute the best block
+     *
+     * @param fasta
+     * @param length
+     * @param a
+     * @param b
+     * @param positions
+     * @param starts
+     * @param stops
+     * @param minSequences
+     * @param minPercentSites
+     * @return
+     */
+    private static Block computeBlock(boolean lengthOverNumber, FastA fasta, int length, int a, int b, BitSet positions, int[] starts, int[] stops, int minSequences, int minPercentSites) {
+        // setup active sequences:
+        BitSet active = new BitSet();
+        int[] nonGapCount = new int[fasta.getSize()];
+
+        for (int s = 0; s < fasta.getSize(); s++) {
+            String sequence = fasta.getSequence(s);
+            int count = 0;
+            for (int pos = a; pos <= b; pos++) {
+                if (sequence.charAt(pos) != '-')
+                    count++;
+            }
+            nonGapCount[s] = count;
+            if (100 * count / (b - a + 1) >= minPercentSites)
+                active.set(s);
+        }
+
+        // extend to the left
+        int left;
+        if (active.cardinality() >= minSequences) {
+            for (left = a; left >= 0; left--) {
+                for (int s = active.nextSetBit(0); s != -1; s = active.nextSetBit(s + 1))
+                    if (fasta.getSequence(s).charAt(left) != '-')
+                        nonGapCount[s]++;
+                if (positions.get(left)) {
+                    BitSet dead = new BitSet();
+                    for (int s = active.nextSetBit(0); s != -1; s = active.nextSetBit(s + 1)) {
+                        if (left <= starts[s])
+                            dead.set(s);
+                        else {
+                            int z = 0;
+                            boolean ok = false;
+                            for (int j = left - 1; !ok && z <= 10 && j >= 0; j--, z++) {
+                                if (fasta.getSequence(s).charAt(j) != '-')
+                                    ok = true;
+                            }
+                            if (!ok)
+                                dead.set(s);
+                        }
+                    }
+                    if (lengthOverNumber && active.cardinality() - dead.cardinality() > minSequences)
+                        active.andNot(dead);
+                    else if (dead.cardinality() >= 0.2 * active.cardinality())
+                        break;
+
+                    for (int s = active.nextSetBit(0); s != -1; s = active.nextSetBit(s + 1)) {
+                        if (100 * nonGapCount[s] / (b - left + 1) < minPercentSites) {
+                            active.set(s, false);
+                            if (active.cardinality() <= minSequences)
+                                break;
+                        } else {
+                            int z = 0;
+                            boolean ok = false;
+                            for (int j = left - 1; !ok && z <= 10 && j >= 0; j--, z++) {
+                                if (fasta.getSequence(s).charAt(j) != '-')
+                                    ok = true;
+                            }
+                            if (!ok) {
+                                active.set(s, false);
+                                if (active.cardinality() <= minSequences)
+                                    break;
+                            }
+                        }
+                    }
+                }
+            }
+
+            // extend to the right
+            if (active.cardinality() >= minSequences) {
+                int right;
+                for (right = b; right < length - 2; right++) {
+                    for (int s = active.nextSetBit(0); s != -1; s = active.nextSetBit(s + 1))
+                        if (fasta.getSequence(s).charAt(right) != '-')
+                            nonGapCount[s]++;
+                    if (positions.get(right)) {
+                        BitSet dead = new BitSet();
+                        for (int s = active.nextSetBit(0); s != -1; s = active.nextSetBit(s + 1)) {
+                            if (right >= stops[s])
+                                dead.set(s);
+                            else {
+                                int z = 0;
+                                boolean ok = false;
+                                for (int j = right + 1; !ok && z <= 10 && j < length; j++, z++) {
+                                    if (fasta.getSequence(s).charAt(j) != '-')
+                                        ok = true;
+                                }
+                                if (!ok)
+                                    dead.set(s);
+                            }
+                        }
+                        if (lengthOverNumber && active.cardinality() - dead.cardinality() > minSequences)
+                            active.andNot(dead);
+                        else if (dead.cardinality() >= 0.2 * active.cardinality())
+                            break;
+
+                        for (int s = active.nextSetBit(0); s != -1; s = active.nextSetBit(s + 1)) {
+                            if (100 * nonGapCount[s] / (right - left + 1) < minPercentSites) {
+                                active.set(s, false);
+                                if (active.cardinality() <= minSequences)
+                                    break;
+                            } else {
+                                int z = 0;
+                                boolean ok = false;
+                                for (int j = right + 1; !ok && z <= 10 && j < length; j++, z++) {
+                                    if (fasta.getSequence(s).charAt(j) != '-')
+                                        ok = true;
+                                }
+                                if (!ok) {
+                                    active.set(s, false);
+                                    if (active.cardinality() <= minSequences)
+                                        break;
+                                }
+                            }
+                        }
+                    }
+                }
+                if (active.cardinality() >= minSequences) {
+                    Block block = new Block();
+                    block.startPos = left;
+                    block.stopPos = right;
+                    block.taxa = active;
+                    return block;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * builds the alignment block for the given clique
+     *
+     * @param clique
+     * @param starts
+     * @param stops
+     * @return alignment block
+     */
+    private static Block buildBlock(BitSet clique, int[] starts, int[] stops) {
+        int startPos = 10000000;
+        int stopPos = -1;
+        for (int v = clique.nextSetBit(0); v >= 0; v = clique.nextSetBit(v + 1)) {
+            if (starts[v] < startPos)
+                startPos = starts[v];
+            if (stops[v] > stopPos)
+                stopPos = stops[v];
+        }
+        Block block = new Block();
+        block.startPos = startPos;
+        block.stopPos = stopPos;
+        block.taxa = clique;
+        return block;
+    }
+
+    /**
+     * given the start positions for all sequences, returns an array of sequence ids
+     * ordered by the starts
+     *
+     * @param numSequences
+     * @param starts
+     * @param ordering
+     * @param inverse
+     */
+    private static void computeEliminationScheme(int numSequences, int[] starts, int[] ordering, int[] inverse) {
+        List events = new LinkedList();
+        for (int i = 0; i < numSequences; i++) {
+            Pair pair = new Pair(starts[i], i);
+            events.add(pair);
+        }
+        Collections.sort(events);
+        Iterator it = events.iterator();
+        int i = 0;
+        while (it.hasNext()) {
+            Pair pair = (Pair) it.next();
+            ordering[i++] = pair.getSecondInt();
+        }
+        for (i = 0; i < numSequences; i++)
+            inverse[ordering[i]] = i;
+    }
+
+
+    /**
+     * computes the pairwise coverage matrix
+     *
+     * @param numSequences
+     * @param starts
+     * @param stops
+     * @param minOverlap
+     * @return pairwise coverage matrix
+     */
+    private static int[][] computeCoverageMatrix(int numSequences, int[] starts, int[] stops, int minOverlap,
+                                                 int[] ordering, boolean usePercentOfPrevious) {
+        int[][] matrix = new int[numSequences][numSequences];
+
+        for (int i = 0; i < numSequences; i++) {
+            int p = ordering[i];
+            for (int j = i + 1; j < numSequences; j++) {
+                int q = ordering[j];
+                int startPQ = Math.max(starts[p], starts[q]);
+                int stopPQ = Math.min(stops[p], stops[q]);
+                int overlap = stopPQ - startPQ + 1;
+                if (usePercentOfPrevious) {
+                    if (overlap >= minOverlap / 100.0 * (stops[p] - starts[p] + 1))
+                        matrix[p][q] = matrix[q][p] = overlap;
+                } else {
+                    if (overlap >= minOverlap)
+                        matrix[p][q] = matrix[q][p] = overlap;
+                }
+            }
+        }
+
+/*
+        System.err.println("Matrix for startPos=" + startPos + ", stopPos=" + stopPos + ":");
+        for (int p = 0; p < numSequences; p++) {
+            for (int q = 0; q < numSequences; q++) {
+                System.err.print(" " + matrix[p][q]);
+            }
+            System.err.println();
+        }
+        */
+
+        return matrix;
+    }
+
+    /**
+     * computes the sorted list of fragment start and fragment stop events
+     *
+     * @param fasta
+     * @param length
+     * @param starts
+     * @param stops
+     */
+    private static void stopsStarts(FastA fasta, int length, int[] starts, int[] stops) {
+        for (int s = 0; s < fasta.getSize(); s++) {
+            starts[s] = -1;
+        }
+
+        for (int i = 0; i < length; i++) {
+            for (int s = 0; s < fasta.getSize(); s++) {
+                if (fasta.getSequence(s).charAt(i) != '-') {
+                    if (starts[s] == -1) {
+                        starts[s] = i;
+                    }
+                    stops[s] = i;
+                }
+            }
+        }
+    }
+
+    /**
+     * computes the sorted list of fragment start and fragment stop events
+     *
+     * @param fasta
+     * @param length
+     * @return set of starts and stops
+     */
+    private static BitSet getGapPositions(FastA fasta, int length) {
+
+        BitSet result = new BitSet();
+        result.set(0);
+        result.set(fasta.getFirstSequence().length() - 1);
+        for (int i = 0; i < length; i++) {
+            for (int s = 0; s < fasta.getSize(); s++) {
+                if (i < length - 2 && fasta.getSequence(s).charAt(i) != '-' && fasta.getSequence(s).charAt(i + 1) == '-'
+                        && fasta.getSequence(s).charAt(i + 2) == '-') {
+                    result.set(i);
+                } else if (i >= 2 && fasta.getSequence(s).charAt(i) != '-' && fasta.getSequence(s).charAt(i - 1) == '-'
+                        && fasta.getSequence(s).charAt(i - 2) == '-') {
+                    result.set(i);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * writes all subproblems to files
+     *
+     * @param inFile
+     * @param fasta
+     * @param blocks
+     * @throws java.io.IOException
+     */
+    static public void writeFastAFiles(String inFile, FastA fasta, List blocks) throws java.io.IOException {
+        NumberFormat nf = NumberFormat.getIntegerInstance();
+        nf.setMinimumIntegerDigits(4);
+
+        for (Object block1 : blocks) {
+            Block block = (Block) block1;
+            BitSet taxa = block.taxa;
+            int startPos = block.startPos;
+            int stopPos = block.stopPos;
+            String cname = inFile + ":" + nf.format(startPos) + "_" + nf.format(stopPos);
+            System.err.println("# Writing " + cname);
+            FileWriter w = new FileWriter(new File(cname));
+
+
+            for (int s = taxa.nextSetBit(0); s >= 0; s = taxa.nextSetBit(s + 1)) {
+                String name = "t" + s + "_" + fasta.getHeader(s);
+                name = name.replaceAll(" ", "_");
+                w.write("> " + name + "\n");
+                w.write(Basic.wraparound(fasta.getSequence(s).substring(startPos, stopPos + 1), 65));
+            }
+            w.close();
+        }
+    }
+
+    /**
+     * writes all subproblems to files
+     *
+     * @param inFile
+     * @param fasta
+     * @param blocks
+     * @throws java.io.IOException
+     */
+    static public void writeCGVizFiles(String inFile, FastA fasta, List blocks) throws java.io.IOException {
+        NumberFormat nf = NumberFormat.getIntegerInstance();
+        nf.setMinimumIntegerDigits(4);
+
+        for (Object block1 : blocks) {
+            Block block = (Block) block1;
+            BitSet taxa = block.taxa;
+            int startPos = block.startPos;
+            int stopPos = block.stopPos;
+            String cname = inFile + ":" + nf.format(startPos) + "_" + nf.format(stopPos) + ".cgv";
+
+            System.err.println("# Writing " + cname);
+            FileWriter w = new FileWriter(new File(cname));
+
+            w.write("{DATA " + cname + "\n");
+            w.write("[__GLOBAL__] tracks=" + fasta.getSize() + " dimension=1:\n");
+
+            for (int s = taxa.nextSetBit(0); s >= 0; s = taxa.nextSetBit(s + 1)) {
+                w.write("track=" + (s + 1) + " type=DNA sequence=\"" + fasta.getSequence(s).substring(startPos, stopPos + 1)
+                        + "\": " + startPos + " " + stopPos + "\n");
+            }
+            w.write("}\n");
+            w.close();
+        }
+    }
+
+}
+
+class Block {
+    int startPos;
+    int stopPos;
+    BitSet taxa;
+
+    public String toString() {
+        return "start=" + startPos + " stop=" + stopPos + " " + Basic.toString(taxa);
+    }
+}
diff --git a/src/jloda/progs/MASampler.java b/src/jloda/progs/MASampler.java
new file mode 100644
index 0000000..931d7a9
--- /dev/null
+++ b/src/jloda/progs/MASampler.java
@@ -0,0 +1,75 @@
+/**
+ * MASampler.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.util.CommandLineOptions;
+import jloda.util.PhylipUtils;
+
+import java.io.FileReader;
+import java.util.Random;
+
+/**
+ * Samples fragments from a sequence multiple alignment
+ *
+ * @author huson
+ *         Date: 13-Aug-2004
+ */
+public class MASampler {
+    static public void main(String[] args) throws Exception {
+        CommandLineOptions options = new CommandLineOptions(args);
+        options.setDescription("Sample fragments from a multiple alignment");
+        String cname = options.getMandatoryOption("-i", "input file in Phylip format", "");
+        int meanLength = options.getOption("-m", "mean length of a fragment", 500);
+        int percentStdDev = options.getOption("-d", "standard deviation in percent", 10);
+        int seed = options.getOption("-s", "random number seed", 666);
+        options.done();
+        Random rand = new Random(seed);
+
+        int stdDev = (int) (meanLength / 100.0 * percentStdDev);
+
+        System.err.println("# MASampler: m=" + meanLength + " sd= " + stdDev);
+
+        //System.err.println("# Options: " + options);
+
+        String[][] inData = new String[2][0];
+        FileReader r = new FileReader(cname);
+        PhylipUtils.read(inData, r);
+        int numSequences = inData[0].length - 1;
+        int length = inData[1][1].length();
+        System.err.println("# Input <" + cname + ">: " + numSequences + " sequences of length " + length);
+
+        String[][] outData = new String[2][numSequences + 1];
+        for (int i = 1; i <= numSequences; i++) {
+            int newLength = (int) (meanLength + rand.nextGaussian() * stdDev);
+            int newStart = rand.nextInt(length - newLength);
+            int newStop = newStart + newLength;
+            StringBuilder buf = new StringBuilder();
+            for (int p = 0; p < newStart; p++)
+                buf.append("-");
+            buf.append(inData[1][i].substring(newStart, newStop));
+            for (int p = newStop; p < length; p++)
+                buf.append("-");
+
+            outData[0][i] = inData[0][i];
+            outData[1][i] = buf.toString();
+        }
+        PhylipUtils.print(outData, System.out);
+    }
+}
diff --git a/src/jloda/progs/NewMABlocker.java b/src/jloda/progs/NewMABlocker.java
new file mode 100644
index 0000000..67f6c94
--- /dev/null
+++ b/src/jloda/progs/NewMABlocker.java
@@ -0,0 +1,509 @@
+/**
+ * NewMABlocker.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.graph.Graph;
+import jloda.graph.Node;
+import jloda.graph.NodeIntegerArray;
+import jloda.util.Basic;
+import jloda.util.CommandLineOptions;
+import jloda.util.FastA;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.text.NumberFormat;
+import java.util.BitSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Given a multi-alignment of individual reads, produces blocks of strongly aligned stuff
+ *
+ * @author huson
+ *         Date: 10-Aug-2004
+ */
+public class NewMABlocker {
+    static public void main(String[] args) throws Exception {
+        CommandLineOptions options = new CommandLineOptions(args);
+        String cname = options.getMandatoryOption("-i", "input file", "");
+        int minLength = options.getOption("-l", "min length of block", 100);
+        boolean generateFastA = options.getOption("+f", "create FastASequences files", false, true);
+        boolean generateCGViz = options.getOption("-c", "create CGViz files", true, false);
+        int minSequences = options.getOption("-s", "min number of sequences in a block", 4);
+        int minPercent = options.getOption("-d", "min percent of non-gap positions in a sequence", 80);
+        options.done();
+
+        System.err.println("# Options: " + options);
+
+        FastA fasta = new FastA();
+        fasta.read(new FileReader(new File(cname)));
+        int numSequences = fasta.getSize();
+
+        int length = 0;
+        for (int i = 0; i < fasta.getSize(); i++) {
+            if (i == 0)
+                length = fasta.getSequence(i).length();
+            else if (fasta.getSequence(i).length() != length)
+                throw new Exception("Sequence 0 and " + i + ": lengths differ: " + length
+                        + " and " + fasta.getSequence(i).length());
+        }
+        System.err.println("# Got " + numSequences + " sequences of length " + length);
+
+        int[] starts = new int[numSequences];
+        int[] stops = new int[numSequences];
+        stopsStarts(fasta, length, starts, stops);
+
+
+        List blocks = new LinkedList();
+        BitSet positions = getGapPositions(fasta, length);
+
+        for (int i = minLength; i < length; i += minLength) {
+            positions.set(i);
+        }
+
+        for (int i = positions.nextSetBit(0); i != -1; i = positions.nextSetBit(i + 1)) {
+            int j = positions.nextSetBit(i + 1);
+            if (j != -1 && j - i + 1 >= 25) {
+                for (int which = 0; which < 3; which++) {
+                    NewBlock block = computeBlock(which == 0 || which == 1, which == 0 || which == 2, fasta, length, i, j, positions, starts, stops, minSequences, minPercent);
+                    if (block != null && block.stopPos - block.startPos + 1 >= minLength) {
+                        blocks.add(block);
+                    }
+                }
+            }
+        }
+        makeBlocksUnique(blocks);
+        System.err.println("# Number of blocks: " + blocks.size());
+
+        if (generateFastA)
+            writeFastAFiles(cname, fasta, blocks);
+        if (generateCGViz)
+            writeCGVizFiles(cname, fasta, blocks);
+
+        {
+            FileWriter w = new FileWriter(new File(cname + ".fa"));
+            for (int t = 0; t < numSequences; t++) {
+                w.write("> t" + t + "_" + fasta.getHeader(t) + "\n");
+                w.write(Basic.wraparound(fasta.getSequence(t), 65));
+            }
+            w.close();
+        }
+
+        {
+            FileWriter w = new FileWriter(new File("nexus.header"));
+            {
+                w.write("#nexus\n");
+                w.write("begin taxa;\ndimensions ntax=" + numSequences + ";\ntaxlabels\n");
+                for (int t = 0; t < numSequences; t++) {
+                    w.write("t" + t + "_" + fasta.getHeader(t) + "\n");
+                }
+                w.write(";\nend;\n;");
+                w.write("begin trees;\n");
+            }
+            w.close();
+        }
+
+        {
+            FileWriter w = new FileWriter(new File("overview.cgv"));
+            w.write("{DATA overview\n");
+            w.write("[__GLOBAL__] dimension=2\n");
+            for (int i = 0; i < numSequences; i++) {
+                w.write("seq=" + i + " start= " + starts[i] + " stop= " + stops[i] +
+                        " len= " + (stops[i] - starts[i] + 1) + ": " + starts[i] + " " + i
+                        + " " + stops[i] + " " + i + "\n");
+            }
+            w.write("}\n");
+            w.write("{DATA blocks\n");
+            Iterator it = blocks.iterator();
+            int blockNumber = 0;
+            while (it.hasNext()) {
+                blockNumber++;
+                NewBlock block = (NewBlock) it.next();
+                int minOrder = numSequences + 1;
+                int maxOrder = -1;
+                for (int t = block.taxa.nextSetBit(0); t >= 0; t = block.taxa.nextSetBit(t + 1)) {
+                    if (t < minOrder)
+                        minOrder = t;
+                    if (t > maxOrder)
+                        maxOrder = t;
+                }
+                w.write("num=" + blockNumber + " start=" + block.startPos + " stop=" + block.stopPos
+                        + " minOrder=" + minOrder + " maxOrder=" + maxOrder + " numSequences=" +
+                        block.taxa.cardinality() + ": " + block.startPos + " " + (minOrder) +
+                        " " + block.stopPos + " " + (maxOrder) + "\n");
+            }
+            w.write("}\n");
+            w.close();
+        }
+    }
+
+    /**
+     * make blocks sufficiently unique
+     *
+     * @param blocks
+     */
+    private static void makeBlocksUnique(List blocks) {
+        NewBlock[] blocksArray = (NewBlock[]) blocks.toArray(new NewBlock[blocks.size()]);
+        blocks.clear();
+
+        Graph containmentGraph = new Graph();
+        Node[] id2node = new Node[blocksArray.length];
+        NodeIntegerArray node2id = new NodeIntegerArray(containmentGraph);
+
+        for (int i = 0; i < blocksArray.length; i++) {
+            id2node[i] = containmentGraph.newNode();
+            node2id.set(id2node[i], i);
+        }
+        for (int i = 0; i < blocksArray.length; i++) {
+            for (int j = 0; j < blocksArray.length; j++) {
+                if (i != j) {
+                    // System.err.println("NewBlock "+blocksArray[i]+" contains block "+blocksArray[j]+": "+contains(blocksArray[i], blocksArray[j]));
+
+                    if (contains(blocksArray[i], blocksArray[j])
+                            && (i < j || !contains(blocksArray[j], blocksArray[i])))
+                        containmentGraph.newEdge(id2node[i], id2node[j]);
+                }
+            }
+        }
+        // System.err.println("Graph: "+containmentGraph.toString());
+
+        for (Node v = containmentGraph.getFirstNode(); v != null; v = v.getNext()) {
+            if (v.getInDegree() == 0) {
+                NewBlock block = blocksArray[node2id.getValue(v)];
+                blocks.add(block);
+                /*
+                 {
+                System.err.print("NewBlock "+block+" contains:");
+                Stack stack=new Stack();
+                stack.add(v);
+                while(stack.size()>0) {
+                    Node w=(Node)stack.pop();
+                    for(Edge e=w.getFirstOutEdge();e!=null;e=w.getNextOutEdge(e))
+                    {
+                        Node u=e.getTarget();
+                        System.err.print(" "+blocksArray[node2id.getValue(u)]);
+                        stack.add(u);
+                    }
+                }
+                System.err.println();
+                }
+                */
+            }
+        }
+    }
+
+    /**
+     * does block a contain block b?
+     *
+     * @param a
+     * @param b
+     * @return true, if a contains b
+     */
+    private static boolean contains(NewBlock a, NewBlock b) {
+        BitSet intersection = (BitSet) a.taxa.clone();
+        intersection.and(b.taxa);
+        if (intersection.cardinality() != b.taxa.cardinality())
+            return false;
+
+        for (int s = intersection.nextSetBit(0); s != -1; s = intersection.nextSetBit(s + 1)) {
+            if (a.startPos > b.startPos + 5 || a.stopPos < b.stopPos - 5)
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * for a given seed segment (a,b), attempts to compute the best block
+     *
+     * @param fasta
+     * @param length
+     * @param a
+     * @param b
+     * @param positions
+     * @param starts
+     * @param stops
+     * @param minSequences
+     * @param minPercentSites
+     * @return
+     */
+    private static NewBlock computeBlock(boolean lengthOverNumber, boolean leftFirst, FastA fasta, int length, int a, int b, BitSet positions, int[] starts, int[] stops, int minSequences, int minPercentSites) {
+        // setup active sequences:
+        BitSet active = new BitSet();
+        int[] nonGapCount = new int[fasta.getSize()];
+
+        for (int s = 0; s < fasta.getSize(); s++) {
+            String sequence = fasta.getSequence(s);
+            int count = 0;
+            for (int pos = a; pos <= b; pos++) {
+                if (sequence.charAt(pos) != '-')
+                    count++;
+            }
+            nonGapCount[s] = count;
+            if (100 * count / (b - a + 1) >= minPercentSites)
+                active.set(s);
+        }
+
+        int left = -1;
+        int right = -1;
+
+        for (int which = 0; which < 2; which++) {
+            if (leftFirst == (which == 0)) {
+                // extend to the left
+                if (active.cardinality() >= minSequences) {
+                    for (left = a; left >= 0; left--) {
+                        for (int s = active.nextSetBit(0); s != -1; s = active.nextSetBit(s + 1))
+                            if (fasta.getSequence(s).charAt(left) != '-')
+                                nonGapCount[s]++;
+                        if (positions.get(left)) {
+                            BitSet dead = new BitSet();
+                            for (int s = active.nextSetBit(0); s != -1; s = active.nextSetBit(s + 1)) {
+                                if (left <= starts[s])
+                                    dead.set(s);
+                                else {
+                                    int z = 0;
+                                    boolean ok = false;
+                                    for (int j = left - 1; !ok && z <= 10 && j >= 0; j--, z++) {
+                                        if (fasta.getSequence(s).charAt(j) != '-')
+                                            ok = true;
+                                    }
+                                    if (!ok)
+                                        dead.set(s);
+                                }
+                            }
+                            if (lengthOverNumber && active.cardinality() - dead.cardinality() >= minSequences)
+                                active.andNot(dead);
+                            else if (dead.cardinality() >= 0.2 * active.cardinality())
+                                break;
+
+                            for (int s = active.nextSetBit(0); s != -1; s = active.nextSetBit(s + 1)) {
+                                if (100 * nonGapCount[s] / (b - left + 1) < minPercentSites) {
+                                    active.set(s, false);
+                                    if (active.cardinality() <= minSequences)
+                                        break;
+                                } else {
+                                    int z = 0;
+                                    boolean ok = false;
+                                    for (int j = left - 1; !ok && z <= 10 && j >= 0; j--, z++) {
+                                        if (fasta.getSequence(s).charAt(j) != '-')
+                                            ok = true;
+                                    }
+                                    if (!ok) {
+                                        active.set(s, false);
+                                        if (active.cardinality() <= minSequences)
+                                            break;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            } else {
+                // extend to the right
+                if (active.cardinality() >= minSequences) {
+                    for (right = b; right < length - 2; right++) {
+                        for (int s = active.nextSetBit(0); s != -1; s = active.nextSetBit(s + 1))
+                            if (fasta.getSequence(s).charAt(right) != '-')
+                                nonGapCount[s]++;
+                        if (positions.get(right)) {
+                            BitSet dead = new BitSet();
+                            for (int s = active.nextSetBit(0); s != -1; s = active.nextSetBit(s + 1)) {
+                                if (right >= stops[s])
+                                    dead.set(s);
+                                else {
+                                    int z = 0;
+                                    boolean ok = false;
+                                    for (int j = right + 1; !ok && z <= 10 && j < length; j++, z++) {
+                                        if (fasta.getSequence(s).charAt(j) != '-')
+                                            ok = true;
+                                    }
+                                    if (!ok)
+                                        dead.set(s);
+                                }
+                            }
+                            if (lengthOverNumber && active.cardinality() - dead.cardinality() >= minSequences)
+                                active.andNot(dead);
+                            else if (dead.cardinality() >= 0.2 * active.cardinality())
+                                break;
+
+                            for (int s = active.nextSetBit(0); s != -1; s = active.nextSetBit(s + 1)) {
+                                if (100 * nonGapCount[s] / (right - left + 1) < minPercentSites) {
+                                    active.set(s, false);
+                                    if (active.cardinality() <= minSequences)
+                                        break;
+                                } else {
+                                    int z = 0;
+                                    boolean ok = false;
+                                    for (int j = right + 1; !ok && z <= 10 && j < length; j++, z++) {
+                                        if (fasta.getSequence(s).charAt(j) != '-')
+                                            ok = true;
+                                    }
+                                    if (!ok) {
+                                        active.set(s, false);
+                                        if (active.cardinality() <= minSequences)
+                                            break;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        if (left != -1 && right != -1 && active.cardinality() >= minSequences) {
+            NewBlock block = new NewBlock();
+            block.startPos = left;
+            block.stopPos = right;
+            block.taxa = active;
+            return block;
+        } else
+            return null;
+    }
+
+
+    /**
+     * computes the sorted list of fragment start and fragment stop events
+     *
+     * @param fasta
+     * @param length
+     * @param starts
+     * @param stops
+     */
+    private static void stopsStarts(FastA fasta, int length, int[] starts, int[] stops) {
+        for (int s = 0; s < fasta.getSize(); s++) {
+            starts[s] = -1;
+        }
+
+        for (int i = 0; i < length; i++) {
+            for (int s = 0; s < fasta.getSize(); s++) {
+                if (fasta.getSequence(s).charAt(i) != '-') {
+                    if (starts[s] == -1) {
+                        starts[s] = i;
+                    }
+                    stops[s] = i;
+                }
+            }
+        }
+    }
+
+    /**
+     * computes the sorted list of fragment start and fragment stop events
+     *
+     * @param fasta
+     * @param length
+     * @return set of starts and stops
+     */
+    private static BitSet getGapPositions(FastA fasta, int length) {
+
+        BitSet result = new BitSet();
+        result.set(0);
+        result.set(fasta.getFirstSequence().length() - 1);
+        for (int i = 0; i < length; i++) {
+            for (int s = 0; s < fasta.getSize(); s++) {
+                if (i < length - 2 && fasta.getSequence(s).charAt(i) != '-' && fasta.getSequence(s).charAt(i + 1) == '-'
+                        && fasta.getSequence(s).charAt(i + 2) == '-') {
+                    result.set(i);
+                } else if (i >= 2 && fasta.getSequence(s).charAt(i) != '-' && fasta.getSequence(s).charAt(i - 1) == '-'
+                        && fasta.getSequence(s).charAt(i - 2) == '-') {
+                    result.set(i);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * writes all subproblems to files
+     *
+     * @param inFile
+     * @param fasta
+     * @param blocks
+     * @throws java.io.IOException
+     */
+    static public void writeFastAFiles(String inFile, FastA fasta, List blocks) throws java.io.IOException {
+        NumberFormat nf = NumberFormat.getIntegerInstance();
+        nf.setMinimumIntegerDigits(4);
+
+        for (Object block1 : blocks) {
+            NewBlock block = (NewBlock) block1;
+            BitSet taxa = block.taxa;
+            int startPos = block.startPos;
+            int stopPos = block.stopPos;
+            String cname = inFile + ":" + nf.format(startPos) + "_" + nf.format(stopPos);
+            System.err.println("# Writing " + cname);
+            FileWriter w = new FileWriter(new File(cname));
+
+
+            for (int s = taxa.nextSetBit(0); s >= 0; s = taxa.nextSetBit(s + 1)) {
+                String name = "t" + s + "_" + fasta.getHeader(s);
+                name = name.replaceAll(" ", "_");
+                w.write("> " + name + "\n");
+                w.write(Basic.wraparound(fasta.getSequence(s).substring(startPos, stopPos + 1), 65));
+            }
+            w.close();
+        }
+    }
+
+    /**
+     * writes all subproblems to files
+     *
+     * @param inFile
+     * @param fasta
+     * @param blocks
+     * @throws java.io.IOException
+     */
+    static public void writeCGVizFiles(String inFile, FastA fasta, List blocks) throws java.io.IOException {
+        NumberFormat nf = NumberFormat.getIntegerInstance();
+        nf.setMinimumIntegerDigits(4);
+
+        for (Object block1 : blocks) {
+            NewBlock block = (NewBlock) block1;
+            BitSet taxa = block.taxa;
+            int startPos = block.startPos;
+            int stopPos = block.stopPos;
+            String cname = inFile + ":" + nf.format(startPos) + "_" + nf.format(stopPos) + ".cgv";
+
+            System.err.println("# Writing " + cname);
+            FileWriter w = new FileWriter(new File(cname));
+
+            w.write("{DATA " + cname + "\n");
+            w.write("[__GLOBAL__] tracks=" + fasta.getSize() + " dimension=1:\n");
+
+            for (int s = taxa.nextSetBit(0); s >= 0; s = taxa.nextSetBit(s + 1)) {
+                w.write("track=" + (s + 1) + " type=DNA sequence=\"" + fasta.getSequence(s).substring(startPos, stopPos + 1)
+                        + "\": " + startPos + " " + stopPos + "\n");
+            }
+            w.write("}\n");
+            w.close();
+        }
+    }
+
+}
+
+class NewBlock {
+    int startPos;
+    int stopPos;
+    BitSet taxa;
+
+    public String toString() {
+        return "start=" + startPos + " stop=" + stopPos + " " + Basic.toString(taxa);
+    }
+}
diff --git a/src/jloda/progs/NextMABlocker.java b/src/jloda/progs/NextMABlocker.java
new file mode 100644
index 0000000..a4c7749
--- /dev/null
+++ b/src/jloda/progs/NextMABlocker.java
@@ -0,0 +1,648 @@
+/**
+ * NextMABlocker.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.graph.Graph;
+import jloda.graph.Node;
+import jloda.graph.NodeIntegerArray;
+import jloda.util.Basic;
+import jloda.util.CommandLineOptions;
+import jloda.util.FastA;
+import jloda.util.Pair;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.text.NumberFormat;
+import java.util.*;
+
+/**
+ * program for finding alignment blocks in a gappy alignment
+ * Daniel Huson, 2.2009, Kaikoura
+ */
+public class NextMABlocker {
+    static public void main(String[] args) throws Exception {
+        CommandLineOptions options = new CommandLineOptions(args);
+        String cname = options.getMandatoryOption("-i", "input file", "");
+        int minLength = options.getOption("-l", "min length of block", 200);
+        int gaps2Break = options.getOption("-g", "min number of gaps to break a segment", 20);
+        boolean generateFastA = options.getOption("+f", "create FastASequences files", false, true);
+        boolean generateCGViz = options.getOption("-c", "create CGViz files", true, false);
+        int minSequences = options.getOption("-s", "min number of sequences in a block", 10);
+        int minPercent = options.getOption("-d", "min percent of non-gap positions in a sequence", 90);
+        options.done();
+
+        System.err.println("Options: " + options);
+
+        FastA fasta = new FastA();
+        fasta.read(new FileReader(new File(cname)));
+        int numSequences = fasta.getSize();
+
+        int length = 0;
+        for (int i = 0; i < fasta.getSize(); i++) {
+            if (i == 0)
+                length = fasta.getSequence(i).length();
+            else if (fasta.getSequence(i).length() != length)
+                throw new Exception("Sequence 0 and " + i + ": lengths differ: " + length
+                        + " and " + fasta.getSequence(i).length());
+        }
+        System.err.println("Sequences: " + numSequences + ", length: " + length);
+
+        System.err.print("Computing segments: ");
+        int numberOfSegments = 0;
+        BitSet startPositions = new BitSet();
+        BitSet stopPositions = new BitSet();
+        List[] seq2segments = new LinkedList[fasta.getSize()];
+        for (int s = 0; s < fasta.getSize(); s++) {
+            seq2segments[s] = computeSegments(s, fasta.getSequence(s), gaps2Break);
+            numberOfSegments += seq2segments[s].size();
+            for (Object o : seq2segments[s]) {
+                Segment seg = (Segment) o;
+                startPositions.set(seg.start);
+                stopPositions.set(seg.stop);
+            }
+        }
+        System.err.println(numberOfSegments);
+        System.err.println("Start positions: " + startPositions.cardinality());
+        System.err.println("Stop positions:  " + stopPositions.cardinality());
+
+        System.err.print("Computing blocks: ");
+        List blocks = computeBlocks(fasta, startPositions, stopPositions, seq2segments, minLength, minSequences, minPercent);
+        System.err.println(blocks.size());
+
+        System.err.print("Reducing blocks:  ");
+        makeBlocksUnique(blocks);
+        System.err.println(blocks.size());
+
+        System.err.print("Thining blocks:  ");
+        makeThinCoverage(blocks);
+        System.err.println(blocks.size());
+
+        if (generateFastA)
+            writeFastAFiles(cname, fasta, blocks);
+        if (generateCGViz)
+            writeCGVizFiles(cname, fasta, blocks);
+
+        writeOverview(numSequences, seq2segments, blocks);
+    }
+
+    /**
+     * compute all segments of a given sequence
+     *
+     * @param s
+     * @param sequence
+     * @param gaps2Break
+     * @return list of segments
+     */
+    private static List computeSegments(int s, String sequence, int gaps2Break) {
+        List segments = new LinkedList();
+
+        int i = 0;
+
+        List gapStartStops = new LinkedList();
+        while (i < sequence.length()) {
+            while (i < sequence.length() && sequence.charAt(i) != '-')
+                i++;
+
+            int start = i;
+            int countgaps = 0;
+            while (i < sequence.length() && sequence.charAt(i) == '-') {
+                countgaps++;
+                i++;
+            }
+            if (countgaps >= gaps2Break) {
+                gapStartStops.add(new Pair(start, i));
+            }
+        }
+
+        int start = 0;
+        for (Object gapStartStop : gapStartStops) {
+            Pair pair = (Pair) gapStartStop;
+            if (pair.getFirstInt() > start) {
+                Segment seg = new Segment();
+                seg.start = start;
+                seg.stop = pair.getFirstInt() - 1; // last position still in the alignment
+                seg.taxon = s;
+                segments.add(seg);
+            }
+            start = pair.getSecondInt();
+        }
+        if (start < sequence.length() - 1) {
+            Segment seg = new Segment();
+            seg.start = start;
+            seg.stop = sequence.length() - 1; // last position still in the alignment
+            seg.taxon = s;
+            segments.add(seg);
+        }
+        return segments;
+    }
+
+    /**
+     * compute the blocks
+     *
+     * @param startPositions positions where segments start
+     * @param stopPositions  positions where segments stop
+     * @param seq2segments
+     * @return list of blocks
+     */
+    private static List computeBlocks(FastA fasta, BitSet startPositions, BitSet stopPositions, List[] seq2segments, int minLength, int minSequences,
+                                      int minPercentNonGaps) {
+        List blocks = new LinkedList();
+
+        for (int start = startPositions.nextSetBit(0); start != -1; start = startPositions.nextSetBit(start + 1)) {
+            for (int stop = stopPositions.nextSetBit(start + minLength - 1); stop != -1; stop = stopPositions.nextSetBit(stop + 1)) {
+                BitSet active = new BitSet();
+
+                for (int s = 0; s < seq2segments.length; s++) {
+                    // if (covers(fasta.getSequence(s),minPercentNonGaps,start, stop)) {
+                    if (covers(seq2segments[s], minPercentNonGaps, start, stop)) {
+                        active.set(s);
+                    }
+                }
+                if (active.cardinality() >= minSequences) {
+                    NextBlock block = new NextBlock();
+                    block.start = start;
+                    block.stop = stop;
+                    block.taxa = active;
+                    block.weight = active.cardinality() * (stop - start + 1);
+                    block.segs = new Segments(block.start, block.stop, block.taxa);
+                    blocks.add(block);
+                }
+            }
+        }
+        return blocks;
+    }
+
+    /**
+     * does the given sequence cover the given interval?
+     *
+     * @param sequence
+     * @param start
+     * @param stop
+     * @return true, if interval is covered
+     */
+    private static boolean covers(String sequence, int minPercentNonGaps, int start, int stop) {
+        int nongaps = 0;
+        for (int i = start; i <= stop; i++)
+            if (sequence.charAt(i) != '-')
+                nongaps++;
+        return ((100 * nongaps) / (stop - start + 1) >= minPercentNonGaps);
+    }
+
+    /**
+     * do the given segments cover the given interval?
+     *
+     * @param segments
+     * @param start
+     * @param stop
+     * @return true, if interval is covered
+     */
+    private static boolean covers(List segments, int minPercentNonGaps, int start, int stop) {
+        for (Object segment : segments) {
+            Segment seg = (Segment) segment;
+            int overlap = Math.max(0, Math.min(seg.stop, stop) - Math.max(seg.start, start) + 1);
+            if ((100 * overlap) / (stop - start + 1) >= minPercentNonGaps)
+                return true;
+
+        }
+        return false;
+    }
+
+    /**
+     * make blocks sufficiently unique
+     *
+     * @param blocks
+     */
+    private static void makeBlocksUnique(List blocks) {
+        NextBlock[] blocksArray = (NextBlock[]) blocks.toArray(new NextBlock[blocks.size()]);
+        blocks.clear();
+
+        Graph containmentGraph = new Graph();
+        Node[] id2node = new Node[blocksArray.length];
+        NodeIntegerArray node2id = new NodeIntegerArray(containmentGraph);
+
+        for (int i = 0; i < blocksArray.length; i++) {
+            id2node[i] = containmentGraph.newNode();
+            node2id.set(id2node[i], i);
+        }
+        for (int i = 0; i < blocksArray.length; i++) {
+            for (int j = 0; j < blocksArray.length; j++) {
+                if (i != j) {
+                    // System.err.println("NextBlock "+blocksArray[i]+" contains block "+blocksArray[j]+": "+contains(blocksArray[i], blocksArray[j]));
+
+                    if (contains(blocksArray[i], blocksArray[j]))
+                        containmentGraph.newEdge(id2node[i], id2node[j]);
+                }
+            }
+        }
+        // System.err.println("Graph: "+containmentGraph.toString());
+
+        for (Node v = containmentGraph.getFirstNode(); v != null; v = v.getNext()) {
+            if (v.getInDegree() == 0) {
+                NextBlock block = blocksArray[node2id.getValue(v)];
+                blocks.add(block);
+                /*
+                 {
+                System.err.print("NextBlock "+block+" contains:");
+                Stack stack=new Stack();
+                stack.add(v);
+                while(stack.size()>0) {
+                    Node w=(Node)stack.pop();
+                    for(Edge e=w.getFirstOutEdge();e!=null;e=w.getNextOutEdge(e))
+                    {
+                        Node u=e.getTarget();
+                        System.err.print(" "+blocksArray[node2id.getValue(u)]);
+                        stack.add(u);
+                    }
+                }
+                System.err.println();
+                }
+                */
+            }
+        }
+    }
+
+    /**
+     * does block a contain block b?
+     *
+     * @param a
+     * @param b
+     * @return true, if a contains b
+     */
+    private static boolean contains(NextBlock a, NextBlock b) {
+        for (int i = b.taxa.nextSetBit(0); i != -1; i = b.taxa.nextSetBit(i + 1)) {
+            if (!a.taxa.get(i))
+                return false;
+        }
+        return (a.start <= b.start && a.stop >= b.stop);
+    }
+
+    /**
+     * make blocks sufficiently unique
+     *
+     * @param blocks
+     */
+    private static void makeThinCoverage(List blocks) {
+        SortedSet set = new TreeSet(new NextBlock());
+        set.addAll(blocks);
+        blocks.clear();
+
+        while (set.size() > 0) {
+            NextBlock currentBlock = (NextBlock) set.first();
+            set.remove(currentBlock);
+            blocks.add(currentBlock);
+
+            List overlaps = new LinkedList();
+            for (Object aSet : set) {
+                NextBlock aBlock = (NextBlock) aSet;
+                if (aBlock.getId() != currentBlock.getId() && currentBlock.segs.overlaps(aBlock.segs)) {
+                    overlaps.add(aBlock);
+                }
+            }
+            set.removeAll(overlaps);
+
+            for (Object overlap : overlaps) {
+                NextBlock aBlock = (NextBlock) overlap;
+                Segments remains = aBlock.segs.substract(currentBlock.segs);
+                if (remains.size() > 0) {
+                    aBlock.segs = remains;
+                    aBlock.weight = remains.size();
+                    set.add(aBlock);
+                }
+            }
+        }
+    }
+
+    /**
+     * write an overview in CGViz format
+     *
+     * @param seq2segments
+     * @throws IOException
+     */
+    private static void writeOverview(int numSequences, List[] seq2segments, List blocks) throws IOException {
+        FileWriter w = new FileWriter(new File("overview.cgv"));
+        w.write("{DATA overview\n");
+        w.write("[__GLOBAL__] dimension=2\n");
+        for (int i = 0; i < numSequences; i++) {
+            for (Object o : seq2segments[i]) {
+                Segment seg = (Segment) o;
+
+                w.write("seq=" + i + " start= " + seg.start + " stop= " + seg.stop +
+                        " len= " + (seg.stop - seg.start + 1) + ": " + seg.start + " " + i
+                        + " " + seg.stop + " " + i + "\n");
+            }
+        }
+        w.write("}\n");
+
+        w.write("{DATA blocks\n");
+        Iterator it = blocks.iterator();
+        int blockNumber = 0;
+        while (it.hasNext()) {
+            blockNumber++;
+            NextBlock block = (NextBlock) it.next();
+            int minOrder = numSequences + 1;
+            int maxOrder = -1;
+            for (int t = block.taxa.nextSetBit(0); t >= 0; t = block.taxa.nextSetBit(t + 1)) {
+                if (t < minOrder)
+                    minOrder = t;
+                if (t > maxOrder)
+                    maxOrder = t;
+            }
+            w.write("num=" + blockNumber + " start=" + block.start + " stop=" + block.stop
+                    + " minOrder=" + minOrder + " maxOrder=" + maxOrder + " numSequences=" +
+                    block.taxa.cardinality() + ": " + block.start + " " + (minOrder) +
+                    " " + block.stop + " " + (maxOrder) + "\n");
+        }
+        w.write("}\n");
+        w.close();
+    }
+
+    /**
+     * writes all subproblems to files
+     *
+     * @param inFile
+     * @param fasta
+     * @param blocks
+     * @throws java.io.IOException
+     */
+    static public void writeFastAFiles(String inFile, FastA fasta, List blocks) throws java.io.IOException {
+        NumberFormat nf = NumberFormat.getIntegerInstance();
+        nf.setMinimumIntegerDigits(4);
+
+        for (Object block1 : blocks) {
+            NextBlock block = (NextBlock) block1;
+            BitSet taxa = block.taxa;
+            int startPos = block.start;
+            int stopPos = block.stop;
+            String cname = inFile + ":" + nf.format(startPos) + "_" + nf.format(stopPos);
+            System.err.println("# Writing " + cname);
+            FileWriter w = new FileWriter(new File(cname));
+
+
+            for (int s = taxa.nextSetBit(0); s >= 0; s = taxa.nextSetBit(s + 1)) {
+                String name = "t" + s + "_" + fasta.getHeader(s);
+                name = name.replaceAll(" ", "_");
+                w.write("> " + name + "\n");
+                w.write(Basic.wraparound(fasta.getSequence(s).substring(startPos, stopPos + 1), 65));
+            }
+            w.close();
+        }
+    }
+
+    /**
+     * writes all subproblems to files
+     *
+     * @param inFile
+     * @param fasta
+     * @param blocks
+     * @throws java.io.IOException
+     */
+    static public void writeCGVizFiles(String inFile, FastA fasta, List blocks) throws java.io.IOException {
+        NumberFormat nf = NumberFormat.getIntegerInstance();
+        nf.setMinimumIntegerDigits(4);
+
+        for (Object block1 : blocks) {
+            NextBlock block = (NextBlock) block1;
+            BitSet taxa = block.taxa;
+            int startPos = block.start;
+            int stopPos = block.stop;
+            String cname = inFile + ":" + nf.format(startPos) + "_" + nf.format(stopPos) + ".cgv";
+
+            System.err.println("# Writing " + cname);
+            FileWriter w = new FileWriter(new File(cname));
+
+            w.write("{DATA " + cname + "\n");
+            w.write("[__GLOBAL__] tracks=" + fasta.getSize() + " dimension=1:\n");
+
+            for (int s = taxa.nextSetBit(0); s >= 0; s = taxa.nextSetBit(s + 1)) {
+                w.write("track=" + (s + 1) + " type=DNA sequence=\"" + fasta.getSequence(s).substring(startPos, stopPos + 1)
+                        + "\": " + startPos + " " + stopPos + "\n");
+            }
+            w.write("}\n");
+            w.close();
+        }
+    }
+}
+
+class Segment {
+    int start;
+    int stop;
+    int taxon;
+
+    public Segment() {
+    }
+
+    public Segment(int start, int stop, int taxon) {
+        this.start = start;
+        this.stop = stop;
+        this.taxon = taxon;
+    }
+
+    /**
+     * substract a set of segments from a segment
+     *
+     * @param subs
+     * @return resulting segments
+     */
+    public Segments subtract(Segments subs) {
+        Segments segs = new Segments();
+        segs.add(this);
+        for (Iterator it = subs.iterator(); it.hasNext(); ) {
+            Segment seg = (Segment) it.next();
+            segs = segs.substract(seg);
+
+        }
+        return segs;
+    }
+}
+
+class Segments {
+    private final Collection segments;
+    private int size;
+
+    public Segments() {
+        segments = new LinkedList();
+        size = 0;
+    }
+
+    /**
+     * initialize segment for each taxon from start to stop
+     *
+     * @param start
+     * @param stop
+     * @param taxa
+     */
+    public Segments(int start, int stop, BitSet taxa) {
+        this();
+        for (int t = taxa.nextSetBit(0); t != -1; t = taxa.nextSetBit(t + 1))
+            add(new Segment(start, stop, t));
+        recomputeSize();
+    }
+
+    public Collection getSegments() {
+        return segments;
+    }
+
+    /**
+     * substract all given segments
+     *
+     * @param subs
+     * @return resulting segments
+     */
+    public Segments substract(Segments subs) {
+        Segments result = new Segments();
+
+        for (Iterator it = iterator(); it.hasNext(); ) {
+            Segment seg = (Segment) it.next();
+            Segments segs = seg.subtract(subs);
+            if (segs.size() > 0)
+                result.add(seg);
+        }
+        result.recomputeSize();
+        return result;
+    }
+
+    /**
+     * subtract a segment from a list of segments
+     *
+     * @param sub
+     * @return new list
+     */
+    public Segments substract(Segment sub) {
+        Segments result = new Segments();
+        for (Iterator it = iterator(); it.hasNext(); ) {
+            Segment seg = (Segment) it.next();
+            if (seg.taxon != sub.taxon || sub.stop < seg.start || sub.start > seg.stop)     // misses
+                result.add(seg);
+            else if (sub.start <= seg.start && sub.stop >= seg.start && sub.stop < seg.stop) // overlaps start
+                result.add(new Segment(sub.stop + 1, seg.stop, seg.taxon));
+            else if (sub.start > seg.start && sub.start <= seg.stop && sub.stop >= seg.stop) // overlaps stop
+                result.add(new Segment(seg.start, sub.start - 1, seg.taxon));
+            else if (sub.start > seg.start && sub.stop < seg.stop)  // in the middle
+            {
+                result.add(new Segment(seg.start, sub.start - 1, seg.taxon));
+                result.add(new Segment(sub.stop + 1, seg.stop, seg.taxon));
+            }
+        }
+        result.recomputeSize();
+        return result;
+    }
+
+    public void add(Segment seg) {
+        segments.add(seg);
+        size += seg.stop - seg.start + 1;
+
+    }
+
+    public void addAll(Collection segs) {
+        segments.addAll(segs);
+        recomputeSize();
+    }
+
+    public void recomputeSize() {
+        size = 0;
+        for (Object segment : segments) {
+            Segment seg = (Segment) segment;
+            size += (seg.stop - seg.start + 1);
+        }
+    }
+
+    public int size() {
+        return size;
+    }
+
+    public Iterator iterator() {
+        return segments.iterator();
+    }
+
+    public void clear() {
+        segments.clear();
+        size = 0;
+    }
+
+    /**
+     * do two segments intersect?
+     *
+     * @param segs
+     * @return true, if pair of intersecting segments encountered
+     */
+    public boolean overlaps(Segments segs) {
+        for (Iterator it1 = iterator(); it1.hasNext(); ) {
+            Segment seg1 = (Segment) it1.next();
+            for (Iterator it2 = segs.iterator(); it2.hasNext(); ) {
+                Segment seg2 = (Segment) it2.next();
+                if (!((seg1.start < seg2.start && seg1.stop < seg2.stop) || (seg1.start > seg2.start && seg1.stop > seg2.stop)))
+                    return true;
+            }
+        }
+        return false;
+    }
+}
+
+class NextBlock implements Comparator {
+    int start;
+    int stop;
+    BitSet taxa;
+    int weight;
+    Segments segs;
+    private final int id;
+    static private int maxId = 0;
+
+    public NextBlock() {
+        id = (++maxId);
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public String toString() {
+        return "start=" + start + " stop=" + stop + " wgt=" + weight + " " + Basic.toString(taxa);
+    }
+
+    public int compare(Object o1, Object o2) {
+        NextBlock wb1 = (NextBlock) o1;
+        NextBlock wb2 = (NextBlock) o2;
+
+        if (wb1.weight > wb2.weight)
+            return -1;
+        else if (wb1.weight < wb2.weight)
+            return 1;
+        else if (wb1.taxa.cardinality() > wb2.taxa.cardinality())
+            return -1;
+        else if (wb1.taxa.cardinality() < wb2.taxa.cardinality())
+            return 1;
+        else if (wb1.stop - wb1.start > wb2.stop - wb2.start)
+            return -1;
+        else if (wb1.stop - wb1.start < wb2.stop - wb2.start)
+            return 1;
+        else if (wb1.start < wb2.start)
+            return -1;
+        else if (wb1.start < wb2.start)
+            return 1;
+        else if (wb1.id < wb2.id)
+            return -1;
+        else if (wb1.id > wb2.id)
+            return 1;
+        else
+            return 0;
+    }
+}
diff --git a/src/jloda/progs/QuasiMedianClosure.java b/src/jloda/progs/QuasiMedianClosure.java
new file mode 100644
index 0000000..78999bb
--- /dev/null
+++ b/src/jloda/progs/QuasiMedianClosure.java
@@ -0,0 +1,224 @@
+/**
+ * QuasiMedianClosure.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.graph.Node;
+import jloda.phylo.PhyloGraph;
+import jloda.phylo.PhyloGraphView;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.*;
+
+/**
+ * compute the quasi median closure of a set of sequences
+ * Daniel Huson, 9.2009
+ */
+public class QuasiMedianClosure {
+    public static void main(String[] args) throws IOException {
+        System.err.println("Please enter sequences, followed by a .");
+
+        Set oldSequences = new TreeSet();
+        Set curSequences = new HashSet();
+        Set newSequences = new HashSet();
+        int sequenceLength = 0;
+
+        BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
+
+
+        String aLine;
+        while ((aLine = r.readLine()) != null) {
+            aLine = aLine.trim();
+            if (aLine.length() == 0 || aLine.startsWith("#"))
+                continue;
+            if (aLine.equals("."))
+                break;
+            if (sequenceLength == 0) {
+                sequenceLength = aLine.length();
+            } else if (sequenceLength != aLine.length())
+                throw new IOException("Input line: '" + aLine + "': wrong length (" + aLine.length() + "), should be: " + sequenceLength);
+            if (oldSequences.contains(aLine))
+                System.err.println("Duplicate sequence in input (ignored): " + aLine);
+            else {
+                oldSequences.add(aLine);
+            }
+        }
+
+        // check that all columns differ:
+        for (int i = 0; i < sequenceLength; i++) {
+            for (int j = i + 1; j < sequenceLength; j++) {
+                boolean ok = false;
+                char[] i2j = new char[256];
+                char[] j2i = new char[256];
+
+                for (Iterator it = oldSequences.iterator(); !ok && it.hasNext(); ) {
+                    String sequence = (String) it.next();
+                    char chari = sequence.charAt(i);
+                    char charj = sequence.charAt(j);
+
+                    if (i2j[chari] == (char) 0) {
+                        i2j[chari] = charj;
+                        if (j2i[charj] == (char) 0)
+                            j2i[charj] = chari;
+                        else if (j2i[charj] != chari)
+                            ok = true; // differ
+                    } else if (i2j[chari] != charj)
+                        ok = true; // differ
+                }
+                if (!ok)
+                    throw new IOException("Input has identical pattern in columns: " + i + " and " + j);
+            }
+        }
+
+
+        curSequences.addAll(oldSequences);
+
+        while (curSequences.size() > 0) {
+            String[] oldArray = (String[]) oldSequences.toArray(new String[oldSequences.size()]);
+            newSequences.clear();
+            for (String seqA : oldArray) {
+                for (String seqB : oldArray) {
+                    for (Object curSequence : curSequences) {
+                        String seqC = (String) curSequence;
+                        if (!seqC.equals(seqA) && !seqC.equals(seqB)) {
+                            String[] medianSequences = computeQuasiMedian(seqA, seqB, seqC);
+                            for (String medianSequence : medianSequences) {
+                                if (!oldSequences.contains(medianSequence) && !curSequences.contains(medianSequence)) {
+                                    newSequences.add(medianSequence);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            oldSequences.addAll(curSequences);
+            curSequences.clear();
+            Set tmp = curSequences;
+            curSequences = newSequences;
+            newSequences = tmp;
+        }
+
+        System.out.println("Closure (" + oldSequences.size() + "):");
+        for (Object oldSequence : oldSequences) {
+            System.out.println(oldSequence);
+        }
+
+        showGraph(oldSequences);
+    }
+
+    private static void showGraph(Set oldSequences) {
+        PhyloGraph graph = new PhyloGraph();
+        for (Object oldSequence : oldSequences) {
+            String seq = (String) oldSequence;
+            Node v = graph.newNode();
+            graph.setLabel(v, seq);
+        }
+
+        for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+            for (Node w = v.getNext(); w != null; w = w.getNext()) {
+                int i = computeOneStep(graph.getLabel(v), graph.getLabel(w));
+                if (i != -1)
+                    graph.newEdge(v, w, "" + i);
+            }
+        }
+
+        JFrame frame = new JFrame("quasi-median network");
+        frame.setSize(400, 400);
+
+        PhyloGraphView view = new PhyloGraphView(graph, 400, 400);
+        view.computeSpringEmbedding(1000, false);
+        frame.getContentPane().setLayout(new BorderLayout());
+        frame.getContentPane().add(view.getScrollPane(), BorderLayout.CENTER);
+        frame.setVisible(true);
+
+    }
+
+    /**
+     * if two sequences differ at exactly one position, gets position
+     *
+     * @param seqa
+     * @param seqb
+     * @return single difference position or -1
+     */
+    private static int computeOneStep(String seqa, String seqb) {
+        int pos = -1;
+        for (int i = 0; i < seqa.length(); i++) {
+            if (seqa.charAt(i) != seqb.charAt(i)) {
+                if (pos == -1)
+                    pos = i;
+                else
+                    return -1;
+            }
+        }
+        return pos;
+    }
+
+    /**
+     * computes the quasi median for three sequences
+     *
+     * @param seqA
+     * @param seqB
+     * @param seqC
+     * @return quasi median
+     */
+    private static String[] computeQuasiMedian(String seqA, String seqB, String seqC) {
+        StringBuilder buf = new StringBuilder();
+        boolean hasStar = false;
+        for (int i = 0; i < seqA.length(); i++) {
+            if (seqA.charAt(i) == seqB.charAt(i) || seqA.charAt(i) == seqC.charAt(i))
+                buf.append(seqA.charAt(i));
+            else if (seqB.charAt(i) == seqC.charAt(i))
+                buf.append(seqB.charAt(i));
+            else {
+                buf.append("*");
+                hasStar = true;
+            }
+        }
+        if (!hasStar)
+            return new String[]{buf.toString()};
+
+        Set median = new HashSet();
+        Stack stack = new Stack();
+        stack.add(buf.toString());
+        while (!stack.empty()) {
+            String seq = (String) stack.pop();
+            int pos = seq.indexOf('*');
+            int pos2 = seq.indexOf('*', pos + 1);
+            String first = seq.substring(0, pos) + seqA.charAt(pos) + seq.substring(pos + 1);
+            String second = seq.substring(0, pos) + seqB.charAt(pos) + seq.substring(pos + 1);
+            String third = seq.substring(0, pos) + seqC.charAt(pos) + seq.substring(pos + 1);
+            if (pos2 == -1) {
+                median.add(first);
+                median.add(second);
+                median.add(third);
+            } else {
+                stack.add(first);
+                stack.add(second);
+                stack.add(third);
+            }
+        }
+
+
+        return (String[]) median.toArray(new String[median.size()]);
+    }
+}
diff --git a/src/jloda/progs/QuasiMedianNetwork.java b/src/jloda/progs/QuasiMedianNetwork.java
new file mode 100644
index 0000000..e5f2ce9
--- /dev/null
+++ b/src/jloda/progs/QuasiMedianNetwork.java
@@ -0,0 +1,872 @@
+/**
+ * QuasiMedianNetwork.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.export.PDFExportType;
+import jloda.graph.*;
+import jloda.graphview.GraphView;
+import jloda.graphview.NodeView;
+import jloda.phylo.PhyloGraph;
+import jloda.phylo.PhyloGraphView;
+import jloda.util.Basic;
+import jloda.util.Pair;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.*;
+import java.util.List;
+
+/**
+ * compute the quasi median closure of a set of sequences
+ * Daniel Huson, 9.2009
+ */
+public class QuasiMedianNetwork {
+    public static void main(String[] args) throws IOException {
+        System.err.println("Please enter sequences, followed by a .");
+
+        Set inputSequences = new TreeSet();
+        int sequenceLength = 0;
+
+        BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
+
+
+        String aLine;
+        while ((aLine = r.readLine()) != null) {
+            aLine = aLine.trim();
+            if (aLine.length() == 0 || aLine.startsWith("#"))
+                continue;
+            if (aLine.equals("."))
+                break;
+            if (sequenceLength == 0) {
+                sequenceLength = aLine.length();
+            } else if (sequenceLength != aLine.length())
+                throw new IOException("Input line: '" + aLine + "': wrong length (" + aLine.length() + "), should be: " + sequenceLength);
+            if (inputSequences.contains(aLine))
+                System.err.println("Duplicate sequence in input (ignored): " + aLine);
+            else {
+                inputSequences.add(aLine);
+            }
+        }
+
+        // check that all columns differ:
+        for (int i = 0; i < sequenceLength; i++) {
+            for (int j = i + 1; j < sequenceLength; j++) {
+                boolean ok = false;
+                char[] i2j = new char[256];
+                char[] j2i = new char[256];
+
+                for (Iterator it = inputSequences.iterator(); !ok && it.hasNext(); ) {
+                    String sequence = (String) it.next();
+                    char chari = sequence.charAt(i);
+                    char charj = sequence.charAt(j);
+
+                    if (i2j[chari] == (char) 0) {
+                        i2j[chari] = charj;
+                        if (j2i[charj] == (char) 0)
+                            j2i[charj] = chari;
+                        else if (j2i[charj] != chari)
+                            ok = true; // differ
+                    } else if (i2j[chari] != charj)
+                        ok = true; // differ
+                }
+                if (!ok)
+                    throw new IOException("Input has identical pattern in columns: " + i + " and " + j);
+            }
+        }
+        System.err.println("Input: " + inputSequences.size());
+
+        System.err.println("Enter optional character weights:");
+        double[] weights = new double[sequenceLength];
+        aLine = r.readLine();
+        if (aLine.length() > 0) {
+            StringTokenizer st = new StringTokenizer(aLine);
+            for (int i = 0; i < weights.length; i++)
+                weights[i] = Double.parseDouble(st.nextToken());
+        } else
+            for (int i = 0; i < weights.length; i++)
+                weights[i] = 1;
+
+        Set outputSequences;
+
+        System.err.println("Enter q, g or j");
+        aLine = r.readLine();
+
+        PhyloGraph graph;
+        switch (aLine) {
+            case "q":
+                outputSequences = computeQuasiMedianClosure(inputSequences, null, null);
+                graph = computeOneStepGraph(outputSequences);
+                break;
+            case "g":
+                outputSequences = computeGeodesicPrunedQuasiMedianClosure(inputSequences, sequenceLength);
+                graph = computeOneStepGraph(outputSequences);
+                break;
+            default:
+// if(aLine.equals("j"))
+
+                System.err.println("Enter epsilon");
+                aLine = r.readLine();
+                int epsilon = 0;
+                if (aLine.length() > 0)
+                    epsilon = Integer.parseInt(aLine);
+                outputSequences = new HashSet();
+                graph = computeMedianJoiningNetwork(inputSequences, weights, epsilon);
+                break;
+        }
+
+        System.err.println("Closure (" + outputSequences.size() + "):");
+        for (Object outputSequence : outputSequences) {
+            System.err.println(outputSequence);
+        }
+
+        showGraph(graph);
+    }
+
+    /**
+     * compute the quasi median closure for the given set of sequences
+     *
+     * @param sequences
+     * @param refA      if !=null, use this reference sequence in computation of quasi median
+     * @param refB      if !=null, use this reference sequence in computation of quasi median
+     * @return quasi median closure
+     */
+    public static Set<String> computeQuasiMedianClosure(Set<String> sequences, String refA, String refB) {
+        Set<String> oldSequences = new TreeSet<>();
+        Set<String> curSequences = new HashSet<>();
+        Set<String> newSequences = new HashSet<>();
+
+        System.err.println("Computing quasi-median closure:");
+        oldSequences.addAll(sequences);
+        curSequences.addAll(sequences);
+
+        while (curSequences.size() > 0) {
+            String[] oldArray = oldSequences.toArray(new String[oldSequences.size()]);
+            newSequences.clear();
+            for (String seqA : oldArray) {
+                for (String seqB : oldArray) {
+                    for (String seqC : curSequences) {
+                        if (!seqC.equals(seqA) && !seqC.equals(seqB)) {
+                            String[] medianSequences = refA != null ? computeQuasiMedian(seqA, seqB, seqC, refA, refB) :
+                                    computeQuasiMedian(seqA, seqB, seqC);
+                            for (String medianSequence : medianSequences) {
+                                if (!oldSequences.contains(medianSequence) && !curSequences.contains(medianSequence)) {
+                                    newSequences.add(medianSequence);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            oldSequences.addAll(curSequences);
+            curSequences.clear();
+            Set<String> tmp = curSequences;
+            curSequences = newSequences;
+            newSequences = tmp;
+            System.err.println("Size: " + oldSequences.size());
+        }
+        return oldSequences;
+
+    }
+
+    /**
+     * compute the quasi median closure for the given set of sequences
+     *
+     * @param sequences
+     * @param sequenceLength
+     * @return quasi median closure
+     */
+    public static Set computeGeodesicPrunedQuasiMedianClosure(Set sequences, int sequenceLength) {
+        Set result = new TreeSet();
+
+        System.err.println("Computing geodesically-pruned quasi-median closure:");
+
+        String[] input = (String[]) sequences.toArray(new String[sequences.size()]);
+
+        double[][] scores = computeScores(input, sequenceLength);
+
+        for (int i = 0; i < input.length; i++) {
+            for (int j = i + 1; j < input.length; j++) {
+                System.err.println("Processing " + i + "," + j);
+                BitSet use = new BitSet();
+                for (int pos = 0; pos < sequenceLength; pos++) {
+                    if (input[i].charAt(pos) != input[j].charAt(pos))
+                        use.set(pos);
+                }
+                Set<String> compressed = new HashSet<>();
+                for (String anInput : input) {
+                    StringBuilder buf = new StringBuilder();
+                    for (int p = 0; p < sequenceLength; p++) {
+                        if (use.get(p))
+                            buf.append(anInput.charAt(p));
+                    }
+                    compressed.add(buf.toString());
+                }
+                StringBuilder bufA = new StringBuilder();
+                StringBuilder bufB = new StringBuilder();
+
+                for (int p = 0; p < sequenceLength; p++) {
+                    if (use.get(p)) {
+                        bufA.append(input[i].charAt(p));
+                        bufB.append(input[j].charAt(p));
+                    }
+                }
+                String refA = bufA.toString();
+                String refB = bufB.toString();
+
+
+                Set closure = computeQuasiMedianClosure(compressed, refA, refB);
+
+                Set expanded = new HashSet();
+                for (Object aClosure : closure) {
+                    String current = (String) aClosure;
+                    StringBuilder buf = new StringBuilder();
+                    int p = 0;
+                    for (int k = 0; k < sequenceLength; k++) {
+                        if (!use.get(k))
+                            buf.append(input[i].charAt(k));
+                        else // used in
+                        {
+                            buf.append(current.charAt(p++));
+                        }
+                    }
+                    expanded.add(buf.toString());
+                }
+                Set geodesic = computeGeodesic(input[i], input[j], expanded, scores);
+                result.addAll(geodesic);
+
+
+                    System.err.println("------Sequences :");
+                    System.err.println(input[i]);
+                    System.err.println(input[j]);
+                    for (int x = 0; x < sequenceLength; x++)
+                        System.err.print(use.get(x) ? "x" : " ");
+                    System.err.println();
+                    System.err.println("Refs:");
+
+                    System.err.println(refA);
+                    System.err.println(refB);
+                    System.err.println("Compressed (" + compressed.size() + "):");
+                for (String aCompressed : compressed) System.err.println(aCompressed);
+                    System.err.println("Closure (" + closure.size() + "):");
+                for (Object aClosure : closure) System.err.println(aClosure);
+                    System.err.println("Expanded (" + expanded.size() + "):");
+                for (Object anExpanded : expanded) System.err.println(anExpanded);
+                    System.err.println("Geodesic (" + geodesic.size() + "):");
+                for (Object aGeodesic : geodesic) System.err.println(aGeodesic);
+
+            }
+        }
+        return result;
+    }
+
+    /**
+     * compute the best geodesic between two nodes
+     *
+     * @param seqA
+     * @param seqB
+     * @param expanded
+     * @param scores
+     * @return geodesic
+     */
+    private static Set computeGeodesic(String seqA, String seqB, Set expanded, double[][] scores) {
+        Graph graph = new Graph();
+
+        Node start = null;
+        Node end = null;
+        for (Object anExpanded : expanded) {
+            String seq = (String) anExpanded;
+
+            Node v = graph.newNode(seq);
+            if (start == null && seq.equals(seqA))
+                start = v;
+            else if (end == null && seq.equals(seqB))
+                end = v;
+        }
+        for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+            String aSeq = (String) graph.getInfo(v);
+            for (Node w = v.getNext(); w != null; w = w.getNext()) {
+                String bSeq = (String) graph.getInfo(w);
+                if (computeOneStep(aSeq, bSeq) != -1)
+                    graph.newEdge(v, w);
+            }
+        }
+
+        Set bestPath = new HashSet();
+        NodeSet inPath = new NodeSet(graph);
+        NodeDoubleArray bestScore = new NodeDoubleArray(graph, Double.NEGATIVE_INFINITY);
+        inPath.add(start);
+        System.err.println("Finding best geodesic:");
+        computeBestPathRec(graph, end, start, null, bestScore, inPath, 0, new HashSet(), bestPath, scores);
+        return bestPath;
+    }
+
+    /**
+     * get the best path from start to end
+     *
+     * @param end
+     * @param v
+     * @param e
+     * @param currentScore
+     * @param currentPath
+     * @param bestPath
+     */
+    private static void computeBestPathRec(Graph graph, Node end, Node v, Edge e, NodeDoubleArray bestScore, NodeSet inPath, double currentScore,
+                                           HashSet currentPath,
+                                           Set bestPath, double[][] scores) {
+        if (v == end) {
+            if (currentScore > bestScore.getValue(end)) {
+                System.err.println("Updating best score: " + bestScore.getValue(end) + " -> " + currentScore);
+                bestPath.clear();
+                bestPath.addAll(currentPath);
+                bestScore.set(v, currentScore);
+            } else if (currentScore == bestScore.getValue(end)) {
+                bestPath.addAll(currentPath); // don't break ties
+            }
+        } else {
+            if (currentScore >= bestScore.getValue(v)) {
+                bestScore.set(v, currentScore);
+                for (Edge f = v.getFirstAdjacentEdge(); f != null; f = v.getNextAdjacentEdge(f)) {
+                    if (f != e) {
+                        Node w = f.getOpposite(v);
+                        if (!inPath.contains(w)) {
+                            inPath.add(w);
+                            String seq = (String) graph.getInfo(w);
+                            double add = getScore(seq, scores);
+                            currentPath.add(seq);
+                            computeBestPathRec(graph, end, w, f, bestScore, inPath, currentScore + add, currentPath, bestPath, scores);
+                            currentPath.remove(seq);
+                            inPath.remove(w);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * computes a log score for each state at each   position of the alignment
+     *
+     * @param input
+     * @param sequenceLength
+     * @return scores
+     */
+    private static double[][] computeScores(String[] input, int sequenceLength) {
+        double[][] scores = new double[sequenceLength][256];
+
+        for (int pos = 0; pos < sequenceLength; pos++) {
+            for (String anInput : input) {
+                scores[pos][anInput.charAt(pos)]++;
+            }
+        }
+
+        for (int pos = 0; pos < sequenceLength; pos++) {
+            for (int i = 0; i < 256; i++) {
+                if (scores[pos][i] != 0)
+                    scores[pos][i] = Math.log(scores[pos][i] / input.length);
+            }
+        }
+        return scores;
+    }
+
+    /**
+     * get the log score of a sequence
+     *
+     * @param seq
+     * @param scores
+     * @return log score
+     */
+    private static double getScore(String seq, double[][] scores) {
+        double score = 0;
+        for (int i = 0; i < seq.length(); i++)
+            score += scores[i][seq.charAt(i)];
+        return score;
+    }
+
+
+    /**
+     * if two sequences differ at exactly one position, gets position
+     *
+     * @param seqa
+     * @param seqb
+     * @return single difference position or -1
+     */
+    private static int computeOneStep(String seqa, String seqb) {
+        int pos = -1;
+        for (int i = 0; i < seqa.length(); i++) {
+            if (seqa.charAt(i) != seqb.charAt(i)) {
+                if (pos == -1)
+                    pos = i;
+                else
+                    return -1;
+            }
+        }
+        return pos;
+    }
+
+    /**
+     * computes the quasi median for three sequences
+     *
+     * @param seqA
+     * @param seqB
+     * @param seqC
+     * @return quasi median
+     */
+    private static String[] computeQuasiMedian(String seqA, String seqB, String seqC) {
+        StringBuilder buf = new StringBuilder();
+        boolean hasStar = false;
+        for (int i = 0; i < seqA.length(); i++) {
+            if (seqA.charAt(i) == seqB.charAt(i) || seqA.charAt(i) == seqC.charAt(i))
+                buf.append(seqA.charAt(i));
+            else if (seqB.charAt(i) == seqC.charAt(i))
+                buf.append(seqB.charAt(i));
+            else {
+                buf.append("*");
+                hasStar = true;
+            }
+        }
+        if (!hasStar)
+            return new String[]{buf.toString()};
+
+        Set median = new HashSet();
+        Stack stack = new Stack();
+        stack.add(buf.toString());
+        while (!stack.empty()) {
+            String seq = (String) stack.pop();
+            int pos = seq.indexOf('*');
+            int pos2 = seq.indexOf('*', pos + 1);
+            String first = seq.substring(0, pos) + seqA.charAt(pos) + seq.substring(pos + 1);
+            String second = seq.substring(0, pos) + seqB.charAt(pos) + seq.substring(pos + 1);
+            String third = seq.substring(0, pos) + seqC.charAt(pos) + seq.substring(pos + 1);
+            if (pos2 == -1) {
+                median.add(first);
+                median.add(second);
+                median.add(third);
+            } else {
+                stack.add(first);
+                stack.add(second);
+                stack.add(third);
+            }
+        }
+        return (String[]) median.toArray(new String[median.size()]);
+    }
+
+    /**
+     * computes the quasi median for three sequences. When resolving a three-way median, use only states in reference sequences
+     *
+     * @param seqA
+     * @param seqB
+     * @param seqC
+     * @param refA
+     * @param refB
+     * @return quasi median
+     */
+    private static String[] computeQuasiMedian(String seqA, String seqB, String seqC, String refA, String refB) {
+        StringBuilder buf = new StringBuilder();
+        boolean hasStar = false;
+        for (int i = 0; i < seqA.length(); i++) {
+            if (seqA.charAt(i) == seqB.charAt(i) || seqA.charAt(i) == seqC.charAt(i))
+                buf.append(seqA.charAt(i));
+            else if (seqB.charAt(i) == seqC.charAt(i))
+                buf.append(seqB.charAt(i));
+            else {
+                buf.append("*");
+                hasStar = true;
+            }
+        }
+        if (!hasStar)
+            return new String[]{buf.toString()};
+
+        Set median = new HashSet();
+        Stack stack = new Stack();
+        stack.add(buf.toString());
+        while (!stack.empty()) {
+            String seq = (String) stack.pop();
+            int pos = seq.indexOf('*');
+            int pos2 = seq.indexOf('*', pos + 1);
+            String first = seq.substring(0, pos) + refA.charAt(pos) + seq.substring(pos + 1);
+            if (pos2 == -1) {
+                median.add(first);
+            } else {
+                stack.add(first);
+            }
+            if (refB.charAt(pos) != refA.charAt(pos)) {
+                String second = seq.substring(0, pos) + refB.charAt(pos) + seq.substring(pos + 1);
+                if (pos2 == -1) {
+                    median.add(second);
+                } else {
+                    stack.add(second);
+                }
+            }
+        }
+        return (String[]) median.toArray(new String[median.size()]);
+    }
+
+    /**
+     * computes the one-step graph
+     *
+     * @param sequences
+     * @return one-step graph
+     */
+    public static PhyloGraph computeOneStepGraph(Set sequences) {
+        PhyloGraph graph = new PhyloGraph();
+        for (Object sequence : sequences) {
+            String seq = (String) sequence;
+            Node v = graph.newNode();
+            graph.setLabel(v, seq);
+            graph.setInfo(v, seq);
+        }
+
+        for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+            for (Node w = v.getNext(); w != null; w = w.getNext()) {
+                int i = computeOneStep(graph.getLabel(v), graph.getLabel(w));
+                if (i != -1)
+                    graph.newEdge(v, w, "" + i);
+            }
+        }
+        return graph;
+    }
+
+    /**
+     * display the computed graph
+     *
+     * @param graph
+     */
+    private static void showGraph(PhyloGraph graph) {
+        JFrame frame = new JFrame("quasi-median network");
+        frame.setSize(400, 400);
+
+        PhyloGraphView view = new PhyloGraphView(graph, 400, 400);
+        view.setCanvasColor(Color.WHITE);
+        view.setMaintainEdgeLengths(false);
+        for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+            view.setShape(v, NodeView.NONE_NODE);
+        }
+        for (Edge e = graph.getFirstEdge(); e != null; e = e.getNext()) {
+            view.setLabel(e, graph.getLabel(e));
+            view.setLabelVisible(e, true);
+        }
+        frame.addKeyListener(view.getGraphViewListener());
+        view.computeSpringEmbedding(5000, false);
+        frame.getContentPane().setLayout(new BorderLayout());
+        frame.getContentPane().add(view.getScrollPane(), BorderLayout.CENTER);
+        JPanel bottom = new JPanel();
+        bottom.setLayout(new BoxLayout(bottom, BoxLayout.X_AXIS));
+        bottom.add(new JButton(getSaveImage(view)));
+        bottom.add(Box.createHorizontalGlue());
+        bottom.add(new JButton(getClose()));
+        frame.getContentPane().add(bottom, BorderLayout.SOUTH);
+
+        frame.setVisible(true);
+        view.fitGraphToWindow();
+    }
+
+
+    private static AbstractAction getSaveImage(final GraphView viewer) {
+        AbstractAction action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                try {
+                    PDFExportType.writeToFile(new File("/Users/huson/image.pdf"), viewer);
+                } catch (IOException e) {
+                    Basic.caught(e);
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Save image");
+        return action;
+    }
+
+    private static AbstractAction getClose() {
+        AbstractAction action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                System.exit(0);
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Close");
+        return action;
+    }
+
+    /**
+     * runs the median joining algorithm
+     *
+     * @param inputSequences
+     * @param weights
+     * @param epsilon
+     * @return median joining network
+     */
+    public static PhyloGraph computeMedianJoiningNetwork(Set inputSequences, double[] weights, int epsilon) {
+        System.err.println("Computing the median joining network for epsilon=" + epsilon);
+        PhyloGraph graph;
+        Set<String> outputSequences = computeMedianJoiningMainLoop(inputSequences, weights, epsilon);
+        boolean changed;
+        do {
+            graph = new PhyloGraph();
+            EdgeSet feasibleLinks = new EdgeSet(graph);
+            computeMinimumSpanningNetwork(outputSequences, weights, 0, graph, feasibleLinks);
+            List toDelete = new LinkedList();
+            for (Edge e = graph.getFirstEdge(); e != null; e = graph.getNextEdge(e)) {
+                if (!feasibleLinks.contains(e))
+                    toDelete.add(e);
+            }
+            for (Object aToDelete : toDelete) graph.deleteEdge((Edge) aToDelete);
+            changed = removeObsoleteNodes(graph, inputSequences, outputSequences);
+        }
+        while (changed);
+        return graph;
+    }
+
+    /**
+     * Main loop of the median joining algorithm
+     *
+     * @param input
+     * @param epsilon
+     * @return sequences present in the median joining network
+     */
+    private static Set<String> computeMedianJoiningMainLoop(Set<String> input, double[] weights, int epsilon) {
+        Set<String> sequences = new HashSet<>();
+        sequences.addAll(input);
+
+        boolean changed = true;
+        while (changed) {
+            changed = false;
+            System.err.println("Median joining: Begin of main loop: " + sequences.size() + " sequences");
+            PhyloGraph graph = new PhyloGraph();
+            EdgeSet feasibleLinks = new EdgeSet(graph);
+            computeMinimumSpanningNetwork(sequences, weights, epsilon, graph, feasibleLinks);
+            if (removeObsoleteNodes(graph, input, sequences)) {
+                changed = true;   // sequences have been changed, recompute graph
+            } else {
+                // determine min connection cost:
+                double minConnectionCost = Double.MAX_VALUE;
+
+                for (Node u = graph.getFirstNode(); u != null; u = u.getNext()) {
+                    String seqU = (String) u.getInfo();
+                    for (Edge e = u.getFirstAdjacentEdge(); e != null; e = u.getNextAdjacentEdge(e)) {
+                        Node v = e.getOpposite(u);
+                        String seqV = (String) v.getInfo();
+                        for (Edge f = u.getNextAdjacentEdge(e); f != null; f = u.getNextAdjacentEdge(f)) {
+                            Node w = f.getOpposite(u);
+                            String seqW = (String) w.getInfo();
+                            String[] qm = computeQuasiMedian(seqU, seqV, seqW);
+                            for (String aQm : qm) {
+                                if (!sequences.contains(aQm)) {
+                                    double cost = computeConnectionCost(seqU, seqV, seqW, aQm, weights);
+                                    if (cost < minConnectionCost)
+                                        minConnectionCost = cost;
+                                }
+                            }
+                        }
+                    }
+                }
+                for (Edge e = feasibleLinks.getFirstElement(); e != null; e = feasibleLinks.getNextElement(e)) {
+                    Node u = e.getSource();
+                    Node v = e.getTarget();
+                    String seqU = (String) u.getInfo();
+                    String seqV = (String) v.getInfo();
+                    for (Edge f = feasibleLinks.getNextElement(e); f != null; f = feasibleLinks.getNextElement(f)) {
+                        Node w;
+                        if (f.getSource() == u || f.getSource() == v)
+                            w = f.getTarget();
+                        else if (f.getTarget() == u || f.getTarget() == v)
+                            w = f.getSource();
+                        else
+                            continue;
+                        String seqW = (String) w.getInfo();
+                        String[] qm = computeQuasiMedian(seqU, seqV, seqW);
+                        for (String aQm : qm) {
+                            if (!sequences.contains(aQm)) {
+                                double cost = computeConnectionCost(seqU, seqV, seqW, aQm, weights);
+                                if (cost <= minConnectionCost + epsilon) {
+                                    sequences.add(aQm);
+                                    changed = true;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            System.err.println("Median joining: End of main loop: " + sequences.size() + " sequences");
+        }
+        return sequences;
+    }
+
+    /**
+     * computes the minimum spanning network upto a tolerance of epsilon
+     *
+     * @param sequences
+     * @param weights
+     * @param epsilon
+     * @param graph
+     * @param feasibleLinks
+     */
+    private static void computeMinimumSpanningNetwork(Set sequences, double[] weights, int epsilon, PhyloGraph graph, EdgeSet feasibleLinks) {
+        String[] array = (String[]) sequences.toArray(new String[sequences.size()]);
+        // compute a distance matrix between all sequences:
+        double[][] matrix = new double[array.length][array.length];
+
+        // sort pairs of taxa into groups bey ascending edge length
+        SortedMap<Double, List<Pair<Integer, Integer>>> value2pairs = new TreeMap<>();
+        for (int i = 0; i < array.length; i++) {
+            for (int j = i + 1; j < array.length; j++) {
+                matrix[i][j] = computeDistance(array[i], array[j], weights);
+                Double value = matrix[i][j];
+                List<Pair<Integer, Integer>> pairs = value2pairs.get(value);
+                if (pairs == null) {
+                    pairs = new LinkedList<>();
+                    value2pairs.put(value, pairs);
+                }
+                pairs.add(new Pair<>(i, j));
+            }
+        }
+
+        // set up array of nodes and arrays to track components in the minimum spanning network and in the threshold graph
+        Node[] nodes = new Node[array.length];
+        int[] componentsOfMSN = new int[array.length];
+        int[] componentsOfThresholdGraph = new int[array.length];
+
+        for (int i = 0; i < array.length; i++) {
+            nodes[i] = graph.newNode(array[i]);
+            graph.setLabel(nodes[i], array[i]);
+            componentsOfMSN[i] = i;
+            componentsOfThresholdGraph[i] = i;
+        }
+        int numComponentsMSN = array.length;
+
+        double maxValue = Double.MAX_VALUE;
+
+        // consider each set of pairs of taxa for a given edge length, in ascending order of edge lengths
+        for (Double value : value2pairs.keySet()) {
+            List<Pair<Integer, Integer>> ijPairs = value2pairs.get(value);
+            double threshold = value;
+            if (threshold > maxValue)
+                break;
+
+            // update threshold graph components:
+            for (int i = 0; i < array.length; i++) {
+                for (int j = i + 1; j < array.length; j++) {
+                    if (componentsOfThresholdGraph[i] != componentsOfThresholdGraph[j] && matrix[i][j] < threshold - epsilon) {
+                        int oldComponent = componentsOfMSN[j];
+                        for (int k = 0; k < array.length; k++) {
+                            if (componentsOfThresholdGraph[k] == oldComponent)
+                                componentsOfThresholdGraph[k] = componentsOfThresholdGraph[i];
+                        }
+                    }
+                }
+            }
+
+            // determine new edges for minimum spanning network and determine feasible links
+            List<Pair<Integer, Integer>> newPairs = new LinkedList<>();
+            for (Pair<Integer, Integer> ijPair : ijPairs) {
+                int i = ijPair.getFirst();
+                int j = ijPair.getSecond();
+
+                Edge e = graph.newEdge(nodes[i], nodes[j]);
+                if (feasibleLinks != null && componentsOfThresholdGraph[i] != componentsOfThresholdGraph[j]) {
+                    feasibleLinks.add(e);
+                }
+                newPairs.add(new Pair<>(i, j));
+            }
+
+            // update MSN components
+            for (Pair<Integer, Integer> pair : newPairs) {
+                int i = pair.getFirstInt();
+                int j = pair.getSecondInt();
+                if (componentsOfMSN[i] != componentsOfMSN[j]) {
+                    numComponentsMSN--;
+                    int oldComponent = componentsOfMSN[j];
+                    for (int k = 0; k < array.length; k++)
+                        if (componentsOfMSN[k] == oldComponent)
+                            componentsOfMSN[k] = componentsOfMSN[i];
+                }
+            }
+            if (numComponentsMSN == 1 && maxValue == Double.MAX_VALUE)
+                maxValue = threshold + epsilon; // once network is connected, add all edges upto threshold+epsilon
+        }
+    }
+
+    /**
+     * iteratively removes all nodes that are connected to only two other and are not part of the original input
+     *
+     * @param graph
+     * @param input
+     * @param sequences
+     * @return true, if anything was removed
+     */
+    private static boolean removeObsoleteNodes(PhyloGraph graph, Set<String> input, Set<String> sequences) {
+        int removed = 0;
+        boolean changed = true;
+        while (changed) {
+            changed = false;
+            List<Node> toDelete = new LinkedList<>();
+
+            for (Node v = graph.getFirstNode(); v != null; v = v.getNext()) {
+                String seqV = (String) v.getInfo();
+                if (v.getDegree() <= 2 && !input.contains(seqV))
+                    toDelete.add(v);
+            }
+            if (toDelete.size() > 0) {
+                changed = true;
+                removed += toDelete.size();
+                for (Node v : toDelete) {
+                    sequences.remove(v.getInfo());
+                    graph.deleteNode(v);
+                }
+            }
+        }
+        return removed > 0;
+    }
+
+
+    /**
+     * compute the cost of connecting seqM to the other three sequences
+     *
+     * @param seqU
+     * @param seqV
+     * @param seqW
+     * @param seqM
+     * @return cost
+     */
+    private static double computeConnectionCost(String seqU, String seqV, String seqW, String seqM, double[] weights) {
+        return computeDistance(seqU, seqM, weights) + computeDistance(seqV, seqM, weights) + computeDistance(seqW, seqM, weights);
+    }
+
+    /**
+     * compute weighted distance between two sequences
+     *
+     * @param seqA
+     * @param seqB
+     * @return distance
+     */
+    private static double computeDistance(String seqA, String seqB, double[] weights) {
+        double cost = 0;
+        for (int i = 0; i < seqA.length(); i++) {
+            if (seqA.charAt(i) != seqB.charAt(i))
+                if (weights != null)
+                    cost += weights[i];
+                else
+                    cost++;
+        }
+        return cost;
+    }
+}
diff --git a/src/jloda/progs/RandomDNAGenerator.java b/src/jloda/progs/RandomDNAGenerator.java
new file mode 100644
index 0000000..47eae91
--- /dev/null
+++ b/src/jloda/progs/RandomDNAGenerator.java
@@ -0,0 +1,127 @@
+/**
+ * RandomDNAGenerator.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.util.CommandLineOptions;
+import jloda.util.FastA;
+import jloda.util.UsageException;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.util.Random;
+import java.util.StringTokenizer;
+
+/**
+ * generates random DNA
+ * Daniel Huson, 1.2008
+ */
+public class RandomDNAGenerator {
+    /**
+     * generate a random DNA sequence with user-specified repeats
+     *
+     * @param args
+     * @throws UsageException
+     * @throws IOException
+     */
+    public static void main(String[] args) throws UsageException, IOException {
+        CommandLineOptions options = new CommandLineOptions(args);
+        options.setDescription("Generates Random DNA");
+        options.done();
+
+
+        BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
+        System.out.println("Enter length:");
+        int length = Integer.parseInt(r.readLine());
+
+        char[] sequence = makeRandomSequence(length);
+
+
+        String aLine;
+        System.out.println("Enter repeat length and list of start positions (or . to finish): ");
+
+        while ((aLine = r.readLine()) != null) {
+            aLine = aLine.trim();
+            if (aLine.length() == 0 || aLine.startsWith("#"))
+                continue;
+            else if (aLine.equals("."))
+                break;
+
+            StringTokenizer tok = new StringTokenizer(aLine);
+
+            if (tok.hasMoreTokens()) {
+                int repeatLength = Integer.parseInt(tok.nextToken());
+                int first = -1;
+                while (tok.hasMoreTokens()) {
+                    int pos = Integer.parseInt(tok.nextToken());
+                    if (first == -1)
+                        first = pos;
+                    else {
+                        for (int i = 0; i < repeatLength; i++) {
+                            if (pos + i >= sequence.length)
+                                break;
+                            sequence[pos + i] = sequence[first + i];
+                        }
+                    }
+
+                }
+
+                System.out.println("Enter more repeats or . to finish: ");
+            }
+        }
+
+        FastA fastA = new FastA();
+        fastA.add("Genome", new String(sequence));
+        StringWriter w = new StringWriter();
+        fastA.write(w);
+        System.out.println(w.toString());
+    }
+
+    /**
+     * generate a random DNA sequence
+     *
+     * @param length
+     * @return sequence
+     */
+    private static char[] makeRandomSequence(int length) {
+        char[] sequence = new char[length];
+
+        Random r = new Random();
+
+        for (int i = 0; i < length; i++) {
+            switch (r.nextInt(4)) {
+                case 0:
+                    sequence[i] = 'a';
+                    break;
+                case 1:
+                    sequence[i] = 'c';
+                    break;
+                case 2:
+                    sequence[i] = 'g';
+                    break;
+                case 3:
+                    sequence[i] = 't';
+                    break;
+            }
+        }
+        return sequence;
+    }
+}
diff --git a/src/jloda/progs/RandomizeLines.java b/src/jloda/progs/RandomizeLines.java
new file mode 100644
index 0000000..8040c99
--- /dev/null
+++ b/src/jloda/progs/RandomizeLines.java
@@ -0,0 +1,51 @@
+/**
+ * RandomizeLines.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.util.Basic;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * randomize all lines of input, end input with '.'
+ * Daniel Huson, 5.2009
+ */
+public class RandomizeLines {
+    static public void main(String[] args) throws IOException {
+        List lines = new LinkedList();
+
+
+        BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
+        String aLine;
+        while ((aLine = r.readLine()) != null) {
+            if (aLine.startsWith("."))
+                break;
+            lines.add(aLine);
+        }
+        for (Iterator it = Basic.randomize(lines.iterator(), 666); it.hasNext(); ) {
+            System.out.println(it.next());
+        }
+    }
+}
diff --git a/src/jloda/progs/ReadTrimmer.java b/src/jloda/progs/ReadTrimmer.java
new file mode 100644
index 0000000..dbe9f4e
--- /dev/null
+++ b/src/jloda/progs/ReadTrimmer.java
@@ -0,0 +1,62 @@
+/**
+ * ReadTrimmer.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.util.CommandLineOptions;
+import jloda.util.FastA;
+import jloda.util.UsageException;
+
+import java.io.*;
+
+/**
+ * trims reads to a specific size
+ * Daniel Huson, 9.2009
+ */
+public class ReadTrimmer {
+    public static void main(String[] args) throws UsageException, IOException {
+        CommandLineOptions options = new CommandLineOptions(args);
+        options.setDescription("ReadTrimmer - trims reads");
+
+        String infile = options.getMandatoryOption("-i", "Input file", "");
+        String outfile = options.getMandatoryOption("-o", "Output file", "");
+        int start = options.getOption("-s", "Start position in read", 0);
+        int length = options.getOption("-l", "Maximum length of read", 250);
+        options.done();
+
+        Reader r = new FileReader(new File(infile));
+
+        FastA fastA = new FastA();
+        fastA.read(r);
+        r.close();
+
+        for (int i = 0; i < fastA.getSize(); i++) {
+            String seq = fastA.getSequence(i);
+            if (start > 0 && seq.length() > start)
+                seq = seq.substring(start);
+            if (seq.length() > length)
+                seq = seq.substring(0, length);
+            fastA.set(i, fastA.getHeader(i), seq);
+        }
+
+        Writer w = new FileWriter(new File(outfile));
+        fastA.write(w);
+        w.close();
+    }
+}
diff --git a/src/jloda/progs/SharedGenesDistance.java b/src/jloda/progs/SharedGenesDistance.java
new file mode 100644
index 0000000..f99fb07
--- /dev/null
+++ b/src/jloda/progs/SharedGenesDistance.java
@@ -0,0 +1,162 @@
+/**
+ * SharedGenesDistance.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * gene content distance calculation
+ * @version $Id: SharedGenesDistance.java,v 1.4 2009-09-25 13:47:13 huson Exp $
+ * @author Daniel Huson
+ * 9.2003
+ */
+
+package jloda.progs;
+
+import jloda.util.CommandLineOptions;
+import jloda.util.PhylipUtils;
+
+import java.io.File;
+import java.io.FileReader;
+import java.util.BitSet;
+
+/**
+ * computes shared genes distance as
+ * in Snel, Bork and Huynen, Nature 1999
+ * or using ML based distance of Huson and Steel 2003
+ */
+public class SharedGenesDistance {
+    /**
+     * run the program
+     */
+    public static void main(String args[]) throws Exception {
+        CommandLineOptions options = new CommandLineOptions(args);
+        options.setDescription("SharedGenesDistance" +
+                "- compute distances based on shared genes");
+
+        String fileName = options.getMandatoryOption("-i", "Input tree file", "");
+        boolean useMLDistance = options.getOption("-m", "use ML distance of Huson and Steel 2003", true, false);
+        options.done();
+
+        // read sequences in phylip format
+        String[][] data = new String[2][];
+        PhylipUtils.read(data, new FileReader(new File(fileName)));
+        String names[] = data[0];
+        String sequences[] = data[1];
+
+        BitSet genes[] = computeGenes(sequences);
+        int ntax = names.length - 1;
+
+        float[][] dist;
+
+        if (!useMLDistance)
+            dist = computeSnelBorkDistance(ntax, genes);
+        else
+            dist = computeMLDistance(ntax, genes);
+
+        PhylipUtils.print(names, dist, System.out);
+    }
+
+    /**
+     * comnputes the SnelBork et al distance
+     *
+     * @param ntax
+     * @param genes
+     * @return the distance matrix
+     */
+    private static float[][] computeSnelBorkDistance(int ntax, BitSet[] genes) {
+
+        float[][] dist = new float[ntax + 1][ntax + 1];
+        for (int i = 1; i <= ntax; i++) {
+            dist[i][i] = 0;
+            for (int j = i + 1; j <= ntax; j++) {
+                BitSet intersection = ((BitSet) (genes[i]).clone());
+                intersection.and(genes[j]);
+                dist[i][j] = dist[j][i] = (float) (
+                        1.0 - ((float) intersection.cardinality()
+                                / (float) Math.min(genes[i].cardinality(),
+                                genes[j].cardinality())));
+
+            }
+        }
+        return dist;
+    }
+
+    /**
+     * comnputes the maximum likelihood estimator distance Huson and Steel 2003
+     *
+     * @param ntax
+     * @param genes
+     * @return the distance matrix
+     */
+    private static float[][] computeMLDistance(int ntax, BitSet[] genes) {
+
+        // dtermine average genome size:
+        double m = 0;
+        for (int i = 1; i <= ntax; i++) {
+            m += genes[i].cardinality();
+        }
+        m /= ntax;
+
+        double ai[] = new double[ntax + 1];
+        double aij[][] = new double[ntax + 1][ntax + 1];
+        for (int i = 1; i <= ntax; i++) {
+            ai[i] = ((double) genes[i].cardinality()) / m;
+        }
+        for (int i = 1; i <= ntax; i++) {
+            for (int j = i + 1; j <= ntax; j++) {
+                BitSet intersection = ((BitSet) (genes[i]).clone());
+                intersection.and(genes[j]);
+                aij[i][j] = aij[j][i] = ((double) intersection.cardinality()) / m;
+            }
+        }
+
+        float[][] dist = new float[ntax + 1][ntax + 1];
+        for (int i = 1; i <= ntax; i++) {
+            dist[i][i] = 0;
+            for (int j = i + 1; j <= ntax; j++) {
+                double b = 1.0 + aij[i][j] - ai[i] - ai[j];
+
+                dist[i][j] = dist[j][i] =
+                        (float) -Math.log(0.5 * (b + Math.sqrt(b * b + 4.0 * aij[i][j] * aij[i][j])));
+                if (dist[i][j] < 0)
+                    dist[i][j] = dist[j][i] = 0;
+            }
+        }
+        return dist;
+    }
+
+
+    /**
+     * computes gene sets from strings
+     *
+     * @param sequences as strings
+     * @return sets of genes
+     */
+    static private BitSet[] computeGenes(String[] sequences) {
+        BitSet genes[] = new BitSet[sequences.length];
+
+        for (int s = 1; s < sequences.length; s++) {
+            genes[s] = new BitSet();
+            String seq = sequences[s];
+            for (int i = 0; i < seq.length(); i++) {
+                if (seq.charAt(i) == '1')
+                    genes[s].set(i);
+            }
+        }
+        return genes;
+    }
+}
diff --git a/src/jloda/progs/Tree2MeganCSV.java b/src/jloda/progs/Tree2MeganCSV.java
new file mode 100644
index 0000000..e4d8912
--- /dev/null
+++ b/src/jloda/progs/Tree2MeganCSV.java
@@ -0,0 +1,121 @@
+/**
+ * Tree2MeganCSV.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.graph.Node;
+import jloda.graphview.IGraphDrawer;
+import jloda.phylo.PhyloGraphView;
+import jloda.phylo.PhyloTree;
+import jloda.phylo.TreeDrawerRadial;
+import jloda.util.CommandLineOptions;
+import jloda.util.UsageException;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.*;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * processes a tree containing placements of reads and returns a CVS file that can be processed by MEGAN
+ * Daniel Huson, 10.2008
+ */
+public class Tree2MeganCSV {
+
+    public static void main(String[] args) throws UsageException, IOException {
+        CommandLineOptions options = new CommandLineOptions(args);
+        options.setDescription("Tree2MerganCSV - analyzes placement of reads in a tree and output a MEGAN CSV file");
+
+        String inFile = options.getOption("-i", "Input file containing a tree in Newick format", "");
+        String outFile = options.getOption("-o", "Out file (in CSV) format", "");
+        List readIds = options.getOption("-r", "List of read ids", new LinkedList());
+        boolean showTree = options.getOption("-s", "Show first tree", true, false);
+        options.done();
+
+        System.err.println("Read Ids:");
+        for (Object readId2 : readIds) {
+            System.err.println(" " + readId2);
+        }
+
+
+        BufferedReader r;
+        if (inFile.length() == 0) // no input file given, read from standard in
+            r = new BufferedReader(new InputStreamReader(System.in));
+        else
+            r = new BufferedReader(new FileReader(new File(inFile)));
+
+        String aLine;
+        int treeNumber = 0;
+        while ((aLine = r.readLine()) != null) {
+            if (aLine.length() > 0 && !aLine.startsWith("#")) {
+                PhyloTree tree = new PhyloTree();
+
+                tree.parseBracketNotation(aLine, false);
+                treeNumber++;
+
+                // for debugging purposes, show the  first tree:
+                if (treeNumber == 1 && showTree)
+                    showTree(tree);
+
+                // see if we can find any of the reads in the tree:
+                for (Object readId1 : readIds) {
+                    String readId = (String) readId1;
+
+                    boolean found = false;
+                    for (Node v = tree.getFirstNode(); !found && v != null; v = tree.getNextNode(v)) {
+                        if (tree.getLabel(v) != null && tree.getLabel(v).equals(readId)) {
+                            System.err.println("Found readId " + readId + " in tree " + treeNumber + " on node v=" + v);
+                            found = true;
+                        }
+                    }
+                    if (!found)
+                        System.err.println("Warning: readID " + readId + " in tree " + treeNumber + ": not found");
+                }
+
+            }
+        }
+    }
+
+    /**
+     * draws the tree
+     *
+     * @param tree
+     */
+    public static void showTree(PhyloTree tree) {
+        PhyloGraphView treeView = new PhyloGraphView(tree);
+
+        IGraphDrawer drawer = new TreeDrawerRadial(treeView, tree);
+        drawer.computeEmbedding(true);
+
+        JFrame frame = new JFrame("Tree");
+        frame.setSize(treeView.getSize());
+        frame.addKeyListener(treeView.getGraphViewListener());
+
+        frame.getContentPane().setLayout(new BorderLayout());
+        frame.getContentPane().add(treeView.getScrollPane(), BorderLayout.CENTER);
+        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+        // show the frame:
+        frame.setVisible(true);
+
+        treeView.setSize(600, 600);
+        treeView.fitGraphToWindow();
+
+    }
+}
diff --git a/src/jloda/progs/TreeViewDemo.java b/src/jloda/progs/TreeViewDemo.java
new file mode 100644
index 0000000..8947ff0
--- /dev/null
+++ b/src/jloda/progs/TreeViewDemo.java
@@ -0,0 +1,101 @@
+/**
+ * TreeViewDemo.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.progs;
+
+import jloda.graphview.IGraphDrawer;
+import jloda.graphview.ITransformChangeListener;
+import jloda.graphview.Transform;
+import jloda.phylo.PhyloGraphView;
+import jloda.phylo.PhyloTree;
+import jloda.phylo.TreeDrawerCircular;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.io.IOException;
+
+/**
+ * Demo of using tree view
+ * Daniel Huson, 9.2011
+ */
+public class TreeViewDemo {
+
+    public static void main(String[] args) throws IOException {
+
+
+        // setup small tree:
+        PhyloTree tree = new PhyloTree();
+
+        tree.parseBracketNotation("((a,b),(c,d),e);", true);
+
+        // setup graph view:
+
+        final PhyloGraphView treeView = new PhyloGraphView(tree);
+
+        // add labels to nodes:
+
+        // compute simple layout:
+        IGraphDrawer treeDrawer = new TreeDrawerCircular(treeView, tree);
+        treeDrawer.computeEmbedding(true);
+        treeView.setGraphDrawer(treeDrawer);
+
+        // setup jframe with graphView and quit button:
+        JFrame frame = new JFrame("Tree View Demo");
+        treeView.setSize(800, 800);
+        frame.setSize(treeView.getSize());
+        frame.addKeyListener(treeView.getGraphViewListener());
+
+        frame.getContentPane().setLayout(new BorderLayout());
+        frame.getContentPane().add(treeView.getScrollPane(), BorderLayout.CENTER);
+        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+
+        treeView.trans.addChangeListener(new ITransformChangeListener() {
+            public void hasChanged(Transform trans) {
+                final Dimension ps = trans.getPreferredSize();
+                int x = Math.max(ps.width, treeView.getScrollPane().getWidth() - 20);
+                int y = Math.max(ps.height, treeView.getScrollPane().getHeight() - 20);
+                ps.setSize(x, y);
+                treeView.setPreferredSize(ps);
+                treeView.getScrollPane().getViewport().setViewSize(new Dimension(x, y));
+                treeView.repaint();
+            }
+        });
+
+        treeView.getScrollPane().addComponentListener(new ComponentAdapter() {
+            public void componentResized(ComponentEvent event) {
+                {
+                    if (treeView.getScrollPane().getSize().getHeight() > 400 && treeView.getScrollPane().getSize().getWidth() > 400)
+                        treeView.fitGraphToWindow();
+                    else
+                        treeView.trans.fireHasChanged();
+                }
+            }
+        });
+
+        // show the frame:
+        frame.setVisible(true);
+
+        treeView.trans.setCoordinateRect(treeView.getBBox());
+        treeView.getScrollPane().revalidate();
+        treeView.fitGraphToWindow();
+    }
+
+}
diff --git a/src/jloda/progs/seq4.txt b/src/jloda/progs/seq4.txt
new file mode 100644
index 0000000..a9bf202
--- /dev/null
+++ b/src/jloda/progs/seq4.txt
@@ -0,0 +1,5 @@
+4 40
+t01        1111111111111111111000000000000000000000
+t02        1111111111111000000111111111000000000000
+t03        1111100000000000000000000000111111111110
+t04        1110100000000000000000000000111000000001
diff --git a/src/jloda/util/Alert.java b/src/jloda/util/Alert.java
new file mode 100644
index 0000000..d3045d2
--- /dev/null
+++ b/src/jloda/util/Alert.java
@@ -0,0 +1,59 @@
+/**
+ * Alert.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * show an alert window
+ *
+ * @author huson
+ *         Date: 23-Feb-2004
+ */
+public class Alert {
+    /**
+     * create an Alert window with the given message and display it
+     *
+     * @param message
+     */
+    public Alert(String message) {
+        this(null, message);
+    }
+
+    /**
+     * create an Alert window with the given message and display it
+     *
+     * @param parent  parent window
+     * @param message
+     */
+    public Alert(Component parent, final String message) {
+        if (ProgramProperties.isUseGUI()) {
+            String label;
+            if (ProgramProperties.getProgramName() != null)
+                label = "Alert - " + ProgramProperties.getProgramName();
+            else
+                label = "Alert";
+
+            JOptionPane.showMessageDialog(parent, Basic.toMessageString(message), label, JOptionPane.ERROR_MESSAGE);
+        } else
+            System.err.println("Alert - " + message);
+    }
+}
diff --git a/src/jloda/util/ArgsOptions.java b/src/jloda/util/ArgsOptions.java
new file mode 100644
index 0000000..c9d33c0
--- /dev/null
+++ b/src/jloda/util/ArgsOptions.java
@@ -0,0 +1,664 @@
+/**
+ * ArgsOptions.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import jloda.gui.message.MessageWindow;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.*;
+import java.util.List;
+
+/**
+ * command line arguments
+ * Daniel Huson, 11.2013
+ */
+public class ArgsOptions {
+    public final static String OTHER = "Other:";
+
+    private boolean verbose;
+    private final String programName;
+    private final String description;
+    private String version;
+    private String authors;
+    private String license;
+    private final List<String> arguments;
+    private final List<String> usage;
+
+    private final Set<String> shortKeys = new HashSet<>();
+    private final Set<String> longKeys = new HashSet<>();
+
+    private final boolean usingInstall4j;
+
+    private boolean alreadyHasOtherComment = false;
+
+    private boolean doHelp = false;
+
+    private static MessageWindow messageWindow = null;
+
+    /**
+     * constructor
+     *
+     * @param args        command line arguments
+     * @param main        class that contains main method
+     * @param description program description
+     */
+    public ArgsOptions(String[] args, Object main, String description) throws CanceledException {
+        this(args, main, main != null ? Basic.getShortName(main.getClass()) : "Unknown", description);
+    }
+
+    /**
+     * constructor
+     *
+     * @param args        command line arguments
+     * @param main        class that contains main method
+     * @param programName
+     * @param description program description
+     */
+    public ArgsOptions(String[] args, Object main, String programName, String description) throws CanceledException {
+
+        if (args.length > 0 && args[0].equals("--install4j")) {
+            String[] tmp = new String[args.length - 1];
+            System.arraycopy(args, 1, tmp, 0, tmp.length);
+            args = tmp;
+            usingInstall4j = true;
+        } else
+            usingInstall4j = false;
+
+        if (args.length > 0 && args[args.length - 1].equals("--argsGui")) {
+            args = getDialogInput(args, args.length - 1);
+        }
+        arguments = new LinkedList<>();
+        arguments.addAll(Arrays.asList(args));
+
+        this.programName = programName;
+        if (main != null)
+            this.version = Basic.getVersion(main.getClass(), programName);
+        this.description = description;
+
+        usage = new LinkedList<>();
+
+        try {
+            doHelp = getOption("-h", "--help", "Show help", false, false);
+            setVerbose(getOption("-v", "--verbose", "verbose", false) && !doHelp);
+        } catch (UsageException e) {
+        }
+
+        if (verbose)
+            System.err.println(programName + " - " + getDescription() + "\nOptions:");
+    }
+
+    /**
+     * get description
+     *
+     * @return description
+     */
+    public String getDescription() {
+        return description;
+    }
+
+    public String getUsage() {
+        StringBuilder result = new StringBuilder();
+        result.append("SYNOPSIS\n");
+        result.append("\t").append(programName).append(" [options]\n");
+        result.append("DESCRIPTION\n");
+        result.append("\t").append(getDescription()).append("\n");
+
+        result.append("OPTIONS\n");
+
+        for (String line : usage) {
+            if (line.contains("--verbose") || line.contains("--help"))
+                continue;
+            result.append(replaceFirstColon(line)).append("\n");
+        }
+        result.append(replaceFirstColon("\t-v, --verbose: Echo commandline options and be verbose. Default value: false.\n"));
+        result.append(replaceFirstColon("\t-h, --help: Show program usage and quit.\n"));
+        if (authors != null)
+            result.append("AUTHOR(s)\n\t").append(authors).append(".\n");
+
+        if (version != null)
+            result.append("VERSION\n\t").append(version).append(".\n");
+
+        if (license != null)
+            result.append(license).append(".\n");
+
+        return result.toString();
+    }
+
+    public boolean isDoHelp() {
+        return doHelp;
+    }
+
+    private String replaceFirstColon(String line) {
+        StringBuilder buf = new StringBuilder();
+        int pos = 0;
+        while (pos < line.length()) {
+            if (line.charAt(pos) == ':')
+                break;
+            buf.append(line.charAt(pos));
+            pos++;
+        }
+        if (pos == line.length() - 1) // colon is last character, keep
+            buf.append(":");
+        else {      // replace by two or more spaces
+            buf.append("  ");
+            int top = Math.min(35, line.length());
+            for (int i = pos; i < top; i++)
+                buf.append(" ");
+            pos++;
+            while (pos < line.length()) {
+                buf.append(line.charAt(pos));
+                pos++;
+            }
+        }
+        return buf.toString();
+    }
+
+    /**
+     * call this once all arguments have been parsed. Quit on help
+     *
+     * @throws UsageException
+     */
+    public void done() throws UsageException {
+        if (!alreadyHasOtherComment)
+            comment(OTHER);
+
+        if (verbose) {
+            System.err.println("\t--verbose: true");
+        }
+
+        if (!doHelp) {
+            if (version != null)
+                System.err.println("Version   " + version);
+            if (authors != null)
+                System.err.println("Author(s) " + authors);
+            if (license != null)
+                System.err.println(license);
+        }
+
+
+        if (doHelp) {
+            System.err.println(getUsage());
+            if (!hasMessageWindow())
+                System.exit(0);
+            else
+                throw new UsageException("Help");
+        }
+        if (arguments.size() > 0) {
+            String message = "Invalid, unknown or duplicate option:";
+            for (String arg : arguments) {
+                message += " " + arg;
+            }
+            message += "\n";
+            throw new UsageException(message);
+        }
+    }
+
+    public boolean isVerbose() {
+        return verbose;
+    }
+
+    public void setVerbose(boolean reportValues) {
+        this.verbose = reportValues;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public String getAuthors() {
+        return authors;
+    }
+
+    public void setAuthors(String authors) {
+        this.authors = authors;
+    }
+
+    public String getLicense() {
+        return license;
+    }
+
+    public void setLicense(String license) {
+        this.license = license;
+    }
+
+    /**
+     * add a comment to the usage message
+     *
+     * @param comment
+     */
+    public void comment(String comment) {
+        usage.add(" " + comment);
+        if (verbose)
+            System.err.println(comment);
+        if (comment.equals(OTHER))
+            alreadyHasOtherComment = true;
+    }
+
+    public boolean getOption(String shortKey, String longKey, String description, boolean defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, false);
+    }
+
+    public boolean getOptionMandatory(String shortKey, String longKey, String description, boolean defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, true);
+    }
+
+    public byte getOption(String shortKey, String longKey, String description, Byte defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, false).byteValue();
+    }
+
+    public byte getOptionMandatory(String shortKey, String longKey, String description, Byte defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, true).byteValue();
+    }
+
+    public int getOption(String shortKey, String longKey, String description, Integer defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, false).intValue();
+    }
+
+    public int getOption(String shortKey, String longKey, String description, int defaultValue, int low, int high) throws UsageException {
+        int result = getOption(shortKey, longKey, description, defaultValue, false).intValue();
+        if (!doHelp && (result < low || result > high))
+            throw new UsageException("Option " + longKey + ": value=" + result + ": out of range: " + low + " - " + high);
+        return result;
+    }
+
+    public int getOptionMandatory(String shortKey, String longKey, String description, int defaultValue, int low, int high) throws UsageException {
+        int result = getOption(shortKey, longKey, description, defaultValue, true).intValue();
+        if (!doHelp && (result < low || result > high))
+            throw new UsageException("Option " + longKey + ": value=" + result + ": out of range: " + low + " - " + high);
+        return result;
+    }
+    public int getOptionMandatory(String shortKey, String longKey, String description, Integer defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, true).intValue();
+    }
+
+    public long getOption(String shortKey, String longKey, String description, Long defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, false).longValue();
+    }
+
+    public long getOptionMandatory(String shortKey, String longKey, String description, Long defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, true).longValue();
+    }
+
+    public float getOption(String shortKey, String longKey, String description, Float defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, false).floatValue();
+    }
+
+    public float getOption(String shortKey, String longKey, String description, Float defaultValue, float low, float high) throws UsageException {
+        float result = getOption(shortKey, longKey, description, defaultValue, false).floatValue();
+        if (!doHelp && (result < low || result > high))
+            throw new UsageException("Option " + longKey + ": value=" + result + ": out of range: " + low + " - " + high);
+        return result;
+    }
+
+    public float getOptionMandatory(String shortKey, String longKey, String description, Float defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, true).floatValue();
+    }
+
+    public double getOption(String shortKey, String longKey, String description, Double defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, false).doubleValue();
+    }
+
+    public double getOptionMandatory(String shortKey, String longKey, String description, Double defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, true).doubleValue();
+    }
+
+    public String getOption(String shortKey, String longKey, String description, String defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, null, defaultValue, false);
+    }
+
+    public String getOptionMandatory(String shortKey, String longKey, String description, String defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, null, defaultValue, true);
+    }
+
+    public String getOption(String shortKey, String longKey, String description, Object[] legalValues, String defaultValue) throws UsageException {
+        List<String> strings = new LinkedList<>();
+        for (Object v : legalValues)
+            strings.add(v.toString());
+        return getOption(shortKey, longKey, description, strings, defaultValue, false);
+    }
+
+    public String getOptionMandatory(String shortKey, String longKey, String description, Object[] legalValues, String defaultValue) throws UsageException {
+        List<String> strings = new LinkedList<>();
+        for (Object v : legalValues)
+            strings.add(v.toString());
+        return getOption(shortKey, longKey, description, strings, defaultValue, true);
+    }
+
+    public String getOption(String shortKey, String longKey, String description, java.util.Collection<?> legalValues, String defaultValue) throws UsageException {
+        List<String> strings = new LinkedList<>();
+        for (Object v : legalValues)
+            strings.add(v.toString());
+        return getOption(shortKey, longKey, description, strings, defaultValue, false);
+    }
+
+    public String getOptionMandatory(String shortKey, String longKey, String description, Collection<?> legalValues, String defaultValue) throws UsageException {
+        List<String> strings = new LinkedList<>();
+        for (Object v : legalValues)
+            strings.add(v.toString());
+        return getOption(shortKey, longKey, description, strings, defaultValue, true);
+    }
+
+    public List<String> getOption(String shortKey, String longKey, String description, List<String> defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, false);
+    }
+
+    public List<String> getOptionMandatory(String shortKey, String longKey, String description, List<String> defaultValue) throws UsageException {
+        return getOption(shortKey, longKey, description, defaultValue, true);
+    }
+
+    public String[] getOption(String shortKey, String longKey, String description, String[] defaultValue) throws UsageException {
+        List<String> result = getOption(shortKey, longKey, description, Arrays.asList(defaultValue), false);
+        return result.toArray(new String[result.size()]);
+    }
+
+    public String[] getOptionMandatory(String shortKey, String longKey, String description, String[] defaultValue) throws UsageException {
+        List<String> result = getOption(shortKey, longKey, description, Arrays.asList(defaultValue), true);
+        return result.toArray(new String[result.size()]);
+    }
+
+    public Number getOption(String shortKey, String longKey, String description, Number defaultValue, boolean mandatory) throws UsageException {
+        if (!shortKey.startsWith("-"))
+            shortKey = "-" + shortKey;
+        if (!longKey.startsWith("-"))
+            longKey = "--" + longKey;
+
+        if (shortKeys.contains(shortKey))
+            throw new RuntimeException("Internal error: multiple definitions of short key: " + shortKey);
+        else
+            shortKeys.add(shortKey);
+        if (longKeys.contains(longKey))
+            throw new RuntimeException("Internal error: multiple definitions of long key: " + longKey);
+        else
+            longKeys.add(longKey);
+
+        usage.add("\t" + shortKey + ", " + longKey + " [number]: " + description + ". " + (mandatory ? "Mandatory option." : "Default value: " + defaultValue + "."));
+
+        Number result = defaultValue;
+
+        boolean found = false;
+        Iterator<String> it = arguments.iterator();
+        while (it.hasNext()) {
+            String arg = it.next();
+            if (arg.equals(shortKey) || arg.equals(longKey)) {
+                it.remove();
+                if (!it.hasNext()) {
+                    throw new UsageException("Value for option " + longKey + ": not found");
+                }
+                result = getNumber(defaultValue, it.next());
+                it.remove();
+                found = true;
+                break;
+            }
+        }
+        if (!found) {
+            if (mandatory && !doHelp)
+                throw new UsageException("Mandatory option '" + longKey + "' not specified");
+        }
+        if (verbose)
+            System.err.println("\t" + longKey + ": " + result);
+        return result;
+    }
+
+    private boolean getOption(String shortKey, String longKey, String description, boolean defaultValue, boolean mandatory) throws UsageException {
+        boolean hide = false;
+        if (shortKey.startsWith("!")) {
+            hide = true;
+            shortKey = shortKey.substring(1);
+        }
+
+        if (!shortKey.startsWith("-") && !shortKey.startsWith("+"))
+            shortKey = "-" + shortKey;
+        if (!longKey.startsWith("-"))
+            longKey = "--" + longKey;
+
+        if (shortKeys.contains(shortKey))
+            throw new RuntimeException("Internal error: multiple definitions of short key: " + shortKey);
+        else
+            shortKeys.add(shortKey);
+        if (longKeys.contains(longKey))
+            throw new RuntimeException("Internal error: multiple definitions of long key: " + longKey);
+        else
+            longKeys.add(longKey);
+
+        if (!hide)
+            usage.add("\t" + shortKey + ", " + longKey + ": " + description + ". " + (mandatory ? "Mandatory option." : "Default value: " + defaultValue + "."));
+
+        boolean result = false;
+        boolean found = false;
+        Iterator<String> it = arguments.iterator();
+        while (it.hasNext()) {
+            String arg = it.next();
+            if (arg.equals(shortKey) || arg.equals(longKey)) {
+                it.remove();
+                if (!it.hasNext()) {
+                    result = !defaultValue;
+                    found = true;
+                    break;
+                }
+                String value = it.next();
+                if (value.length() > 0 && (value.startsWith("-") || value.startsWith("+"))) {
+                    result = !defaultValue;
+                    found = true;
+                    break;
+                }
+                it.remove();
+                result = Boolean.parseBoolean(value);
+                found = true;
+                break;
+            }
+        }
+        if (!found) {
+            if (mandatory && !doHelp)
+                throw new UsageException("Mandatory option '" + longKey + "' not specified");
+            else
+                result = defaultValue;
+        }
+        if (!hide && verbose)
+            System.err.println("\t" + longKey + ": " + result);
+        return result;
+    }
+
+    public String getOption(String shortKey, String longKey, String description, Collection<String> legalValues, String defaultValue, boolean mandatory) throws UsageException {
+        if (!shortKey.startsWith("-"))
+            shortKey = "-" + shortKey;
+        if (!longKey.startsWith("-"))
+            longKey = "--" + longKey;
+
+        if (shortKeys.contains(shortKey))
+            throw new RuntimeException("Internal error: multiple definitions of short key: " + shortKey);
+        else
+            shortKeys.add(shortKey);
+        if (longKeys.contains(longKey))
+            throw new RuntimeException("Internal error: multiple definitions of long key: " + longKey);
+        else
+            longKeys.add(longKey);
+
+        String defaultValueString = (defaultValue.length() == 0 ? "" : "Default value: " + defaultValue + ".");
+
+        usage.add("\t" + shortKey + ", " + longKey + " [string]: " + description + ". " + (mandatory ? "Mandatory option." : defaultValueString)
+                + (legalValues != null ? " Legal values: " + Basic.toString(legalValues, ", ") : ""));
+
+        String result = defaultValue;
+
+        boolean found = false;
+        Iterator<String> it = arguments.iterator();
+        while (it.hasNext()) {
+            String arg = it.next();
+            if (arg.equals(shortKey) || arg.equals(longKey)) {
+                it.remove();
+                if (!it.hasNext()) {
+                    throw new UsageException("Value for option " + longKey + ": not found");
+                }
+                result = it.next();
+                it.remove();
+                found = true;
+                if (legalValues != null && !legalValues.contains(result))
+                    throw new UsageException("Illegal value for option " + longKey + ": " + result + ", legal values: " + Basic.toString(legalValues, ", "));
+
+                break;
+            }
+        }
+        if (!found) {
+            if (mandatory && !doHelp)
+                throw new UsageException("Mandatory option '" + longKey + "' not specified" + (legalValues != null ? ", legal values: " + Basic.toString(legalValues, ", ") : "."));
+        }
+        if (verbose && result.length() > 0)
+            System.err.println("\t" + longKey + ": " + result);
+        return result;
+    }
+
+    private List<String> getOption(String shortKey, String longKey, String description, List<String> defaultValue, boolean mandatory) throws UsageException {
+        if (!shortKey.startsWith("-"))
+            shortKey = "-" + shortKey;
+        if (!longKey.startsWith("-"))
+            longKey = "--" + longKey;
+
+        if (shortKeys.contains(shortKey))
+            throw new RuntimeException("Internal error: multiple definitions of short key: " + shortKey);
+        else
+            shortKeys.add(shortKey);
+        if (longKeys.contains(longKey))
+            throw new RuntimeException("Internal error: multiple definitions of long key: " + longKey);
+        else
+            longKeys.add(longKey);
+
+        String defaultValueString = (defaultValue.size() == 0 ? "" : "Default value(s): " + Basic.toString(defaultValue, " ") + ".");
+
+        usage.add("\t" + shortKey + ", " + longKey + " [string(s)]: " + description + ". " + (mandatory ? "Mandatory option." : defaultValueString));
+
+        List<String> result = new LinkedList<>();
+        boolean inArguments = false; // once in arguments, will continue until argument starts with -
+
+        Iterator<String> it = arguments.iterator();
+        while (it.hasNext()) {
+            String arg = it.next();
+            if (arg.equals(shortKey) || arg.equals(longKey)) {
+                it.remove();
+                inArguments = true;
+            }
+            if (inArguments) {
+                boolean done = false;
+                while (it.hasNext()) {
+                    String value = it.next();
+                    if (value.length() > 0 && (value.startsWith("-") || value.startsWith("+"))) {
+                        done = true;
+                        break;
+                    }
+                    it.remove();
+                    result.add(value);
+                }
+                if (done)
+                    break;
+            }
+        }
+        if (!inArguments) {
+            if (mandatory && !doHelp)
+                throw new UsageException("Mandatory option '" + longKey + "' not specified");
+            else
+                result = defaultValue;
+        }
+        if (verbose && result.size() > 0)
+            System.err.println("\t" + longKey + ": " + Basic.toString(result, " "));
+        return result;
+    }
+
+    /**
+     * return number from value as object same as defaultValue
+     *
+     * @param defaultValue
+     * @param value
+     * @return appropriate number object
+     */
+    private static Number getNumber(Number defaultValue, String value) {
+        Number result = null;
+        if (defaultValue instanceof Byte) {
+            result = Byte.parseByte(value);
+        } else if (defaultValue instanceof Short) {
+            result = Short.parseShort(value);
+        } else if (defaultValue instanceof Integer) {
+            result = Integer.parseInt(value);
+        } else if (defaultValue instanceof Long) {
+            result = Long.parseLong(value);
+        } else if (defaultValue instanceof Float) {
+            result = Float.parseFloat(value);
+        } else if (defaultValue instanceof Double) {
+            result = Double.parseDouble(value);
+        }
+        return result;
+    }
+
+    /**
+     * present a dialog box and get the commandline input from it
+     *
+     * @return commands
+     */
+    private String[] getDialogInput(String[] args, int argsLength) throws CanceledException {
+        JOptionPane pane = new JOptionPane("Enter command-line options (-h for help)", JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION, ProgramProperties.getProgramIcon(), null, "");
+        pane.setWantsInput(true);
+        pane.setInitialSelectionValue(Basic.toString(args, 0, argsLength, " "));
+
+        JDialog dialog = pane.createDialog(null, "Input " + ProgramProperties.getProgramName());
+        dialog.setResizable(true);
+        dialog.setSize(600, 150);
+        dialog.setVisible(true);
+
+        if ((Integer) pane.getValue() == JOptionPane.CANCEL_OPTION)
+            throw new CanceledException();
+
+        String result = pane.getInputValue().toString();
+
+        messageWindow = new MessageWindow(ProgramProperties.getProgramIcon(), "Messages " + ProgramProperties.getProgramName(), null, false);
+        messageWindow.getFrame().setSize(600, 400);
+        Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
+        messageWindow.getFrame().setLocation(dim.width / 2 - messageWindow.getFrame().getSize().width / 2, dim.height / 2 - messageWindow.getFrame().getSize().height / 2);
+        messageWindow.setVisible(true);
+        messageWindow.getFrame().setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+        System.err.println("Input: " + result);
+
+        //String result=(String)JOptionPane.showInputDialog(null, "Enter command-line options", "Input "+ProgramProperties.getProgramName(),JOptionPane.QUESTION_MESSAGE, ProgramProperties.getProgramIcon(), null, oldInput);
+
+
+        //String result= JOptionPane.showInputDialog(null,"Enter command-line options",oldInput);
+
+        if (result.trim().length() > 0) {
+            result = result.trim().replaceAll("\\s+", " ");
+            return result.split(" ");
+        } else
+            return new String[0];
+    }
+
+    /**
+     * do we have a message window?
+     *
+     * @return true, if message window open and visible
+     */
+    public static boolean hasMessageWindow() {
+        return messageWindow != null && messageWindow.isVisible();
+    }
+
+    public boolean isUsingInstall4j() {
+        return usingInstall4j;
+    }
+}
diff --git a/src/jloda/util/Basic.java b/src/jloda/util/Basic.java
new file mode 100644
index 0000000..7802530
--- /dev/null
+++ b/src/jloda/util/Basic.java
@@ -0,0 +1,3960 @@
+/**
+ * Basic.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+/**
+ * Some basic useful stuff
+ *
+ * @author Daniel Huson, 2005
+ */
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.ImageObserver;
+import java.awt.image.PixelGrabber;
+import java.io.*;
+import java.io.FileFilter;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.nio.channels.FileChannel;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.List;
+import java.util.Queue;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.*;
+
+public class Basic {
+    public final static int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // maximum length that a Java array can have
+
+    static boolean debugMode = true;
+    static final PrintStream origErr = System.err;
+    static final PrintStream origOut = System.out;
+    static final PrintStream nullOut = new PrintStream(new NullOutStream());
+    static private CollectOutStream collectOut;
+
+    /**
+     * Catch an exception.
+     *
+     * @param ex Exception
+     */
+    public static void caught(Throwable ex) {
+        if (debugMode) {
+            System.err.println("Caught:");
+            ex.printStackTrace();
+        } else
+            System.err.println(ex.getMessage());
+    }
+
+    /**
+     * set debug mode. In debug mode, stack traces are printed
+     *
+     * @param mode
+     */
+    static public void setDebugMode(boolean mode) {
+        debugMode = mode;
+    }
+
+    /**
+     * Get debug mode. In debug mode, stack traces are printed
+     *
+     * @return debug mode
+     */
+    static public boolean getDebugMode() {
+        return debugMode;
+    }
+
+    /**
+     * Ignore all output written to System.err
+     *
+     * @return the current PrintStream connected to System.err
+     */
+    public static PrintStream hideSystemErr() {
+        PrintStream current = System.err;
+        System.setErr(nullOut);
+        return current;
+    }
+
+    /**
+     * send the system err messages to System out
+     */
+    public static void sendSystemErrToSystemOut() {
+        System.setErr(origOut);
+    }
+
+    public static void startCollectionStdErr() {
+        collectOut = new CollectOutStream();
+        System.setErr(new PrintStream(collectOut));
+    }
+
+    public static String stopCollectingStdErr() {
+        if (collectOut != null) {
+            String result = collectOut.toString();
+            collectOut = null;
+            return result;
+        } else
+            return "";
+    }
+
+    /**
+     * Restore the System.err to the given PrintStream
+     *
+     * @param ps the print stream
+     */
+    public static void restoreSystemErr(PrintStream ps) {
+        System.setErr(ps);
+    }
+
+    /**
+     * Restore System.err to the standard error stream, even if it was
+     * set to something else in between
+     */
+    public static void restoreSystemErr() {
+        System.setErr(origErr);
+    }
+
+    /**
+     * Ignore all output written to System.out
+     *
+     * @return the current PrintStream connected to System.out
+     */
+    public static PrintStream hideSystemOut() {
+        PrintStream current = System.out;
+        System.setOut(nullOut);
+        return current;
+    }
+
+    /**
+     * Restore the System.out stream to the given PrintStream
+     *
+     * @param ps the new print stream
+     */
+    public static void restoreSystemOut(PrintStream ps) {
+        System.setOut(ps);
+    }
+
+    /**
+     * Restore System.out to the standard output stream, even if it was
+     * set to something else in between
+     */
+    public static void restoreSystemOut() {
+        System.setOut(origOut);
+    }
+
+    /**
+     * returns the decodeable description of a font
+     *
+     * @param font
+     * @return family-style-size
+     */
+    public static String getCode(Font font) {
+        String result = font.getFamily();
+        switch (font.getStyle()) {
+            default:
+            case Font.PLAIN:
+                result += "-PLAIN";
+                break;
+            case Font.ITALIC:
+                result += "-ITALIC";
+                break;
+            case Font.BOLD:
+                result += "-BOLD";
+                break;
+            case Font.BOLD + Font.ITALIC:
+                result += "-BOLDITALIC";
+                break;
+        }
+        result += "-" + font.getSize();
+        return result;
+    }
+
+    /**
+     * skip all spaces starting at position i
+     *
+     * @param str
+     * @param i
+     * @return first position containing a non-space character or str.length()
+     */
+    public static int skipSpaces(String str, int i) {
+        while (i < str.length() && Character.isSpaceChar(str.charAt(i)))
+            i++;
+        return i;
+    }
+
+    /**
+     * Matches prefix of string and return remainder of string.
+     * Prefix need not match string, i.e. only length of prefix is used
+     *
+     * @param string
+     * @param prefix
+     * @return remainder of string after prefix, trimmed
+     * @exeception IOException if given prefix doesn't match prefix of string
+     */
+    public static String matchPrefix(String string, String prefix) throws IOException {
+        if (!string.startsWith(prefix))
+            throw new IOException("Prefix <" + prefix + "> not matched in <" + string + ">");
+        return string.substring(prefix.length(), string.length()).trim();
+    }
+
+    /**
+     * Matches prefix of string and return remainder of string.
+     * Prefix need not match string, i.e. only length of prefix is used
+     *
+     * @param string
+     * @param prefix
+     * @return remainder of string after prefix, trimmed
+     * @exeception IOException if given prefix doesn't match prefix of string
+     */
+    public static String matchPrefix(String string, String prefix, String altPrefix) throws IOException {
+        if (string.startsWith(prefix))
+            return string.substring(prefix.length(), string.length()).trim();
+        else if (string.startsWith(altPrefix))
+            return string.substring(altPrefix.length(), string.length()).trim();
+        else
+            throw new IOException("Prefix <" + prefix + "> or <" + altPrefix + "> not matched in <" + string + ">");
+    }
+
+    /**
+     * returns the size in device coordinates of the string str
+     *
+     * @param str
+     * @return size
+     */
+    public static Dimension getStringSize(Graphics gc, String str, Font font) {
+        if (str == null)
+            return new Dimension(1, 1);
+        Font gcFont = gc.getFont();
+        if (font != null && !font.equals(gcFont))
+            gc.setFont(font);
+        int width = gc.getFontMetrics().stringWidth(str);
+        int height = gc.getFont().getSize();
+        if (!gc.getFont().equals(gcFont))
+            gc.setFont(gcFont);
+        return new Dimension(width, height);
+    }
+
+    /**
+     * replaces all white spaces in the given string str  by the given character c.
+     * Represents consecutive spaces by one c
+     *
+     * @param str
+     * @param c
+     * @return string was spaces replaced
+     */
+    public static String replaceSpaces(String str, char c) {
+        StringBuilder buf = new StringBuilder();
+        boolean prevWasSpace = false;
+        for (int i = 0; i < str.length(); i++) {
+            if (Character.isWhitespace(str.charAt(i))) {
+                if (!prevWasSpace) {
+                    buf.append(c);
+                    prevWasSpace = true;
+                }
+            } else {
+                buf.append(str.charAt(i));
+                prevWasSpace = false;
+            }
+        }
+        return buf.toString();
+    }
+
+    /**
+     * formats a string so that it looks nice in a dialog box
+     *
+     * @param message
+     * @return formated string
+     */
+    public static String toMessageString(String message) {
+        // insert line breaks
+        StringBuilder buf = new StringBuilder();
+        int lineLength = 0;
+        int numLines = 0;
+        for (int i = 0; i < message.length(); i++) {
+            if (lineLength > 50 && Character.isSpaceChar(message.charAt(i))) {
+                buf.append("\n");
+                lineLength = 0;
+                numLines++;
+                if (numLines > 10) {
+                    buf.append("...");
+                    break;
+                }
+            }
+            if (lineLength == 80) {
+                buf.append("\n").append(message.charAt(i));
+                lineLength = 0;
+                numLines++;
+            } else {
+                buf.append(message.charAt(i));
+                if (message.charAt(i) == '\n')
+                    lineLength = 0;
+                else
+                    lineLength++;
+            }
+            if (buf.length() > 2000) {
+                buf.append("...");
+                break;
+            }
+        }
+        return buf.toString();
+    }
+
+    /**
+     * gets the text in quotes, removing any quotes already present
+     *
+     * @param text
+     * @return quoted text
+     */
+    public static String getInCleanQuotes(String text) {
+        return "\"" + text.replaceAll("\"", "") + "\"";
+    }
+
+    /**
+     * given a iterator, returns a new iterator in random order
+     *
+     * @param it
+     * @param seed
+     * @return iterator in random order
+     */
+    public static <T> Iterator<T> randomize(Iterator<T> it, int seed) {
+        return randomize(it, new Random(seed));
+    }
+
+    /**
+     * given a iterator, returns a new iterator in random order
+     *
+     * @param it
+     * @param random
+     * @return iterator in random order
+     */
+    public static <T> Iterator<T> randomize(Iterator<T> it, Random random) {
+        final ArrayList<T> list = new ArrayList<>();
+        while (it.hasNext())
+            list.add(it.next());
+        final Object[] array = randomize(list.toArray(), random);
+        list.clear();
+        return new Iterator<T>() {
+            private int i = 0;
+
+            @Override
+            public boolean hasNext() {
+                return i < array.length;
+            }
+
+            @Override
+            public T next() {
+                return (T) array[i++];
+            }
+        };
+    }
+
+    /**
+     * given an array, returns it randomized (Durstenfeld 1964)
+     *
+     * @param array
+     * @param seed
+     * @return array in random order
+     */
+    public static <T> T[] randomize(T[] array, int seed) {
+        return randomize(array, new Random(seed));
+    }
+
+    /**
+     * given an array, returns it randomized (Durstenfeld 1964)
+     *
+     * @param array
+     * @param random
+     * @return array in random order
+     */
+    public static <T> T[] randomize(T[] array, Random random) {
+        T[] result = (T[]) new Object[array.length];
+        System.arraycopy(array, 0, result, 0, array.length);
+
+        for (int i = result.length - 1; i >= 1; i--) {
+            int j = random.nextInt(i + 1);
+            T tmp = result[i];
+            result[i] = result[j];
+            result[j] = tmp;
+        }
+        return result;
+    }
+
+    /**
+     * randomize array of longs using (Durstenfeld 1964)
+     *
+     * @param array
+     * @param seed
+     */
+    public static void randomize(long[] array, int seed) {
+        Random random = new Random(seed);
+        for (int i = array.length - 1; i >= 1; i--) {
+            int j = random.nextInt(i + 1);
+            long tmp = array[i];
+            array[i] = array[j];
+            array[j] = tmp;
+        }
+    }
+
+    /**
+     * Round to a given number of significant figures
+     *
+     * @param num    double
+     * @param digits number of digits
+     * @return double
+     */
+    public static double roundSigFig(double num, int digits) {
+        if (num == 0) {
+            return 0;
+        }
+
+        final double d = Math.ceil(Math.log10(num < 0 ? -num : num));
+        final int power = digits - (int) d;
+
+        final double magnitude = Math.pow(10, power);
+        final long shifted = Math.round(num * magnitude);
+        return shifted / magnitude;
+    }
+
+
+    /**
+     * returns the sign of x
+     *
+     * @param x
+     * @return sign of x
+     */
+    public static int sign(double x) {
+        if (x > 0)
+            return 1;
+        else if (x < 0)
+            return -1;
+        else
+            return 0;
+    }
+
+    /**
+     * returns a wrapped around string
+     *
+     * @param str
+     * @param lineLength
+     * @return wrapped around string
+     */
+    public static String wraparound(String str, int lineLength) {
+        StringBuilder buf = new StringBuilder();
+
+        for (int p = 0; p < str.length(); p += lineLength) {
+            buf.append(str.substring(p, Math.min(str.length(), p + lineLength))).append("\n");
+        }
+        return buf.toString();
+    }
+
+    /**
+     * returns a collection in a space-separated string
+     *
+     * @param collection
+     * @return space-separated string
+     */
+    public static String collection2string(Collection collection) {
+        return collection2string(collection, " ");
+    }
+
+    /**
+     * returns a collection in a string
+     *
+     * @param collection
+     * @return space-separated string
+     */
+    public static String collection2string(Collection collection, String separator) {
+        StringBuilder buf = new StringBuilder();
+        Iterator it = collection.iterator();
+        boolean first = true;
+        while (it.hasNext()) {
+            if (first)
+                first = false;
+            else
+                buf.append(separator);
+            buf.append(it.next());
+        }
+        return buf.toString();
+    }
+
+    private static final Set<String> usedFileNames = new HashSet<>();
+
+    /**
+     * given a file name, returns a file with a unique file name.
+     * The file returned has not been seen during the run of this program and doesn't
+     * exist in the file system
+     *
+     * @param name
+     * @return file with new and unique name
+     */
+    public static File getFileWithNewUniqueName(String name) {
+        String prefix;
+        String suffix = "";
+        int cpyPos = name.lastIndexOf("_cpy");
+        int lastDot = name.lastIndexOf(".");
+
+        if (cpyPos > 0)
+            prefix = name.substring(0, cpyPos);
+        else if (lastDot > 0)
+            prefix = name.substring(0, lastDot);
+        else
+            prefix = name;
+
+        if (lastDot > cpyPos)
+            suffix = name.substring(lastDot);
+
+        int count = 0;
+        while (true) {
+            String newName;
+            if (count == 0)
+                newName = name;
+            else if (count == 1)
+                newName = prefix + "_cpy" + suffix;
+            else
+                newName = prefix + "_cpy" + count + suffix;
+            File newFile = new File(newName);
+            if (!newFile.exists() && !usedFileNames.contains(newName)) {
+                usedFileNames.add(newName);
+                return newFile;
+            }
+            count++;
+        }
+    }
+
+    /**
+     * selects a line in a text area
+     *
+     * @param ta
+     * @param lineno
+     */
+    public static void selectLine(JTextArea ta, int lineno) {
+        if (ta == null || lineno < 0)
+            return;
+        try {
+            String text = ta.getText();
+            if (lineno > 0) {
+                int start = 0;
+                int end;
+                int count = 1;
+                while (count++ < lineno) {
+                    start = text.indexOf('\n', start) + 1;
+                }
+
+                end = text.indexOf('\n', start);
+                if (end == -1)
+                    end = text.length() - 1;
+
+                if (start > 0 && end >= start) {
+                    ta.select(start, end);
+                }
+            }
+        } catch (Exception ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * sort a list using the given comparator
+     *
+     * @param list
+     * @param comparator
+     */
+    public static <T> void sort(List<T> list, Comparator<T> comparator) {
+        T[] array = (T[]) list.toArray();
+        Arrays.sort(array, comparator);
+        list.clear();
+        list.addAll(Arrays.asList(array));
+    }
+
+    /**
+     * converts int[] to list of Integers
+     *
+     * @param array
+     * @return list of Integers
+     */
+    public static List<Integer> asList(int[] array) {
+        List<Integer> list = new LinkedList<>();
+        for (int value : array) list.add(value);
+        return list;
+    }
+
+    /**
+     * returns the suffix of a file name
+     *
+     * @param fileName
+     * @return file name extension
+     */
+    public static String getSuffix(String fileName) {
+        if (fileName == null)
+            return null;
+        int pos = fileName.lastIndexOf(".");
+        if (pos == -1 || pos == fileName.length() - 1)
+            return null;
+        else {
+            return fileName.substring(pos + 1);
+        }
+    }
+
+    /**
+     * returns the short name of a class
+     *
+     * @param clazz
+     * @return short name
+     */
+    public static String getShortName(Class clazz) {
+        return getSuffix(clazz.getName());
+    }
+
+    /**
+     * converts a string containing spaces into an array of strings.
+     *
+     * @param str
+     * @return array of strings that where originally separated by spaces
+     */
+    public static String[] toArray(String str) {
+        List<String> list = new LinkedList<>();
+
+        for (int j, i = skipSpaces(str, 0); i < str.length(); i = skipSpaces(str, j)) {
+            for (j = i + 1; j < str.length(); j++)
+                if (Character.isSpaceChar(str.charAt(j)))
+                    break; // found next space
+            list.add(str.substring(i, j));
+        }
+        return list.toArray(new String[list.size()]);
+
+    }
+
+
+    /**
+     * converts a string containing newlines into a list of string
+     *
+     * @param str
+     * @return list of strings
+     */
+    public static List<String> toList(String str) {
+        List<String> list = new LinkedList<>();
+
+        int i = 0;
+        while (i < str.length()) {
+            int j = i + 1;
+            while (j < str.length() && str.charAt(j) != '\n')
+                j++;
+            list.add(str.substring(i, j));
+            i = j + 1;
+        }
+        return list;
+    }
+
+    /**
+     * removes all text between any pair of left- and right-delimiters.
+     * No nesting
+     *
+     * @param str
+     * @param leftDelimiter
+     * @param rightDelimiter
+     * @return string with comments removed
+     */
+    public static String removeComments(String str, char leftDelimiter, char rightDelimiter) {
+        StringBuilder buf = new StringBuilder();
+
+        boolean inComment = false;
+        for (int i = 0; i < str.length(); i++) {
+            char ch = str.charAt(i);
+            if (inComment && ch == rightDelimiter) {
+                inComment = false;
+            } else if (ch == leftDelimiter) {
+                inComment = true;
+            } else if (!inComment)
+                buf.append(ch);
+        }
+        return buf.toString();
+    }
+
+    /**
+     * folds the given string so that no line is longer than max length, if possible.
+     * Replaces all spaces by single spaces
+     *
+     * @param str
+     * @param maxLength
+     * @return string folded at spaces
+     */
+    public static String fold(String str, int maxLength) {
+        return fold(str, maxLength, "\n");
+    }
+
+    /**
+     * folds the given string so that no line is longer than max length, if possible.
+     * Replaces all spaces by single spaces
+     *
+     * @param str
+     * @param maxLength
+     * @return string folded at spaces
+     */
+    public static String fold(String str, int maxLength, String lineBreakString) {
+        StringBuilder buf = new StringBuilder();
+        StringTokenizer st = new StringTokenizer(str);
+        int lineLength = 0;
+        boolean first = true;
+        while (st.hasMoreTokens()) {
+            String token = st.nextToken();
+            int pos = token.lastIndexOf(lineBreakString);
+            if (pos != -1) {
+                if (!first) {
+                    buf.append(" ");
+                } else
+                    first = false;
+                buf.append(token.substring(0, pos + lineBreakString.length()));
+                lineLength = 0;
+                token = token.substring(pos + lineBreakString.length());
+            }
+            if (lineLength > 0 && lineLength + token.length() >= maxLength) {
+                buf.append(lineBreakString);
+                lineLength = 0;
+            } else {
+                if (!first) {
+                    buf.append(" ");
+                    lineLength++;
+                } else
+                    first = false;
+            }
+            lineLength += token.length();
+            buf.append(token);
+        }
+        return buf.toString();
+    }
+
+
+    /**
+     * fold to given length
+     *
+     * @param str
+     * @param length
+     * @return folded string
+     */
+    public static String foldHard(String str, int length) {
+        StringBuilder buf = new StringBuilder();
+        int pos = 0;
+        for (int i = 0; i < str.length(); i++) {
+            buf.append(str.charAt(i));
+            if (str.charAt(i) == '\n')
+                pos = 0;
+            else
+                pos++;
+            if (pos == length) {
+                buf.append("\n");
+                pos = 0;
+            }
+        }
+        // if ((str.length() % length) != 0)
+        //     buf.append("\n");
+        return buf.toString();
+    }
+
+    /**
+     * sorts all menu items alphabetically starting at first item
+     *
+     * @param menu
+     * @param firstItem
+     */
+    public static void sortMenuAlphabetically(JMenu menu, int firstItem) {
+        if (menu.getItemCount() - firstItem <= 0)
+            return;
+
+        JMenuItem[] array = new JMenuItem[menu.getItemCount() - firstItem];
+
+        for (int i = firstItem; i < menu.getItemCount(); i++) {
+            if (menu.getItem(i).getText() == null)
+                return; // won't be able to sort these!
+            array[i - firstItem] = menu.getItem(i);
+        }
+        Arrays.sort(array, new Comparator<JMenuItem>() {
+            public int compare(JMenuItem o1, JMenuItem o2) {
+                String name1 = o1.getText();
+                String name2 = o2.getText();
+                return name1.compareTo(name2);
+            }
+        });
+
+        while (menu.getItemCount() > firstItem)
+            menu.remove(menu.getItemCount() - 1);
+
+        for (JMenuItem anArray : array) menu.add(anArray);
+    }
+
+    /**
+     * returns the delta between two binary strings
+     *
+     * @param a
+     * @param b
+     * @return delta
+     */
+    static public String deltaBinarySequences(String a, String b) {
+        StringBuilder buf = new StringBuilder();
+        int diffStart = -1;
+        boolean first = true;
+        for (int i = 0; i < a.length(); i++) {
+            if (a.charAt(i) == b.charAt(i)) {
+                if (diffStart > -1) {
+                    if (first)
+                        first = false;
+                    else
+                        buf.append(",");
+                    if (i - 1 == diffStart)
+                        buf.append(diffStart + 1);
+                    else
+                        buf.append(diffStart + 1).append("-").append(i);
+                    diffStart = -1;
+                }
+            } else // chars differ
+            {
+                if (diffStart == -1)
+                    diffStart = i;
+            }
+        }
+        if (diffStart > -1) {
+            if (!first)
+                buf.append(",");
+            if (diffStart == a.length() - 1)
+                buf.append(diffStart + 1);
+            else
+                buf.append(diffStart + 1).append("-").append(a.length());
+        }
+        if (buf.length() > 0)
+            return buf.toString();
+        else
+            return null;
+    }
+
+    /**
+     * compute the majority sequence of three binary sequences
+     *
+     * @param a
+     * @param b
+     * @param c
+     * @return majority sequence
+     */
+    static public String majorityBinarySequences(String a, String b, String c) {
+        StringBuilder buf = new StringBuilder();
+        for (int i = 0; i < a.length(); i++) {
+            if ((a.charAt(i) == '1' && (b.charAt(i) == '1' || c.charAt(i) == '1'))
+                    || (b.charAt(i) == '1' && c.charAt(i) == '1'))
+                buf.append('1');
+            else
+                buf.append('0');
+        }
+        return buf.toString();
+    }
+
+    /**
+     * gets the min value of an array
+     *
+     * @param array
+     * @return min
+     */
+    public static int min(int[] array) {
+        int m = Integer.MAX_VALUE;
+        for (int x : array) {
+            if (x < m)
+                m = x;
+
+        }
+        return m;
+    }
+
+    /**
+     * gets the max value of an array
+     *
+     * @param array
+     * @return max
+     */
+    public static int max(int[] array) {
+        int m = Integer.MIN_VALUE;
+        for (int x : array) {
+            if (x > m)
+                m = x;
+        }
+        return m;
+    }
+
+    /**
+     * returns an array of integers as a separated string
+     *
+     * @param array
+     * @return string representation
+     */
+    public static String toString(int[] array) {
+        return toString(array, 0, array.length, ", ");
+    }
+
+    /**
+     * returns an array of integers as a string
+     *
+     * @param array
+     * @param separator
+     * @return string representation
+     */
+    public static String toString(int[] array, String separator) {
+        return toString(array, 0, array.length, separator);
+    }
+
+
+    /**
+     * returns an array of integers as astring
+     *
+     * @param array
+     * @return string representation
+     */
+    public static String toString(int[] array, int offset, int length, String separator) {
+        final StringBuilder buf = new StringBuilder();
+
+        boolean first = true;
+        length = Math.min(offset + length, array.length);
+        for (int i = offset; i < length; i++) {
+            int x = array[i];
+            if (first)
+                first = false;
+            else
+                buf.append(separator);
+            buf.append(x);
+        }
+        return buf.toString();
+    }
+
+    /**
+     * returns an array of integers as a separated string
+     *
+     * @param array
+     * @param separator
+     * @return string representation
+     */
+    public static String toString(Object[] array, String separator) {
+        return toString(array, 0, array.length, separator);
+    }
+
+    /**
+     * returns an array of integers as a separated string
+     *
+     * @param array
+     * @param offset where to start reading array
+     * @param length how many entries to read
+     * @param separator
+     * @return string representation
+     */
+    public static String toString(Object[] array, int offset, int length, String separator) {
+        final StringBuilder buf = new StringBuilder();
+
+        boolean first = true;
+        for (int i = 0; i < length; i++) {
+            Object anArray = array[i + offset];
+            if (first)
+                first = false;
+            else
+                buf.append(separator);
+            buf.append(anArray);
+        }
+        return buf.toString();
+    }
+
+    /**
+     * returns an array of integers as a separated string
+     *
+     * @param array
+     * @param separator
+     * @return string representation
+     */
+    public static String toString(long[] array, String separator) {
+        final StringBuilder buf = new StringBuilder();
+
+        boolean first = true;
+        for (long a : array) {
+            if (first)
+                first = false;
+            else
+                buf.append(separator);
+            buf.append(a);
+        }
+        return buf.toString();
+    }
+
+
+    /**
+     * returns an array of double as a separated string
+     *
+     * @param array
+     * @param separator
+     * @return string representation
+     */
+    public static String toString(double[] array, String separator) {
+        final StringBuilder buf = new StringBuilder();
+
+        boolean first = true;
+        for (double a : array) {
+            if (first)
+                first = false;
+            else
+                buf.append(separator);
+            buf.append(a);
+        }
+        return buf.toString();
+    }
+
+    /**
+     * returns an array of double as a separated string
+     *
+     * @param array
+     * @param separator
+     * @return string representation
+     */
+    public static String toString(String format, double[] array, String separator) {
+        final StringBuilder buf = new StringBuilder();
+
+        boolean first = true;
+        for (double a : array) {
+            if (first)
+                first = false;
+            else
+                buf.append(separator);
+            buf.append(String.format(format, a));
+        }
+        return buf.toString();
+    }
+
+    /**
+     * returns a collection of objects a separated string
+     *
+     * @param collection
+     * @param separator
+     * @return string representation
+     */
+    public static String toString(Collection collection, String separator) {
+        if (collection == null)
+            return "";
+        final StringBuilder buf = new StringBuilder();
+
+        boolean first = true;
+        for (Object aCollection : collection) {
+            if (aCollection != null) {
+                if (first)
+                    first = false;
+                else if (separator != null)
+                    buf.append(separator);
+                buf.append(aCollection);
+            }
+        }
+        return buf.toString();
+    }
+
+    /**
+     * concatenates a collection of strings and removes any white spaces
+     *
+     * @param strings
+     * @return concatenated string with no white spaces
+     */
+    public static String concatenateAndRemoveWhiteSpaces(Collection<String> strings) {
+        final StringBuilder buf = new StringBuilder();
+
+        for (String s : strings) {
+            for (int pos = 0; pos < s.length(); pos++) {
+                char ch = s.charAt(pos);
+                if (!Character.isWhitespace(ch))
+                    buf.append(ch);
+            }
+        }
+        return buf.toString();
+    }
+
+
+    /**
+     * returns a set of bits as a comma separated string
+     *
+     * @param bits
+     * @return string representation
+     */
+    public static String toString(BitSet bits) {
+        if (bits == null)
+            return "null";
+
+        final StringBuilder buf = new StringBuilder();
+
+        int startRun = 0;
+        int inRun = 0;
+        boolean first = true;
+        for (int i = bits.nextSetBit(0); i >= 0; i = bits.nextSetBit(i + 1)) {
+            if (first) {
+                first = false;
+                buf.append(i);
+                startRun = inRun = i;
+            } else {
+                if (i == inRun + 1) {
+                    inRun = i;
+                } else if (i > inRun + 1) {
+                    if (inRun == startRun || i == startRun + 1)
+                        buf.append(",").append(i);
+                    else if (inRun == startRun + 1)
+                        buf.append(",").append(inRun).append(",").append(i);
+                    else
+                        buf.append("-").append(inRun).append(",").append(i);
+                    inRun = startRun = i;
+                }
+            }
+        }
+        // dump last:
+        if (inRun == startRun + 1)
+            buf.append(",").append(inRun);
+        else if (inRun > startRun + 1)
+            buf.append("-").append(inRun);
+        return buf.toString();
+    }
+
+    /**
+     * Fetch all resources (i.e. files) that are directly under the specified package structure.
+     *
+     * @param pckg
+     * @return files in given package
+     * @throws IOException
+     */
+    public static String[] fetchResources(String pckg) throws IOException {
+        return fetchResources(pckg, getBasicClassLoader());
+    }
+
+    /**
+     * Get the classloader that can find all resources.
+     * Currently this is the system classloader.
+     *
+     * @return basic class loader
+     */
+    public static ClassLoader getBasicClassLoader() {
+        ClassLoader loaderPlugin = Basic.class.getClassLoader();
+        if (loaderPlugin == null) loaderPlugin = ClassLoader.getSystemClassLoader();
+        return loaderPlugin;
+    }
+
+    /**
+     * Get a class instance for the given fully qualified classname.
+     * The plugin classloader is used as returned by {@link #getBasicClassLoader()}.
+     * <p/>
+     * <p/>
+     * It is discouraged to use {@link Class#forName(java.lang.String)}.
+     *
+     * @param name
+     * @return
+     * @throws ClassNotFoundException
+     */
+    public static Class classForName(String name) throws ClassNotFoundException {
+        return getBasicClassLoader().loadClass(name);
+    }
+
+    /**
+     * get all resources under the given package name
+     * @param packageName
+     * @param loaderPlugin
+     * @return list of resources
+     * @throws IOException
+     */
+    static String[] fetchResources(String packageName, ClassLoader loaderPlugin) throws IOException {
+        packageName = packageName.replaceAll("\\.", "/").concat("/");
+
+        Enumeration e = loaderPlugin.getResources(packageName);
+        Set<String> resources = new TreeSet<>();
+        while (e.hasMoreElements()) {
+            final URL url = ((URL) e.nextElement());
+            String urlString = URLDecoder.decode(url.getPath(), "UTF-8");
+            if (urlString.matches(".+!.+")) //the zip/jar - entry delimiter
+            {
+                String[] split = urlString.split("!", 2);
+                urlString = split[0];
+                if (urlString.startsWith("file:"))
+                    urlString = urlString.substring("file:".length());
+
+                //recurse through the jar
+                try {
+                    ZipFile archive = (urlString.endsWith(".jar") ? new JarFile(urlString) : new ZipFile(urlString));
+                    Enumeration entries = archive.entries();
+                    while (entries.hasMoreElements()) {
+                        ZipEntry ze = (ZipEntry) entries.nextElement();
+                        String name = ze.getName();
+                        if (name.startsWith(packageName)) {
+                            if (!ze.isDirectory() && name.indexOf('/', packageName.length()) < 0) {
+                                resources.add(name.substring(packageName.length()));
+                            } else        // subpackages
+                            {
+                                name = name.replaceAll("/", ".");
+                                if (name.endsWith("."))
+                                    name = name.substring(0, name.length() - 1);
+                                resources.add(name);
+                            }
+                        }
+                    }
+                } catch (IOException ex) {
+                    System.err.println("URL=" + urlString);
+                    Basic.caught(ex);
+                }
+            } else //we are still in the file system
+            {
+                final File file = new File(urlString);
+                File[] contents = null;
+                if (file.isDirectory())
+                    contents = file.listFiles();
+
+                if (contents != null)
+                    for (int i = 0; i != contents.length; ++i) {
+                        if (contents[i].isDirectory()) {
+                            String subPackageName = packageName + contents[i].getName();
+                            subPackageName = subPackageName.replaceAll("/", ".");
+                            resources.add(subPackageName);
+                        } else {
+                            resources.add(contents[i].getName());
+                        }
+                    }
+            }
+        }
+        return resources.toArray(new String[resources.size()]);
+    }
+
+    /**
+     * centers a dialog in a parent frame
+     *
+     * @param dialog
+     * @param parent
+     */
+    public static void centerDialogInParent(JDialog dialog, JFrame parent) {
+        if (parent != null)   // center
+            dialog.setLocation(new Point(parent.getLocation().x + (parent.getWidth() - dialog.getWidth()) / 2,
+                    parent.getLocation().y + (parent.getHeight() - dialog.getHeight()) / 2));
+        else
+            dialog.setLocation(new Point(300, 300));
+    }
+
+    /**
+     * centers a dialog on the screen
+     *
+     * @param dialog
+     */
+    static public void centerDialogOnScreen(JDialog dialog) {
+        Dimension dim = dialog.getToolkit().getScreenSize();
+        Rectangle abounds = dialog.getBounds();
+        dialog.setLocation((dim.width - abounds.width) / 2,
+                (dim.height - abounds.height) / 2);
+    }
+
+    /**
+     * returns true, if string can be parsed as int
+     *
+     * @param next
+     * @return true, if int
+     */
+    public static boolean isInteger(String next) {
+        try {
+            Integer.parseInt(next);
+        } catch (Exception ex) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * returns true, if string can be parsed as long
+     *
+     * @param next
+     * @return true, if int
+     */
+    public static boolean isLong(String next) {
+        try {
+            Long.parseLong(next);
+        } catch (Exception ex) {
+            return false;
+        }
+        return true;
+    }
+
+
+    /**
+     * returns true, if string can be parsed as float
+     *
+     * @param next
+     * @return true, if int
+     */
+    public static boolean isFloat(String next) {
+        try {
+            Float.parseFloat(next);
+        } catch (Exception ex) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * returns true, if string can be parsed as double
+     *
+     * @param next
+     * @return true, if int
+     */
+    public static boolean isDouble(String next) {
+        try {
+            Double.parseDouble(next);
+        } catch (Exception ex) {
+            return false;
+        }
+        return true;
+    }
+
+
+    /**
+     * double backslashes
+     *
+     * @param str
+     * @return string with doubled back slashes
+     */
+    public static String doubleBackSlashes(String str) {
+        StringBuilder buf = new StringBuilder();
+        for (int i = 0; i < str.length(); i++) {
+            if (str.charAt(i) == '\\')
+                buf.append('\\');
+            buf.append(str.charAt(i));
+        }
+        return buf.toString();
+    }
+
+    /**
+     * returns name with .suffix removed
+     *
+     * @param name
+     * @return name without .suffix
+     */
+    public static String getFileBaseName(String name) {
+            if (name != null) {
+                int pos = name.lastIndexOf(".");
+                if (pos > 0)
+                    name = name.substring(0, pos);
+            }
+        return name;
+    }
+
+    /**
+     * returns the suffix of a file name. Returns null name is null
+     *
+     * @param name
+     * @return suffix   or null
+     */
+    public static String getFileSuffix(String name) {
+        if (name == null)
+            return null;
+        name = getFileNameWithoutPath(name);
+        int index = name.lastIndexOf('.');
+        if (index > 0)
+            return name.substring(index);
+        else
+            return "";
+    }
+
+    /**
+     * returns name with path removed
+     *
+     * @param name
+     * @return name without path
+     */
+    public static String getFileNameWithoutPath(String name) {
+            if (name != null) {
+                int pos = name.lastIndexOf(File.separatorChar);
+                if (pos != -1 && pos < name.length() - 1) {
+                    name = name.substring(pos + 1);
+                }
+            }
+        return name;
+    }
+
+    /**
+     * remove all characters except for letters and digits
+     *
+     * @param str
+     * @return string of letters and digits
+     */
+    public static String removeAllButLettersDigits(String str) {
+        StringBuilder buf = new StringBuilder();
+        for (int i = 0; i < str.length(); i++) {
+            char ch = str.charAt(i);
+            if (Character.isLetterOrDigit(ch))
+                buf.append(ch);
+        }
+        return buf.toString();
+    }
+
+    /**
+     * converts a list of objects to a string
+     *
+     * @param result
+     * @param separator
+     * @return string
+     */
+    public static <T> String listAsString(List<T> result, String separator) {
+        final StringBuilder buf = new StringBuilder();
+        boolean first = true;
+        for (T aResult : result) {
+            if (first)
+                first = false;
+            else
+                buf.append(separator);
+            buf.append(aResult.toString());
+        }
+        return buf.toString();
+    }
+
+    /**
+     * get list will objects in reverse order
+     *
+     * @param list
+     * @return reverse order list
+     */
+    public static <T> List<T> reverseList(Collection<T> list) {
+        final List<T> result = new LinkedList<>();
+        for (T aList : list) {
+            result.add(0, aList);
+        }
+        return result;
+    }
+
+    /**
+     * get list will objects in rotated order
+     *
+     * @param list
+     * @return rotated order
+     */
+    public static <T> List<T> rotateList(Collection<T> list) {
+        final List<T> result = new LinkedList<>();
+        if (list.size() > 0) {
+            result.addAll(list);
+            result.add(result.remove(0));
+        }
+        return result;
+    }
+
+    /**
+     * reduce the number of elements in a list to n
+     *
+     * @param list
+     * @param n
+     * @return sublist with n elements
+     */
+    public static <T> List reduceList(List<T> list, int n) {
+        List<T> result = new LinkedList<>();
+        int mod = list.size() / n;
+        int i = 0;
+        int count = 0;
+        for (T t : list) {
+            if (++i == mod) {
+                result.add(t);
+                i = 0;
+                if (++count == n)
+                    break;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * gets color as 'r g b' or 'r g b a' string  or string "null"
+     *
+     * @param color
+     * @return r g b a
+     */
+    public static String toString3Int(Color color) {
+        if (color == null)
+            return "null";
+        final StringBuilder buf = new StringBuilder().append(color.getRed()).append(" ").append(color.getGreen()).append(" ").append(color.getBlue());
+        if (color.getAlpha() < 255)
+            buf.append(" ").append(color.getAlpha());
+        return buf.toString();
+    }
+
+    /**
+     * trims away empty lines at the beginning and end of a string
+     *
+     * @param str
+     * @return string without leading and trailing empty lines
+     */
+    public static String trimEmptyLines(String str) {
+        int startOfLine = 0;
+        for (int p = 0; p < str.length(); p++) {
+            if (!Character.isSpaceChar(str.charAt(p)))
+                break;
+            else if (str.charAt(p) == '\n' || str.charAt(p) == '\r')
+                startOfLine = p + 1;
+        }
+
+        int endOfLine = str.length();
+        for (int p = str.length() - 1; p >= 0; p--) {
+            if (!Character.isSpaceChar(str.charAt(p)))
+                break;
+            else if (str.charAt(p) == '\n' || str.charAt(p) == '\r')
+                endOfLine = p;
+        }
+
+        if (startOfLine < endOfLine && endOfLine <= str.length()) {
+            return str.substring(startOfLine, endOfLine);
+        } else
+            return str;
+    }
+
+    /**
+     * counts the number of occurrences of c in string str
+     *
+     * @param str
+     * @param c
+     * @return count
+     */
+    public static int countOccurrences(String str, char c) {
+        int count = 0;
+        if (str != null) {
+            for (int i = 0; i < str.length(); i++)
+                if (str.charAt(i) == c)
+                    count++;
+        }
+        return count;
+    }
+
+    /**
+     * counts the number of occurrences of c at beginning of string str
+     *
+     * @param str
+     * @param c
+     * @return count
+     */
+    public static int countLeadingOccurrences(String str, char c) {
+        int count = 0;
+        if (str != null) {
+            for (int i = 0; i < str.length(); i++) {
+                if (str.charAt(i) == c)
+                    count++;
+                else break;
+            }
+        }
+        return count;
+    }
+
+    /**
+     * counts the number of occurrences of c in byte[] str
+     *
+     * @param str
+     * @param c
+     * @return count
+     */
+    public static int countOccurrences(byte[] str, char c) {
+        int count = 0;
+        if (str != null) {
+            for (byte aStr : str)
+                if (aStr == c)
+                    count++;
+        }
+        return count;
+    }
+
+
+    /**
+     * converts an image to a buffered image
+     *
+     * @param image
+     * @param imageObserver
+     * @return buffered image
+     */
+    public static BufferedImage convertToBufferedImage(Image image, ImageObserver imageObserver) throws IOException, InterruptedException {
+        int imageWidth = image.getWidth(imageObserver);
+        int imageHeight = image.getHeight(imageObserver);
+        int[] array = convertToArray(image, imageWidth, imageHeight);
+        BufferedImage bufferedImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
+        bufferedImage.setRGB(0, 0, imageWidth, imageHeight, array, 0, imageWidth);
+        return bufferedImage;
+    }
+
+
+    /**
+     * converts the image to a 1-D image
+     *
+     * @param image
+     * @return 1-d image
+     */
+    private static int[] convertToArray(Image image, int imageWidth, int imageHeight) throws InterruptedException, IOException {
+        int[] array = new int[imageWidth * imageHeight];
+        PixelGrabber pixelGrabber = new PixelGrabber(image, 0, 0, imageWidth, imageHeight, array, 0, imageWidth);
+        if (pixelGrabber.grabPixels() && ((pixelGrabber.getStatus() & ImageObserver.ALLBITS) != 0))
+            return array;
+        else
+            throw new IOException("Internal error: failed to convert image to 1D array");
+    }
+
+    /**
+     * cleans a taxon name so that it only contains of letters, digits, .'s and _'s
+     *
+     * @param name
+     * @return clean taxon name
+     */
+    public static String toCleanName(String name) {
+        if (name == null)
+            return "";
+        StringBuilder buf = new StringBuilder();
+        for (int i = 0; i < name.length(); i++) {
+            char ch = name.charAt(i);
+            if (Character.isLetterOrDigit(ch) || ch == '.' || ch == '_' || ch == '-')
+                buf.append(ch);
+            else
+                buf.append("_");
+        }
+        String str = buf.toString();
+        while (str.length() > 1 && str.startsWith("_"))
+            str = str.substring(1);
+        while (str.length() > 1 && str.endsWith("_"))
+            str = str.substring(0, str.length() - 1);
+
+        return str;
+    }
+
+    /**
+     * get the lines of a files as a list of strings
+     *
+     * @param file
+     * @return list of strings
+     * @throws IOException
+     */
+    public static List<String> getLinesFromFile(String file) throws IOException {
+        List<String> result = new LinkedList<>();
+        BufferedReader r = new BufferedReader(new FileReader(file));
+        String aLine;
+        while ((aLine = r.readLine()) != null) {
+            result.add(aLine);
+        }
+        return result;
+    }
+
+    /**
+     * gets all individual non-empty lines from a string
+     *
+     * @param string
+     * @return lines
+     */
+    public static List<String> getLinesFromString(String string) {
+        List<String> result = new LinkedList<>();
+        BufferedReader r = new BufferedReader(new StringReader(string));
+        String aLine;
+        try {
+            while ((aLine = r.readLine()) != null) {
+                aLine = aLine.trim();
+                if (aLine.length() > 0)
+                    result.add(aLine);
+            }
+        } catch (IOException e) {
+        }
+        return result;
+    }
+
+    /**
+     * remove any strings that are empty or start with #, after trimming. Keep all non-string objects
+     *
+     * @param list
+     * @return cleaned listed of strings
+     */
+    public static <T> List<T> cleanListOfStrings(Collection<T> list) {
+        List<T> result = new LinkedList<>();
+        for (T obj : list) {
+            if (obj instanceof String) {
+                String str = ((String) obj).trim();
+                if (str.length() > 0 && !str.startsWith("#"))
+                    result.add((T) str);
+            } else
+                result.add(obj);
+        }
+        return result;
+    }
+
+    /**
+     * concatenates two collections and a returns a list
+     *
+     * @param listA
+     * @param listB
+     * @return concatenated list
+     */
+    public static <T> List<T> getConcatenation(Collection<T> listA, Collection<T> listB) {
+        List<T> all = new LinkedList<>();
+        all.addAll(listA);
+        all.addAll(listB);
+        return all;
+    }
+
+    /**
+     * insert spaces before uppercases that follow lower case letters
+     *
+     * @param x
+     * @return
+     */
+    public static String insertSpacesBetweenLowerCaseAndUpperCaseLetters(String x) {
+        StringBuilder buf = new StringBuilder();
+
+        for (int i = 0; i < x.length(); i++) {
+            if (i > 0 && Character.isLowerCase(x.charAt(i - 1)) && Character.isUpperCase(x.charAt(i)))
+                buf.append(' ');
+            buf.append(x.charAt(i));
+        }
+        return buf.toString();
+    }
+
+    /**
+     * is file an image file?
+     *
+     * @param file
+     * @return true, if image file
+     */
+    public static boolean isImageFile(File file) {
+        if (file.isDirectory())
+            return false;
+        final String name = file.getName().toLowerCase();
+        return name.endsWith(".gif") || name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".bmp") || name.endsWith(".png");
+    }
+
+    /**
+     * get bits as list of integers
+     *
+     * @param bits
+     * @return list of integers
+     */
+    public static List<Integer> asList(BitSet bits) {
+        List<Integer> result = new LinkedList<>();
+        if (bits != null) {
+            for (int i = bits.nextSetBit(0); i != -1; i = bits.nextSetBit(i + 1))
+                result.add(i);
+        }
+        return result;
+    }
+
+    /**
+     * get list of integers as bit set
+     *
+     * @param integers
+     * @return bits
+     */
+    public static BitSet asBitSet(List<Integer> integers) {
+        BitSet bits = new BitSet();
+        if (integers != null) {
+            for (Integer i : integers) {
+                bits.set(i);
+            }
+        }
+        return bits;
+    }
+
+    static boolean memoryWarned = false;
+
+    /**
+     * gets the memory usage string in MB
+     *
+     * @return current memory usage
+     */
+    public static String getMemoryUsageString() {
+        long used = ((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1048576);
+        long available = (Runtime.getRuntime().maxMemory() / 1048576);
+        if (available < 1024) {
+            return String.format("%d of %dM", used, available);
+        } else {
+            return String.format("%.1f of %.1fG", (double) used / 1024.0, (double) available / 1024.0);
+        }
+    }
+
+    /**
+     * gets the memory usage string in MB
+     *
+     * @param warnLevel warn when less than this amount of memory available
+     * @return current memory usage
+     */
+    public static String getMemoryUsageString(int warnLevel) {
+        long used = ((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1048576);
+        long available = (Runtime.getRuntime().maxMemory() / 1048576);
+        if (!memoryWarned && warnLevel > 0 && used + warnLevel >= available) {
+            String program = ProgramProperties.getProgramName();
+            System.gc();
+            new Alert(program + " may require more memory to open this file. Possible fix: cancel the current task, assign more memory to " + program + " and restart");
+            memoryWarned = true;
+        }
+        return used + " of " + available + "M";
+    }
+
+    /**
+     * does label match pattern?
+     *
+     * @param pattern
+     * @param label
+     * @return true, if match
+     */
+    public static boolean matches(Pattern pattern, String label) {
+        if (label == null)
+            label = "";
+        Matcher matcher = pattern.matcher(label);
+        return matcher.find();
+    }
+
+    /**
+     * convert bytes to a string
+     *
+     * @return string
+     */
+    static public String toString(byte[] bytes) {
+        if (bytes == null)
+            return "";
+        char[] array = new char[bytes.length];
+        for (int i = 0; i < bytes.length; i++)
+            array[i] = (char) bytes[i];
+        return new String(array);
+    }
+
+    /**
+     * convert bytes to a string
+     * @param length number of bytes, starting at index 0
+     * @return string
+     */
+    static public String toString(byte[] bytes, int length) {
+        if (bytes == null)
+            return "";
+        char[] array = new char[length];
+        for (int i = 0; i < length; i++)
+            array[i] = (char) bytes[i];
+        return new String(array);
+    }
+
+    /**
+     * convert bytes to a string
+     * @param offset start here
+     * @param length number of bytes
+     * @return string
+     */
+    static public String toString(byte[] bytes, int offset, int length) {
+        if (bytes == null)
+            return "";
+        char[] array = new char[length];
+        for (int i = 0; i < length; i++)
+            array[i] = (char) bytes[i + offset];
+        return new String(array);
+    }
+
+    /**
+     * convert boolean to a string
+     *
+     * @return string
+     */
+    static public String toString(boolean[] bools) {
+        StringBuilder buf = new StringBuilder();
+        if (bools != null) {
+            for (boolean a : bools) {
+                buf.append(a ? "1" : "0");
+            }
+        }
+        return buf.toString();
+    }
+
+    /**
+     * convert chars to a string
+     *
+     * @return string
+     */
+    static public String toString(char[] chars) {
+        return new String(chars);
+    }
+
+    /**
+     * make a version info file
+     *
+     * @param fileName
+     * @throws IOException
+     */
+    public static void saveVersionInfo(String fileName) throws IOException {
+        fileName = Basic.replaceFileSuffix(fileName, ".info");
+        Writer w = new FileWriter(fileName);
+        w.write("Created on " + (new Date()) + "\n");
+        w.close();
+    }
+
+    /**
+     * gets the index of a string s in an array of strings
+     *
+     * @param s
+     * @param array
+     * @return index or -1
+     */
+    public static int getIndex(String s, String[] array) {
+        for (int i = 0; i < array.length; i++)
+            if (s.equals(array[i]))
+                return i;
+        return -1;
+    }
+
+    /**
+     * gets the index of a string s in a collection of strings
+     *
+     * @param s
+     * @param collection
+     * @return index or -1
+     */
+    public static int getIndex(String s, Collection<String> collection) {
+        int count = 0;
+        for (String a : collection) {
+            if (a.equals(s))
+                return count;
+            count++;
+        }
+        return -1;
+    }
+
+    /**
+     * gets the number of non-white space characters in a string
+     *
+     * @param string
+     * @return non-space chars
+     */
+    static public int getNumberOfNonSpaceCharacters(String string) {
+        int count = 0;
+        for (int i = 0; i < string.length(); i++) {
+            if (!Character.isWhitespace(string.charAt(i)))
+                count++;
+        }
+        return count;
+    }
+
+    /**
+     * attempts to parse the string as an integer, skipping leading chars and trailing characters, if necessary.
+     * Returns 0, if no number found
+     *
+     * @param string
+     * @return value or 0
+     */
+    public static int parseInt(String string) {
+        try {
+            if (string != null) {
+                int start = 0;
+                while (start < string.length()) {
+                    int ch = string.charAt(start);
+                    if (Character.isDigit(ch) || ch == '-')
+                        break;
+                    start++;
+                }
+                if (start < string.length()) {
+                    int finish = start + 1;
+                    while (finish < string.length() && Character.isDigit(string.charAt(finish)))
+                        finish++;
+                    if (start < finish)
+                        return Integer.parseInt(string.substring(start, finish));
+                }
+            }
+        } catch (Exception ex) {
+        }
+        return 0;
+    }
+
+    /**
+     * attempts to parse the string as a long, skipping leading chars and trailing characters, if necessary
+     *
+     * @param string
+     * @return value or 0
+     */
+    public static long parseLong(String string) {
+        try {
+            if (string != null) {
+                int start = 0;
+                while (start < string.length()) {
+                    int ch = string.charAt(start);
+                    if (Character.isDigit(ch) || ch == '+' || ch == '-')
+                        break;
+                    start++;
+                }
+                if (start < string.length()) {
+                    int finish = start + 1;
+                    while (finish < string.length() && Character.isDigit(string.charAt(finish)))
+                        finish++;
+                    if (start < finish)
+                        return Long.parseLong(string.substring(start, finish));
+                }
+            }
+        } catch (Exception ex) {
+        }
+        return 0;
+    }
+
+    /**
+     * attempts to parse the string as an float, skipping leading chars and trailing characters, if necessary
+     *
+     * @param string
+     * @return value or 0
+     */
+    public static float parseFloat(String string) {
+        try {
+            if (string != null) {
+                int start = 0;
+                while (start < string.length()) {
+                    int ch = string.charAt(start);
+                    if (Character.isDigit(ch) || ch == '+' || ch == '-')
+                        break;
+                    start++;
+                }
+                if (start < string.length()) {
+                    int finish = start + 1;
+                    while (finish < string.length() && (Character.isDigit(string.charAt(finish)) || string.charAt(finish) == '.'
+                            || string.charAt(finish) == 'E' || string.charAt(finish) == 'e' || string.charAt(finish) == '-'))
+                        finish++;
+                    if (start < finish)
+                        return Float.parseFloat(string.substring(start, finish));
+                }
+            }
+        } catch (Exception ex) {
+        }
+        return 0;
+    }
+
+    /**
+     * attempts to parse the string as a double, skipping leading chars and trailing characters, if necessary
+     *
+     * @param string
+     * @return value or 0
+     */
+    public static double parseDouble(String string) {
+        try {
+            if (string != null) {
+                int start = 0;
+                while (start < string.length()) {
+                    int ch = string.charAt(start);
+                    if (Character.isDigit(ch) || ch == '+' || ch == '-')
+                        break;
+                    start++;
+                }
+                if (start < string.length()) {
+                    int finish = start + 1;
+                    while (finish < string.length() && (Character.isDigit(string.charAt(finish)) || string.charAt(finish) == '.'
+                            || string.charAt(finish) == 'E' || string.charAt(finish) == 'e' || string.charAt(finish) == '-' || string.charAt(finish) == '+'))
+                        finish++;
+                    if (start < finish)
+                        return Double.parseDouble(string.substring(start, finish));
+                }
+            }
+        } catch (Exception ex) {
+        }
+        return 0;
+    }
+
+    static public boolean deleteDirectory(File path) {
+        if (path.exists()) {
+            if (path.listFiles() != null) {
+                for (File file : path.listFiles()) {
+                    if (file.isDirectory()) {
+                        if (!deleteDirectory(file))
+                            return false;
+                    } else {
+                        if (!file.delete())
+                            return false;
+                    }
+                }
+            }
+        }
+        return path.delete();
+    }
+
+    /**
+     * gets the length of the longest common prefix of the two strings
+     *
+     * @param a
+     * @param b
+     * @return length of lcp
+     */
+    static public int getLongestCommonPrefixLength(String a, String b) {
+        int top = Math.min(a.length(), b.length());
+        for (int i = 0; i < top; i++)
+            if (a.charAt(i) != b.charAt(i))
+                return i;
+        return top;
+    }
+
+    /**
+     * gets the total count
+     *
+     * @param counts
+     * @return total
+     */
+    public static int getTotal(int[] counts) {
+        int total = 0;
+        for (int count : counts) {
+            total += count;
+        }
+        return total;
+    }
+
+    /**
+     * gets the total count
+     *
+     * @param counts
+     * @return total
+     */
+    public static long getTotal(long[] counts) {
+        long total = 0;
+        for (long count : counts) {
+            total += count;
+        }
+        return total;
+    }
+
+    /**
+     * replace the suffix of a file
+     *
+     * @param fileName
+     * @param newSuffix
+     * @return new file name
+     */
+    public static String replaceFileSuffix(String fileName, String newSuffix) {
+        return replaceFileSuffix(new File(fileName), newSuffix).getPath();
+    }
+
+    /**
+     * replace the suffix of a file
+     *
+     * @param file
+     * @param newSuffix
+     * @return new file
+     */
+    public static File replaceFileSuffix(File file, String newSuffix) {
+        String name = Basic.getFileBaseName(file.getName());
+        if (newSuffix != null && !name.endsWith(newSuffix))
+            name = name + newSuffix;
+        return new File(file.getParent(), name);
+    }
+
+    public static String getFileNameWithoutZipOrGZipSuffix(String fileName) {
+        if (Basic.isZIPorGZIPFile(fileName))
+            return replaceFileSuffix(fileName, "");
+        else
+            return fileName;
+    }
+
+    /**
+     * get reverse complement
+     *
+     * @param sequence
+     * @return reverse complement
+     */
+    public static String getReverseComplement(String sequence) {
+        StringBuilder buf = new StringBuilder();
+        for (int i = sequence.length() - 1; i >= 0; i--)
+            switch (sequence.charAt(i)) {
+                case 'A':
+                    buf.append('T');
+                    break;
+                case 'C':
+                    buf.append('G');
+                    break;
+                case 'G':
+                    buf.append('C');
+                    break;
+                case 'T':
+                    buf.append('A');
+                    break;
+                case 'a':
+                    buf.append('t');
+                    break;
+                case 'c':
+                    buf.append('g');
+                    break;
+                case 'g':
+                    buf.append('c');
+                    break;
+                case 't':
+                    buf.append('a');
+                    break;
+                case 'U':
+                    buf.append('A');
+                    break;
+                case 'u':
+                    buf.append('a');
+                    break;
+                default:
+                    buf.append(sequence.charAt(i));
+            }
+        return buf.toString();
+    }
+
+    /**
+     * get format string that has enough leading zeros to display this number
+     *
+     * @param number
+     * @return format string
+     */
+    public static String getIntegerFormatLeadingZeros(int number) {
+        if (number < 10)
+            return "%d";
+        else if (number < 100)
+            return "%02d";
+        else if (number < 1000)
+            return "%03d";
+        else
+            return "%04d";
+    }
+
+    final private static long kilo = 1024;
+    final private static long mega = 1024 * kilo;
+    final private static long giga = 1024 * mega;
+    final private static long tera = 1024 * giga;
+
+    /**
+     * get memory size string (using TB, GB, MB, kB or B)
+     *
+     * @param bytes
+     * @return string
+     */
+    public static String getMemorySizeString(long bytes) {
+
+        if (Math.abs(bytes) >= tera)
+            return String.format("%3.1f TB", (bytes / (double) tera));
+        else if (Math.abs(bytes) >= giga)
+            return String.format("%3.1f GB", (bytes / (double) giga));
+        else if (Math.abs(bytes) >= mega)
+            return String.format("%3.1f MB", (bytes / (double) mega));
+        else if (Math.abs(bytes) >= kilo)
+            return String.format("%3.1f kB", (bytes / (double) kilo));
+        else
+            return String.format("%3d B", bytes);
+    }
+    /**
+     * capitalize the first letter of a string
+     *
+     * @param s
+     * @return capitalized word
+     */
+    public static String capitalizeFirstLetter(String s) {
+        if (s.length() > 0 && Character.isLetter(s.charAt(0)) && Character.isLowerCase(s.charAt(0))) {
+            return Character.toUpperCase(s.charAt(0)) + s.substring(1);
+        } else
+            return s;
+    }
+
+    /**
+     * returns the first line of a text
+     *
+     * @param text
+     * @return first line1
+     */
+    public static String getFirstLine(String text) {
+        if (text == null)
+            return "";
+        int pos = text.indexOf("\r");
+        if (pos != -1)
+            return text.substring(0, pos);
+        pos = text.indexOf("\n");
+        if (pos != -1)
+            return text.substring(0, pos);
+        return text;
+    }
+
+    /**
+     * returns the first block of a text up to an empty line. Consecutive lines are separated by single spaces
+     *
+     * @param text
+     * @return first block
+     */
+    public static String getFirstParagraphAsALine(String text) {
+        if (text == null)
+            return "";
+        StringBuilder buf = new StringBuilder();
+        BufferedReader r = new BufferedReader(new StringReader(text));
+        String aLine;
+        try {
+            boolean first = true;
+            while ((aLine = r.readLine()) != null) {
+                aLine = aLine.trim();
+                if (first)
+                    first = false;
+                else {
+                    if (aLine.length() == 0)
+                        break;  // found empty line, break;
+                    buf.append(" ");
+                }
+                buf.append(aLine);
+            }
+        } catch (IOException e) {
+        }
+        return buf.toString();
+    }
+
+    /**
+     * get the last line in a text
+     *
+     * @param text
+     * @return last line or empty string
+     */
+    public static String getLastLine(String text) {
+        if (text == null)
+            return "";
+        int pos = text.lastIndexOf("\r");
+        if (pos == text.length() - 1)
+            pos = text.lastIndexOf("\r", pos - 1);
+        if (pos != -1)
+            return text.substring(pos + 1, text.length());
+        pos = text.lastIndexOf("\n");
+        if (pos == text.length() - 1)
+            pos = text.lastIndexOf("\n", pos - 1);
+        if (pos != -1)
+            return text.substring(pos + 1, text.length());
+        return text;
+    }
+
+
+    /**
+     * gets a color as a background color
+     *
+     * @param color
+     * @return color
+     */
+    static public String getBackgroundColorHTML(Color color) {
+        return String.format("<font bgcolor=#%x>", (color.getRGB() & 0xFFFFFF));
+    }
+
+    /**
+     * gets the index of the first space in the string
+     *
+     * @param string
+     * @return index or -1
+     */
+    public static int getIndexOfFirstWhiteSpace(String string) {
+        for (int i = 0; i < string.length(); i++)
+            if (Character.isWhitespace(string.charAt(i)))
+                return i;
+        return -1;
+    }
+
+    /**
+     * gets the first word in the given string
+     *
+     * @param string
+     * @return word (delimited by a white space) or empty string, if the first character is a white space
+     */
+    public static String getFirstWord(String string) {
+        int i = getIndexOfFirstWhiteSpace(string);
+        if (i != -1)
+            return string.substring(0, i);
+        else
+            return string;
+    }
+
+    /**
+     * gets the first word in the given src string and returns it in the target string
+     *
+     * @param src
+     * @param target
+     * @return length
+     */
+    public static int getFirstWord(byte[] src, byte[] target) {
+        for (int i = 0; i < src.length; i++) {
+            if (Character.isWhitespace((char) src[i]) || src[i] == 0) {
+                return i;
+            }
+            target[i] = src[i];
+        }
+        return src.length;
+    }
+
+
+    /**
+     * remove all white spaces
+     *
+     * @param s
+     * @return string without white spaces
+     */
+    public static String removeAllWhiteSpaces(String s) {
+        StringBuilder buf = new StringBuilder();
+        for (int i = 0; i < s.length(); i++) {
+            if (!Character.isWhitespace(s.charAt(i)))
+                buf.append(s.charAt(i));
+        }
+        return buf.toString();
+    }
+
+    /**
+     * reverse a string
+     *
+     * @param s
+     * @return reversed string
+     */
+    public static String reverseString(String s) {
+        StringBuilder buf = new StringBuilder();
+        for (int i = s.length() - 1; i >= 0; i--)
+            buf.append(s.charAt(i));
+        return buf.toString();
+    }
+
+    /**
+     * get a string of spaces
+     *
+     * @param count
+     * @return spaces
+     */
+    public static String spaces(int count) {
+        StringBuilder buf = new StringBuilder();
+        for (int i = 0; i < count; i++)
+            buf.append(" ");
+        return buf.toString();
+    }
+
+    /**
+     * change font size
+     *
+     * @param component
+     * @param newFontSize
+     */
+    static public void changeFontSize(Component component, int newFontSize) {
+        Font font = new Font(component.getFont().getName(), component.getFont().getStyle(), newFontSize);
+        component.setFont(font);
+    }
+
+    /**
+     * gets the common name prefix of a set of files
+     *
+     * @param files
+     * @param defaultPrefix
+     * @return prefix
+     */
+    public static String getCommonPrefix(File[] files, String defaultPrefix) {
+        List<String> names = new LinkedList<>();
+        for (File file : files)
+            names.add(file.getName());
+        return getCommonPrefix(names, defaultPrefix);
+    }
+
+    /**
+     * gets the common prefix of a set of names
+     *
+     * @param names
+     * @param defaultPrefix
+     * @return prefix
+     */
+    public static String getCommonPrefix(List<String> names, String defaultPrefix) {
+        if (names.size() == 0)
+            return "";
+        else if (names.size() == 1)
+            return Basic.getFileBaseName(names.get(0));
+
+        int posOfFirstDifference = 0;
+
+        boolean ok = true;
+        while (ok) {
+            int ch = 0;
+            for (String name : names) {
+                if (posOfFirstDifference >= name.length()) {
+                    ok = false;
+                    break;
+                }
+                if (ch == 0)
+                    ch = name.charAt(posOfFirstDifference);
+                else if (name.charAt(posOfFirstDifference) != ch) {
+                    ok = false;
+                    break;
+                }
+            }
+            posOfFirstDifference++;
+        }
+
+        // get rid of trailing spaces, _ and .
+        String name = names.get(0);
+        while (posOfFirstDifference > 0) {
+            int ch = name.charAt(posOfFirstDifference - 1);
+            if (ch != '.' && ch != '_' && !Character.isWhitespace(ch))
+                break;
+            posOfFirstDifference--;
+        }
+
+        if (posOfFirstDifference > 0)
+            return name.substring(0, posOfFirstDifference);
+        return defaultPrefix;
+    }
+
+    /**
+     * swallow a leading >, if present
+     *
+     * @param word
+     * @return string with leading > removed
+     */
+    public static String swallowLeadingGreaterSign(String word) {
+        if (word.startsWith(">"))
+            return word.substring(1).trim();
+        else
+            return word;
+    }
+
+    /**
+     * get the sum of values
+     *
+     * @param values
+     * @return sum
+     */
+    public static int getSum(Integer[] values) {
+        int sum = 0;
+        for (Integer value : values) {
+            if (value != null)
+                sum += value;
+        }
+        return sum;
+    }
+
+    /**
+     * get the sum of values
+     *
+     * @param values
+     * @return sum
+     */
+    public static int getSum(int[] values) {
+        int sum = 0;
+        for (Integer value : values) {
+            sum += value;
+        }
+        return sum;
+    }
+
+    /**
+     * get the sum of values
+     *
+     * @param values
+     * @return sum
+     */
+    public static int getSum(Collection<Integer> values) {
+        int sum = 0;
+        for (Number value : values)
+            sum += value.intValue();
+        return sum;
+    }
+
+    /**
+     * get the sum of values
+     *
+     * @param values
+     * @return sum
+     */
+    public static int getSum(int[] values, int offset, int len) {
+        int sum = 0;
+        for (int i = offset; i < len; i++) {
+            Integer value = values[i];
+            sum += value;
+        }
+        return sum;
+    }
+
+    public static long getSum(long[] array) {
+        long result = 0;
+        for (long value : array)
+            result += value;
+        return result;
+    }
+
+    /**
+     * get all the lines found in a reader
+     *
+     * @param r0
+     * @return lines
+     * @throws IOException
+     */
+    public static String[] getLines(Reader r0) throws IOException {
+        BufferedReader r = new BufferedReader(r0);
+        LinkedList<String> lines = new LinkedList<>();
+        String aLine;
+        while ((aLine = r.readLine()) != null) {
+            lines.add(aLine);
+        }
+        return lines.toArray(new String[lines.size()]);
+    }
+
+    public static String capitalizeWords(String str) {
+        StringBuilder buf = new StringBuilder();
+        boolean previousWasSpaceOrPunctuation = true;
+        for (int i = 0; i < str.length(); i++) {
+            int ch = str.charAt(i);
+            if (Character.isWhitespace(ch) || ".:".contains("" + ch)) {
+                buf.append((char) ch);
+                previousWasSpaceOrPunctuation = true;
+            } else {
+                if (previousWasSpaceOrPunctuation && Character.isLetter(ch))
+                    buf.append((char) Character.toUpperCase(ch));
+                else if (Character.isLetter(ch))
+                    buf.append((char) Character.toLowerCase(ch));
+                else
+                    buf.append((char) ch);
+                previousWasSpaceOrPunctuation = false;
+            }
+        }
+        return buf.toString();
+    }
+
+    public static boolean fileExistsAndIsNonEmpty(String fileName) {
+        File file = new File(fileName);
+        return file.exists() && file.length() > 0;
+    }
+
+    public static void checkFileReadableNonEmpty(String fileName) throws IOException {
+        File file = new File(fileName);
+        if (!file.exists())
+            throw new IOException("No such file: " + fileName);
+        if (file.length() == 0)
+            throw new IOException("File is empty: " + fileName);
+        if (!file.canRead())
+            throw new IOException("File not readable: " + fileName);
+    }
+
+    /**
+     * encode a font as a string that can be decoded using Font.decode()
+     *
+     * @param font
+     * @return string
+     */
+    public static String encode(Font font) {
+        String style = "";
+        if (font.isBold())
+            style += "BOLD";
+        if (font.isItalic())
+            style += "ITALIC";
+        if (style.length() == 0)
+            style = "PLAIN";
+        return font.getFontName() + "-" + style + "-" + font.getSize();
+    }
+
+    public static boolean isDate(String s) {
+        long time;
+        try {
+            time = DateFormat.getDateInstance().parse(s).getTime();
+        } catch (Exception ex) {
+            return false;
+        }
+        return time > 1000;
+    }
+
+    /**
+     * replace all backslashes by double backslashes
+     *
+     * @param str
+     * @return string with protected back slashes
+     */
+    public static String protectBackSlashes(String str) {
+        if (str == null)
+            return null;
+        StringBuilder buf = new StringBuilder();
+        for (int i = 0; i < str.length(); i++) {
+            buf.append(str.charAt(i));
+            if (str.charAt(i) == '\\')
+                buf.append('\\');
+        }
+        return buf.toString();
+    }
+
+    /**
+     * skip all characters upto first digit or '-'
+     *
+     * @param token
+     * @return first number   or null
+     */
+    public static String skipToNumber(String token) {
+        int pos = 0;
+        while (pos < token.length()) {
+            if (Character.isDigit(token.charAt(pos)) || token.charAt(pos) == '-')
+                return token.substring(pos);
+            pos++;
+        }
+        return null;
+    }
+
+    /**
+     * gets next long
+     *
+     * @param rand
+     * @param max
+     * @return long
+     */
+    public static long nextLong(Random rand, long max) {
+        if (max <= 0)
+            return 0;
+        else if (max < Integer.MAX_VALUE)
+            return rand.nextInt((int) max);
+        else {
+            return (long) (rand.nextDouble() * max);
+        }
+    }
+
+    /**
+     * split a string by the given separator, but honoring quotes around items
+     *
+     * @param string
+     * @param separator
+     * @return tokens
+     */
+    public static String[] splitWithQuotes(String string, char separator) {
+        //return string.split(""+separator);
+
+        List<String> list = new LinkedList<>();
+        int i = 0;
+        while (i < string.length()) {
+            if (string.charAt(i) == '\"') { // start of quoted item
+                int j = string.indexOf('\"', i + 1);
+                if (j == -1) {
+                    list.add(string.substring(i + 1, string.length()).trim());
+                    break;  // unfinished quote, really should throw an exception
+                } else {
+                    list.add(string.substring(i + 1, j).trim());
+                    i = j + 2;
+                }
+            } else if (string.charAt(i) == separator) { // separator that follows a separator...
+                list.add("");
+                i++;
+            } else // start of unquoted item
+            {
+                int j = i + 1;
+                while (j < string.length()) {
+                    if (string.charAt(j) == separator)
+                        break;
+                    j++;
+                }
+                list.add(string.substring(i, j).trim());
+                i = j + 1;
+            }
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
+    /**
+     * returns a quoted string if the string for value contains a tab and not a quote
+     *
+     * @param value
+     * @return string or quoted string
+     */
+    public static String quoteIfContainsTab(Object value) {
+        String string = value.toString();
+        if (string.contains("\t") && !string.contains("\""))
+            return "\"" + string + "\"";
+        else
+            return string;
+    }
+
+    /**
+     * restrict a value to a given range
+     *
+     * @param min
+     * @param max
+     * @param value
+     * @return value between min and max
+     */
+    public static int restrictToRange(int min, int max, int value) {
+        if (value < min)
+            return min;
+        if (value >= max)
+            return max;
+        return value;
+    }
+
+    /**
+     * restrict a value to a given range
+     *
+     * @param min
+     * @param max
+     * @param value
+     * @return value between min and max
+     */
+    public static double restrictToRange(double min, double max, double value) {
+        if (value < min)
+            return min;
+        if (value >= max)
+            return max;
+        return value;
+    }
+
+
+    /**
+     * gets the name of a read. This is the first word in the line, skipping any '>' or '@' at first position
+     *
+     * @param aLine
+     * @return word (delimited by a space)
+     */
+    public static String getReadName(String aLine) {
+        if (aLine.length() == 0)
+            return "";
+        int start;
+        if (aLine.charAt(0) == '@' || aLine.charAt(0) == '>')
+            start = 1;
+        else
+            start = 0;
+        while (start < aLine.length() && Character.isWhitespace(aLine.charAt(start)))
+            start++;
+        int finish = start;
+        while (finish < aLine.length() && !Character.isWhitespace(aLine.charAt(finish)))
+            finish++;
+        return aLine.substring(start, finish);
+    }
+
+    /**
+     * gets the compile time version of the given class
+     *
+     * @param clazz
+     * @return compile time version
+     */
+    public static String getVersion(final Class clazz) {
+        return getVersion(clazz, clazz.getName().substring(clazz.getName().lastIndexOf(".") + 1));
+    }
+
+    /**
+     * gets the compile time version of the given class
+     *
+     * @param clazz
+     * @param name
+     * @return compile time version
+     */
+    public static String getVersion(final Class clazz, final String name) {
+        String version;
+        try {
+            final ClassLoader classLoader = clazz.getClassLoader();
+            String threadContexteClass = clazz.getName().replace('.', '/');
+            URL url = classLoader.getResource(threadContexteClass + ".class");
+            if (url == null) {
+                version = name + " $ (no manifest) $";
+            } else {
+                final String path = url.getPath();
+                final String jarExt = ".jar";
+                int index = path.indexOf(jarExt);
+                SimpleDateFormat sdf = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z");
+                if (index != -1) {
+                    final String jarPath = path.substring(0, index + jarExt.length());
+                    final File file = new File(jarPath);
+                    final String jarVersion = file.getName();
+                    final JarFile jarFile = new JarFile(new File(new URI(jarPath)));
+                    final JarEntry entry = jarFile.getJarEntry("META-INF/MANIFEST.MF");
+                    version = name + " $ " + jarVersion.substring(0, jarVersion.length() - jarExt.length()) + " $ " + sdf.format(new Date(entry.getTime()));
+                    jarFile.close();
+                } else {
+                    final File file = new File(path);
+                    version = name + " $ " + sdf.format(new Date(file.lastModified()));
+                }
+            }
+        } catch (Exception e) {
+            //Basic.caught(e);
+            version = name + " $ " + e.toString();
+        }
+        return version;
+    }
+
+    /**
+     * is either a single word or consists only of spaces
+     *
+     * @param str
+     * @return true if a single word or consists only of spaces
+     */
+    public static boolean isOneWord(String str) {
+        str = str.trim();
+        for (int i = 0; i < str.length(); i++) {
+            if (Character.isWhitespace(str.charAt(i)))
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * is named file a directory?
+     *
+     * @param fileName
+     * @return true if directory
+     */
+    public static boolean isDirectory(String fileName) {
+        return ((new File(fileName)).isDirectory());
+    }
+
+    /**
+     * copy a file
+     *
+     * @param source
+     * @param dest
+     * @throws java.io.IOException
+     */
+    public static void copyFile(File source, File dest) throws IOException {
+        FileChannel sourceChannel = null;
+        FileChannel destChannel = null;
+        try {
+            sourceChannel = new FileInputStream(source).getChannel();
+            destChannel = new FileOutputStream(dest).getChannel();
+            destChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
+        } finally {
+            if (sourceChannel != null)
+                sourceChannel.close();
+            if (destChannel != null)
+                destChannel.close();
+        }
+    }
+
+    /**
+     * write a stream to a file
+     *
+     * @param inputStream
+     * @param outputFile
+     * @throws IOException
+     */
+    public static void writeStreamToFile(InputStream inputStream, File outputFile) throws IOException {
+        System.err.println("Writing file: " + outputFile);
+        if (inputStream == null)
+            throw new IOException("Input stream is null");
+
+        try (BufferedOutputStream outs = new BufferedOutputStream(new FileOutputStream(outputFile), 1048576)) {
+            while (true) {
+                int a = inputStream.read();
+                if (a == -1)
+                    break;
+                else
+                    outs.write((byte) a);
+            }
+        }
+    }
+
+    /**
+     * append a file
+     *
+     * @param source
+     * @param dest
+     * @throws java.io.IOException
+     */
+    public static void appendFile(File source, File dest) throws IOException {
+        FileChannel sourceChannel = null;
+        RandomAccessFile raf = null;
+        FileChannel destChannel = null;
+        try {
+            sourceChannel = new FileInputStream(source).getChannel();
+            raf = new RandomAccessFile(dest, "rw");
+            destChannel = raf.getChannel();
+            destChannel.transferFrom(sourceChannel, raf.length(), sourceChannel.size());
+        } finally {
+            if (sourceChannel != null)
+                sourceChannel.close();
+            if (destChannel != null)
+                destChannel.close();
+            if (raf != null)
+                raf.close();
+        }
+    }
+
+    /**
+     * copy a file and uncompress if necessary
+     *
+     * @param source
+     * @param dest
+     * @throws java.io.IOException
+     */
+    public static void copyFileUncompressed(File source, File dest) throws IOException {
+        if (Basic.isZIPorGZIPFile(source.getPath())) {
+            try (InputStream ins = Basic.getInputStreamPossiblyZIPorGZIP(source.getPath()); OutputStream outs = new BufferedOutputStream(new FileOutputStream(dest), 8192)) {
+                byte[] buffer = new byte[8192];
+                int len = ins.read(buffer);
+                while (len != -1) {
+                    outs.write(buffer, 0, len);
+                    len = ins.read(buffer);
+                }
+            }
+        } else
+            copyFile(source, dest);
+    }
+
+    /**
+     * gets a inputstream. If file ends on gz or zip opens appropriate unzipping stream
+     *
+     * @param fileName
+     * @return input stream
+     * @throws IOException
+     */
+    public static InputStream getInputStreamPossiblyZIPorGZIP(String fileName) throws IOException {
+        final File file = new File(fileName);
+        if (file.isDirectory())
+            throw new IOException("Directory, not a file: " + file);
+        if (!file.exists())
+            throw new IOException("No such file: " + file);
+        final InputStream ins;
+        if (fileName.toLowerCase().endsWith(".gz")) {
+            ins = new GZIPInputStream(new FileInputStream(file));
+        } else if (fileName.toLowerCase().endsWith(".zip")) {
+            ZipFile zf = new ZipFile(file);
+            Enumeration e = zf.entries();
+            ZipEntry entry = (ZipEntry) e.nextElement(); // your only file
+            ins = zf.getInputStream(entry);
+        } else
+            ins = new FileInputStream(file);
+        return ins;
+    }
+
+    /**
+     * gets a outputstream. If file ends on gz or zip opens appropriate zipping stream
+     *
+     * @param fileName
+     * @return input stream
+     * @throws IOException
+     */
+    public static OutputStream getOutputStreamPossiblyZIPorGZIP(String fileName) throws IOException {
+        OutputStream outs = new FileOutputStream(fileName);
+        if (fileName.toLowerCase().endsWith(".gz")) {
+            outs = new GZIPOutputStream(outs);
+        } else if (fileName.toLowerCase().endsWith(".zip")) {
+            final ZipOutputStream out = new ZipOutputStream(outs);
+            ZipEntry e = new ZipEntry(Basic.replaceFileSuffix(fileName, ""));
+            out.putNextEntry(e);
+        }
+        return outs;
+    }
+
+    /**
+     * is this a gz or zip file?
+     *
+     * @param fileName
+     * @return true, if gz or zip file
+     */
+    public static boolean isZIPorGZIPFile(String fileName) {
+        fileName = fileName.toLowerCase();
+        return fileName.endsWith(".gz") || fileName.endsWith(".zip");
+    }
+
+    /**
+     * get approximate uncompressed size of file (for use with ProgressListener)
+     *
+     * @param fileName
+     * @return approximate umcompressed size of file
+     */
+    public static long guessUncompressedSizeOfFile(String fileName) {
+        return (isZIPorGZIPFile(fileName) ? 10 : 1) * (new File(fileName)).length();
+    }
+
+    /**
+     * returns a file that has the same given path and one of the given file extensions
+     *
+     * @param path
+     * @param fileExtensions
+     * @return file name or null
+     */
+    public static String getAnExistingFileWithGivenExtension(String path, final List<String> fileExtensions) {
+        if (isZIPorGZIPFile(path))
+            path = Basic.replaceFileSuffix(path, "");
+        String prev;
+        do {
+            prev = path;
+            for (String ext : fileExtensions) {
+                final File file = new File(Basic.replaceFileSuffix(path, ext));
+                if (file.exists())
+                    return file.getPath();
+            }
+            path = Basic.getFileBaseName(path); // removes last suffix
+        }
+        while (path.length() < prev.length()); // while a suffix was actually removed
+        return null;
+    }
+
+    /**
+     * split string on given character. Note that results are subsequently trimmed
+     *
+     * @param aLine
+     * @param splitChar
+     * @return split string, trimmed
+     */
+    public static String[] split(String aLine, char splitChar) {
+        if (aLine.length() == 0)
+            return new String[0];
+
+        int count = (aLine.charAt(aLine.length() - 1) == splitChar ? 0 : 1);
+        for (int i = 0; i < aLine.length(); i++)
+            if (aLine.charAt(i) == splitChar)
+                count++;
+        if (count == 1)
+            return new String[]{aLine};
+        final String[] result = new String[count];
+        int prev = 0;
+        int which = 0;
+        int pos = 0;
+        for (; pos < aLine.length(); pos++) {
+            if (aLine.charAt(pos) == splitChar) {
+                result[which++] = aLine.substring(prev, pos).trim();
+                prev = pos + 1;
+            }
+        }
+        if (pos > prev) {
+            result[which] = aLine.substring(prev, pos).trim();
+        }
+        return result;
+    }
+
+    /**
+     * split string on given characters. Note that results are subsequently trimmed
+     *
+     * @param aLine
+     * @param splitChar
+     * @return split string, trimmed
+     */
+    public static String[] split(String aLine, char splitChar, char... splitChars) {
+        if (aLine.length() == 0)
+            return new String[0];
+
+        int count = (aLine.charAt(aLine.length() - 1) == splitChar || contains(splitChars, aLine.charAt(aLine.length() - 1)) ? 0 : 1);
+
+        for (int i = 0; i < aLine.length(); i++)
+            if (aLine.charAt(i) == splitChar || contains(splitChars, aLine.charAt(i)))
+                count++;
+        if (count == 1)
+            return new String[]{aLine};
+        final String[] result = new String[count];
+        int prev = 0;
+        int which = 0;
+        int pos = 0;
+        for (; pos < aLine.length(); pos++) {
+            if (aLine.charAt(pos) == splitChar || contains(splitChars, aLine.charAt(pos))) {
+                result[which++] = aLine.substring(prev, pos).trim();
+                prev = pos + 1;
+            }
+        }
+        if (pos > prev) {
+            result[which] = aLine.substring(prev, pos).trim();
+        }
+        return result;
+    }
+
+    /**
+     * computes the symmetric different of two hash sets
+     *
+     * @param set1
+     * @param set2
+     * @param <T>
+     * @return symmetric different
+     */
+    public static <T> HashSet<T> symmetricDifference(final HashSet<T> set1, final HashSet<T> set2) {
+        final HashSet<T> result = new HashSet<>();
+        for (T element : set1) {
+            if (!set2.contains(element))
+                result.add(element);
+        }
+        for (T element : set2) {
+            if (!set1.contains(element))
+                result.add(element);
+        }
+        return result;
+    }
+
+    /**
+     * computes the symmetric different of two hash sets
+     *
+     * @param set1
+     * @param set2
+     * @param <T>
+     * @return symmetric different
+     */
+    public static <T> HashSet<T> intersection(final HashSet<T> set1, final HashSet<T> set2) {
+        final HashSet<T> result = new HashSet<>();
+        for (T element : set1) {
+            if (set2.contains(element))
+                result.add(element);
+        }
+        return result;
+    }
+
+    /**
+     * read and verify a magic number from a stream
+     *
+     * @param ins
+     * @param expectedMagicNumber
+     * @throws java.io.IOException
+     */
+    public static void readAndVerifyMagicNumber(InputStream ins, byte[] expectedMagicNumber) throws IOException {
+        {
+            byte[] magicNumber = new byte[expectedMagicNumber.length];
+            if (ins.read(magicNumber) != expectedMagicNumber.length || !equal(magicNumber, expectedMagicNumber)) {
+                System.err.println("Expected: " + toString(expectedMagicNumber));
+                System.err.println("Got:      " + toString(magicNumber));
+                throw new IOException("Index is too old or incorrect file (wrong magic number). Please recompute index.");
+            }
+        }
+    }
+
+    /**
+     * compare two byte arrays of the same length
+     *
+     * @param a
+     * @param b
+     * @return true, if equal values
+     */
+    public static boolean equal(byte[] a, byte[] b) {
+        if (a == null)
+            return b == null; // either a==null, b!=null or both null
+        else if (b == null)
+            return false; // a!=null, b==null
+
+        if (a.length != b.length)
+            return false;
+        for (int i = 0; i < a.length; i++) {
+            if (a[i] != b[i])
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * copy an int array to an integer array
+     *
+     * @param array
+     * @return integer array copy
+     */
+    public static Integer[] copyAsIntegerArray(int[] array) {
+        Integer[] result = new Integer[array.length];
+        for (int i = 0; i < array.length; i++)
+            result[i] = array[i];
+        return result;
+    }
+
+    /**
+     * copy an Integer array to an int array
+     *
+     * @param array
+     * @return int array copy
+     */
+    public static int[] copyAsIntArray(Collection<Integer> array) {
+        int[] result = new int[array.size()];
+        int i = 0;
+        for (Integer value : array) {
+            result[i++] = value;
+        }
+        return result;
+    }
+
+    /**
+     * Finds the value of the given enumeration by name, case-insensitive.
+     * Throws an IllegalArgumentException if no match is found.
+     */
+    public static <T extends Enum<T>> T valueOfIgnoreCase(Class<T> enumeration, String name) {
+        for (T enumValue : enumeration.getEnumConstants()) {
+            if (enumValue.name().equalsIgnoreCase(name)) {
+                return enumValue;
+            }
+        }
+        throw new IllegalArgumentException("There is no value with name '" + name + " in Enum " + enumeration.getClass().getName());
+    }
+
+    /**
+     * returns file with .gz ending if only that exists
+     *
+     * @param file
+     * @return file or file.gz
+     */
+    public static File gzippedIfNecessary(File file) {
+        if (file.exists() || !(new File(file.getPath() + ".gz")).exists())
+            return file;
+        else
+            return new File(file.getPath() + ".gz");
+    }
+
+    /**
+     * determines whether the given string contains the given subword, ignoring case. Uses stupid slow algorithm
+     *
+     * @param string
+     * @param subWord
+     * @return true, if contained
+     */
+    public static boolean containsIgnoringCase(String string, int[] subWord) {
+        int pos = 0;
+        while (pos + subWord.length < string.length()) {
+            int i = 0;
+            for (; i < subWord.length; i++) {
+                if (Character.toLowerCase(string.charAt(pos + i)) != Character.toLowerCase(subWord[i])) {
+                    break;
+                }
+            }
+            if (i == subWord.length)
+                return true;
+            pos++;
+        }
+        return false;
+    }
+
+    /**
+     * gets the file type (based on suffix)
+     *
+     * @param name
+     * @return file type or "Unknown"
+     */
+    public static String getFileType(String name) {
+        int pos = name.lastIndexOf(".");
+        if (pos != 1 && pos < name.length() - 1) {
+            return name.substring(pos + 1).toUpperCase();
+        } else
+            return "Unknown";
+    }
+
+    /**
+     * counts commands in a string.
+     *
+     * @param s
+     * @return
+     */
+    public static int countCommands(String s) {
+        s = s.trim();
+        if (s.endsWith(";"))
+            return countOccurrences(s, ';');
+        else
+            return countOccurrences(s, ';') + 1;
+    }
+
+    /**
+     * gets a temporary file name modelled on the given name
+     *
+     * @param name
+     * @return temporary file name
+     */
+    public static String getTemporaryFileName(String name) {
+        String zipSuffix = null;
+        if (isZIPorGZIPFile(name)) {
+            zipSuffix = getSuffix(name);
+            name = getFileNameWithoutZipOrGZipSuffix(name);
+        }
+        final String suffix = getSuffix(name);
+        name = getFileBaseName(name);
+        final int number = (int) (System.currentTimeMillis() & ((1 << 20) - 1));
+        return String.format("%s-tmp%d.%s%s", name, number, suffix, zipSuffix != null ? "." + zipSuffix : "");
+    }
+
+    /**
+     * Get string representation of a double matrix
+     *
+     * @param matrix
+     * @return string representation
+     */
+    public static String toString(double[][] matrix) {
+        StringBuilder buf = new StringBuilder();
+        for (double[] row : matrix) {
+            buf.append(Basic.toString(row, " ")).append("\n");
+        }
+        return buf.toString();
+    }
+
+    /**
+     * open the given URI in a web browser
+     *
+     * @param uri
+     */
+    public static void openWebPage(URI uri) {
+        Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
+        if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) {
+            try {
+                desktop.browse(uri);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * open the given URL in a web browser
+     *
+     * @param url
+     */
+    public static void openWebPage(URL url) {
+        try {
+            openWebPage(url.toURI());
+        } catch (URISyntaxException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * return the lowest power of 2 that is greater or equal to the given number
+     *
+     * @param i
+     * @return next power of 2
+     */
+    public static int nextPowerOf2(int i) {
+        int k = 1;
+        while (k < Integer.MAX_VALUE) {
+            if (k >= i)
+                break;
+            k <<= 1;
+        }
+        return k;
+    }
+
+    /**
+     * gets value as binary string, always showing all 64 positions
+     *
+     * @param value
+     * @return binary string
+     */
+    public static String toBinaryString(long value) {
+        StringBuilder buf = new StringBuilder();
+        for (int shift = 63; shift >= 0; shift--) {
+            buf.append((value & (1l << shift)) >>> shift);
+        }
+        return buf.toString();
+    }
+
+    /**
+     * gets value as binary string, always showing all 64 positions
+     *
+     * @param value
+     * @return binary string
+     */
+    public static String toBinaryString(int value) {
+        StringBuilder buf = new StringBuilder();
+        for (int shift = 31; shift >= 0; shift--) {
+            buf.append((value & (1 << shift)) >>> shift);
+        }
+        return buf.toString();
+    }
+
+    /**
+     * get all files listed below the given root directory
+     *
+     * @param rootDirectory
+     * @param fileFilter
+     * @param recursively
+     * @return list of files
+     */
+    public static List<File> getAllFilesInDirectory(File rootDirectory, FileFilter fileFilter, boolean recursively, ProgressListener progress) {
+        final List<File> result = new LinkedList<>();
+
+        try {
+            int totalCount = 0;
+            final Queue<File> queue = new LinkedList<>();
+            File[] list = rootDirectory.listFiles();
+            if (list != null) {
+                Collections.addAll(queue, list);
+                totalCount += queue.size();
+                progress.setMaximum(totalCount);
+                while (queue.size() > 0) {
+                    File file = queue.poll();
+                    if (file.isDirectory()) {
+                        if (recursively) {
+                            File[] below = file.listFiles();
+                            if (below != null) {
+                                Collections.addAll(queue, below);
+                                totalCount += below.length;
+                                progress.setMaximum(totalCount);
+                            }
+                        }
+                    } else if (fileFilter == null || fileFilter.accept(file)) {
+                        result.add(file);
+                    }
+                    progress.incrementProgress();
+                }
+            }
+        } catch (CanceledException ex) {
+            System.err.println("USER CANCELED, list of files may be incomplete");
+        }
+        return result;
+    }
+
+    /**
+     * Returns the path of one File relative to another.
+     *
+     * @param target the target directory
+     * @param base   the base directory
+     * @return target's path relative to the base directory
+     * @throws IOException if an error occurs while resolving the files' canonical names
+     */
+    public static File getRelativeFile(File target, File base) throws IOException {
+        String[] baseComponents = base.getCanonicalPath().split(Pattern.quote(File.separator));
+        String[] targetComponents = target.getCanonicalPath().split(Pattern.quote(File.separator));
+
+        // skip common components
+        int index = 0;
+        for (; index < targetComponents.length && index < baseComponents.length; ++index) {
+            if (!targetComponents[index].equals(baseComponents[index]))
+                break;
+        }
+
+        StringBuilder result = new StringBuilder();
+        if (index != baseComponents.length) {
+            // backtrack to base directory
+            for (int i = index; i < baseComponents.length; ++i)
+                result.append("..").append(File.separator);
+        }
+        for (; index < targetComponents.length; ++index)
+            result.append(targetComponents[index]).append(File.separator);
+        if (!target.getPath().endsWith("/") && !target.getPath().endsWith("\\")) {
+            // remove final path separator
+            result.delete(result.length() - File.separator.length(), result.length());
+        }
+        return new File(result.toString());
+    }
+
+    /**
+     * returns all trimmed lines in a file, excluding empty lines or lines that start with #
+     *
+     * @param fileName
+     * @return lines
+     * @throws java.io.IOException
+     */
+    public static List<String> getAllLines(String fileName) throws IOException {
+        final List<String> list = new ArrayList<>();
+        FileInputIterator it = new FileInputIterator(fileName);
+        while (it.hasNext()) {
+            String aLine = it.next().trim();
+            if (aLine.length() > 0 && !aLine.startsWith("#"))
+                list.add(aLine);
+        }
+        it.close();
+        return list;
+    }
+
+    /**
+     * gets the file path to the named file using the directory of the referenceFile
+     *
+     * @param referenceFile
+     * @param fileName
+     * @return
+     */
+    public static String getFilePath(String referenceFile, String fileName) {
+        if (referenceFile == null || referenceFile.length() == 0)
+            return fileName;
+        else {
+            return new File(((new File(referenceFile).getParent())), getFileNameWithoutPath(fileName)).getPath();
+        }
+    }
+
+    /**
+     * gets the desired column from a tab-separated line of tags
+     *
+     * @param aLine
+     * @param column
+     * @return
+     */
+    public static String getTokenFromTabSeparatedLine(String aLine, int column) {
+        int a = 0;
+        int count = 0;
+        for (int i = 0; i < aLine.length(); i++) {
+            if (aLine.charAt(i) == '\t') {
+                if (count == column)
+                    return aLine.substring(a, i);
+                count++;
+                if (count == column)
+                    a = i + 1;
+            }
+        }
+        if (count == column)
+            return aLine.substring(a);
+        else
+            return "";
+    }
+
+    /**
+     * return array in reverse order
+     *
+     * @param strings
+     * @return
+     */
+    public static String[] reverse(String[] strings) {
+        String[] result = new String[strings.length];
+        for (int i = 0; i < strings.length; i++)
+            result[strings.length - 1 - i] = strings[i];
+        return result;
+    }
+
+
+    /**
+     * return array in reverse order
+     *
+     * @param strings
+     * @return
+     */
+    public static String[] reverse(Collection<String> strings) {
+        final String[] result = new String[strings.size()];
+        int pos = strings.size();
+        for (String str : strings) {
+            result[--pos] = str;
+        }
+        return result;
+    }
+
+    /**
+     * gets the rank of a value in a list
+     *
+     * @param list
+     * @param value
+     * @return rank or -1
+     */
+    public static <T> int getRank(List<T> list, T value) {
+        for (int i = 0; i < list.size(); i++) {
+            if (list.get(i).equals(value))
+                return i;
+        }
+        return -1;
+    }
+
+    /**
+     * gets the rank of a value in an array
+     *
+     * @param list
+     * @param value
+     * @return rank or -1
+     */
+    public static <T> int getRank(T[] list, T value) {
+        for (int i = 0; i < list.length; i++) {
+            if (list[i] != null && list[i].equals(value))
+                return i;
+        }
+        return -1;
+    }
+
+    /**
+     * gets the first line in a file. File may be zgipped or zipped
+     *
+     * @param file
+     * @return first line or null
+     * @throws IOException
+     */
+    public static String getFirstLineFromFile(File file) {
+        try {
+            try (BufferedReader ins = new BufferedReader(new InputStreamReader(getInputStreamPossiblyZIPorGZIP(file.getPath())))) {
+                return ins.readLine();
+            }
+        } catch (IOException ex) {
+            return null;
+        }
+    }
+
+    /**
+     * gets the first line in a file. File may be zgipped or zipped
+     *
+     * @param file
+     * @return first line or null
+     * @throws IOException
+     */
+    public static String[] getFirstLinesFromFile(File file, int count) {
+        try {
+            String[] lines = new String[count];
+            try (BufferedReader ins = new BufferedReader(new InputStreamReader(getInputStreamPossiblyZIPorGZIP(file.getPath())))) {
+                for (int i = 0; i < count; i++) {
+                    lines[i] = ins.readLine();
+                    if (lines[i] == null)
+                        break;
+                }
+            }
+            return lines;
+        } catch (IOException ex) {
+            return null;
+        }
+    }
+
+    /**
+     * gets the first bytes from a file. File may be zgipped or zipped
+     *
+     * @param file
+     * @return first bytes
+     * @throws IOException
+     */
+    public static byte[] getFirstBytesFromFile(File file, int count) {
+        try {
+            try (InputStream ins = getInputStreamPossiblyZIPorGZIP(file.getPath())) {
+                byte[] bytes = new byte[count];
+                ins.read(bytes, 0, count);
+                return bytes;
+            }
+        } catch (IOException ex) {
+            return null;
+        }
+    }
+
+    /**
+     * does the given string contain the given count of character ch?
+     *
+     * @param string
+     * @param ch
+     * @param count
+     * @return true, if string contains atleast count occurrences of ch
+     */
+    public static boolean contains(String string, char ch, int count) {
+        for (int i = 0; i < string.length(); i++) {
+            if (string.charAt(i) == ch && --count == 0)
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * does the given array of characters contain the given one?
+     *
+     * @param string
+     * @param ch
+     * @return true, if contained
+     */
+    public static boolean contains(char[] string, char ch) {
+        for (char a : string) {
+            if (a == ch)
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * abbreviate a string to the given length
+     *
+     * @param string
+     * @param length
+     * @return abbreviated string
+     */
+    public static String abbreviate(String string, int length) {
+        if (string.length() <= length)
+            return string;
+        else
+            return string.substring(0, length - 1) + ".";
+    }
+
+    /**
+     * abbreviate a string to the given length
+     *
+     * @param string
+     * @param length
+     * @return abbreviated string
+     */
+    public static String abbreviateDotDotDot(String string, int length) {
+        if (string.length() <= length)
+            return string;
+        else
+            return string.substring(0, length - 1) + "...";
+
+    }
+
+    /**
+     * skip the first line in a string
+     *
+     * @param string
+     * @return first line
+     */
+    public static String skipFirstLine(String string) {
+        int pos = string.indexOf('\n');
+        if (pos != -1)
+            return string.substring(pos + 1);
+        else
+            return string;
+    }
+
+    /**
+     * skip the first word in a string and trim
+     *
+     * @param string
+     * @return first line
+     */
+    public static String skipFirstWord(String string) {
+        for (int pos = 0; pos < string.length(); pos++) {
+            if (Character.isWhitespace(string.charAt(pos)))
+                return string.substring(pos).trim();
+        }
+        return "";
+    }
+
+    /**
+     * convert a string with spaces and/or underscores to camel case
+     *
+     * @param string
+     * @return camel case
+     */
+    public static String toCamelCase(String string) {
+        int pos = 0;
+        while (pos < string.length() && (Character.isWhitespace(string.charAt(pos)) || string.charAt(pos) == '_'))
+            pos++;
+        boolean afterWhiteSpace = false;
+        StringBuilder buf = new StringBuilder();
+        while (pos < string.length()) {
+            final char ch = string.charAt(pos);
+            if (Character.isWhitespace(ch) || ch == '_')
+                afterWhiteSpace = true;
+            else if (afterWhiteSpace) {
+                buf.append(Character.toUpperCase(ch));
+                afterWhiteSpace = false;
+            } else
+                buf.append(Character.toLowerCase(ch));
+            pos++;
+        }
+        return buf.toString();
+    }
+
+    /**
+     * convert a string with spaces and/or underscores to camel case
+     *
+     * @param string
+     * @return camel case
+     */
+    public static String fromCamelCase(String string) {
+        boolean afterWhiteSpace = true;
+        StringBuilder buf = new StringBuilder();
+        for (int pos = 0; pos < string.length(); pos++) {
+            final char ch = string.charAt(pos);
+            if (Character.isUpperCase(ch)) {
+                if (!afterWhiteSpace) {
+                    buf.append(" ");
+                }
+            }
+            buf.append(ch);
+            afterWhiteSpace = (Character.isWhitespace(ch));
+        }
+        return buf.toString();
+    }
+
+    /**
+     * gets next word after given first word
+     * @param first
+     * @param aLine
+     * @return next word or null
+     */
+    public static String getWordAfter(String first, String aLine) {
+        int start = aLine.indexOf(first);
+        if (start == -1)
+            return null;
+        start += first.length();
+        while (start < aLine.length() && Character.isWhitespace(aLine.charAt(start)))
+            start++;
+        int finish = start;
+        while (finish < aLine.length() && !Character.isWhitespace(aLine.charAt(finish)))
+            finish++;
+        if (finish < aLine.length())
+            return aLine.substring(start, finish);
+        else
+            return aLine.substring(start);
+
+    }
+
+    /**
+     * gets everything after the first word
+     *
+     * @param first
+     * @param aLine
+     * @return everything after the given word or null
+     */
+    public static String getAfter(String first, String aLine) {
+        int start = aLine.indexOf(first);
+        if (start == -1)
+            return null;
+        start += first.length();
+        while (start < aLine.length() && Character.isWhitespace(aLine.charAt(start)))
+            start++;
+        int finish = start;
+        return aLine.substring(start);
+
+    }
+
+    /**
+     * determines whether a ends with b, ignoring case
+     *
+     * @param a
+     * @param b
+     * @return true, if a ends with b, ignoring case
+     */
+    public static boolean endsWithIgnoreCase(String a, String b) {
+        return a.toLowerCase().endsWith(b.toLowerCase());
+    }
+
+     /**
+     * get the number of bytes used to terminate a line
+      *
+      * @param file
+      * @return 1 or 2
+      */
+    public static int determineEndOfLinesBytes(File file) {
+        try {
+            RandomAccessFile r = new RandomAccessFile(file, "r");
+            int count = 0;
+            long length = 0;
+            for (; count < 5; count++) {
+                String aLine = r.readLine();
+                if (aLine == null)
+                    break;
+                length += aLine.length();
+            }
+            long diff = r.getFilePointer() - length;
+            r.close();
+            return (int) (diff / count);
+        } catch (Exception e) {
+            //Basic.caught(e);
+            return 1;
+        }
+    }
+
+    /**
+     * replace value by replacement, if null
+     *
+     * @param value
+     * @param replacementValue
+     * @param <T>
+     * @return value, if non-null, else replacment
+     */
+    public static <T> T replaceNull(T value, T replacementValue) {
+        if (value == null)
+            return replacementValue;
+        else
+            return value;
+    }
+
+    /**
+     * get comparator that compares by decreasing length of second and then lexicographical on first
+     *
+     * @return comparator
+     */
+    public static Comparator<Pair<String, String>> getComparatorDecreasingLengthOfSecond() {
+        return new Comparator<Pair<String, String>>() {
+            @Override
+            public int compare(Pair<String, String> pair1, Pair<String, String> pair2) { // sorting in decreasing order of length
+                if (pair1.getSecond().length() > pair2.getSecond().length())
+                    return -1;
+                else if (pair1.getSecond().length() < pair2.getSecond().length())
+                    return 1;
+                else
+                    return pair1.getFirst().compareTo(pair2.getFirst());
+            }
+        };
+    }
+
+    /**
+     * transposes a matrix
+     *
+     * @param matrix
+     * @return transposed
+     */
+    public static float[][] transposeMatrix(float[][] matrix) {
+        final float[][] transposed = new float[matrix[0].length][matrix.length];
+        for (int i = 0; i < matrix.length; i++) {
+            for (int j = 0; j < transposed.length; j++) {
+                transposed[j][i] = matrix[i][j];
+            }
+        }
+        return transposed;
+    }
+}
+
+/**
+ * silent stream
+ */
+class NullOutStream extends OutputStream {
+    public void write(int b) {
+    }
+}
+
+class CollectOutStream extends OutputStream {
+    private StringBuilder buf = new StringBuilder();
+
+    @Override
+    public void write(int b) throws IOException {
+        Basic.origErr.write(b);
+        buf.append((char) b);
+    }
+
+    public String toString() {
+        return buf.toString();
+    }
+}
+
+// EOF
diff --git a/src/jloda/util/BlastFileFilter.java b/src/jloda/util/BlastFileFilter.java
new file mode 100644
index 0000000..68651d2
--- /dev/null
+++ b/src/jloda/util/BlastFileFilter.java
@@ -0,0 +1,51 @@
+/**
+ * BlastFileFilter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.FilenameFilter;
+
+/**
+ * The blast file filter
+ * Daniel Huson         2.2006
+ */
+public class BlastFileFilter extends FileFilterBase implements FilenameFilter {
+    /**
+     * constructor
+     */
+    public BlastFileFilter() {
+        add("blast");
+        add("blastx");
+        add("blastn");
+        add("blastp");
+        add("blastout");
+        add("blastxml");
+        add("tab");
+        add("blasttab");
+        add("txt");
+        add("xml");
+    }
+
+    /**
+     * @return description of file matching the filter
+     */
+    public String getBriefDescription() {
+        return "BLAST files";
+    }
+}
diff --git a/src/jloda/util/Cache.java b/src/jloda/util/Cache.java
new file mode 100644
index 0000000..8086e9d
--- /dev/null
+++ b/src/jloda/util/Cache.java
@@ -0,0 +1,67 @@
+/**
+ * Cache.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * simple RLU cache
+ * Source: http://stackoverflow.com/questions/224868/easy-simple-to-use-lru-cache-in-java
+ *
+ * @param <K> key
+ * @param <V> value
+ */
+public class Cache<K, V> {
+    final Map<K, V> MRUdata;
+    final Map<K, V> LRUdata;
+
+    public Cache(final int capacity) {
+        LRUdata = new WeakHashMap<>();
+
+        MRUdata = new LinkedHashMap<K, V>(capacity + 1, 1.0f, true) {
+            protected boolean removeEldestEntry(Map.Entry<K, V> entry) {
+                if (entry != null && this.size() > capacity) {
+                    LRUdata.put(entry.getKey(), entry.getValue());
+                    return true;
+                }
+                return false;
+            }
+        };
+    }
+
+    public V tryGet(K key) {
+        V value = MRUdata.get(key);
+        if (value != null)
+            return value;
+        value = LRUdata.get(key);
+        if (value != null) {
+            LRUdata.remove(key);
+            MRUdata.put(key, value);
+        }
+        return value;
+    }
+
+    public void set(K key, V value) {
+        LRUdata.remove(key);
+        MRUdata.put(key, value);
+    }
+}
diff --git a/src/jloda/util/CanceledException.java b/src/jloda/util/CanceledException.java
new file mode 100644
index 0000000..af9e181
--- /dev/null
+++ b/src/jloda/util/CanceledException.java
@@ -0,0 +1,36 @@
+/**
+ * CanceledException.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+/**
+ * User canceled exception
+ *
+ * @author huson
+ *         Date: 04-Dec-2003
+ */
+public class CanceledException extends Exception {
+    public CanceledException() {
+        super();
+    }
+
+    public CanceledException(String message) {
+        super(message);
+    }
+}
diff --git a/src/jloda/util/Colors.java b/src/jloda/util/Colors.java
new file mode 100644
index 0000000..d978f6d
--- /dev/null
+++ b/src/jloda/util/Colors.java
@@ -0,0 +1,704 @@
+/**
+ * Colors.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.awt.*;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * the X11 color table
+ * Daniel Huson, 11.2011
+ */
+public class Colors {
+    private final static Map<String, Color> table = new HashMap<>();
+
+    private static void init() {
+        table.put("snow", new Color(0xfffafa));
+        table.put("ghostwhite", new Color(0xf8f8ff));
+        table.put("whitesmoke", new Color(0xf5f5f5));
+        table.put("gainsboro", new Color(0xdcdcdc));
+        table.put("floralwhite", new Color(0xfffaf0));
+        table.put("oldlace", new Color(0xfdf5e6));
+        table.put("linen", new Color(0xfaf0e6));
+        table.put("antiquewhite", new Color(0xfaebd7));
+        table.put("papayawhip", new Color(0xffefd5));
+        table.put("blanchedalmond", new Color(0xffebcd));
+        table.put("bisque", new Color(0xffe4c4));
+        table.put("peachpuff", new Color(0xffdab9));
+        table.put("navajowhite", new Color(0xffdead));
+        table.put("moccasin", new Color(0xffe4b5));
+        table.put("cornsilk", new Color(0xfff8dc));
+        table.put("ivory", new Color(0xfffff0));
+        table.put("lemonchiffon", new Color(0xfffacd));
+        table.put("seashell", new Color(0xfff5ee));
+        table.put("honeydew", new Color(0xf0fff0));
+        table.put("mintcream", new Color(0xf5fffa));
+        table.put("azure", new Color(0xf0ffff));
+        table.put("aliceblue", new Color(0xf0f8ff));
+        table.put("lavender", new Color(0xe6e6fa));
+        table.put("lavenderblush", new Color(0xfff0f5));
+        table.put("mistyrose", new Color(0xffe4e1));
+        table.put("white", new Color(0xffffff));
+        table.put("black", new Color(0x000000));
+        table.put("darkslategray", new Color(0x2f4f4f));
+        table.put("darkslategrey", new Color(0x2f4f4f));
+        table.put("dimgray", new Color(0x696969));
+        table.put("dimgrey", new Color(0x696969));
+        table.put("slategray", new Color(0x708090));
+        table.put("slategrey", new Color(0x708090));
+        table.put("lightslategray", new Color(0x778899));
+        table.put("lightslategrey", new Color(0x778899));
+        table.put("gray", new Color(0xbebebe));
+        table.put("grey", new Color(0xbebebe));
+        table.put("lightgrey", new Color(0xd3d3d3));
+        table.put("lightgray", new Color(0xd3d3d3));
+        table.put("midnightblue", new Color(0x191970));
+        table.put("navy", new Color(0x000080));
+        table.put("navyblue", new Color(0x000080));
+        table.put("cornflowerblue", new Color(0x6495ed));
+        table.put("darkslateblue", new Color(0x483d8b));
+        table.put("slateblue", new Color(0x6a5acd));
+        table.put("mediumslateblue", new Color(0x7b68ee));
+        table.put("lightslateblue", new Color(0x8470ff));
+        table.put("mediumblue", new Color(0x0000cd));
+        table.put("royalblue", new Color(0x4169e1));
+        table.put("blue", new Color(0x0000ff));
+        table.put("dodgerblue", new Color(0x1e90ff));
+        table.put("deepskyblue", new Color(0x00bfff));
+        table.put("skyblue", new Color(0x87ceeb));
+        table.put("lightskyblue", new Color(0x87cefa));
+        table.put("steelblue", new Color(0x4682b4));
+        table.put("lightsteelblue", new Color(0xb0c4de));
+        table.put("lightblue", new Color(0xadd8e6));
+        table.put("powderblue", new Color(0xb0e0e6));
+        table.put("paleturquoise", new Color(0xafeeee));
+        table.put("darkturquoise", new Color(0x00ced1));
+        table.put("mediumturquoise", new Color(0x48d1cc));
+        table.put("turquoise", new Color(0x40e0d0));
+        table.put("cyan", new Color(0x00ffff));
+        table.put("lightcyan", new Color(0xe0ffff));
+        table.put("cadetblue", new Color(0x5f9ea0));
+        table.put("mediumaquamarine", new Color(0x66cdaa));
+        table.put("aquamarine", new Color(0x7fffd4));
+        table.put("darkgreen", new Color(0x006400));
+        table.put("darkolivegreen", new Color(0x556b2f));
+        table.put("darkseagreen", new Color(0x8fbc8f));
+        table.put("seagreen", new Color(0x2e8b57));
+        table.put("mediumseagreen", new Color(0x3cb371));
+        table.put("lightseagreen", new Color(0x20b2aa));
+        table.put("palegreen", new Color(0x98fb98));
+        table.put("springgreen", new Color(0x00ff7f));
+        table.put("lawngreen", new Color(0x7cfc00));
+        table.put("chartreuse", new Color(0x7fff00));
+        table.put("mediumspringgreen", new Color(0x00fa9a));
+        table.put("greenyellow", new Color(0xadff2f));
+        table.put("limegreen", new Color(0x32cd32));
+        table.put("yellowgreen", new Color(0x9acd32));
+        table.put("forestgreen", new Color(0x228b22));
+        table.put("olivedrab", new Color(0x6b8e23));
+        table.put("darkkhaki", new Color(0xbdb76b));
+        table.put("khaki", new Color(0xf0e68c));
+        table.put("palegoldenrod", new Color(0xeee8aa));
+        table.put("lightgoldenrodyellow", new Color(0xfafad2));
+        table.put("lightyellow", new Color(0xffffe0));
+        table.put("yellow", new Color(0xffff00));
+        table.put("gold", new Color(0xffd700));
+        table.put("lightgoldenrod", new Color(0xeedd82));
+        table.put("goldenrod", new Color(0xdaa520));
+        table.put("darkgoldenrod", new Color(0xb8860b));
+        table.put("rosybrown", new Color(0xbc8f8f));
+        table.put("indianred", new Color(0xcd5c5c));
+        table.put("saddlebrown", new Color(0x8b4513));
+        table.put("sienna", new Color(0xa0522d));
+        table.put("peru", new Color(0xcd853f));
+        table.put("burlywood", new Color(0xdeb887));
+        table.put("beige", new Color(0xf5f5dc));
+        table.put("wheat", new Color(0xf5deb3));
+        table.put("sandybrown", new Color(0xf4a460));
+        table.put("tan", new Color(0xd2b48c));
+        table.put("chocolate", new Color(0xd2691e));
+        table.put("firebrick", new Color(0xb22222));
+        table.put("brown", new Color(0xa52a2a));
+        table.put("darksalmon", new Color(0xe9967a));
+        table.put("salmon", new Color(0xfa8072));
+        table.put("lightsalmon", new Color(0xffa07a));
+        table.put("orange", new Color(0xffa500));
+        table.put("darkorange", new Color(0xff8c00));
+        table.put("coral", new Color(0xff7f50));
+        table.put("lightcoral", new Color(0xf08080));
+        table.put("tomato", new Color(0xff6347));
+        table.put("orangered", new Color(0xff4500));
+        table.put("red", new Color(0xff0000));
+        table.put("hotpink", new Color(0xff69b4));
+        table.put("deeppink", new Color(0xff1493));
+        table.put("pink", new Color(0xffc0cb));
+        table.put("lightpink", new Color(0xffb6c1));
+        table.put("palevioletred", new Color(0xdb7093));
+        table.put("maroon", new Color(0xb03060));
+        table.put("mediumvioletred", new Color(0xc71585));
+        table.put("violetred", new Color(0xd02090));
+        table.put("magenta", new Color(0xff00ff));
+        table.put("violet", new Color(0xee82ee));
+        table.put("plum", new Color(0xdda0dd));
+        table.put("orchid", new Color(0xda70d6));
+        table.put("mediumorchid", new Color(0xba55d3));
+        table.put("darkorchid", new Color(0x9932cc));
+        table.put("darkviolet", new Color(0x9400d3));
+        table.put("blueviolet", new Color(0x8a2be2));
+        table.put("purple", new Color(0xa020f0));
+        table.put("mediumpurple", new Color(0x9370db));
+        table.put("thistle", new Color(0xd8bfd8));
+        table.put("snow1", new Color(0xfffafa));
+        table.put("snow2", new Color(0xeee9e9));
+        table.put("snow3", new Color(0xcdc9c9));
+        table.put("snow4", new Color(0x8b8989));
+        table.put("seashell1", new Color(0xfff5ee));
+        table.put("seashell2", new Color(0xeee5de));
+        table.put("seashell3", new Color(0xcdc5bf));
+        table.put("seashell4", new Color(0x8b8682));
+        table.put("antiquewhite1", new Color(0xffefdb));
+        table.put("antiquewhite2", new Color(0xeedfcc));
+        table.put("antiquewhite3", new Color(0xcdc0b0));
+        table.put("antiquewhite4", new Color(0x8b8378));
+        table.put("bisque1", new Color(0xffe4c4));
+        table.put("bisque2", new Color(0xeed5b7));
+        table.put("bisque3", new Color(0xcdb79e));
+        table.put("bisque4", new Color(0x8b7d6b));
+        table.put("peachpuff1", new Color(0xffdab9));
+        table.put("peachpuff2", new Color(0xeecbad));
+        table.put("peachpuff3", new Color(0xcdaf95));
+        table.put("peachpuff4", new Color(0x8b7765));
+        table.put("navajowhite1", new Color(0xffdead));
+        table.put("navajowhite2", new Color(0xeecfa1));
+        table.put("navajowhite3", new Color(0xcdb38b));
+        table.put("navajowhite4", new Color(0x8b795e));
+        table.put("lemonchiffon1", new Color(0xfffacd));
+        table.put("lemonchiffon2", new Color(0xeee9bf));
+        table.put("lemonchiffon3", new Color(0xcdc9a5));
+        table.put("lemonchiffon4", new Color(0x8b8970));
+        table.put("cornsilk1", new Color(0xfff8dc));
+        table.put("cornsilk2", new Color(0xeee8cd));
+        table.put("cornsilk3", new Color(0xcdc8b1));
+        table.put("cornsilk4", new Color(0x8b8878));
+        table.put("ivory1", new Color(0xfffff0));
+        table.put("ivory2", new Color(0xeeeee0));
+        table.put("ivory3", new Color(0xcdcdc1));
+        table.put("ivory4", new Color(0x8b8b83));
+        table.put("honeydew1", new Color(0xf0fff0));
+        table.put("honeydew2", new Color(0xe0eee0));
+        table.put("honeydew3", new Color(0xc1cdc1));
+        table.put("honeydew4", new Color(0x838b83));
+        table.put("lavenderblush1", new Color(0xfff0f5));
+        table.put("lavenderblush2", new Color(0xeee0e5));
+        table.put("lavenderblush3", new Color(0xcdc1c5));
+        table.put("lavenderblush4", new Color(0x8b8386));
+        table.put("mistyrose1", new Color(0xffe4e1));
+        table.put("mistyrose2", new Color(0xeed5d2));
+        table.put("mistyrose3", new Color(0xcdb7b5));
+        table.put("mistyrose4", new Color(0x8b7d7b));
+        table.put("azure1", new Color(0xf0ffff));
+        table.put("azure2", new Color(0xe0eeee));
+        table.put("azure3", new Color(0xc1cdcd));
+        table.put("azure4", new Color(0x838b8b));
+        table.put("slateblue1", new Color(0x836fff));
+        table.put("slateblue2", new Color(0x7a67ee));
+        table.put("slateblue3", new Color(0x6959cd));
+        table.put("slateblue4", new Color(0x473c8b));
+        table.put("royalblue1", new Color(0x4876ff));
+        table.put("royalblue2", new Color(0x436eee));
+        table.put("royalblue3", new Color(0x3a5fcd));
+        table.put("royalblue4", new Color(0x27408b));
+        table.put("blue1", new Color(0x0000ff));
+        table.put("blue2", new Color(0x0000ee));
+        table.put("blue3", new Color(0x0000cd));
+        table.put("blue4", new Color(0x00008b));
+        table.put("dodgerblue1", new Color(0x1e90ff));
+        table.put("dodgerblue2", new Color(0x1c86ee));
+        table.put("dodgerblue3", new Color(0x1874cd));
+        table.put("dodgerblue4", new Color(0x104e8b));
+        table.put("steelblue1", new Color(0x63b8ff));
+        table.put("steelblue2", new Color(0x5cacee));
+        table.put("steelblue3", new Color(0x4f94cd));
+        table.put("steelblue4", new Color(0x36648b));
+        table.put("deepskyblue1", new Color(0x00bfff));
+        table.put("deepskyblue2", new Color(0x00b2ee));
+        table.put("deepskyblue3", new Color(0x009acd));
+        table.put("deepskyblue4", new Color(0x00688b));
+        table.put("skyblue1", new Color(0x87ceff));
+        table.put("skyblue2", new Color(0x7ec0ee));
+        table.put("skyblue3", new Color(0x6ca6cd));
+        table.put("skyblue4", new Color(0x4a708b));
+        table.put("lightskyblue1", new Color(0xb0e2ff));
+        table.put("lightskyblue2", new Color(0xa4d3ee));
+        table.put("lightskyblue3", new Color(0x8db6cd));
+        table.put("lightskyblue4", new Color(0x607b8b));
+        table.put("slategray1", new Color(0xc6e2ff));
+        table.put("slategray2", new Color(0xb9d3ee));
+        table.put("slategray3", new Color(0x9fb6cd));
+        table.put("slategray4", new Color(0x6c7b8b));
+        table.put("lightsteelblue1", new Color(0xcae1ff));
+        table.put("lightsteelblue2", new Color(0xbcd2ee));
+        table.put("lightsteelblue3", new Color(0xa2b5cd));
+        table.put("lightsteelblue4", new Color(0x6e7b8b));
+        table.put("lightblue1", new Color(0xbfefff));
+        table.put("lightblue2", new Color(0xb2dfee));
+        table.put("lightblue3", new Color(0x9ac0cd));
+        table.put("lightblue4", new Color(0x68838b));
+        table.put("lightcyan1", new Color(0xe0ffff));
+        table.put("lightcyan2", new Color(0xd1eeee));
+        table.put("lightcyan3", new Color(0xb4cdcd));
+        table.put("lightcyan4", new Color(0x7a8b8b));
+        table.put("paleturquoise1", new Color(0xbbffff));
+        table.put("paleturquoise2", new Color(0xaeeeee));
+        table.put("paleturquoise3", new Color(0x96cdcd));
+        table.put("paleturquoise4", new Color(0x668b8b));
+        table.put("cadetblue1", new Color(0x98f5ff));
+        table.put("cadetblue2", new Color(0x8ee5ee));
+        table.put("cadetblue3", new Color(0x7ac5cd));
+        table.put("cadetblue4", new Color(0x53868b));
+        table.put("turquoise1", new Color(0x00f5ff));
+        table.put("turquoise2", new Color(0x00e5ee));
+        table.put("turquoise3", new Color(0x00c5cd));
+        table.put("turquoise4", new Color(0x00868b));
+        table.put("cyan1", new Color(0x00ffff));
+        table.put("cyan2", new Color(0x00eeee));
+        table.put("cyan3", new Color(0x00cdcd));
+        table.put("cyan4", new Color(0x008b8b));
+        table.put("darkslategray1", new Color(0x97ffff));
+        table.put("darkslategray2", new Color(0x8deeee));
+        table.put("darkslategray3", new Color(0x79cdcd));
+        table.put("darkslategray4", new Color(0x528b8b));
+        table.put("aquamarine1", new Color(0x7fffd4));
+        table.put("aquamarine2", new Color(0x76eec6));
+        table.put("aquamarine3", new Color(0x66cdaa));
+        table.put("aquamarine4", new Color(0x458b74));
+        table.put("darkseagreen1", new Color(0xc1ffc1));
+        table.put("darkseagreen2", new Color(0xb4eeb4));
+        table.put("darkseagreen3", new Color(0x9bcd9b));
+        table.put("darkseagreen4", new Color(0x698b69));
+        table.put("seagreen1", new Color(0x54ff9f));
+        table.put("seagreen2", new Color(0x4eee94));
+        table.put("seagreen3", new Color(0x43cd80));
+        table.put("seagreen4", new Color(0x2e8b57));
+        table.put("palegreen1", new Color(0x9aff9a));
+        table.put("palegreen2", new Color(0x90ee90));
+        table.put("palegreen3", new Color(0x7ccd7c));
+        table.put("palegreen4", new Color(0x548b54));
+        table.put("springgreen1", new Color(0x00ff7f));
+        table.put("springgreen2", new Color(0x00ee76));
+        table.put("springgreen3", new Color(0x00cd66));
+        table.put("springgreen4", new Color(0x008b45));
+        table.put("green1", new Color(0x00ff00));
+        table.put("green2", new Color(0x00ee00));
+        table.put("green3", new Color(0x00cd00));
+        table.put("green4", new Color(0x008b00));
+        table.put("chartreuse1", new Color(0x7fff00));
+        table.put("chartreuse2", new Color(0x76ee00));
+        table.put("chartreuse3", new Color(0x66cd00));
+        table.put("chartreuse4", new Color(0x458b00));
+        table.put("olivedrab1", new Color(0xc0ff3e));
+        table.put("olivedrab2", new Color(0xb3ee3a));
+        table.put("olivedrab3", new Color(0x9acd32));
+        table.put("olivedrab4", new Color(0x698b22));
+        table.put("darkolivegreen1", new Color(0xcaff70));
+        table.put("darkolivegreen2", new Color(0xbcee68));
+        table.put("darkolivegreen3", new Color(0xa2cd5a));
+        table.put("darkolivegreen4", new Color(0x6e8b3d));
+        table.put("khaki1", new Color(0xfff68f));
+        table.put("khaki2", new Color(0xeee685));
+        table.put("khaki3", new Color(0xcdc673));
+        table.put("khaki4", new Color(0x8b864e));
+        table.put("lightgoldenrod1", new Color(0xffec8b));
+        table.put("lightgoldenrod2", new Color(0xeedc82));
+        table.put("lightgoldenrod3", new Color(0xcdbe70));
+        table.put("lightgoldenrod4", new Color(0x8b814c));
+        table.put("lightyellow1", new Color(0xffffe0));
+        table.put("lightyellow2", new Color(0xeeeed1));
+        table.put("lightyellow3", new Color(0xcdcdb4));
+        table.put("lightyellow4", new Color(0x8b8b7a));
+        table.put("yellow1", new Color(0xffff00));
+        table.put("yellow2", new Color(0xeeee00));
+        table.put("yellow3", new Color(0xcdcd00));
+        table.put("yellow4", new Color(0x8b8b00));
+        table.put("gold1", new Color(0xffd700));
+        table.put("gold2", new Color(0xeec900));
+        table.put("gold3", new Color(0xcdad00));
+        table.put("gold4", new Color(0x8b7500));
+        table.put("goldenrod1", new Color(0xffc125));
+        table.put("goldenrod2", new Color(0xeeb422));
+        table.put("goldenrod3", new Color(0xcd9b1d));
+        table.put("goldenrod4", new Color(0x8b6914));
+        table.put("darkgoldenrod1", new Color(0xffb90f));
+        table.put("darkgoldenrod2", new Color(0xeead0e));
+        table.put("darkgoldenrod3", new Color(0xcd950c));
+        table.put("darkgoldenrod4", new Color(0x8b6508));
+        table.put("rosybrown1", new Color(0xffc1c1));
+        table.put("rosybrown2", new Color(0xeeb4b4));
+        table.put("rosybrown3", new Color(0xcd9b9b));
+        table.put("rosybrown4", new Color(0x8b6969));
+        table.put("indianred1", new Color(0xff6a6a));
+        table.put("indianred2", new Color(0xee6363));
+        table.put("indianred3", new Color(0xcd5555));
+        table.put("indianred4", new Color(0x8b3a3a));
+        table.put("sienna1", new Color(0xff8247));
+        table.put("sienna2", new Color(0xee7942));
+        table.put("sienna3", new Color(0xcd6839));
+        table.put("sienna4", new Color(0x8b4726));
+        table.put("burlywood1", new Color(0xffd39b));
+        table.put("burlywood2", new Color(0xeec591));
+        table.put("burlywood3", new Color(0xcdaa7d));
+        table.put("burlywood4", new Color(0x8b7355));
+        table.put("wheat1", new Color(0xffe7ba));
+        table.put("wheat2", new Color(0xeed8ae));
+        table.put("wheat3", new Color(0xcdba96));
+        table.put("wheat4", new Color(0x8b7e66));
+        table.put("tan1", new Color(0xffa54f));
+        table.put("tan2", new Color(0xee9a49));
+        table.put("tan3", new Color(0xcd853f));
+        table.put("tan4", new Color(0x8b5a2b));
+        table.put("chocolate1", new Color(0xff7f24));
+        table.put("chocolate2", new Color(0xee7621));
+        table.put("chocolate3", new Color(0xcd661d));
+        table.put("chocolate4", new Color(0x8b4513));
+        table.put("firebrick1", new Color(0xff3030));
+        table.put("firebrick2", new Color(0xee2c2c));
+        table.put("firebrick3", new Color(0xcd2626));
+        table.put("firebrick4", new Color(0x8b1a1a));
+        table.put("brown1", new Color(0xff4040));
+        table.put("brown2", new Color(0xee3b3b));
+        table.put("brown3", new Color(0xcd3333));
+        table.put("brown4", new Color(0x8b2323));
+        table.put("salmon1", new Color(0xff8c69));
+        table.put("salmon2", new Color(0xee8262));
+        table.put("salmon3", new Color(0xcd7054));
+        table.put("salmon4", new Color(0x8b4c39));
+        table.put("lightsalmon1", new Color(0xffa07a));
+        table.put("lightsalmon2", new Color(0xee9572));
+        table.put("lightsalmon3", new Color(0xcd8162));
+        table.put("lightsalmon4", new Color(0x8b5742));
+        table.put("orange1", new Color(0xffa500));
+        table.put("orange2", new Color(0xee9a00));
+        table.put("orange3", new Color(0xcd8500));
+        table.put("orange4", new Color(0x8b5a00));
+        table.put("darkorange1", new Color(0xff7f00));
+        table.put("darkorange2", new Color(0xee7600));
+        table.put("darkorange3", new Color(0xcd6600));
+        table.put("darkorange4", new Color(0x8b4500));
+        table.put("coral1", new Color(0xff7256));
+        table.put("coral2", new Color(0xee6a50));
+        table.put("coral3", new Color(0xcd5b45));
+        table.put("coral4", new Color(0x8b3e2f));
+        table.put("tomato1", new Color(0xff6347));
+        table.put("tomato2", new Color(0xee5c42));
+        table.put("tomato3", new Color(0xcd4f39));
+        table.put("tomato4", new Color(0x8b3626));
+        table.put("orangered1", new Color(0xff4500));
+        table.put("orangered2", new Color(0xee4000));
+        table.put("orangered3", new Color(0xcd3700));
+        table.put("orangered4", new Color(0x8b2500));
+        table.put("red1", new Color(0xff0000));
+        table.put("red2", new Color(0xee0000));
+        table.put("red3", new Color(0xcd0000));
+        table.put("red4", new Color(0x8b0000));
+        table.put("deeppink1", new Color(0xff1493));
+        table.put("deeppink2", new Color(0xee1289));
+        table.put("deeppink3", new Color(0xcd1076));
+        table.put("deeppink4", new Color(0x8b0a50));
+        table.put("hotpink1", new Color(0xff6eb4));
+        table.put("hotpink2", new Color(0xee6aa7));
+        table.put("hotpink3", new Color(0xcd6090));
+        table.put("hotpink4", new Color(0x8b3a62));
+        table.put("pink1", new Color(0xffb5c5));
+        table.put("pink2", new Color(0xeea9b8));
+        table.put("pink3", new Color(0xcd919e));
+        table.put("pink4", new Color(0x8b636c));
+        table.put("lightpink1", new Color(0xffaeb9));
+        table.put("lightpink2", new Color(0xeea2ad));
+        table.put("lightpink3", new Color(0xcd8c95));
+        table.put("lightpink4", new Color(0x8b5f65));
+        table.put("palevioletred1", new Color(0xff82ab));
+        table.put("palevioletred2", new Color(0xee799f));
+        table.put("palevioletred3", new Color(0xcd6889));
+        table.put("palevioletred4", new Color(0x8b475d));
+        table.put("maroon1", new Color(0xff34b3));
+        table.put("maroon2", new Color(0xee30a7));
+        table.put("maroon3", new Color(0xcd2990));
+        table.put("maroon4", new Color(0x8b1c62));
+        table.put("violetred1", new Color(0xff3e96));
+        table.put("violetred2", new Color(0xee3a8c));
+        table.put("violetred3", new Color(0xcd3278));
+        table.put("violetred4", new Color(0x8b2252));
+        table.put("magenta1", new Color(0xff00ff));
+        table.put("magenta2", new Color(0xee00ee));
+        table.put("magenta3", new Color(0xcd00cd));
+        table.put("magenta4", new Color(0x8b008b));
+        table.put("orchid1", new Color(0xff83fa));
+        table.put("orchid2", new Color(0xee7ae9));
+        table.put("orchid3", new Color(0xcd69c9));
+        table.put("orchid4", new Color(0x8b4789));
+        table.put("plum1", new Color(0xffbbff));
+        table.put("plum2", new Color(0xeeaeee));
+        table.put("plum3", new Color(0xcd96cd));
+        table.put("plum4", new Color(0x8b668b));
+        table.put("mediumorchid1", new Color(0xe066ff));
+        table.put("mediumorchid2", new Color(0xd15fee));
+        table.put("mediumorchid3", new Color(0xb452cd));
+        table.put("mediumorchid4", new Color(0x7a378b));
+        table.put("darkorchid1", new Color(0xbf3eff));
+        table.put("darkorchid2", new Color(0xb23aee));
+        table.put("darkorchid3", new Color(0x9a32cd));
+        table.put("darkorchid4", new Color(0x68228b));
+        table.put("purple1", new Color(0x9b30ff));
+        table.put("purple2", new Color(0x912cee));
+        table.put("purple3", new Color(0x7d26cd));
+        table.put("purple4", new Color(0x551a8b));
+        table.put("mediumpurple1", new Color(0xab82ff));
+        table.put("mediumpurple2", new Color(0x9f79ee));
+        table.put("mediumpurple3", new Color(0x8968cd));
+        table.put("mediumpurple4", new Color(0x5d478b));
+        table.put("thistle1", new Color(0xffe1ff));
+        table.put("thistle2", new Color(0xeed2ee));
+        table.put("thistle3", new Color(0xcdb5cd));
+        table.put("thistle4", new Color(0x8b7b8b));
+        table.put("gray0", new Color(0x000000));
+        table.put("grey0", new Color(0x000000));
+        table.put("gray1", new Color(0x030303));
+        table.put("grey1", new Color(0x030303));
+        table.put("gray2", new Color(0x050505));
+        table.put("grey2", new Color(0x050505));
+        table.put("gray3", new Color(0x080808));
+        table.put("grey3", new Color(0x080808));
+        table.put("gray4", new Color(0x0a0a0a));
+        table.put("grey4", new Color(0x0a0a0a));
+        table.put("gray5", new Color(0x0d0d0d));
+        table.put("grey5", new Color(0x0d0d0d));
+        table.put("gray6", new Color(0x0f0f0f));
+        table.put("grey6", new Color(0x0f0f0f));
+        table.put("gray7", new Color(0x121212));
+        table.put("grey7", new Color(0x121212));
+        table.put("gray8", new Color(0x141414));
+        table.put("grey8", new Color(0x141414));
+        table.put("gray9", new Color(0x171717));
+        table.put("grey9", new Color(0x171717));
+        table.put("gray10", new Color(0x1a1a1a));
+        table.put("grey10", new Color(0x1a1a1a));
+        table.put("gray11", new Color(0x1c1c1c));
+        table.put("grey11", new Color(0x1c1c1c));
+        table.put("gray12", new Color(0x1f1f1f));
+        table.put("grey12", new Color(0x1f1f1f));
+        table.put("gray13", new Color(0x212121));
+        table.put("grey13", new Color(0x212121));
+        table.put("gray14", new Color(0x242424));
+        table.put("grey14", new Color(0x242424));
+        table.put("gray15", new Color(0x262626));
+        table.put("grey15", new Color(0x262626));
+        table.put("gray16", new Color(0x292929));
+        table.put("grey16", new Color(0x292929));
+        table.put("gray17", new Color(0x2b2b2b));
+        table.put("grey17", new Color(0x2b2b2b));
+        table.put("gray18", new Color(0x2e2e2e));
+        table.put("grey18", new Color(0x2e2e2e));
+        table.put("gray19", new Color(0x303030));
+        table.put("grey19", new Color(0x303030));
+        table.put("gray20", new Color(0x333333));
+        table.put("grey20", new Color(0x333333));
+        table.put("gray21", new Color(0x363636));
+        table.put("grey21", new Color(0x363636));
+        table.put("gray22", new Color(0x383838));
+        table.put("grey22", new Color(0x383838));
+        table.put("gray23", new Color(0x3b3b3b));
+        table.put("grey23", new Color(0x3b3b3b));
+        table.put("gray24", new Color(0x3d3d3d));
+        table.put("grey24", new Color(0x3d3d3d));
+        table.put("gray25", new Color(0x404040));
+        table.put("grey25", new Color(0x404040));
+        table.put("gray26", new Color(0x424242));
+        table.put("grey26", new Color(0x424242));
+        table.put("gray27", new Color(0x454545));
+        table.put("grey27", new Color(0x454545));
+        table.put("gray28", new Color(0x474747));
+        table.put("grey28", new Color(0x474747));
+        table.put("gray29", new Color(0x4a4a4a));
+        table.put("grey29", new Color(0x4a4a4a));
+        table.put("gray30", new Color(0x4d4d4d));
+        table.put("grey30", new Color(0x4d4d4d));
+        table.put("gray31", new Color(0x4f4f4f));
+        table.put("grey31", new Color(0x4f4f4f));
+        table.put("gray32", new Color(0x525252));
+        table.put("grey32", new Color(0x525252));
+        table.put("gray33", new Color(0x545454));
+        table.put("grey33", new Color(0x545454));
+        table.put("gray34", new Color(0x575757));
+        table.put("grey34", new Color(0x575757));
+        table.put("gray35", new Color(0x595959));
+        table.put("grey35", new Color(0x595959));
+        table.put("gray36", new Color(0x5c5c5c));
+        table.put("grey36", new Color(0x5c5c5c));
+        table.put("gray37", new Color(0x5e5e5e));
+        table.put("grey37", new Color(0x5e5e5e));
+        table.put("gray38", new Color(0x616161));
+        table.put("grey38", new Color(0x616161));
+        table.put("gray39", new Color(0x636363));
+        table.put("grey39", new Color(0x636363));
+        table.put("gray40", new Color(0x666666));
+        table.put("grey40", new Color(0x666666));
+        table.put("gray41", new Color(0x696969));
+        table.put("grey41", new Color(0x696969));
+        table.put("gray42", new Color(0x6b6b6b));
+        table.put("grey42", new Color(0x6b6b6b));
+        table.put("gray43", new Color(0x6e6e6e));
+        table.put("grey43", new Color(0x6e6e6e));
+        table.put("gray44", new Color(0x707070));
+        table.put("grey44", new Color(0x707070));
+        table.put("gray45", new Color(0x737373));
+        table.put("grey45", new Color(0x737373));
+        table.put("gray46", new Color(0x757575));
+        table.put("grey46", new Color(0x757575));
+        table.put("gray47", new Color(0x787878));
+        table.put("grey47", new Color(0x787878));
+        table.put("gray48", new Color(0x7a7a7a));
+        table.put("grey48", new Color(0x7a7a7a));
+        table.put("gray49", new Color(0x7d7d7d));
+        table.put("grey49", new Color(0x7d7d7d));
+        table.put("gray50", new Color(0x7f7f7f));
+        table.put("grey50", new Color(0x7f7f7f));
+        table.put("gray51", new Color(0x828282));
+        table.put("grey51", new Color(0x828282));
+        table.put("gray52", new Color(0x858585));
+        table.put("grey52", new Color(0x858585));
+        table.put("gray53", new Color(0x878787));
+        table.put("grey53", new Color(0x878787));
+        table.put("gray54", new Color(0x8a8a8a));
+        table.put("grey54", new Color(0x8a8a8a));
+        table.put("gray55", new Color(0x8c8c8c));
+        table.put("grey55", new Color(0x8c8c8c));
+        table.put("gray56", new Color(0x8f8f8f));
+        table.put("grey56", new Color(0x8f8f8f));
+        table.put("gray57", new Color(0x919191));
+        table.put("grey57", new Color(0x919191));
+        table.put("gray58", new Color(0x949494));
+        table.put("grey58", new Color(0x949494));
+        table.put("gray59", new Color(0x969696));
+        table.put("grey59", new Color(0x969696));
+        table.put("gray60", new Color(0x999999));
+        table.put("grey60", new Color(0x999999));
+        table.put("gray61", new Color(0x9c9c9c));
+        table.put("grey61", new Color(0x9c9c9c));
+        table.put("gray62", new Color(0x9e9e9e));
+        table.put("grey62", new Color(0x9e9e9e));
+        table.put("gray63", new Color(0xa1a1a1));
+        table.put("grey63", new Color(0xa1a1a1));
+        table.put("gray64", new Color(0xa3a3a3));
+        table.put("grey64", new Color(0xa3a3a3));
+        table.put("gray65", new Color(0xa6a6a6));
+        table.put("grey65", new Color(0xa6a6a6));
+        table.put("gray66", new Color(0xa8a8a8));
+        table.put("grey66", new Color(0xa8a8a8));
+        table.put("gray67", new Color(0xababab));
+        table.put("grey67", new Color(0xababab));
+        table.put("gray68", new Color(0xadadad));
+        table.put("grey68", new Color(0xadadad));
+        table.put("gray69", new Color(0xb0b0b0));
+        table.put("grey69", new Color(0xb0b0b0));
+        table.put("gray70", new Color(0xb3b3b3));
+        table.put("grey70", new Color(0xb3b3b3));
+        table.put("gray71", new Color(0xb5b5b5));
+        table.put("grey71", new Color(0xb5b5b5));
+        table.put("gray72", new Color(0xb8b8b8));
+        table.put("grey72", new Color(0xb8b8b8));
+        table.put("gray73", new Color(0xbababa));
+        table.put("grey73", new Color(0xbababa));
+        table.put("gray74", new Color(0xbdbdbd));
+        table.put("grey74", new Color(0xbdbdbd));
+        table.put("gray75", new Color(0xbfbfbf));
+        table.put("grey75", new Color(0xbfbfbf));
+        table.put("gray76", new Color(0xc2c2c2));
+        table.put("grey76", new Color(0xc2c2c2));
+        table.put("gray77", new Color(0xc4c4c4));
+        table.put("grey77", new Color(0xc4c4c4));
+        table.put("gray78", new Color(0xc7c7c7));
+        table.put("grey78", new Color(0xc7c7c7));
+        table.put("gray79", new Color(0xc9c9c9));
+        table.put("grey79", new Color(0xc9c9c9));
+        table.put("gray80", new Color(0xcccccc));
+        table.put("grey80", new Color(0xcccccc));
+        table.put("gray81", new Color(0xcfcfcf));
+        table.put("grey81", new Color(0xcfcfcf));
+        table.put("gray82", new Color(0xd1d1d1));
+        table.put("grey82", new Color(0xd1d1d1));
+        table.put("gray83", new Color(0xd4d4d4));
+        table.put("grey83", new Color(0xd4d4d4));
+        table.put("gray84", new Color(0xd6d6d6));
+        table.put("grey84", new Color(0xd6d6d6));
+        table.put("gray85", new Color(0xd9d9d9));
+        table.put("grey85", new Color(0xd9d9d9));
+        table.put("gray86", new Color(0xdbdbdb));
+        table.put("grey86", new Color(0xdbdbdb));
+        table.put("gray87", new Color(0xdedede));
+        table.put("grey87", new Color(0xdedede));
+        table.put("gray88", new Color(0xe0e0e0));
+        table.put("grey88", new Color(0xe0e0e0));
+        table.put("gray89", new Color(0xe3e3e3));
+        table.put("grey89", new Color(0xe3e3e3));
+        table.put("gray90", new Color(0xe5e5e5));
+        table.put("grey90", new Color(0xe5e5e5));
+        table.put("gray91", new Color(0xe8e8e8));
+        table.put("grey91", new Color(0xe8e8e8));
+        table.put("gray92", new Color(0xebebeb));
+        table.put("grey92", new Color(0xebebeb));
+        table.put("gray93", new Color(0xededed));
+        table.put("grey93", new Color(0xededed));
+        table.put("gray94", new Color(0xf0f0f0));
+        table.put("grey94", new Color(0xf0f0f0));
+        table.put("gray95", new Color(0xf2f2f2));
+        table.put("grey95", new Color(0xf2f2f2));
+        table.put("gray96", new Color(0xf5f5f5));
+        table.put("grey96", new Color(0xf5f5f5));
+        table.put("gray97", new Color(0xf7f7f7));
+        table.put("grey97", new Color(0xf7f7f7));
+        table.put("gray98", new Color(0xfafafa));
+        table.put("grey98", new Color(0xfafafa));
+        table.put("gray99", new Color(0xfcfcfc));
+        table.put("grey99", new Color(0xfcfcfc));
+        table.put("gray100", new Color(0xffffff));
+        table.put("grey100", new Color(0xffffff));
+        table.put("darkgrey", new Color(0xa9a9a9));
+        table.put("darkgray", new Color(0xa9a9a9));
+        table.put("darkblue", new Color(0x00008b));
+        table.put("darkcyan", new Color(0x008b8b));
+        table.put("darkmagenta", new Color(0x8b008b));
+        table.put("darkred", new Color(0x8b0000));
+        table.put("lightgreen", new Color(0x90ee90));
+    }
+
+    /**
+     * parse a color
+     *
+     * @param name
+     * @return color or null
+     */
+    public static Color parseColor(String name) {
+        if (table.size() == 0)
+            init();
+        return table.get(name.toLowerCase());
+    }
+
+}
diff --git a/src/jloda/util/CommandLineOptions.java b/src/jloda/util/CommandLineOptions.java
new file mode 100644
index 0000000..043da18
--- /dev/null
+++ b/src/jloda/util/CommandLineOptions.java
@@ -0,0 +1,898 @@
+/**
+ * CommandLineOptions.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**@version $Id: CommandLineOptions.java,v 1.22 2007-07-15 11:02:36 huson Exp $
+ *
+ * Unix style command line option handling
+ *
+ *@author Daniel Huson
+ * 11.02
+ */
+package jloda.util;
+
+import javax.swing.*;
+import javax.swing.table.AbstractTableModel;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * Unix style command line option handling
+ * <p/>
+ * Command-line mode:
+ * Construct object, use getOption is query options, then call done() to verify that
+ * all options have been found
+ * <p/>
+ * GUI mode:
+ * (1) must specify -arggui as a boolean option using getOption
+ * (2) all getOption calls must be contained in a do{} while() loop with condition !done()
+ * GUI mode is used when commandline -arggui is provided
+ */
+public class CommandLineOptions {
+    private String description;
+    private String[] args;
+    private boolean[] seen;
+    private final List<String> usage;
+    private final List<String> settings;
+    private final List<String> options;
+    private boolean exitOnHelp;
+
+    private int stage = 0; // 0: doing normal commandline string processing
+    // 1-2: gui processing, 1: collecting options to build GUI, 2: looping to get entered values
+    private GUI gui = null;
+    private boolean doHelp = false;
+
+    /**
+     * construct a command line options parser
+     *
+     * @param args
+     */
+    public CommandLineOptions(String[] args) {
+        this(args, true);
+    }
+
+    /**
+     * construct a command line options parser
+     *
+     * @param args
+     * @param exitOnHelp
+     */
+    public CommandLineOptions(String[] args, boolean exitOnHelp) {
+        description = "Main program";
+        this.args = args;
+        seen = new boolean[args.length];
+        usage = new LinkedList<>();
+        settings = new LinkedList<>();
+        options = new LinkedList<>();
+        this.exitOnHelp = exitOnHelp;
+
+        // scan to see whether arguments are to be set by GUI, if so, set stage=1:
+        for (String arg1 : args) {
+            if (arg1.equals("-arggui")) {
+                stage = 1;
+                gui = new GUI();
+                break;
+            }
+        }
+
+        // scan to see whether arguments are for help:
+        for (String arg : args) {
+            if (arg.equals("-h")) {
+                doHelp = true;
+                break;
+            }
+        }
+    }
+
+    /**
+     * Returns a string option
+     *
+     * @param label    the option label
+     * @param describe a short description
+     * @param def      the default value
+     * @return the value following the label
+     */
+    public String getOption(String label, String describe, String def) throws UsageException {
+        options.add(grow20(label));
+        if (describe.charAt(0) != '!')
+            usage.add(grow20(label + " <String>") + " (default=\"" + def + "\"): " + describe);
+        else
+            usage.add(null);
+        String val = getStringOption(label, def, describe, false);
+        if (describe.charAt(0) != '!') {
+            settings.add("" + val);
+            if (stage == 1) {
+                gui.addRow(label, describe, val);
+            }
+        } else
+            settings.add(null);
+        return val;
+    }
+
+    /**
+     * Returns a string option
+     *
+     * @param label    the option label
+     * @param describe a short description
+     * @param def      the default value
+     * @return the value following the label
+     */
+    public String getOption(String label, String describe, String[] legalValues, String def)
+            throws UsageException {
+        options.add(grow20(label));
+        String str = grow20(label + " <String>") + " (default=\"" + def + "\", legal=";
+        boolean first = true;
+        for (String legalValue : legalValues) {
+            if (first)
+                first = false;
+            else
+                str += ",";
+            str += "\"" + legalValue + "\"";
+        }
+        str += ") " + describe;
+        if (describe.charAt(0) != '!')
+            usage.add(str);
+        else
+            usage.add(null);
+        String val = getStringOption(label, def, describe, false);
+        boolean ok = false;
+        for (int i = 0; !ok && i < legalValues.length; i++)
+            if (legalValues[i].equalsIgnoreCase(val))
+                ok = true;
+        if (!ok)
+            throw new UsageException("Option " + label + ": illegal value: " + val);
+
+        if (describe.charAt(0) != '!') {
+            settings.add("" + val);
+            if (stage == 1) {
+                gui.addRow(label, describe, val);
+            }
+        } else
+            settings.add(null);
+        return val;
+    }
+
+    /**
+     * Returns the value of a mandatory string option
+     *
+     * @param label    the option label
+     * @param describe a short description
+     * @param def      the default value
+     * @return the value following the label
+     */
+    public String getMandatoryOption(String label, String describe, String def)
+            throws UsageException {
+        options.add(grow20(label));
+        usage.add(grow20(label + " <String>") + " (default=\"" + def + "\"): " + describe + " (mandatory option)");
+
+        String val = getStringOption(label, def, describe, true);
+        settings.add(val);
+        if (stage == 1) {
+            gui.addRow(label, describe + " (mandatory option)", val);
+        }
+
+        return val;
+    }
+
+    /**
+     * Returns an integer option
+     *
+     * @param label    the option label
+     * @param describe a short description
+     * @param def      the default value
+     * @return the value following the label
+     */
+    public int getOption(String label, String describe, int def)
+            throws UsageException {
+        options.add(grow20(label));
+        if (describe.charAt(0) != '!')
+            usage.add(grow20(label + " <int>") + " (default=" + def + "): " + describe);
+        else
+            usage.add(null);
+        try {
+            String val = getStringOption(label, Integer.toString(def), describe, false);
+            if (describe.charAt(0) != '!') {
+                settings.add("" + Integer.parseInt(val));
+                if (stage == 1) {
+                    gui.addRow(label, describe, val);
+                }
+
+            } else
+                settings.add(null);
+            return Integer.parseInt(val);
+        } catch (Exception ex) {
+            throw new UsageException("option  " + label + ": integer expected");
+        }
+    }
+
+    /**
+     * Returns the value of a mandatory integer option
+     *
+     * @param label    the option label
+     * @param describe a short description
+     * @param def      the default value
+     * @return the value following the label
+     */
+    public int getMandatoryOption(String label, String describe, int def)
+            throws UsageException {
+        usage.add(grow20("mandatory: " + label + " <int>") + " (default=" + def + "): " + describe);
+        options.add(grow20(label));
+        try {
+            String val = getStringOption(label, Integer.toString(def), describe, true);
+            settings.add("" + Integer.parseInt(val));
+            if (stage == 1) {
+                gui.addRow(label, describe + " (mandatory option)", val);
+            }
+            return Integer.parseInt(val);
+        } catch (Exception ex) {
+            throw new UsageException("option  " + label + ": integer expected");
+        }
+    }
+
+
+    /**
+     * Returns an integer option
+     *
+     * @param label    the option label
+     * @param describe a short description
+     * @param def      the default value
+     * @return the value following the label
+     */
+    public long getOption(String label, String describe, long def)
+            throws UsageException {
+        options.add(grow20(label));
+        if (describe.charAt(0) != '!')
+            usage.add(grow20(label + " <long>") + " (default=" + def + "): " + describe);
+        else
+            usage.add(null);
+        try {
+            String val = getStringOption(label, Long.toString(def), describe, false);
+            if (describe.charAt(0) != '!') {
+                settings.add("" + Long.parseLong(val));
+                if (stage == 1) {
+                    gui.addRow(label, describe, val);
+                }
+
+            } else
+                settings.add(null);
+            return Long.parseLong(val);
+        } catch (Exception ex) {
+            throw new UsageException("option  " + label + ": long expected");
+        }
+    }
+
+    /**
+     * Returns the value of a mandatory longeger option
+     *
+     * @param label    the option label
+     * @param describe a short description
+     * @param def      the default value
+     * @return the value following the label
+     */
+    public long getMandatoryOption(String label, String describe, long def)
+            throws UsageException {
+        usage.add(grow20("mandatory: " + label + " <long>") + " (default=" + def + "): " + describe);
+        options.add(grow20(label));
+        try {
+            String val = getStringOption(label, Long.toString(def), describe, true);
+            settings.add("" + Long.parseLong(val));
+            if (stage == 1) {
+                gui.addRow(label, describe + " (mandatory option)", val);
+            }
+            return Long.parseLong(val);
+        } catch (Exception ex) {
+            throw new UsageException("option  " + label + ": Long expected");
+        }
+    }
+
+
+    /**
+     * Returns a double option
+     *
+     * @param label    the option label
+     * @param describe a short description
+     * @param def      the default value
+     * @return the value following the label
+     */
+    public double getOption(String label, String describe, double def) throws UsageException {
+        options.add(grow20(label));
+        if (describe.charAt(0) != '!')
+            usage.add(grow20(label + " <double>") + " (default=" + def + "): " + describe);
+        else
+            usage.add(null);
+        try {
+            String val = getStringOption(label, Double.toString(def), describe, false);
+            if (describe.charAt(0) != '!') {
+                settings.add("" + Double.parseDouble(val));
+                if (stage == 1) {
+                    gui.addRow(label, describe, val);
+                }
+            } else
+                settings.add(null);
+            return Double.parseDouble(val);
+        } catch (Exception ex) {
+            throw new UsageException("option  " + label + ": double expected");
+        }
+    }
+
+    /**
+     * Returns the value of a mandatory double option
+     *
+     * @param label    the option label
+     * @param describe a short description
+     * @param def      the default value
+     * @return the value following the label
+     */
+    public double getMandatoryOption(String label, String describe, double def)
+            throws UsageException {
+        options.add(grow20(label));
+        usage.add(grow20("mandatory: " + label + " <double>") + " (default=" + def + "): " + describe);
+        try {
+            String val = getStringOption(label, Double.toString(def), describe, true);
+            settings.add("" + Double.parseDouble(val));
+            if (stage == 1) {
+                gui.addRow(label, describe + " (mandatory option)", val);
+            }
+            return Double.parseDouble(val);
+        } catch (Exception ex) {
+            throw new UsageException("option  " + label + ": double expected");
+        }
+    }
+
+    /**
+     * Returns a specified value, if the named label is a command line
+     * option, otherwise returns the default value.
+     * If description label starts with !, then this is a secret and undocumented option
+     *
+     * @param label    the option label
+     * @param describe
+     * @param result   the result returned if the option is present
+     * @param def      the default value
+     * @return the value following the label
+     */
+    public boolean getOption(String label, String describe, boolean result, boolean def) {
+        if (label.startsWith("+")) {
+            if (def)
+                label = "-" + label.substring(1, label.length());
+            else
+                throw new RuntimeException("Internal error: '+' switch must have default=true");
+        }
+
+        options.add(grow20(label));
+        if (describe.charAt(0) != '!')
+            usage.add(grow20(label + " <switch>") + " (default=" + def + "): " + describe);
+        else
+            usage.add(null);
+        for (int i = 0; i < args.length; i++) {
+            String arg = args[i];
+            if (arg.length() > 1 && arg.charAt(0) == '+' && label.length() > 1 && arg.substring(1, arg.length()).equals(label.substring(1, label.length())))
+                args[i] = "-" + arg.substring(1, arg.length());
+            if (!seen[i] && args[i].equals(label)) {
+                seen[i] = true;
+                if (describe.charAt(0) != '!') {
+                    settings.add("" + result);
+                    if (stage == 1) {
+                        gui.addRow(label, describe, "" + result);
+                    }
+                } else
+                    settings.add(null);
+
+                if (i + 1 < args.length && !seen[i + 1]) {
+                    if (args[i + 1].equalsIgnoreCase("true")) {
+                        seen[i + 1] = true;
+                        return true;
+                    } else if (args[i + 1].equalsIgnoreCase("false")) {
+                        seen[i + 1] = true;
+                        return false;
+                    }
+                }
+                return result;
+            }
+        }
+        if (describe.charAt(0) != '!') {
+            settings.add("" + def);
+            if (stage == 1) {
+                gui.addRow(label, describe, "" + def);
+            }
+        } else
+            settings.add(null);
+        return def;
+    }
+
+    /**
+     * Results a list of tokens following the given label.
+     * All tokens following the given label are returned up until before
+     * the next token that has not yet been grabbed by a call to getOption.
+     *
+     * @param label the option label
+     * @param def   the default value
+     * @return the tokens following the label
+     */
+    public List<String> getOption(String label, String describe, List<String> def) {
+        options.add(grow20(label));
+        if (describe.charAt(0) != '!')
+            usage.add(grow20(label + " <String*>") + " (default=" + def + "): " + describe);
+        else
+            usage.add(null);
+        List<String> result = new LinkedList<>();
+
+        boolean found = false;
+        for (int i = 0; i < seen.length; i++) {
+            if (!found && !seen[i] && args[i].equals(label)) {
+                seen[i] = true;
+                found = true;
+            } else if (found && !seen[i] && !args[i].startsWith("-")) {
+                result.add(args[i]);
+                seen[i] = true;
+            } else if (found && (seen[i] || args[i].startsWith("-")))
+                break;
+        }
+        if (found) {
+            if (describe.charAt(0) != '!') {
+                settings.add("" + result);
+                if (stage == 1) {
+                    gui.addRow(label, describe, "" + Basic.listAsString(result, " "));
+                }
+            } else
+                settings.add(null);
+            return result;
+        } else {
+            if (describe.charAt(0) != '!') {
+                settings.add("" + def);
+                if (stage == 1) {
+                    gui.addRow(label, describe, "" + def);
+                }
+            } else
+                settings.add(null);
+            return def;
+        }
+    }
+
+    /**
+     * Results is a list of tokens following the given label.
+     * All tokens following the given label are returned up until before
+     * the next token that has not yet been grabbed by a call to getOption.
+     *
+     * @param label the option label
+     * @param def   the default value
+     * @return the tokens following the label
+     */
+    public String[] getOption(String label, String describe, String[] def)
+            throws UsageException {
+        List<String> result = getOption(label, describe, Arrays.asList(def));
+        if (result == null)
+            return null;
+        else
+            return result.toArray(new String[result.size()]);
+    }
+
+
+    /**
+     * Returns a mandatory list of tokens following the given label.
+     * All tokens following the given label are returned up until before
+     * the next token that has not yet been grabbed by a call to getOption.
+     *
+     * @param label the option label
+     * @param def   the default value
+     * @return the tokens following the label
+     */
+    public List<String> getMandatoryOption(String label, String describe, List<String> def)
+            throws UsageException {
+        options.add(grow20(label));
+        if (describe.charAt(0) != '!')
+            usage.add(grow20("mandatory: " + label + " <String*>") + " (default=" + def + "): " + describe);
+        else usage.add(null);
+        List<String> result = new LinkedList<>();
+
+        boolean found = false;
+        for (int i = 0; i < seen.length; i++) {
+            if (!found && !seen[i] && args[i].equals(label)) {
+                seen[i] = true;
+                found = true;
+            } else if (found && !seen[i] && !args[i].startsWith("-") && !args[i].startsWith("+")) {
+                result.add(args[i]);
+                seen[i] = true;
+            } else if (found && (seen[i] || args[i].startsWith("-") || args[i].startsWith("+")))
+                break;
+        }
+        if (found || stage == 1) {
+            if (describe.charAt(0) != '!') {
+                settings.add("" + result);
+                if (stage == 1) {
+                    gui.addRow(label, describe + " (mandatory option)", Basic.listAsString(result, " "));
+                }
+            } else
+                settings.add(null);
+            return result;
+        } else {
+            if (!doHelp)
+                throw new UsageException("mandatory option: " + label + " (" + describe + ")");
+
+            else
+                exitOnHelp = true; // mandatory option missing, show help then quit
+            return result;
+        }
+    }
+
+    /**
+     * Results a mandatory list of tokens following the given label.
+     * All tokens following the given label are returned up until before
+     * the next token that has not yet been grabbed by a call to getOption.
+     *
+     * @param label the option label
+     * @param def   the default value
+     * @return the tokens following the label
+     */
+    public String[] getMandatoryOption(String label, String describe, String[] def)
+            throws UsageException {
+        List<String> result = getMandatoryOption(label, describe, Arrays.asList(def));
+        if (result == null)
+            return null;
+        else
+            return result.toArray(new String[result.size()]);
+    }
+
+
+    /* does the work */
+
+    private String getStringOption(String label, String def, String describe, boolean mandatory) throws
+            UsageException {
+        for (int i = 0; i < seen.length; i++) {
+            if (!seen[i] && args[i].equals(label)) {
+                seen[i] = true;
+                if (i + 1 == seen.length || seen[i + 1])
+                    throw new UsageException
+                            ("option " + label + ": missing argument" + " (" + describe + ")");
+                else {
+                    seen[i + 1] = true;
+                    return args[i + 1];
+                }
+            }
+        }
+        if (mandatory && stage != 1) {
+            if (doHelp)
+                exitOnHelp = true;
+            else
+                throw new UsageException("mandatory option: " + label + " (" + describe + ")");
+        }
+        return def;
+    }
+
+
+    /**
+     * Sets the program description
+     *
+     * @param description the description
+     */
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * Call this after processing all options to check for superfluous
+     * options
+     */
+    public boolean done() throws UsageException {
+        if (stage == 0 || stage == 2) {
+            try {
+                boolean help = getOption("-h", "Show usage", true, false);
+                if (help) {
+                    String str = "\n" + description + "\n";
+                    str += "\nProgram usage:\n";
+                    for (String anUsage : usage)
+                        if (anUsage != null)
+                            str += "\t" + anUsage + "\n";
+                    str += "\n";
+                    System.out.print(str);
+                    if (getExitOnHelp())
+                        System.exit(0);
+                }
+                for (int i = 0; i < args.length; i++) {
+                    if (!seen[i]) {
+                        String str = "\n" + description + "\n";
+                        str += "Illegal option: '" + args[i] + "'\n";
+                        str += "\nProgram usage:\n";
+                        for (String anUsage : usage)
+                            if (anUsage != null)
+                                str += "\t" + anUsage + "\n";
+                        str += "\n";
+                        throw new UsageException(str);
+                    }
+                }
+            } catch (UsageException ex) {
+                if (stage == 2) {
+                    new Alert(null, "Usage exception: " + ex);
+                    stage = 1;
+                    return false;
+                } else
+                    throw ex;
+            }
+            return true;
+        } else // stage==1: have setup GUI, now show the GUI, get the values, modify the arg string and rerun
+        {
+            gui.finishAndShow();
+            args = gui.getArgs();
+            System.err.println("Command-line arguments:");
+            if (args != null) {
+                for (String arg : args) {
+                    System.err.print(" " + arg);
+                }
+                System.err.println();
+            }
+
+            stage = 2;
+            // prepare to redo:
+            if (args != null)
+                seen = new boolean[args.length];
+            usage.clear();
+            settings.clear();
+            options.clear();
+
+            return false;
+        }
+    }
+
+    /**
+     * Gets the set options as a string
+     *
+     * @return the set options
+     */
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("\n");
+        for (int i = 0; i < options.size(); i++) {
+            if (options.get(i) != null && settings.get(i) != null)
+                buf.append("\t").append(options.get(i)).append("= ").append(settings.get(i)).append("\n");
+        }
+        return buf.toString();
+    }
+
+    /**
+     * Gets the set options as a string
+     *
+     * @return the set options
+     */
+    public String toOptionsString() {
+        StringBuilder buf = new StringBuilder();
+        for (int i = 0; i < options.size(); i++) {
+            if (options.get(i) != null && settings.get(i) != null) {
+                buf.append(" ").append(options.get(i).trim()).append(" ").append(settings.get(i));
+            }
+        }
+        return buf.toString();
+    }
+
+    /**
+     * exit after displaying program help?
+     *
+     * @return exit on help?
+     */
+    public boolean getExitOnHelp() {
+        return exitOnHelp;
+    }
+
+    /**
+     * exit after displaying program help?
+     *
+     * @param exitOnHelp
+     */
+    public void setExitOnHelp(boolean exitOnHelp) {
+        this.exitOnHelp = exitOnHelp;
+    }
+
+    /**
+     * add a label to the usage message
+     *
+     * @param label
+     */
+    public void addLabel(String label) {
+        options.add(null);
+        usage.add("\n  " + label);
+        settings.add(null);
+        if (stage == 1)
+            gui.addLabel(label);
+    }
+
+    /**
+     * grow a label to length 20
+     *
+     * @param label
+     * @return label of length at least 20
+     */
+    private String grow20(String label) {
+        while (label.length() < 20) {
+            label += " ";
+        }
+        return label;
+    }
+
+    /**
+     * gets the GUI
+     *
+     * @return the GUI or null
+     */
+    public JDialog getGUI() {
+        return gui;
+    }
+
+    /**
+     * the commandline option GUI
+     */
+    private class GUI extends JDialog {
+        private final Vector<Vector<String>> data = new Vector<>();
+        private JTable table = null;
+        private String[] args = null;
+
+        GUI() {
+            super();
+            setSize(800, 500);
+            setLocation(100, 100);
+            setModal(true);
+            getContentPane().setLayout(new BorderLayout());
+            addWindowListener(new WindowAdapter() {
+                public void windowClosing(WindowEvent windowEvent) {
+                    System.exit(0);
+                }
+            });
+        }
+
+        void addRow(String label, String description, String defaultValue) {
+            Vector<String> row = new Vector<>();
+            row.add(label);
+            row.add(description);
+            row.add(defaultValue);
+            data.add(row);
+        }
+
+
+        void addLabel(String label) {
+            Vector<String> row = new Vector<>();
+            row.add(label);
+            row.add("");
+            row.add("");
+            data.add(row);
+        }
+
+        // finish gui and show
+
+        private void finishAndShow() {
+            table = new JTable(new AbstractTableModel() {
+                private final String[] columnNames = {"Option", "Description", "Value"};
+
+                public int getColumnCount() {
+                    return columnNames.length;
+                }
+
+                public int getRowCount() {
+                    return data.size();
+                }
+
+                public String getColumnName(int col) {
+                    return columnNames[col];
+                }
+
+                public Object getValueAt(int row, int col) {
+                    return ((Vector) data.elementAt(row)).elementAt(col);
+                }
+
+                public Class getColumnClass(int c) {
+                    return String.class;
+                }
+
+                public boolean isCellEditable(int row, int col) {
+                    return !(col < 2 || getValueAt(row, 1).equals(""));
+                }
+
+                public void setValueAt(Object value, int row, int col) {
+                    data.elementAt(row).setElementAt((String) value, col);
+                    fireTableCellUpdated(row, col);
+                }
+            });
+            table.getColumnModel().getColumn(0).setPreferredWidth(200);
+            table.getColumnModel().getColumn(1).setPreferredWidth(600);
+            table.getColumnModel().getColumn(2).setPreferredWidth(100);
+            table.setShowVerticalLines(true);
+            table.setShowHorizontalLines(true);
+
+            JScrollPane scrollPane = new JScrollPane(table);
+            table.setPreferredScrollableViewportSize(new Dimension(400, 70));
+            getContentPane().add(scrollPane, BorderLayout.CENTER);
+
+            JPanel bottomPanel = new JPanel();
+            bottomPanel.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 50));
+            bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.X_AXIS));
+
+            JButton cancelButton = new JButton("Cancel");
+            cancelButton.addActionListener(new AbstractAction() {
+                public void actionPerformed(ActionEvent actionEvent) {
+                    System.err.println("User canceled");
+                    System.exit(0);
+                }
+            });
+            bottomPanel.add(cancelButton);
+
+            JButton applyButton = new JButton("Apply");
+            applyButton.addActionListener(new AbstractAction() {
+                public void actionPerformed(ActionEvent actionEvent) {
+                    String missingOption = makeArgs();
+                    if (missingOption == null)
+                        setVisible(false);
+                    else // mandatory options   missing
+                    {
+                        new Alert("Mandatory option '" + missingOption + "' has not been supplied");
+                    }
+                }
+            });
+            bottomPanel.add(applyButton);
+            JPanel wrapper = new JPanel();
+            wrapper.setLayout(new BorderLayout());
+            wrapper.add(bottomPanel, BorderLayout.EAST);
+            getContentPane().add(wrapper, BorderLayout.SOUTH);
+
+            setTitle("Command line arguments for " + description); // this late because program description is set after GUi is constructed
+            setModal(true);
+            setVisible(true);
+        }
+
+        String[] getArgs() {
+            return args;
+        }
+
+        /**
+         * returns label for missing mandatory option, or null, if everything is fine
+         *
+         * @return missing option or null
+         */
+        private String makeArgs() {
+            String missingOption = null;
+            List<String> list = new LinkedList<>();
+            boolean ok = false;
+            for (int i = 0; i < table.getRowCount(); i++) {
+                String label = (String) table.getModel().getValueAt(i, 0);
+                if (!label.equals("-arggui"))
+                    ok = true;
+                String description = (String) table.getModel().getValueAt(i, 1);
+                String value = (String) table.getModel().getValueAt(i, 2);
+
+                if (value.length() > 0) {
+                    list.add(label);
+                    list.add(value);
+                } else if (missingOption == null && description.contains("(mandatory option)"))
+                    missingOption = label;
+            }
+            args = list.toArray(new String[list.size()]);
+            if (!ok)
+                throw new RuntimeException("Internal error: not setup for -arggui option");
+            return missingOption;
+        }
+    }
+
+
+}
+
+
diff --git a/src/jloda/util/ConvexHull.java b/src/jloda/util/ConvexHull.java
new file mode 100644
index 0000000..87333c1
--- /dev/null
+++ b/src/jloda/util/ConvexHull.java
@@ -0,0 +1,175 @@
+/**
+ * ConvexHull.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+/*
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * No usage, copying or distribution without explicit permission.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+*/
+
+import java.awt.geom.Point2D;
+import java.util.ArrayList;
+
+/**
+ * computes convex hull for a collection of two dimensional points
+ * <p>
+ * daniel huson, 4.2015
+ */
+public class ConvexHull {
+    /**
+     * computes the convex hull of a set of two-dimensional points using the quick hull algorithm
+     *
+     * @param points0
+     * @return convex hull
+     */
+    public static ArrayList<Point2D> quickHull(final ArrayList<Point2D> points0) {
+        final ArrayList<Point2D> points = new ArrayList<>();
+        points.addAll(points0);
+
+        final ArrayList<Point2D> convexHull = new ArrayList<>();
+        if (points.size() <= 3) {
+            ArrayList<Point2D> result = new ArrayList<>(points.size());
+            result.addAll(points);
+            return result;
+        }
+        int minPointIndex = -1;
+        int maxPointIndex = -1;
+        double minX = Double.MAX_VALUE;
+        double maxX = Double.NEGATIVE_INFINITY;
+        for (int i = 0; i < points.size(); i++) {
+            final Point2D apt = points.get(i);
+            if (apt.getX() < minX) {
+                minX = apt.getX();
+                minPointIndex = i;
+            }
+            if (apt.getX() > maxX) {
+                maxX = apt.getX();
+                maxPointIndex = i;
+            }
+        }
+        final Point2D a = points.get(minPointIndex);
+        final Point2D b = points.get(maxPointIndex);
+        convexHull.add(a);
+        convexHull.add(b);
+        points.remove(a);
+        points.remove(b);
+
+        final ArrayList<Point2D> leftSet = new ArrayList<>();
+        final ArrayList<Point2D> rightSet = new ArrayList<>();
+
+        for (Point2D p : points) {
+            if (!isLeftOf(a, b, p))
+                leftSet.add(p);
+            else
+                rightSet.add(p);
+        }
+        hullSet(a, b, rightSet, convexHull);
+        hullSet(b, a, leftSet, convexHull);
+
+        return convexHull;
+    }
+
+    /**
+     * compute the hull set
+     *
+     * @param a
+     * @param b
+     * @param set
+     * @param hull
+     */
+    private static void hullSet(final Point2D a, final Point2D b, final ArrayList<Point2D> set, final ArrayList<Point2D> hull) {
+        if (set.size() == 0) return;
+
+        if (set.size() == 1) {
+            Point2D p = set.get(0);
+            set.remove(p);
+            final int insertPosition = hull.indexOf(b);
+            hull.add(insertPosition, p);
+            return;
+        }
+
+        double maxDistance = Double.NEGATIVE_INFINITY;
+        int maxDistancePointIndex = -1;
+        for (int i = 0; i < set.size(); i++) {
+            final Point2D p = set.get(i);
+            double distance = distance(a, b, p);
+            if (distance > maxDistance) {
+                maxDistance = distance;
+                maxDistancePointIndex = i;
+            }
+        }
+
+        final Point2D p = set.get(maxDistancePointIndex);
+        set.remove(maxDistancePointIndex);
+        final int insertPosition = hull.indexOf(b);
+        hull.add(insertPosition, p);
+
+        // Determine who's to the left of a
+        final ArrayList<Point2D> leftOfA = new ArrayList<>();
+        for (final Point2D m : set) {
+            if (isLeftOf(a, p, m)) {
+                leftOfA.add(m);
+            }
+        }
+
+        // Determine who's to the left of b
+        final ArrayList<Point2D> leftOfB = new ArrayList<>();
+        for (final Point2D m : set) {
+            if (isLeftOf(p, b, m)) {
+                leftOfB.add(m);
+            }
+        }
+
+        hullSet(a, p, leftOfA, hull);
+        hullSet(p, b, leftOfB, hull);
+    }
+
+    /**
+     * is z to the left of the line from a to b?
+     *
+     * @param a
+     * @param b
+     * @param z
+     * @return true, if z to left of line from a to b
+     */
+    private static boolean isLeftOf(final Point2D a, final Point2D b, final Point2D z) {
+        return ((b.getX() - a.getX()) * (z.getY() - a.getY()) - (b.getY() - a.getY()) * (z.getX() - a.getX())) > 0;
+    }
+
+    /**
+     * returns distance of point z from line through a and b
+     *
+     * @param a
+     * @param b
+     * @param z
+     * @return distance to line
+     */
+    private static double distance(final Point2D a, final Point2D b, final Point2D z) {
+        final double ABx = b.getX() - a.getX();
+        final double ABy = b.getY() - a.getY();
+        return Math.abs(ABx * (a.getY() - z.getY()) - ABy * (a.getX() - z.getX()));
+    }
+}
diff --git a/src/jloda/util/Correlation.java b/src/jloda/util/Correlation.java
new file mode 100644
index 0000000..a89026c
--- /dev/null
+++ b/src/jloda/util/Correlation.java
@@ -0,0 +1,124 @@
+/*
+ *  Copyright (C) 2015 Daniel H. Huson
+ *
+ *  (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package jloda.util;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+/**
+ * calculates basic statistics
+ * Daniel Huson, 5.2006
+ */
+public class Correlation {
+
+    /**
+     * compute Pearson's correlation coefficient
+     *
+     * @param n
+     * @param x
+     * @param y
+     * @return correlation coefficient between -1 and 1
+     */
+    public static double computePersonsCorrelationCoefficent(int n, double[] x, double[] y) {
+        double sumX = 0;
+        double sumY = 0;
+        double sumXY = 0;
+        double sumX2 = 0;
+        double sumY2 = 0;
+
+        for (int i = 0; i < n; i++) {
+            sumX += x[i];
+            sumY += y[i];
+            sumXY += x[i] * y[i];
+            sumX2 += x[i] * x[i];
+            sumY2 += y[i] * y[i];
+        }
+
+        final double bottom = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
+        if (bottom == 0)
+            return 0;
+        final double top = n * sumXY - sumX * sumY;
+        return (float) (top / bottom);
+    }
+
+    /**
+     * compute Pearson's correlation coefficient
+     *
+     * @param n
+     * @param x
+     * @param y
+     * @return correlation coefficient between -1 and 1
+     */
+    public static float computePersonsCorrelationCoefficent(int n, float[] x, float[] y) {
+        double sumX = 0;
+        double sumY = 0;
+        double sumXY = 0;
+        double sumX2 = 0;
+        double sumY2 = 0;
+
+        for (int i = 0; i < n; i++) {
+            sumX += x[i];
+            sumY += y[i];
+            sumXY += x[i] * y[i];
+            sumX2 += x[i] * x[i];
+            sumY2 += y[i] * y[i];
+        }
+
+        final double bottom = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
+        if (bottom == 0)
+            return 0;
+        final double top = n * sumXY - sumX * sumY;
+        return (float) (top / bottom);
+    }
+
+    /**
+     * compute Pearson's correlation coefficient
+     *
+     * @param n
+     * @param xValues
+     * @param yValues
+     * @return correlation coefficient between -1 and 1
+     */
+    public static <T extends Number> double computePersonsCorrelationCoefficent(int n, Collection<T> xValues, Collection<T> yValues) {
+        double sumX = 0;
+        double sumY = 0;
+        double sumXY = 0;
+        double sumX2 = 0;
+        double sumY2 = 0;
+
+        final Iterator<T> itX = xValues.iterator();
+        final Iterator<T> itY = yValues.iterator();
+        for (int i = 0; i < n; i++) {
+            double x = itX.next().doubleValue();
+            double y = itY.next().doubleValue();
+
+            sumX += x;
+            sumY += y;
+            sumXY += x * y;
+            sumX2 += x * x;
+            sumY2 += y * y;
+        }
+
+        final double bottom = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
+        if (bottom == 0)
+            return 0;
+        final double top = n * sumXY - sumX * sumY;
+        return (float) (top / bottom);
+    }
+}
diff --git a/src/jloda/util/Counter.java b/src/jloda/util/Counter.java
new file mode 100644
index 0000000..822fbde
--- /dev/null
+++ b/src/jloda/util/Counter.java
@@ -0,0 +1,104 @@
+/**
+ * Counter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+/**
+ * object for counting. All methods are thread-safe (except addUnsynchronized)
+ * Daniel Huson, 10.2011
+ */
+public class Counter {
+    private long value;
+
+    /**
+     * constructor
+     */
+    public Counter() {
+        this.value = 0;
+    }
+
+    /**
+     * constructor
+     *
+     * @param value
+     */
+    public Counter(long value) {
+        this.value = value;
+    }
+
+    /**
+     * getter
+     */
+    public long get() {
+        synchronized (this) {
+            return value;
+        }
+    }
+
+    /**
+     * settter
+     *
+     * @param value
+     */
+    public void set(long value) {
+        synchronized (this) {
+            this.value = value;
+        }
+    }
+
+    /**
+     * increment
+     */
+    public void increment() {
+        synchronized (this) {
+            value++;
+        }
+    }
+
+    /**
+     * increment
+     */
+    public void add(long add) {
+        synchronized (this) {
+            value += add;
+        }
+    }
+
+    /**
+     * increment by value, unsynchronized
+     *
+     * @param add
+     */
+    public void addUnsynchronized(long add) {
+        value += add;
+    }
+
+    /**
+     * decrement
+     */
+    public void decrement() {
+        synchronized (this) {
+            value--;
+        }
+    }
+
+    public String toString() {
+        return "" + get();
+    }
+}
diff --git a/src/jloda/util/Cursors.java b/src/jloda/util/Cursors.java
new file mode 100644
index 0000000..5ec6d53
--- /dev/null
+++ b/src/jloda/util/Cursors.java
@@ -0,0 +1,149 @@
+/**
+ * Cursors.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.awt.*;
+import java.awt.image.MemoryImageSource;
+
+/**
+ * open and closed hand cursors
+ * Daniel Huson, 12.2006
+ * Original author: RedSmurf
+ */
+public class Cursors {
+    static private Cursor openHand = null;
+    static private Cursor closedHand = null;
+
+    /**
+     * get the open hand cursor
+     *
+     * @return open hand cursor
+     */
+    static public Cursor getOpenHand() {
+        if (openHand == null)
+            init();
+        return openHand;
+    }
+
+    /**
+     * get the closed hand cursor
+     *
+     * @return closed hand cursor
+     */
+    static public Cursor getClosedHand() {
+        if (closedHand == null)
+            init();
+        return closedHand;
+    }
+
+    /**
+     * generate the two cursors
+     */
+    static private void init() {
+        int curWidth = 32;
+        int curHeight = 32;
+        int curCol;
+        Image img;
+        int x, y;
+        int closed_black[] = {6, 5, 7, 5, 9, 5, 10, 5, 12, 5, 13, 5, 5, 6, 8, 6, 11, 6, 14, 6,
+                15, 6, 5, 7, 14, 7, 16, 7, 6, 8, 16, 8, 5, 9, 6, 9, 16, 9, 4, 10,
+                16, 10, 4, 11, 16, 11, 4, 12, 15, 12, 5, 13, 15, 13, 6, 14, 14, 14,
+                7, 15, 14, 15, 7, 16, 14, 16, 0};
+        int closed_white[] = {6, 4, 7, 4, 9, 4, 10, 4, 12, 4, 13, 4, 5, 5, 8, 5, 11, 5, 14, 5, 15, 5,
+                4, 6, 6, 6, 7, 6, 9, 6, 10, 6, 12, 6, 13, 6, 16, 6, 4, 7, 15, 7, 17, 7,
+                5, 8, 17, 8, 4, 9, 17, 9, 3, 10, 5, 10, 15, 10, 17, 10, 3, 11, 17, 11,
+                3, 12, 16, 12, 4, 13, 16, 13, 5, 14, 15, 14, 6, 15, 15, 15, 6, 16,
+                15, 16, 7, 17, 14, 17, 0};
+        int closed_whiteruns[] = {6, 13, 7, 15, 7, 15, 5, 15, 5, 15, 5, 14, 6, 14, 7, 13, 8, 13, 8, 13, 0};
+
+        int open_black[] = {10, 3, 11, 3, 6, 4, 7, 4, 9, 4, 12, 4, 13, 4, 14, 4, 5, 5, 8, 5, 9, 5, 12, 5,
+                15, 5, 5, 6, 8, 6, 9, 6, 12, 6, 15, 6, 17, 6, 6, 7, 9, 7, 12, 7, 15, 7, 16, 7, 18, 7,
+                6, 8, 9, 8, 12, 8, 15, 8, 18, 8, 4, 9, 5, 9, 7, 9, 15, 9, 18, 9, 3, 10, 6, 10, 7, 10,
+                18, 10, 3, 11, 7, 11, 17, 11, 4, 12, 17, 12, 5, 13, 17, 13, 5, 14, 16, 14, 6, 15,
+                16, 15, 7, 16, 15, 16, 8, 17, 15, 17, 8, 18, 15, 18, 0};
+
+        int open_white[] = {10, 2, 11, 2, 6, 3, 7, 3, 9, 3, 12, 3, 13, 3, 5, 4, 8, 4, 10, 4, 11, 4, 15, 4,
+                4, 5, 6, 5, 7, 5, 10, 5, 11, 5, 13, 5, 14, 5, 16, 5, 17, 5, 4, 6, 6, 6, 7, 6, 10, 6,
+                11, 6, 13, 6, 14, 6, 16, 6, 18, 6, 5, 7, 7, 7, 8, 7, 10, 7, 11, 7, 13, 7, 14, 7, 17, 7,
+                19, 7, 4, 8, 5, 8, 7, 8, 8, 8, 10, 8, 11, 8, 13, 8, 14, 8, 16, 8, 17, 7, 19, 8, 3, 9, 6, 9,
+                16, 9, 17, 9, 19, 9, 2, 10, 4, 10, 5, 10, 19, 10, 2, 11, 18, 11, 3, 12, 18, 12, 4, 13,
+                18, 13, 4, 14, 17, 14, 5, 15, 17, 15, 6, 16, 16, 16, 7, 17, 18, 17, 7, 18, 16, 18,
+                8, 19, 15, 19, 0};
+
+        int open_whiteruns[] = {9, 14, 8, 17, 4, 16, 5, 16, 6, 16, 6, 15, 7, 15, 8, 14, 9, 14, 9, 14, 0};
+
+        int pix[] = new int[curWidth * curHeight];
+        for (y = 0; y <= curHeight; y++) for (x = 0; x <= curWidth; x++) pix[y + x] = 0; // all points transparent
+
+        // black pixels
+        curCol = Color.black.getRGB();
+        int n = 0;
+        while (closed_black[n] != 0)
+            pix[closed_black[n++] + closed_black[n++] * curWidth] = curCol;
+
+        // white pixels
+        curCol = Color.white.getRGB();
+        n = 0;
+        while (closed_white[n] != 0)
+            pix[closed_white[n++] + closed_white[n++] * curWidth] = curCol;
+
+        // white pixel runs
+        n = 0;
+        y = 7;
+        while (closed_whiteruns[n] != 0) {
+            for (x = closed_whiteruns[n++]; x < closed_whiteruns[n]; x++)
+                pix[x + y * curWidth] = curCol;
+            n++;
+            y++;
+        }
+
+
+        img = Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(curWidth, curHeight, pix, 0, curWidth));
+        closedHand = Toolkit.getDefaultToolkit().createCustomCursor(img, new Point(5, 5), "closedhand");
+
+        for (y = 0; y <= curHeight; y++) for (x = 0; x <= curWidth; x++) pix[y + x] = 0; // all points transparent
+
+        // black pixels
+        curCol = Color.black.getRGB();
+        n = 0;
+        while (open_black[n] != 0)
+            pix[open_black[n++] + open_black[n++] * curWidth] = curCol;
+
+        // white pixels
+        curCol = Color.white.getRGB();
+        n = 0;
+        while (open_white[n] != 0)
+            pix[open_white[n++] + open_white[n++] * curWidth] = curCol;
+
+        // white pixel runs
+        n = 0;
+        y = 9;
+        while (open_whiteruns[n] != 0) {
+            for (x = open_whiteruns[n++]; x < open_whiteruns[n]; x++)
+                pix[x + y * curWidth] = curCol;
+            n++;
+            y++;
+        }
+
+
+        img = Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(curWidth, curHeight, pix, 0, curWidth));
+        openHand = Toolkit.getDefaultToolkit().createCustomCursor(img, new Point(5, 5), "openhand");
+    }
+}
diff --git a/src/jloda/util/DNAComplexityMeasure.java b/src/jloda/util/DNAComplexityMeasure.java
new file mode 100644
index 0000000..f5d5cbe
--- /dev/null
+++ b/src/jloda/util/DNAComplexityMeasure.java
@@ -0,0 +1,110 @@
+/**
+ * DNAComplexityMeasure.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+/**
+ * computes the minimum complexity encountered in a DNA string
+ * Daniel Huson, 9.2012
+ */
+public class DNAComplexityMeasure {
+    private static final int N = 4; // alphabet size
+    private static final int L = 16;  // window size
+    private static final double LFactorial = 20922789888000.0;
+    private static double[] factorial = null;
+
+    /**
+     * uses Wootten and Federhen to compute the complexity of a sequence
+     *
+     * @param s
+     * @return average complexity
+     */
+    public static float getMinimumDNAComplexityWoottenFederhen(String s) {
+        if (s == null || s.length() < L)
+            return 0;
+
+        int[] counts = new int[N];
+
+        for (int pos = 0; pos < L; pos++) // first 12 values
+        {
+            counts[getIndex(s.charAt(pos))]++;
+        }
+        double minComplexity = 1;
+
+        // System.err.print("Values: ");
+        for (int pos = L; pos < s.length() - L; pos += L) {
+            double product = computeProductOfFactorials(counts);
+            double K = 1.0 / L * Math.log(LFactorial / product) / Math.log(N);
+            counts[getIndex(s.charAt(pos - L))]--;
+            counts[getIndex(s.charAt(pos))]++;
+            // System.err.print(" "+K);
+            if (K < minComplexity)
+                minComplexity = K;
+        }
+        // System.err.println("minComplexity="+minComplexity+", sequence: "+s);
+        return (float) Math.max(0.0001, minComplexity);   // MEGAN interprets 0 as being turned off...
+    }
+
+    /**
+     * computes the produce of factorials (of values up to L)
+     *
+     * @param counts
+     * @return produce of factorials
+     */
+    private static double computeProductOfFactorials(int[] counts) {
+        if (factorial == null) {
+            factorial = new double[L + 1];
+            double value = 1.0;
+            for (int i = 0; i <= L; i++) {
+                if (i > 0)
+                    value *= i;
+                factorial[i] = value;
+            }
+        }
+        double result = 1.0;
+        for (int count : counts) {
+            result *= factorial[count];
+        }
+        return result;
+    }
+
+    /**
+     * gets the index
+     *
+     * @param c
+     * @return index
+     */
+    private static int getIndex(char c) {
+        switch (c) {
+            default:
+                return 0;
+            case 'c':
+            case 'C':
+                return 1;
+            case 'g':
+            case 'G':
+                return 2;
+            case 't':
+            case 'T':
+            case 'u':
+            case 'U':
+                return 3;
+        }
+    }
+}
diff --git a/src/jloda/util/DrawOval.java b/src/jloda/util/DrawOval.java
new file mode 100644
index 0000000..784e43a
--- /dev/null
+++ b/src/jloda/util/DrawOval.java
@@ -0,0 +1,165 @@
+/**
+ * DrawOval.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.geom.AffineTransform;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+
+/**
+ * Draw an oval
+ * Daniel Huson, 2014
+ */
+public class DrawOval {
+
+    public static void main(String[] args) {
+        final LinkedList<Oval> list = new LinkedList<>();
+        final LinkedList<Point> points = new LinkedList<>();
+
+        final JFrame frame = new JFrame();
+        frame.setSize(500, 500);
+        final JPanel panel = new JPanel() {
+            @Override
+            public void paint(Graphics g0) {
+                Graphics2D g = (Graphics2D) g0;
+                super.paint(g);
+                for (Oval oval : list) {
+                    oval.draw(g);
+                }
+                g.setColor(Color.BLACK);
+                for (Point point : points) {
+                    g.drawRect(point.x - 1, point.y - 1, 2, 2);
+                }
+            }
+        };
+        frame.getContentPane().setLayout(new BorderLayout());
+        frame.getContentPane().add(panel);
+        frame.setVisible(true);
+        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+        panel.addComponentListener(new ComponentAdapter() {
+            public void componentResized(ComponentEvent e) {
+                super.componentResized(e);
+                panel.repaint();
+            }
+        });
+
+
+        points.add(new Point(105, 100));
+        points.add(new Point(405, 300));
+        points.add(new Point(150, 120));
+
+        list.add(Oval.createOval(points));
+
+        list.add(new Oval(new Point(100, 100), 50, 100, 0));
+
+
+        for (float z = (float) Math.PI; z >= 0f; z -= 0.4f)
+            list.add(new Oval(new Point(300, 300), 120, 10, z));
+
+        list.add(new Oval(new Point(200, 100), 50, 100, 0));
+    }
+
+    public static Point getCenter(Collection<Point> points) {
+        double x = 0;
+        double y = 0;
+        for (Point point : points) {
+            x += point.x;
+            y += point.y;
+        }
+        x /= points.size();
+        y /= points.size();
+        return new Point((int) Math.round(x), (int) Math.round(y));
+
+    }
+
+    public static float getAngleOfMainDirection(Collection<Point> points, Point center) {
+        points = normalize(points, center);
+
+        final Point result = new Point();
+
+        for (Point point : points) {
+            if (point.x < 0) {
+                result.x += -point.x;
+                result.y += -point.y;
+            } else {
+                result.x += point.x;
+                result.y += point.y;
+            }
+        }
+        result.x = (int) ((float) result.x / points.size());
+        result.y = (int) ((float) result.y / points.size());
+        return (float) Geometry.computeAngle(result);
+    }
+
+    private static Collection<Point> normalize(Collection<Point> points, Point center) {
+        ArrayList<Point> result = new ArrayList<>(points.size());
+        for (Point point : points) {
+            result.add(new Point(point.x - center.x, point.y - center.y));
+        }
+        return result;
+    }
+}
+
+class Oval {
+    Point center;
+    int width;
+    int height;
+    float angle;
+
+    public Oval() {
+    }
+
+    public Oval(Point center, int width, int height, float angle) {
+        this.center = center;
+        this.width = width;
+        this.height = height;
+        this.angle = angle;
+    }
+
+    public void draw(Graphics2D gc) {
+        gc.setColor(Color.BLUE);
+        gc.drawRect(0, 0, 100, 100);
+        if (angle != 0.0) {
+            AffineTransform saveTransform = gc.getTransform();
+            gc.rotate(angle, center.getX(), center.getY());
+            gc.drawOval(center.x - width / 2, center.y - height / 2, width, height);
+            gc.setTransform(saveTransform);
+        } else
+            gc.drawOval(center.x - width / 2, center.y - height / 2, width, height);
+    }
+
+    public static Oval createOval(Collection<Point> points) {
+        Oval oval = new Oval();
+        oval.center = DrawOval.getCenter(points);
+        oval.angle = DrawOval.getAngleOfMainDirection(points, oval.center);
+        oval.width = 100;
+        oval.height = 50;
+        System.err.println("Center: " + oval.center);
+        System.err.println("Angle: " + oval.angle);
+        return oval;
+
+    }
+
+}
diff --git a/src/jloda/util/EditDistance.java b/src/jloda/util/EditDistance.java
new file mode 100644
index 0000000..e7dbbbc
--- /dev/null
+++ b/src/jloda/util/EditDistance.java
@@ -0,0 +1,320 @@
+/**
+ * EditDistance.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package jloda.util;
+
+import java.io.*;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Stack;
+
+/**
+ * compute the edit distance between two sequences
+ * Daniel Huson, 2003
+ */
+public class EditDistance {
+    private String sequence1 = null;
+    private String sequence2 = null;
+    private String aligned1 = null;
+    private String aligned2 = null;
+    private int score = 0;
+
+
+    /**
+     * compute the edit distance between two sequences
+     *
+     * @param seq1
+     * @param seq2
+     * @return edit distance
+     */
+    static public int compute(String seq1, String seq2) {
+        int rows = seq1.length();
+        int cols = seq2.length();
+
+        int[][] D = new int[rows + 1][cols + 1];
+
+        // set base conditions:
+        for (int r = 0; r <= rows; r++)
+            D[r][0] = r;
+        for (int c = 0; c <= cols; c++)
+            D[0][c] = c;
+
+        // recursion:
+        for (int r = 1; r <= rows; r++) {
+            for (int c = 1; c <= cols; c++) {
+                D[r][c] = min(D[r - 1][c] + 1,
+                        D[r][c - 1] + 1,
+                        D[r - 1][c - 1] + match(seq1.charAt(r - 1), seq2.charAt(c - 1)));
+
+            }
+        }
+
+        return D[rows][cols];
+    }
+
+    /**
+     * computes the edit distance and an alignment
+     */
+    public void compute() {
+        int rows = getSequence1().length();
+        int cols = getSequence2().length();
+
+        int[][] D = new int[rows + 1][cols + 1];
+
+        // set base conditions:
+        for (int r = 0; r <= rows; r++)
+            D[r][0] = r;
+        for (int c = 0; c <= cols; c++)
+            D[0][c] = c;
+
+        // recursion:
+        for (int r = 1; r <= rows; r++) {
+            for (int c = 1; c <= cols; c++) {
+                D[r][c] = min(D[r - 1][c] + 1,
+                        D[r][c - 1] + 1,
+                        D[r - 1][c - 1]
+                                + match(getSequence1().charAt(r - 1), getSequence2().charAt(c - 1)));
+
+            }
+        }
+
+        // trace back alignment:
+        int r = rows;
+        int c = cols;
+        Stack stack1 = new Stack();
+        Stack stack2 = new Stack();
+
+        while (r > 0 || c > 0) {
+            if (r == 0 || D[r][c] == D[r - 1][c] + 1) // insertion in x
+            {
+                stack1.push(getSequence1().charAt(r-- - 1));
+                stack2.push('-');
+            } else if (c == 0 || D[r][c] == D[r][c - 1] + 1) // insertion in y
+            {
+                stack1.push('-');
+                stack2.push(getSequence2().charAt(c-- - 1));
+            } else // match-mismatch
+            {
+                stack1.push(getSequence1().charAt(r-- - 1));
+                stack2.push(getSequence2().charAt(c-- - 1));
+            }
+        }
+
+        // setup aligned sequences
+        StringBuilder buffer1 = new StringBuilder();
+        StringBuilder buffer2 = new StringBuilder();
+
+        while (!stack1.empty())
+            buffer1.append(((Character) stack1.pop()).charValue());
+        setAligned1(buffer1.toString());
+
+        while (!stack2.empty())
+            buffer2.append(((Character) stack2.pop()).charValue());
+        setAligned2(buffer2.toString());
+
+        setScore(D[rows][cols]);
+    }
+
+    /**
+     * get sequence 1
+     *
+     * @return sequence 1
+     */
+    public String getSequence1() {
+        return sequence1;
+    }
+
+    /**
+     * set sequence 1
+     *
+     * @param sequence1
+     */
+    public void setSequence1(String sequence1) {
+        this.sequence1 = sequence1;
+    }
+
+    /**
+     * get sequence 2
+     *
+     * @return sequence 2
+     */
+    public String getSequence2() {
+        return sequence2;
+    }
+
+    /**
+     * set sequence 2
+     *
+     * @param sequence2
+     */
+    public void setSequence2(String sequence2) {
+        this.sequence2 = sequence2;
+    }
+
+    /**
+     * get aligned version of sequence 1
+     *
+     * @return aligned sequence 1
+     */
+    public String getAligned1() {
+        return aligned1;
+    }
+
+    /**
+     * set aligned sequence 1
+     *
+     * @param aligned1
+     */
+    protected void setAligned1(String aligned1) {
+        this.aligned1 = aligned1;
+    }
+
+    /**
+     * get aligned version of sequence 2
+     *
+     * @return aligned sequence 2
+     */
+    public String getAligned2() {
+        return aligned2;
+    }
+
+    /**
+     * set aligned sequence 2
+     *
+     * @param aligned2
+     */
+    protected void setAligned2(String aligned2) {
+        this.aligned2 = aligned2;
+    }
+
+    /**
+     * get the computed score
+     *
+     * @return score
+     */
+    public int getScore() {
+        return score;
+    }
+
+    /**
+     * set the computed score
+     *
+     * @param score
+     */
+    protected void setScore(int score) {
+        this.score = score;
+    }
+
+    /**
+     * returns 0, if a=b, 1, else
+     *
+     * @param a
+     * @param b
+     * @return 0, if a=b, 1, else
+     */
+    static private int match(char a, char b) {
+        if (a == b)
+            return 0;
+        else
+            return 1;
+    }
+
+    /**
+     * returns minimum of three numbers
+     *
+     * @param a
+     * @param b
+     * @param c
+     * @return minimum
+     */
+    static private int min(int a, int b, int c) {
+        return Math.min(a, Math.min(b, c));
+    }
+
+    /**
+     * compute a distance matrix from chinese character codes
+     *
+     * @param args
+     * @throws UsageException
+     * @throws IOException
+     */
+    public static void main(String[] args) throws UsageException, IOException {
+        CommandLineOptions options = new CommandLineOptions(args);
+        options.setDescription("Compute distance matrix from Chinese Character codes");
+
+        String infile = options.getMandatoryOption("-i", "Input file", "");
+        String outfile = options.getOption("-o", "Output file", "");
+
+
+        options.done();
+
+        PrintStream outs = System.out;
+
+        if (outfile.length() > 0)
+            outs = new PrintStream(new FileOutputStream(new File(outfile)));
+
+        FastA fastA = new FastA();
+        fastA.read(new FileReader(new File(infile)));
+
+        List<Pair<String, String>> lines = new LinkedList<>();
+
+        /*
+        NexusStreamParser np=new NexusStreamParser(r);
+        while(np.peekNextToken()!=NexusStreamParser.TT_EOF)
+        {
+            np.matchIgnoreCase("(");
+            String ch=np.getWordRespectCase();
+            String code=np.getWordRespectCase();
+            np.matchIgnoreCase(")");
+            lines.add(new Pair(ch,code));
+        }
+        */
+        for (int i = 0; i < fastA.getSize(); i++) {
+            String name = fastA.getHeader(i);
+            String code = fastA.getSequence(i);
+            lines.add(new Pair<>(name, code));
+        }
+
+
+        outs.println("#NEXUS");
+        outs.println("begin taxa;");
+        outs.println("dimensions ntax=" + lines.size() + ";");
+        outs.println("end;");
+
+        outs.println("begin distances;");
+        outs.println("dimensions ntax=" + lines.size() + ";");
+        outs.println("format triangle=both;");
+
+        Pair[] data = lines.toArray(new Pair[lines.size()]);
+
+        outs.println("matrix");
+        for (Pair pi : data) {
+            outs.print("'" + pi.getFirst() + "'");
+            for (Pair pj : data) {
+                int dist = compute((String) pi.getSecond(), (String) pj.getSecond());
+                outs.print(" " + dist);
+            }
+            outs.println();
+        }
+        outs.println(";");
+        outs.println("end;");
+
+    }
+}
diff --git a/src/jloda/util/FastA.java b/src/jloda/util/FastA.java
new file mode 100644
index 0000000..3b1304f
--- /dev/null
+++ b/src/jloda/util/FastA.java
@@ -0,0 +1,231 @@
+/**
+ * FastA.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+/**
+ * Fasta i/o
+ * Daniel Huson, 12.10.2003
+ */
+
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.Vector;
+
+/**
+ * Fasta i/o  class
+ */
+public class FastA {
+    private int size;
+    private final Vector<String> headers;
+    private final Vector<String> sequences;
+
+    /**
+     * constructor
+     */
+    public FastA() {
+        size = 0;
+        headers = new Vector<>();
+        sequences = new Vector<>();
+
+    }
+
+    /**
+     * construct a new FastA object and add the given record
+     *
+     * @param header   header of record
+     * @param sequence sequence of record
+     */
+    public FastA(String header, String sequence) {
+        this();
+        add(header, sequence);
+    }
+
+    /**
+     * add a header and sequence
+     *
+     * @param header
+     * @param sequence
+     */
+    public void add(String header, String sequence) {
+        set(getSize(), header, sequence);
+    }
+
+    /**
+     * erase the data
+     */
+    public void clear() {
+        headers.clear();
+        sequences.clear();
+        size = 0;
+    }
+
+    /**
+     * gets the header
+     *
+     * @param i the index
+     * @return header string
+     */
+    public String getHeader(int i) {
+        return headers.get(i);
+    }
+
+    /**
+     * sets the header and sequence
+     *
+     * @param i      the index
+     * @param header
+     */
+    public void set(int i, String header, String sequence) {
+        if (header.startsWith(">"))
+            header = header.substring(1);
+        if (i < getSize()) {
+            this.headers.set(i, header);
+            this.sequences.set(i, sequence);
+        } else {
+            setSize(i + 1);
+            this.headers.add(i, header);
+            this.sequences.add(i, sequence);
+        }
+    }
+
+    /**
+     * gets the sequence
+     *
+     * @param i the index
+     * @return the sequence
+     */
+    public String getSequence(int i) {
+        return sequences.get(i);
+    }
+
+    /**
+     * gets the first header
+     *
+     * @return first header
+     */
+    public String getFirstHeader() {
+        return headers.firstElement();
+    }
+
+    /**
+     * gets the first sequence
+     *
+     * @return first sequence
+     */
+    public String getFirstSequence() {
+        return sequences.firstElement();
+    }
+
+    /**
+     * sets the size of sequences
+     *
+     * @param n the size
+     */
+    public void setSize(int n) {
+        if (n > size) {
+            headers.setSize(n);
+            sequences.setSize(n);
+        }
+        size = n;
+    }
+
+    /**
+     * get the size of sequences
+     *
+     * @return size of sequences
+     */
+    public int getSize() {
+        return size;
+    }
+
+    /**
+     * read header and sequence in fastA format
+     *
+     * @param r
+     * @throws java.io.IOException
+     */
+    public void read(Reader r) throws IOException {
+        clear();
+
+        BufferedReader br = new BufferedReader(r);
+
+        String header = "";
+        StringBuilder sequence = null;
+
+        String aLine = br.readLine();
+
+        while (aLine != null) {
+            aLine = aLine.trim();
+            if (aLine.length() > 0) {
+
+                if (aLine.charAt(0) == '>') // new fasta header
+                {
+                    if (header.length() > 0 && sequence != null) {
+                        add(header, sequence.toString());
+                    }
+                    header = aLine.substring(1).trim();
+                    sequence = new StringBuilder();
+                } else if (sequence != null)
+                    sequence.append(aLine);
+            }
+            aLine = br.readLine();
+        }
+        if (header.length() > 0) {
+            add(header, sequence != null ? sequence.toString() : null);
+        }
+    }
+
+    /**
+     * write header and sequence in fastA format
+     *
+     * @param w
+     * @throws IOException
+     */
+    public void write(Writer w) throws IOException {
+        for (int i = 0; i < getSize(); i++) {
+            if (getHeader(i) != null) {
+                String header = getHeader(i);
+                if (!header.startsWith(">"))
+                    w.write(">");
+                w.write(header);
+                if (!header.endsWith("\n"))
+                    w.write("\n");
+                int lineLength = 0;
+                for (int c = 0; c < getSequence(i).length(); c++) {
+                    int ch = getSequence(i).charAt(c);
+                    if (!Character.isSpaceChar(ch)) {
+                        w.write(ch);
+                        lineLength++;
+                        if (lineLength == 0) {
+                            w.write('\n');
+                            lineLength = 0;
+                        }
+                    }
+                }
+                if (lineLength > 0)
+                    w.write('\n');
+            }
+        }
+        w.flush();
+    }
+}
diff --git a/src/jloda/util/FastaFileFilter.java b/src/jloda/util/FastaFileFilter.java
new file mode 100644
index 0000000..e361770
--- /dev/null
+++ b/src/jloda/util/FastaFileFilter.java
@@ -0,0 +1,76 @@
+/**
+ * FastaFileFilter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.File;
+import java.io.FilenameFilter;
+
+/**
+ * The fastA file filter
+ * Daniel Huson         2.2006
+ */
+public class FastaFileFilter extends FileFilterBase implements FilenameFilter {
+    private static FastaFileFilter instance;
+
+    /**
+     * gets an instance
+     *
+     * @return instance
+     */
+    public static FastaFileFilter getInstance() {
+        if (instance == null) {
+            instance = new FastaFileFilter();
+            instance.setAllowGZipped(true);
+            instance.setAllowZipped(true);
+        }
+        return instance;
+    }
+
+
+    /**
+     * constructor
+     */
+    public FastaFileFilter() {
+        add(".dna");
+        add(".fa");
+        add(".faa");
+        add(".fna");
+        add(".fasta");
+    }
+
+    /**
+     * @return description of file matching the filter
+     */
+    public String getBriefDescription() {
+        return "Sequence files in FastA format";
+    }
+
+    /**
+     * does this look like a FastA file name?
+     *
+     * @param fileName
+     * @return true, if fastA file name
+     */
+    public static boolean accept(String fileName, boolean allowGZipped) {
+        final FastaFileFilter fastaFileFilter = (new FastaFileFilter());
+        fastaFileFilter.setAllowGZipped(allowGZipped);
+        return fastaFileFilter.accept(new File(fileName));
+    }
+}
diff --git a/src/jloda/util/FileFilter.java b/src/jloda/util/FileFilter.java
new file mode 100644
index 0000000..85c80d0
--- /dev/null
+++ b/src/jloda/util/FileFilter.java
@@ -0,0 +1,41 @@
+/**
+ * FileFilter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.FilenameFilter;
+
+/**
+ * @author Daniel Huson
+ *         file filter
+ *         12.03
+ */
+
+public class FileFilter extends FileFilterBase implements FilenameFilter {
+    public FileFilter(String suffix) {
+        add(suffix);
+    }
+
+    /**
+     * @return description of file matching the filter
+     */
+    public String getBriefDescription() {
+        return "Files";
+    }
+}
diff --git a/src/jloda/util/FileFilterBase.java b/src/jloda/util/FileFilterBase.java
new file mode 100644
index 0000000..234b331
--- /dev/null
+++ b/src/jloda/util/FileFilterBase.java
@@ -0,0 +1,184 @@
+/**
+ * FileFilterBase.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import javax.swing.filechooser.FileFilter;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * base class for file filters
+ * Daniel Huson, 11.2008
+ */
+public abstract class FileFilterBase extends FileFilter implements FilenameFilter {
+    private final List<String> extensions = new LinkedList<>();
+    private final List<FileFilterBase> others = new LinkedList<>();
+    private boolean allowGZipped = false;
+    private boolean allowZipped = false;
+
+    public boolean isAllowGZipped() {
+        return allowGZipped;
+    }
+
+    public void setAllowGZipped(boolean allowGZipped) {
+        this.allowGZipped = allowGZipped;
+    }
+
+    public boolean isAllowZipped() {
+        return allowZipped;
+    }
+
+    public void setAllowZipped(boolean allowZipped) {
+        this.allowZipped = allowZipped;
+    }
+
+    /**
+     * set brief description (without list of extensions
+     *
+     * @return
+     */
+    abstract public String getBriefDescription();
+
+    /**
+     * gets the list of file extensions
+     *
+     * @return file extensions
+     */
+    public List<String> getFileExtensions() {
+        return extensions;
+    }
+
+    /**
+     * @return description of file matching the filter
+     */
+
+    /**
+     * @return description of file matching the filter
+     */
+    public String getDescription() {
+        StringBuilder buf = new StringBuilder();
+        buf.append(getBriefDescription()).append(" (extension: ");
+        boolean first = true;
+        for (String ex : getFileExtensions()) {
+            if (first)
+                first = false;
+            else
+                buf.append(", ");
+            buf.append(ex);
+        }
+        if (allowGZipped) {
+            if (first)
+                first = false;
+            else
+                buf.append(", ");
+            buf.append(".gz");
+        }
+        if (allowZipped) {
+            if (first)
+                first = false;
+            else
+                buf.append(", ");
+            buf.append(".zip");
+        }
+        buf.append(")");
+        return buf.toString();
+    }
+
+    /**
+     * add another possible extension
+     *
+     * @param extension
+     */
+    public void add(String extension) {
+        if (!extension.startsWith("."))
+            extension = "." + extension;
+        if (!extensions.contains(extension))
+            extensions.add(extension);
+    }
+
+    /**
+     * add another file filter
+     *
+     * @param fileFilter
+     */
+    public void add(FileFilterBase fileFilter) {
+        if (!others.contains(fileFilter))
+            others.add(fileFilter);
+    }
+
+    /**
+     * Tests if a specified file should be included in a file list.
+     *
+     * @param fileName
+     * @return
+     */
+    public boolean accept(String fileName) {
+        return accept(null, fileName);
+    }
+
+    /**
+     * Tests if a specified file should be included in a file list.
+     *
+     * @param file
+     * @return true if acceptable
+     */
+    public boolean accept(File file) {
+        return accept(file.getPath());
+    }
+
+    /**
+     * Tests if a specified file should be included in a file list.
+     *
+     * @param dir  the directory in which the file was found (or null)
+     * @param name the name of the file (or null)
+     * @return <code>true</code> if and only if the name should be
+     * included in the file list; <code>false</code> otherwise.
+     */
+    @Override
+    public boolean accept(File dir, String name) {
+        if (dir == null && name == null)
+            return false;
+        final File file;
+        if (dir == null)
+            file = new File(name);
+        else if (name == null)
+            file = dir;
+        else
+            file = new File(dir, name);
+
+        if (file.isDirectory())
+            return true;
+
+
+        for (String extension : extensions) {
+            if (file.getName().endsWith(extension) || isAllowGZipped() && file.getName().endsWith(extension + ".gz") || isAllowZipped() && file.getName().endsWith(extension + ".zip"))
+                return true;
+        }
+
+        for (FileFilterBase filter : others) {
+            if (filter.accept(dir, name))
+                return true;
+        }
+
+        return extensions.contains(".txt") && !file.getName().contains(".");
+    }
+}
diff --git a/src/jloda/util/FileInputIterator.java b/src/jloda/util/FileInputIterator.java
new file mode 100644
index 0000000..fde3ec6
--- /dev/null
+++ b/src/jloda/util/FileInputIterator.java
@@ -0,0 +1,294 @@
+/**
+ * FileInputIterator.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.*;
+
+/**
+ * iterates over all lines in a file. File can also be a .gz file.
+ * Daniel Huson, 3.2012
+ */
+public class FileInputIterator implements IFileIterator {
+    public static final String PREFIX_TO_INDICATE_TO_PARSE_FILENAME_STRING = "!!!";
+    private final BufferedReader reader;
+    private String nextLine = null;
+    private long lineNumber = 0;
+    private boolean done;
+    private long position = -1;
+    private long numberOfBytes = 0;
+    private final int endOfLineBytes;
+    private boolean skipEmptyLines = false;
+    private boolean skipCommentLines = false;
+    public static final int bufferSize = 128000;
+
+    private final long maxProgress;
+
+    private String pushedBackLine = null;
+
+    private String fileName;
+    private ProgressPercentage progress;
+
+    /**
+     * constructor
+     *
+     * @param fileName
+     * @throws java.io.FileNotFoundException
+     */
+    public FileInputIterator(String fileName) throws IOException {
+        this(fileName, false);
+    }
+
+    /**
+     * constructor
+     *
+     * @param file
+     * @throws java.io.FileNotFoundException
+     */
+    public FileInputIterator(File file, boolean reportProgress) throws IOException {
+        this(file.getPath(), reportProgress);
+    }
+
+    /**
+     * constructor
+     *
+     * @param file
+     * @throws java.io.FileNotFoundException
+     */
+    public FileInputIterator(File file) throws IOException {
+        this(file, false);
+    }
+
+    /**
+     * constructor
+     *
+     * @param fileName
+     * @throws java.io.FileNotFoundException
+     */
+    public FileInputIterator(String fileName, boolean reportProgress) throws IOException {
+        this.fileName = fileName;
+
+        if (fileName.startsWith(PREFIX_TO_INDICATE_TO_PARSE_FILENAME_STRING)) {
+            reader = new BufferedReader(new StringReader(fileName.substring(3)));
+            endOfLineBytes = 1;
+            maxProgress = fileName.length() - PREFIX_TO_INDICATE_TO_PARSE_FILENAME_STRING.length();
+        } else {
+            final File file = new File(fileName);
+            if (Basic.isZIPorGZIPFile(file.getPath())) {
+                reader = new BufferedReader(new InputStreamReader(Basic.getInputStreamPossiblyZIPorGZIP(file.getPath())));
+                endOfLineBytes = Basic.determineEndOfLinesBytes(new File(fileName));
+                maxProgress = 5 * file.length(); // assuming compression factor of 5-to-1
+            } else {
+                reader = new BufferedReader(new FileReader(file), bufferSize);
+                endOfLineBytes = 1;
+                maxProgress = file.length();
+            }
+        }
+        done = (maxProgress <= 0);
+        setReportProgress(reportProgress);
+    }
+
+
+    /**
+     * constructor
+     *
+     * @param r
+     * @throws java.io.FileNotFoundException
+     */
+    public FileInputIterator(Reader r, String fileName) throws IOException {
+        this(r, fileName, false);
+    }
+
+    /**
+     * constructor
+     *
+     * @param r
+     * @throws java.io.FileNotFoundException
+     */
+    public FileInputIterator(Reader r, String fileName, boolean reportProgress) throws IOException {
+        this.fileName = fileName;
+
+        if (fileName.startsWith(PREFIX_TO_INDICATE_TO_PARSE_FILENAME_STRING)) {
+            reader = new BufferedReader(new StringReader(fileName.substring(3)));
+            endOfLineBytes = 1;
+            maxProgress = fileName.length() - PREFIX_TO_INDICATE_TO_PARSE_FILENAME_STRING.length();
+        } else {
+            reader = new BufferedReader(r, bufferSize);
+            endOfLineBytes = Basic.determineEndOfLinesBytes(new File(fileName));
+
+            File file = new File(fileName);
+            if (file.exists())
+                maxProgress = file.length();
+            else
+                maxProgress = 10000000;  // unknown
+        }
+
+        setReportProgress(reportProgress);
+    }
+
+    /**
+     * report progress
+     */
+    public void setReportProgress(boolean reportProgress) {
+        if (reportProgress) {
+            if (progress == null) {
+                if (!fileName.startsWith(PREFIX_TO_INDICATE_TO_PARSE_FILENAME_STRING))
+                    progress = new ProgressPercentage("Processing file: " + fileName, getMaximumProgress());
+                else
+                    progress = new ProgressPercentage("Processing string", getMaximumProgress());
+            }
+        } else {
+            if (progress != null) {
+                progress.close();
+                progress = null;
+            }
+        }
+    }
+
+    /**
+     * position of item in file
+     *
+     * @return position
+     */
+    public long getPosition() {
+        return position;
+    }
+
+    /**
+     * number of bytes of item in file
+     *
+     * @return number of bytes
+     */
+    public long getNumberOfBytes() {
+        return numberOfBytes;
+    }
+
+    /**
+     * close associated file or database
+     */
+    public void close() throws IOException {
+        reader.close();
+        if (progress != null)
+            progress.close();
+    }
+
+    /**
+     * gets the maximum progress value
+     *
+     * @return maximum progress value
+     */
+    public long getMaximumProgress() {
+        return maxProgress;
+    }
+
+    /**
+     * gets the current progress value
+     *
+     * @return current progress value
+     */
+    public long getProgress() {
+        return position;
+    }
+
+    /**
+     * is there another line
+     *
+     * @return true, if there is another line in the file
+     */
+    public boolean hasNext() {
+        if (pushedBackLine != null)
+            return true;
+        if (done)
+            return false;
+        if (nextLine != null)
+            return true;
+        try {
+            position += numberOfBytes + endOfLineBytes;
+            nextLine = reader.readLine();
+
+            if (nextLine != null) {
+                numberOfBytes = nextLine.length();
+                if ((skipEmptyLines && nextLine.length() == 0) || (skipCommentLines && nextLine.startsWith("#"))) {
+                    nextLine = null;
+                    return hasNext();
+                }
+            }
+        } catch (IOException e) {
+            done = true;
+            nextLine = null;
+        }
+        return nextLine != null;
+    }
+
+    /**
+     * gets the next line in the file
+     *
+     * @return next line
+     */
+    public String next() {
+        if (pushedBackLine != null) {
+            String value = pushedBackLine;
+            pushedBackLine = null;
+            return value;
+        }
+        if (done)
+            return null;
+        if (nextLine == null)
+            hasNext();
+        if (nextLine != null) {
+            String result = nextLine;
+            nextLine = null;
+            lineNumber++;
+            if (progress != null) {
+                progress.setProgress(position);
+            }
+            return result;
+        }
+        return null;
+    }
+
+    public long getLineNumber() {
+        return lineNumber;
+    }
+
+    public void remove() {
+    }
+
+    public boolean isSkipEmptyLines() {
+        return skipEmptyLines;
+    }
+
+    public void setSkipEmptyLines(boolean skipEmptyLines) {
+        this.skipEmptyLines = skipEmptyLines;
+    }
+
+    public boolean isSkipCommentLines() {
+        return skipCommentLines;
+    }
+
+    public void setSkipCommentLines(boolean skipCommentLines) {
+        this.skipCommentLines = skipCommentLines;
+    }
+
+    public void pushBack(String aLine) throws IOException {
+        if (pushedBackLine != null)
+            throw new IOException("FileInputIterator: pushBack buffer overflow");
+        pushedBackLine = aLine;
+    }
+}
diff --git a/src/jloda/util/FileIterator.java b/src/jloda/util/FileIterator.java
new file mode 100644
index 0000000..da7ff48
--- /dev/null
+++ b/src/jloda/util/FileIterator.java
@@ -0,0 +1,187 @@
+/**
+ * FileIterator.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Iterator;
+
+/**
+ * File iterator
+ * Daniel Huson, 2014
+ */
+public class FileIterator implements ICloseableIterator<byte[]>, Iterator<byte[]> {
+    private byte[] bytes = new byte[1000];
+
+    private final InputStreamReader reader;
+
+    private long linePosition = 0;
+    private int lineLength = 0;
+
+    private byte firstByteOfNextLine = 0;
+
+    private long position = 0; // this is the position in the unzipped file
+
+    private final long maxProgress;
+
+    /**
+     * constructor
+     *
+     * @param fileName
+     * @throws IOException
+     */
+    public FileIterator(String fileName) throws IOException {
+        reader = new InputStreamReader(Basic.getInputStreamPossiblyZIPorGZIP(fileName));
+        if (Basic.isZIPorGZIPFile(fileName))
+            maxProgress = 20 * ((new File(fileName))).length();
+        else
+            maxProgress = ((new File(fileName))).length();
+
+        // get first letter
+        firstByteOfNextLine = (byte) reader.read();
+        if (hasNext())
+            position++;
+        while (firstByteOfNextLine == '\r' || firstByteOfNextLine == '\n' && hasNext()) {
+            firstByteOfNextLine = (byte) reader.read();
+            if (hasNext())
+                position++;
+        }
+
+    }
+
+    @Override
+    public boolean hasNext() {
+        return firstByteOfNextLine != -1;
+    }
+
+    /**
+     * get the next newline terminated line
+     *
+     * @return next line
+     */
+    @Override
+    public byte[] next() { // get bytes as 0 terminated
+        try {
+            linePosition = position - 1; // -1 because we have already read the first character on the line
+
+            lineLength = 0;
+            while (firstByteOfNextLine != '\r' && firstByteOfNextLine != '\n') {
+                if ((lineLength + 3) == bytes.length) {
+                    byte[] tmp = new byte[2 * bytes.length];
+                    System.arraycopy(bytes, 0, tmp, 0, bytes.length);
+                    bytes = tmp;
+                }
+                bytes[lineLength++] = firstByteOfNextLine;
+                if (hasNext()) {
+                    firstByteOfNextLine = (byte) reader.read();
+                    if (hasNext())
+                        position++;
+                } else
+                    break;
+            }
+            bytes[lineLength++] = '\n';
+            bytes[lineLength] = 0;
+
+            // move to next first letter...
+            firstByteOfNextLine = (byte) reader.read();
+            if (hasNext())
+                position++;
+            while (firstByteOfNextLine == '\r' || firstByteOfNextLine == '\n' && hasNext()) {
+                firstByteOfNextLine = (byte) reader.read();
+                if (hasNext())
+                    position++;
+            }
+
+        } catch (IOException e) {
+            return null;
+        }
+        return bytes;
+    }
+
+    /**
+     * peeks at the next byte.
+     *
+     * @return next byte or -1, if no next line
+     */
+    public byte peekNextByte() {
+        return firstByteOfNextLine;
+    }
+
+    /**
+     * get the line return by the last next() call
+     *
+     * @return last line
+     */
+    public byte[] getLine() {
+        return bytes;
+    }
+
+    /**
+     * gets the length of latest returned line
+     *
+     * @return
+     */
+    public int getLineLength() {
+        return lineLength;
+    }
+
+    /**
+     * get the position of a line (in the uncompressed file)
+     *
+     * @return position of last line retrieved by next()
+     */
+    public long getLinePosition() {
+        return linePosition;
+    }
+
+    /**
+     * get current position in (uncompressed) file. Equals file length, once getLetterCodeIterator has completed
+     *
+     * @return current position
+     */
+    public long getPosition() {
+        return position;
+    }
+
+    @Override
+    public void remove() {
+
+    }
+
+    @Override
+    public void close() {
+        try {
+            reader.close();
+        } catch (IOException e) {
+            Basic.caught(e);
+        }
+    }
+
+    @Override
+    public long getMaximumProgress() {
+        return maxProgress;
+    }
+
+    @Override
+    public long getProgress() {
+        return position;
+    }
+}
diff --git a/src/jloda/util/GZipUtils.java b/src/jloda/util/GZipUtils.java
new file mode 100644
index 0000000..fcd7d78
--- /dev/null
+++ b/src/jloda/util/GZipUtils.java
@@ -0,0 +1,100 @@
+/**
+ * GZipUtils.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * GZIP utilities
+ * Daniel Huson, 6.2014
+ */
+public class GZipUtils {
+
+    /**
+     * deflate a file in gzip format
+     *
+     * @param sourceFile
+     * @param compressedFile
+     */
+    public static void deflate(String sourceFile, String compressedFile) {
+        byte[] buffer = new byte[512000];
+        try {
+            final ProgressPercentage progress = new ProgressPercentage("Deflating file: " + sourceFile, ((new File(sourceFile)).length()));
+            long total = 0;
+
+            final FileInputStream fileInput = new FileInputStream(sourceFile);
+            final GZIPOutputStream gzipOuputStream = new GZIPOutputStream(new FileOutputStream(compressedFile));
+
+            int numberOfBytes;
+            while ((numberOfBytes = fileInput.read(buffer)) > 0) {
+                gzipOuputStream.write(buffer, 0, numberOfBytes);
+                progress.setProgress(total += numberOfBytes);
+            }
+
+            fileInput.close();
+
+            gzipOuputStream.finish();
+            gzipOuputStream.close();
+
+            progress.close();
+
+        } catch (IOException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+
+    /**
+     * inflate a gzip file
+     *
+     * @param compressedFile
+     * @param decompressedFile
+     */
+    public static void inflate(String compressedFile, String decompressedFile) {
+        byte[] buffer = new byte[512000];
+
+        try {
+            final ProgressPercentage progress = new ProgressPercentage("Inflating file: " + compressedFile, ((new File(compressedFile)).length()));
+            long total = 0;
+
+            final GZIPInputStream gZIPInputStream = new GZIPInputStream(new FileInputStream(compressedFile));
+            final FileOutputStream fileOutputStream = new FileOutputStream(decompressedFile);
+
+            int numberOfBytes;
+            while ((numberOfBytes = gZIPInputStream.read(buffer)) > 0) {
+                fileOutputStream.write(buffer, 0, numberOfBytes);
+                progress.setProgress(total += numberOfBytes);
+            }
+
+            gZIPInputStream.close();
+            fileOutputStream.close();
+
+            progress.close();
+        } catch (IOException ex) {
+            ex.printStackTrace();
+        }
+    }
+}
+
diff --git a/src/jloda/util/Geometry.java b/src/jloda/util/Geometry.java
new file mode 100644
index 0000000..6596c11
--- /dev/null
+++ b/src/jloda/util/Geometry.java
@@ -0,0 +1,419 @@
+/**
+ * Geometry.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+/**
+ * Some useful geometry stuff.
+ * @author Daniel Huson
+ * 7.01
+ */
+
+import java.awt.*;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+
+public class Geometry {
+    /**
+     * Translate a point in the direction specified by an angle.
+     *
+     * @param apt   Point2D
+     * @param alpha double
+     * @param dist  double
+     * @return Point2D
+     */
+    public static Point2D translateByAngle(Point2D apt, double alpha, double dist) {
+        double dx = dist * Math.cos(alpha);
+        double dy = dist * Math.sin(alpha);
+        if (Math.abs(dx) < 0.000000001)
+            dx = 0;
+        if (Math.abs(dy) < 0.000000001)
+            dy = 0;
+        return new Point2D.Double(apt.getX() + dx, apt.getY() + dy);
+    }
+
+    /**
+     * Does line segment between a and b contain point (x,y)?
+     *
+     * @param a       Point
+     * @param b       Point
+     * @param x       double
+     * @param y       double
+     * @param maxDist double
+     * @return true if line segment between a and b contain point (x,y)
+     */
+    public static boolean hitSegment(Point a, Point b, double x, double y,
+                                     double maxDist) {
+        if (Math.min(a.x, b.x) <= x + 1 && x <= Math.max(a.x, b.x + 1)
+                && Math.min(a.y, b.y) <= y + 1 && y <= Math.max(a.y, b.y) + 1) {
+            Line2D.Float line = new Line2D.Float(a, b);
+            if (line.ptLineDist(x, y) <= maxDist)
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * Computes the angle of a two-dimensional vector.
+     *
+     * @param p Point2D
+     * @return angle double
+     */
+    public static double computeAngle(Point2D p) {
+        if (p.getX() != 0) {
+            double x = Math.abs(p.getX());
+            double y = Math.abs(p.getY());
+            double a = Math.atan(y / x);
+
+            if (p.getX() > 0) {
+                if (p.getY() > 0)
+                    return a;
+                else
+                    return 2.0 * Math.PI - a;
+            } else // p.getX()<0
+            {
+                if (p.getY() > 0)
+                    return Math.PI - a;
+                else
+                    return Math.PI + a;
+            }
+        } else if (p.getY() > 0)
+            return 0.5 * Math.PI;
+        else // p.y<0
+            return -0.5 * Math.PI;
+    }
+
+    /**
+     * Computes the angle of a two-dimensional vector.
+     *
+     * @param p Point
+     * @return angle double
+     */
+    public static double computeAngle(Point p) {
+        if (p.getX() != 0) {
+            double x = Math.abs(p.getX());
+            double y = Math.abs(p.getY());
+            double a = Math.atan(y / x);
+
+            if (p.getX() > 0) {
+                if (p.getY() > 0)
+                    return a;
+                else
+                    return 2.0 * Math.PI - a;
+            } else // p.getX()<0
+            {
+                if (p.getY() > 0)
+                    return Math.PI - a;
+                else
+                    return Math.PI + a;
+            }
+        } else if (p.getY() > 0)
+            return 0.5 * Math.PI;
+        else // p.y<0
+            return -0.5 * Math.PI;
+    }
+
+    /**
+     * computes the angle difference between a and b as viewed from center
+     *
+     * @param center
+     * @param a
+     * @param b
+     * @return angle
+     */
+    public static double computeObservedAngle(Point2D center, Point2D a, Point2D b) {
+        Point2D da = Geometry.diff(a, center);
+        Point2D db = Geometry.diff(b, center);
+        double angle = Math.abs(Geometry.computeAngle(da) - Geometry.computeAngle(db));
+        if (angle > Math.PI)
+            angle = 2 * Math.PI - angle;
+
+        double det = da.getX() * db.getY() - da.getY() * db.getX();
+
+        if (det >= 0)
+            return angle;
+        else
+            return -angle;
+    }
+
+    /**
+     * Rotates a two-dimensional vector by the angle alpha.
+     *
+     * @param p     point
+     * @param alpha angle in radian
+     * @return q point rotated around origin
+     */
+    public static Point2D rotate(Point2D p, double alpha) {
+        double sina = Math.sin(alpha);
+        double cosa = Math.cos(alpha);
+        Point2D q = new Point2D.Double();
+        q.setLocation(p.getX() * cosa - p.getY() * sina, p.getX() * sina + p.getY() * cosa);
+        return q;
+    }
+
+    /**
+     * Rotates a two-dimensional vector by the angle alpha.
+     *
+     * @param p     Point
+     * @param alpha double
+     * @return q Point
+     */
+    public static Point rotate(Point p, double alpha) {
+        double sina = Math.sin(alpha);
+        double cosa = Math.cos(alpha);
+        Point q = new Point();
+        q.setLocation(p.getX() * cosa - p.getY() * sina, p.getX() * sina + p.getY() * cosa);
+        return q;
+    }
+
+    /**
+     * Rotates a point by angle alpha around a second point
+     *
+     * @param pt     the point to be rotated
+     * @param alpha  the angle
+     * @param anchor the anchor point
+     * @return the rotated point
+     */
+    public static Point2D rotateAbout(Point2D pt, double alpha, Point2D anchor) {
+        return rotateAbout(pt, alpha, anchor, new Point2D.Double());
+    }
+
+    /**
+     * Rotates a point by angle alpha around a second point
+     *
+     * @param src    the point to be rotated
+     * @param alpha  the angle
+     * @param anchor the anchor point
+     * @param tar    the target point
+     * @return the rotated point
+     */
+    public static Point2D rotateAbout(Point2D src, double alpha, Point2D anchor, Point2D tar) {
+        tar.setLocation(src.getX() - anchor.getX(), src.getY() - anchor.getY());
+        tar.setLocation(rotate(tar, alpha));
+        tar.setLocation(tar.getX() + anchor.getX(), tar.getY() + anchor.getY());
+        return tar;
+
+    }
+
+    /**
+     * Rotates a point by angle alpha around a second point
+     *
+     * @param x      the point to be rotated
+     * @param y      the anchor poin
+     * @param alpha  the angle
+     * @param anchor the anchor point
+     * @param tar    the target point
+     * @return the rotated point
+     */
+    public static Point2D rotateAbout(double x, double y, double alpha, Point2D anchor, Point2D tar) {
+        tar.setLocation(x - anchor.getX(), y - anchor.getY());
+        tar.setLocation(rotate(tar, alpha));
+        tar.setLocation(tar.getX() + anchor.getX(), tar.getY() + anchor.getY());
+        return tar;
+    }
+
+    static final double PI2 = 2 * Math.PI;
+
+    /**
+     * clamp to range 0..2PI
+     *
+     * @param x
+     * @return modulo 2PI
+     */
+    static public double moduloTwoPI(double x) {
+        while (x < 0)
+            x += PI2;
+        while (x > PI2)
+            x -= PI2;
+        return x;
+    }
+
+    /**
+     * gets the difference of two points
+     *
+     * @param tar
+     * @param src
+     * @return difference
+     */
+    public static Point2D diff(Point2D tar, Point2D src) {
+        Point2D result = (Point2D) tar.clone();
+        result.setLocation(result.getX() - src.getX(), result.getY() - src.getY());
+        return result;
+    }
+
+    /**
+     * gets the intersection between two secant segments [O,T] qnd [P,S]
+     *
+     * @param PointO
+     * @param PointP
+     * @param PointS
+     * @param PointT
+     */
+    public static Point2D intersect(Point2D PointO, Point2D PointP, Point2D PointS, Point2D PointT) {
+        double xo = PointO.getX();
+        double yo = PointO.getY();
+        double xt = PointT.getX();
+        double yt = PointT.getY();
+        double xp = PointP.getX();
+        double yp = PointP.getY();
+        double xs = PointS.getX();
+        double ys = PointS.getY();
+
+        double DA = yt - yo;
+        double DB = xo - xt;
+        double DC = yo * DB - xo * DA;
+        double DD = ys - yp;
+        double DE = xp - xs;
+        double DF = yp * DE - xp * DD;
+
+        return new Point2D.Double((DB * DF - DC * DE) / (DA * DE - DB * DD), (DC * DD - DA * DF) / (DA * DE - DB * DD));
+    }
+
+
+    /**
+     * returns the scalar product O.P
+     *
+     * @param PointO
+     * @param PointP
+     */
+    public static double scalar(Point2D PointO, Point2D PointP) {
+        return PointP.getX() * PointO.getX() + PointP.getY() * PointO.getY();
+    }
+
+
+    /**
+     * returns the average of angles A and B
+     *
+     * @param AngleA
+     * @param AngleB
+     */
+    public static double midAngle(double AngleA, double AngleB) {
+        if (moduloTwoPI(AngleA - AngleB) < Math.PI) {
+            return moduloTwoPI(AngleB + (moduloTwoPI(AngleA - AngleB)) / 2);
+        } else {
+            return moduloTwoPI(AngleB - (moduloTwoPI(AngleB - AngleA)) / 2);
+        }
+
+    }
+
+    /**
+     * returns the difference of angles A and B
+     *
+     * @param AngleA
+     * @param AngleB
+     */
+    public static double diffAngle(double AngleA, double AngleB) {
+        if (moduloTwoPI(AngleA - AngleB) > Math.PI) {
+            return 2 * Math.PI - moduloTwoPI(AngleA - AngleB);
+        } else {
+            return moduloTwoPI(AngleA - AngleB);
+        }
+    }
+
+
+    /**
+     * returns the difference of angles A and B
+     *
+     * @param AngleA
+     * @param AngleB
+     */
+    public static double signedDiffAngle(double AngleA, double AngleB) {
+        if (moduloTwoPI(AngleA - AngleB) > Math.PI) {
+            return -(2 * Math.PI - moduloTwoPI(AngleA - AngleB));
+        } else {
+            return moduloTwoPI(AngleA - AngleB);
+        }
+
+    }
+
+
+    /**
+     * returns the difference of angles A and B
+     *
+     * @param A
+     * @param B
+     */
+    public static double squaredDistance(Point2D A, Point2D B) {
+
+        return (B.getX() - A.getX()) * (B.getX() - A.getX()) + (B.getY() - A.getY()) * (B.getY() - A.getY());
+    }
+
+    /**
+     * computes the angle difference between a and b as viewed from center
+     *
+     * @param center
+     * @param a
+     * @param b
+     * @return angle
+     */
+    public static double basicComputeAngle(Point2D center, Point2D a, Point2D b) {
+        Point2D da = Geometry.diff(a, center);
+        Point2D db = Geometry.diff(b, center);
+        return Geometry.moduloTwoPI(Geometry.computeAngle(db) - Geometry.computeAngle(da));
+    }
+
+    final static double factor1 = Math.PI / 180.0;
+
+    /**
+     * convert degree to radian
+     *
+     * @param deg angle in degrees
+     * @return angle in radian
+     */
+    public static double deg2rad(double deg) {
+        return deg * factor1;
+    }
+
+    final static double factor2 = 180.0 / Math.PI;
+
+    /**
+     * convert radian   to degree
+     *
+     * @param rad angle in radian
+     * @return angle in degrees
+     */
+    public static double rad2deg(double rad) {
+        return rad * factor2;
+    }
+
+    /**
+     * computes the squared distance between points (x1,y1) and (x2,y2)
+     *
+     * @param x1
+     * @param y1
+     * @param x2
+     * @param y2
+     * @return squared distance
+     */
+    public static double squaredDistance(double x1, double y1, double x2, double y2) {
+        return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
+    }
+
+    /**
+     * gets the length of a vector
+     *
+     * @param vector
+     * @return length
+     */
+    public static double length(Point2D vector) {
+        return Math.sqrt(vector.getX() * vector.getX() + vector.getY() * vector.getY());
+    }
+}
+
+// EOF
diff --git a/src/jloda/util/GrowlNetwork.java b/src/jloda/util/GrowlNetwork.java
new file mode 100644
index 0000000..733326a
--- /dev/null
+++ b/src/jloda/util/GrowlNetwork.java
@@ -0,0 +1,198 @@
+package jloda.util;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * Interacts with Growl by using the socket service.
+ *
+ * @author chamerling
+ */
+public class GrowlNetwork {
+    public static final int DEFAULT_PORT = 9887;
+    public static final String DEFAULT_HOST = "localhost";
+    private static final byte TYPE_REGISTRATION = 0;
+    private static final byte TYPE_NOTIFICATION = 1;
+    private static final String NOTIFICATION_NAME = "JavaGrowler";
+    private final byte PROTOCOL_VERSION = 1;
+
+    private String host;
+    private int port;
+
+    private static String appName = "GrowlNetwork";
+
+    private static GrowlNetwork instance = null;
+
+    /**
+     * gets the instance of the GrowlNetwork object
+     *
+     * @return instance
+     */
+    public static GrowlNetwork getInstance() {
+        if (instance == null) {
+            String name = ProgramProperties.getProgramName();
+            if (name != null && name.length() > 0)
+                appName = name;
+            return getInstance(appName, "");
+        }
+        return instance;
+    }
+
+    /**
+     * gets an instance of the GrowlNetwork object
+     *
+     * @return instance
+     */
+    public static GrowlNetwork getInstance(String name, String passwd) {
+        if (instance == null) {
+            instance = GrowlNetwork.register(name, passwd);
+        }
+        return instance;
+    }
+
+    /**
+     * notify
+     *
+     * @param title
+     * @param message
+     */
+    public static void notify(String title, String message) {
+        GrowlNetwork g = GrowlNetwork.getInstance();
+        g.notify(appName, title, message, "");
+    }
+
+    /**
+     * @param host
+     * @param port
+     */
+    private GrowlNetwork(String host, int port) {
+        this.host = host;
+        this.port = port;
+    }
+
+    /*
+      * (non-Javadoc)
+      *
+      * @see org.chamerling.javagrowl.Growl#notify(java.lang.String,
+      * java.lang.String, java.lang.String, java.lang.String)
+      */
+    public void notify(String appName, String title, String message, String password) {
+        sendPacket(notificationPacket(appName, title, message, password).array());
+    }
+
+    private byte[] stringEnc(String str) {
+        try {
+            return str.getBytes("UTF8");
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        }
+        return new byte[0];
+    }
+
+    private byte[] md5(byte[] bytes, String password) {
+        MessageDigest md;
+        try {
+            md = MessageDigest.getInstance("MD5");
+            md.update(bytes);
+
+            if (password != null && password.length() > 0) {
+                md.update(stringEnc(password));
+            }
+            return md.digest();
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    private ByteBuffer registrationPacket(String appName, String password) {
+        byte[] name = stringEnc(appName);
+        byte[] notName = stringEnc(NOTIFICATION_NAME);
+
+        int len = 6 + name.length + 2 + notName.length + 1 + 16;
+
+        ByteBuffer bb = ByteBuffer.allocate(len);
+        bb.put(PROTOCOL_VERSION);
+        bb.put(TYPE_REGISTRATION);
+        bb.putShort((short) name.length);
+        bb.put((byte) 1); // nall
+        bb.put((byte) 1); // ndef
+        bb.put(name);
+        bb.putShort((short) notName.length);
+        bb.put(notName);
+        bb.put((byte) 0); // defaults
+        bb.put(md5(Arrays.copyOf(bb.array(), len - 16), password));
+        return bb;
+    }
+
+    private ByteBuffer notificationPacket(String appName, String title, String message, String password) {
+        byte[] uappName = stringEnc(appName);
+        byte[] unotif = stringEnc(NOTIFICATION_NAME);
+        byte[] utitle = stringEnc(title);
+        byte[] umessage = stringEnc(message);
+
+        int len = 12 + unotif.length + utitle.length + umessage.length + uappName.length + 16;
+        ByteBuffer bb = ByteBuffer.allocate(len);
+
+        bb.put(PROTOCOL_VERSION);
+        bb.put(TYPE_NOTIFICATION);
+        bb.putShort((short) 1); // Not sure what the flag value is for...
+        bb.putShort((short) unotif.length);
+        bb.putShort((short) utitle.length);
+        bb.putShort((short) umessage.length);
+        bb.putShort((short) uappName.length);
+        bb.put(unotif);
+        bb.put(utitle);
+        bb.put(umessage);
+        bb.put(uappName);
+        bb.put(md5(Arrays.copyOf(bb.array(), len - 16), password));
+
+        return bb;
+    }
+
+    private void sendPacket(byte[] bytes) {
+        try {
+            DatagramSocket sct = new DatagramSocket();
+            sct.connect(InetAddress.getByName(host), port);
+            DatagramPacket pkt = new DatagramPacket(bytes, bytes.length);
+            sct.send(pkt);
+            sct.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void doRegistration(String appName, String password) {
+        sendPacket(registrationPacket(appName, password).array());
+    }
+
+    public static GrowlNetwork register(String appName, String password) {
+        return register(appName, password, DEFAULT_HOST, DEFAULT_PORT);
+    }
+
+    public static GrowlNetwork register(String appName, String password, String host) {
+        return register(appName, password, host, DEFAULT_PORT);
+    }
+
+    public static GrowlNetwork register(String appName, String password, String host, int port) {
+        GrowlNetwork g = new GrowlNetwork(host, port);
+        g.doRegistration(appName, password);
+        return g;
+    }
+
+    /**
+     * test the grow
+     *
+     * @param args
+     */
+    public static void main(String[] args) {
+        GrowlNetwork.notify("The title", "This is the notification message...");
+    }
+}
\ No newline at end of file
diff --git a/src/jloda/util/HeatSpectrum.java b/src/jloda/util/HeatSpectrum.java
new file mode 100644
index 0000000..28aae00
--- /dev/null
+++ b/src/jloda/util/HeatSpectrum.java
@@ -0,0 +1,560 @@
+/**
+ * HeatSpectrum.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.awt.*;
+
+/**
+ * provides a heat spectrum from 0=cold to 500=hot
+ * Daniel Huson, 12.2008
+ */
+public class HeatSpectrum {
+    final static private Color[] spectrum =
+            new Color[]
+                    {new Color(-13290435),
+                            new Color(-13290435),
+                            new Color(-13356227),
+                            new Color(-13487555),
+                            new Color(-13487554),
+                            new Color(-13553345),
+                            new Color(-13619137),
+                            new Color(-13684929),
+                            new Color(-13750720),
+                            new Color(-13816512),
+                            new Color(-13882303),
+                            new Color(-13948095),
+                            new Color(-14013886),
+                            new Color(-14079678),
+                            new Color(-14145470),
+                            new Color(-14211260),
+                            new Color(-14342846),
+                            new Color(-14408636),
+                            new Color(-14474427),
+                            new Color(-14606012),
+                            new Color(-14671804),
+                            new Color(-14737594),
+                            new Color(-14869180),
+                            new Color(-14934970),
+                            new Color(-15066555),
+                            new Color(-15132346),
+                            new Color(-15198137),
+                            new Color(-15329721),
+                            new Color(-15395512),
+                            new Color(-15461303),
+                            new Color(-15592887),
+                            new Color(-15658679),
+                            new Color(-15724470),
+                            new Color(-15790261),
+                            new Color(-15856052),
+                            new Color(-15921844),
+                            new Color(-15987635),
+                            new Color(-16053427),
+                            new Color(-16119219),
+                            new Color(-16185011),
+                            new Color(-16250802),
+                            new Color(-16316594),
+                            new Color(-16316592),
+                            new Color(-16382383),
+                            new Color(-16448173),
+                            new Color(-16448170),
+                            new Color(-16513705),
+                            new Color(-16513705),
+                            new Color(-16579494),
+                            new Color(-16579235),
+                            new Color(-16645025),
+                            new Color(-16644768),
+                            new Color(-16644764),
+                            new Color(-16710300),
+                            new Color(-16710295),
+                            new Color(-16710040),
+                            new Color(-16709780),
+                            new Color(-16709523),
+                            new Color(-16709261),
+                            new Color(-16774796),
+                            new Color(-16774537),
+                            new Color(-16774023),
+                            new Color(-16773765),
+                            new Color(-16773506),
+                            new Color(-16772992),
+                            new Color(-16772478),
+                            new Color(-16771961),
+                            new Color(-16771448),
+                            new Color(-16770932),
+                            new Color(-16770417),
+                            new Color(-16769903),
+                            new Color(-16769133),
+                            new Color(-16768618),
+                            new Color(-16767847),
+                            new Color(-16767333),
+                            new Color(-16766562),
+                            new Color(-16765792),
+                            new Color(-16765021),
+                            new Color(-16764251),
+                            new Color(-16763480),
+                            new Color(-16762710),
+                            new Color(-16761940),
+                            new Color(-16760914),
+                            new Color(-16760143),
+                            new Color(-16759117),
+                            new Color(-16758090),
+                            new Color(-16757319),
+                            new Color(-16756295),
+                            new Color(-16755524),
+                            new Color(-16754499),
+                            new Color(-16753729),
+                            new Color(-16752702),
+                            new Color(-16751676),
+                            new Color(-16750907),
+                            new Color(-16749881),
+                            new Color(-16749111),
+                            new Color(-16748598),
+                            new Color(-16747828),
+                            new Color(-16747315),
+                            new Color(-16746545),
+                            new Color(-16746032),
+                            new Color(-16745006),
+                            new Color(-16743723),
+                            new Color(-16742441),
+                            new Color(-16741414),
+                            new Color(-16740132),
+                            new Color(-16739361),
+                            new Color(-16738335),
+                            new Color(-16736796),
+                            new Color(-16736026),
+                            new Color(-16734744),
+                            new Color(-16733717),
+                            new Color(-16732691),
+                            new Color(-16731409),
+                            new Color(-16730639),
+                            new Color(-16729614),
+                            new Color(-16728588),
+                            new Color(-16727562),
+                            new Color(-16726793),
+                            new Color(-16725767),
+                            new Color(-16724998),
+                            new Color(-16724229),
+                            new Color(-16723204),
+                            new Color(-16722691),
+                            new Color(-16721921),
+                            new Color(-16720897),
+                            new Color(-16720641),
+                            new Color(-16719617),
+                            new Color(-16719105),
+                            new Color(-16718849),
+                            new Color(-16718337),
+                            new Color(-16717825),
+                            new Color(-16717313),
+                            new Color(-16717058),
+                            new Color(-16717058),
+                            new Color(-16717058),
+                            new Color(-16717060),
+                            new Color(-16717061),
+                            new Color(-16717062),
+                            new Color(-16717063),
+                            new Color(-16717065),
+                            new Color(-16717067),
+                            new Color(-16717068),
+                            new Color(-16717069),
+                            new Color(-16717071),
+                            new Color(-16717074),
+                            new Color(-16717076),
+                            new Color(-16717078),
+                            new Color(-16717080),
+                            new Color(-16717082),
+                            new Color(-16717085),
+                            new Color(-16717087),
+                            new Color(-16717090),
+                            new Color(-16717092),
+                            new Color(-16717095),
+                            new Color(-16717098),
+                            new Color(-16717100),
+                            new Color(-16717360),
+                            new Color(-16717619),
+                            new Color(-16717878),
+                            new Color(-16718393),
+                            new Color(-16718653),
+                            new Color(-16718911),
+                            new Color(-16719170),
+                            new Color(-16719686),
+                            new Color(-16719945),
+                            new Color(-16720205),
+                            new Color(-16720464),
+                            new Color(-16720979),
+                            new Color(-16721239),
+                            new Color(-16721498),
+                            new Color(-16721756),
+                            new Color(-16722014),
+                            new Color(-16722529),
+                            new Color(-16722531),
+                            new Color(-16722790),
+                            new Color(-16723048),
+                            new Color(-16723306),
+                            new Color(-16723564),
+                            new Color(-16723823),
+                            new Color(-16724081),
+                            new Color(-16724339),
+                            new Color(-16724598),
+                            new Color(-16724856),
+                            new Color(-16725115),
+                            new Color(-16725116),
+                            new Color(-16725631),
+                            new Color(-16725633),
+                            new Color(-16726147),
+                            new Color(-16726150),
+                            new Color(-16726407),
+                            new Color(-16726922),
+                            new Color(-16726924),
+                            new Color(-16727182),
+                            new Color(-16727185),
+                            new Color(-16727699),
+                            new Color(-16727957),
+                            new Color(-16727959),
+                            new Color(-16728217),
+                            new Color(-16728475),
+                            new Color(-16728734),
+                            new Color(-16728735),
+                            new Color(-16728993),
+                            new Color(-16728995),
+                            new Color(-16729509),
+                            new Color(-16729511),
+                            new Color(-16729769),
+                            new Color(-16729770),
+                            new Color(-16729773),
+                            new Color(-16730031),
+                            new Color(-16730032),
+                            new Color(-16730290),
+                            new Color(-16730548),
+                            new Color(-16730550),
+                            new Color(-16730551),
+                            new Color(-16730553),
+                            new Color(-16730810),
+                            new Color(-16730812),
+                            new Color(-16731070),
+                            new Color(-16731071),
+                            new Color(-16731072),
+                            new Color(-16731074),
+                            new Color(-16731075),
+                            new Color(-16731076),
+                            new Color(-16731077),
+                            new Color(-16731079),
+                            new Color(-16600007),
+                            new Color(-16468681),
+                            new Color(-16468682),
+                            new Color(-16337612),
+                            new Color(-16272077),
+                            new Color(-16140749),
+                            new Color(-16009679),
+                            new Color(-15944144),
+                            new Color(-15812817),
+                            new Color(-15681746),
+                            new Color(-15615955),
+                            new Color(-15484884),
+                            new Color(-15353557),
+                            new Color(-15222230),
+                            new Color(-15091159),
+                            new Color(-14894296),
+                            new Color(-14828761),
+                            new Color(-14631641),
+                            new Color(-14500570),
+                            new Color(-14368988),
+                            new Color(-14172380),
+                            new Color(-14041053),
+                            new Color(-13909725),
+                            new Color(-13712606),
+                            new Color(-13515999),
+                            new Color(-13384672),
+                            new Color(-13187809),
+                            new Color(-13056482),
+                            new Color(-12859618),
+                            new Color(-12662499),
+                            new Color(-12531172),
+                            new Color(-12334309),
+                            new Color(-12137445),
+                            new Color(-12006117),
+                            new Color(-11743719),
+                            new Color(-11612391),
+                            new Color(-11415271),
+                            new Color(-11218409),
+                            new Color(-11087081),
+                            new Color(-10824682),
+                            new Color(-10627562),
+                            new Color(-10430698),
+                            new Color(-10233835),
+                            new Color(-10036716),
+                            new Color(-9905388),
+                            new Color(-9642989),
+                            new Color(-9511661),
+                            new Color(-9249262),
+                            new Color(-9052142),
+                            new Color(-8855278),
+                            new Color(-8658415),
+                            new Color(-8461296),
+                            new Color(-8329968),
+                            new Color(-8133104),
+                            new Color(-7870704),
+                            new Color(-7739377),
+                            new Color(-7476978),
+                            new Color(-7280114),
+                            new Color(-7148531),
+                            new Color(-6886131),
+                            new Color(-6689267),
+                            new Color(-6557940),
+                            new Color(-6295284),
+                            new Color(-6098676),
+                            new Color(-5901813),
+                            new Color(-5770485),
+                            new Color(-5508085),
+                            new Color(-5376758),
+                            new Color(-5114358),
+                            new Color(-4983030),
+                            new Color(-4786166),
+                            new Color(-4589559),
+                            new Color(-4392439),
+                            new Color(-4261368),
+                            new Color(-4130040),
+                            new Color(-3867896),
+                            new Color(-3736569),
+                            new Color(-3539960),
+                            new Color(-3343353),
+                            new Color(-3212281),
+                            new Color(-3081210),
+                            new Color(-2884602),
+                            new Color(-2753530),
+                            new Color(-2556922),
+                            new Color(-2360315),
+                            new Color(-2294779),
+                            new Color(-2098171),
+                            new Color(-1967099),
+                            new Color(-1836027),
+                            new Color(-1639420),
+                            new Color(-1508348),
+                            new Color(-1377276),
+                            new Color(-1246205),
+                            new Color(-1180669),
+                            new Color(-1049597),
+                            new Color(-852990),
+                            new Color(-852990),
+                            new Color(-721918),
+                            new Color(-590846),
+                            new Color(-459775),
+                            new Color(-328703),
+                            new Color(-263167),
+                            new Color(-132095),
+                            new Color(-132096),
+                            new Color(-66560),
+                            new Color(-1280),
+                            new Color(-1536),
+                            new Color(-1792),
+                            new Color(-2048),
+                            new Color(-2304),
+                            new Color(-2816),
+                            new Color(-3072),
+                            new Color(-3584),
+                            new Color(-3840),
+                            new Color(-4352),
+                            new Color(-4864),
+                            new Color(-5120),
+                            new Color(-5632),
+                            new Color(-6400),
+                            new Color(-6912),
+                            new Color(-7424),
+                            new Color(-7936),
+                            new Color(-8704),
+                            new Color(-9216),
+                            new Color(-9728),
+                            new Color(-10240),
+                            new Color(-11008),
+                            new Color(-11520),
+                            new Color(-12288),
+                            new Color(-12800),
+                            new Color(-13568),
+                            new Color(-14336),
+                            new Color(-14848),
+                            new Color(-15872),
+                            new Color(-16384),
+                            new Color(-17152),
+                            new Color(-17920),
+                            new Color(-18944),
+                            new Color(-19456),
+                            new Color(-20480),
+                            new Color(-20992),
+                            new Color(-21760),
+                            new Color(-22784),
+                            new Color(-23552),
+                            new Color(-24064),
+                            new Color(-25088),
+                            new Color(-25856),
+                            new Color(-26880),
+                            new Color(-27648),
+                            new Color(-28416),
+                            new Color(-29184),
+                            new Color(-30208),
+                            new Color(-30976),
+                            new Color(-31744),
+                            new Color(-32512),
+                            new Color(-33536),
+                            new Color(-34304),
+                            new Color(-34816),
+                            new Color(-35840),
+                            new Color(-36608),
+                            new Color(-37632),
+                            new Color(-38400),
+                            new Color(-39168),
+                            new Color(-39936),
+                            new Color(-40960),
+                            new Color(-41728),
+                            new Color(-42496),
+                            new Color(-43520),
+                            new Color(-44032),
+                            new Color(-45056),
+                            new Color(-45824),
+                            new Color(-46592),
+                            new Color(-47360),
+                            new Color(-48128),
+                            new Color(-48896),
+                            new Color(-49664),
+                            new Color(-50432),
+                            new Color(-51200),
+                            new Color(-51968),
+                            new Color(-52736),
+                            new Color(-53248),
+                            new Color(-54016),
+                            new Color(-54784),
+                            new Color(-55296),
+                            new Color(-56064),
+                            new Color(-56832),
+                            new Color(-57344),
+                            new Color(-58112),
+                            new Color(-58368),
+                            new Color(-59136),
+                            new Color(-59648),
+                            new Color(-60160),
+                            new Color(-60928),
+                            new Color(-61440),
+                            new Color(-61952),
+                            new Color(-62208),
+                            new Color(-62720),
+                            new Color(-63488),
+                            new Color(-63744),
+                            new Color(-64000),
+                            new Color(-64512),
+                            new Color(-64768),
+                            new Color(-65280),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65536),
+                            new Color(-65279),
+                            new Color(-65279),
+                            new Color(-65279),
+                            new Color(-65279),
+                            new Color(-65279),
+                            new Color(-65022),
+                            new Color(-65022),
+                            new Color(-65022),
+                            new Color(-64765),
+                            new Color(-64765),
+                            new Color(-64508),
+                            new Color(-64251),
+                            new Color(-63994),
+                            new Color(-63994),
+                            new Color(-63480),
+                            new Color(-63223),
+                            new Color(-62966),
+                            new Color(-62452),
+                            new Color(-62195),
+                            new Color(-61681),
+                            new Color(-61167),
+                            new Color(-60396),
+                            new Color(-59882),
+                            new Color(-59111),
+                            new Color(-58340),
+                            new Color(-57312),
+                            new Color(-56284),
+                            new Color(-54999),
+                            new Color(-53971),
+                            new Color(-52686),
+                            new Color(-51401),
+                            new Color(-49859),
+                            new Color(-48317),
+                            new Color(-46518),
+                            new Color(-44719),
+                            new Color(-43177),
+                            new Color(-41121),
+                            new Color(-39322),
+                            new Color(-37009),
+                            new Color(-34953),
+                            new Color(-32640),
+                            new Color(-30841),
+                            new Color(-28528),
+                            new Color(-26729),
+                            new Color(-23902),
+                            new Color(-21846),
+                            new Color(-19533),
+                            new Color(-17477),
+                            new Color(-15164),
+                            new Color(-13108),
+                            new Color(-11052),
+                            new Color(-8996),
+                            new Color(-7197),
+                            new Color(-5398),
+                            new Color(-3856)};
+
+    /**
+     * get the color for a specific value
+     *
+     * @param value
+     * @return color
+     */
+    public static Color getColor(int value) {
+        return spectrum[value];
+    }
+
+    /**
+     * change the color associated with the given value
+     *
+     * @param value
+     * @param color
+     */
+    public static void setColor(int value, Color color) {
+        spectrum[value] = color;
+    }
+
+    /**
+     * get number of colors. size()-1 is max value
+     *
+     * @return size
+     */
+    public static int size() {
+        return spectrum.length;
+    }
+}
diff --git a/src/jloda/util/ICloseableIterator.java b/src/jloda/util/ICloseableIterator.java
new file mode 100644
index 0000000..45142b4
--- /dev/null
+++ b/src/jloda/util/ICloseableIterator.java
@@ -0,0 +1,50 @@
+/**
+ * ICloseableIterator.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Iterator;
+
+/**
+ * A closeable iterator, e.g. based on a file or database
+ * Daniel Huson, 4.2010
+ */
+public interface ICloseableIterator<T> extends Iterator<T>, Closeable {
+    /**
+     * close associated file or database
+     */
+    void close() throws IOException;
+
+    /**
+     * gets the maximum progress value
+     *
+     * @return maximum progress value
+     */
+    long getMaximumProgress();
+
+    /**
+     * gets the current progress value
+     *
+     * @return current progress value
+     */
+    long getProgress();
+
+}
diff --git a/src/jloda/util/IFileIterator.java b/src/jloda/util/IFileIterator.java
new file mode 100644
index 0000000..d0cc895
--- /dev/null
+++ b/src/jloda/util/IFileIterator.java
@@ -0,0 +1,33 @@
+/**
+ * ICloseableIterator.java
+ * Copyright (C) 2016 Daniel H. Huson
+ * <p/>
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ * <p/>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * <p/>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * <p/>
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package jloda.util;
+
+/**
+ * A file iterator
+ * Daniel Huson, 6.2015
+ */
+public interface IFileIterator extends ICloseableIterator<String> {
+    /**
+     * gets the current line number
+     * @return line number
+     */
+    long getLineNumber();
+
+}
diff --git a/src/jloda/util/IStateChecker.java b/src/jloda/util/IStateChecker.java
new file mode 100644
index 0000000..fa32dfc
--- /dev/null
+++ b/src/jloda/util/IStateChecker.java
@@ -0,0 +1,12 @@
+package jloda.util;
+
+/**
+ * checks the state
+ * daniel Huson, 4.2015
+ */
+public interface IStateChecker {
+    /**
+     * checks the state
+     */
+    void check();
+}
diff --git a/src/jloda/util/IteratorAdapter.java b/src/jloda/util/IteratorAdapter.java
new file mode 100644
index 0000000..b489259
--- /dev/null
+++ b/src/jloda/util/IteratorAdapter.java
@@ -0,0 +1,75 @@
+/**
+ * IteratorAdapter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.NoSuchElementException;
+
+/**
+ * An abstract base class for iterators with single element caching. Derived
+ * classes need only implement the method <code>findNext</code>.
+ */
+public abstract class IteratorAdapter<T> implements Iterator<T> {
+    private final LinkedList<T> cache = new LinkedList<>();
+
+    /**
+     * Returns the next available element or throws an exception.
+     *
+     * @return the next element.
+     * @throws NoSuchElementException if no more elements are available.
+     */
+    protected abstract T findNext() throws NoSuchElementException;
+
+    /* (non-Javadoc)
+     * @see java.util.Iterator#hasNext()
+     */
+
+    public boolean hasNext() {
+        if (cache.size() == 0) {
+            try {
+                cache.addLast(findNext());
+            } catch (NoSuchElementException ex) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Iterator#next()
+     */
+
+    public T next() {
+        if (cache.size() == 0) {
+            return findNext();
+        } else {
+            return cache.removeFirst();
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Iterator#remove()
+     */
+
+    public void remove() {
+        throw new UnsupportedOperationException("not supported");
+    }
+}
diff --git a/src/jloda/util/License.java b/src/jloda/util/License.java
new file mode 100644
index 0000000..d1ff77f
--- /dev/null
+++ b/src/jloda/util/License.java
@@ -0,0 +1,353 @@
+/**
+ * License.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.*;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * a license data object
+ * Daniel Huson, 4.2013
+ */
+public class License {
+    public enum Item {Program, User, Email, Organization, Address, City, Country, IPAddress, LicenseType, ExpireDate, Signature}
+
+    public enum Type {
+        Academic, SingleUser, Site, Company, Temporary, NonProfitResearchLab;
+
+        public String getInfo() {
+            switch (this) {
+                default:
+                case Academic:
+                    return "academic research, publication and teaching only.";
+                case SingleUser:
+                    return "only to be used by person specified under 'User'.";
+                case Site:
+                    return "only to be used at the site specified under 'Address' and 'City'.";
+                case Company:
+                    return "only to be used within the company specified under 'Organization'.";
+                case Temporary:
+                    return "only to be used for evaluation or teaching purposes.";
+                case NonProfitResearchLab:
+                    return "non-profit research, publication and teaching only.";
+            }
+        }
+    }
+
+    private final Map<Item, String> item2value = new HashMap<>();
+
+    /**
+     * set a list of name to value pairs
+     *
+     * @param pairs
+     */
+    public void setPairs(java.util.Collection<Pair<String, String>> pairs) {
+        for (Pair<String, String> name2value : pairs) {
+            final Item item = Item.valueOf(name2value.get1());
+            if (item != null) {
+                String value = name2value.get2().trim();
+                if (item == Item.ExpireDate) {
+                    if (value.length() == 0 || value.equals("0")) {
+                        if (item2value.containsKey(item))
+                            item2value.remove(item);
+                    } else {
+                        if (value.equals("1")) // one year
+                        {
+                            value = (new Date((long) (System.currentTimeMillis() + 3.419e+10))).toString();
+                        } else if (value.equals("2")) // two years
+                        {
+                            value = (new Date((long) (System.currentTimeMillis() + 2 * 3.419e+10))).toString();
+                        }
+                        item2value.put(item, value);
+                    }
+                } else
+                    item2value.put(item, value);
+            }
+        }
+    }
+
+
+    /**
+     * string representation
+     *
+     * @return as string
+     */
+    public String toString() {
+        final StringBuilder buf = new StringBuilder();
+        for (Item item : Item.values()) {
+            if (item2value.containsKey(item)) {
+                buf.append(item.toString()).append(": ").append(item2value.get(item)).append("\n");
+            }
+        }
+        return buf.toString();
+    }
+
+    /**
+     * returns the size
+     *
+     * @return size
+     */
+    public int size() {
+        return item2value.size();
+    }
+
+    /**
+     * erase the license info
+     */
+    public void clear() {
+        item2value.clear();
+    }
+
+    /**
+     * string representation
+     *
+     * @return as string
+     */
+    public String toStringWithoutSignature() {
+        StringBuilder buf = new StringBuilder();
+        for (Item item : Item.values()) {
+            if (item != Item.Signature && item2value.containsKey(item)) {
+                String value = item2value.get(item);
+                if (item == Item.LicenseType && !value.contains(" -"))
+                    value = value + " - " + Type.valueOf(value).getInfo();
+                buf.append(item.toString()).append(": ").append(value.trim()).append("\n");
+            }
+        }
+        return buf.toString();
+    }
+
+    /**
+     * put the value for an item
+     *
+     * @param item
+     * @param value
+     */
+    public void put(Item item, String value) {
+        item2value.put(item, value);
+    }
+
+    /**
+     * get the value for an item
+     *
+     * @param item
+     * @return value
+     */
+    public String get(Item item) {
+        return item2value.get(item);
+    }
+
+    /**
+     * gets data as bytes  without the signature (for verifying)
+     *
+     * @return as bytes
+     */
+    public byte[] getBytes() {
+        try {
+            // String str= toStringWithoutSignature();
+            // System.err.println("String:\n"+str);
+            //  System.err.println("Length: "+str.length());
+            //  System.err.println("Hash: "+str.hashCode());
+
+            // replace all non-ascii characters by double ?? before computing score
+            // use ?? because this is what non-ascii characters result in on web page
+            return toStringWithoutSignature().replaceAll("\\P{InBasic_Latin}", "??").getBytes("ISO-8859-1");
+        } catch (UnsupportedEncodingException e) {
+            return toStringWithoutSignature().getBytes();
+        }
+    }
+
+    /**
+     * save to file
+     *
+     * @param fileName
+     * @throws IOException
+     */
+    public void writeToFile(String fileName) throws IOException {
+        try (BufferedWriter w = new BufferedWriter(new FileWriter(fileName))) {
+            w.write(toString());
+        }
+    }
+
+    /**
+     * load from a file
+     *
+     * @param file
+     * @throws IOException
+     */
+    public void loadFromFile(File file) {
+        item2value.clear();
+
+        ICloseableIterator<String> it = null;
+        try {
+            if ((new RTFFileFilter()).accept(file.getParentFile(), file.getName())) {
+                System.err.println("This file appears to be in RTF format, not TXT format, will try to parse. If unsuccessful, please save as a TXT file and let me try again.");
+                final String[] lines = RTFFileFilter.getStrippedLines(file);
+                it = new ICloseableIterator<String>() {
+                    private int i = 0;
+
+                    public void close() throws IOException {
+                    }
+
+                    public long getMaximumProgress() {
+                        return lines.length;
+                    }
+
+                    public long getProgress() {
+                        return i;
+                    }
+
+                    public boolean hasNext() {
+                        return i < lines.length;
+                    }
+
+                    public String next() {
+                        return lines[i++];
+                    }
+
+                    public void remove() {
+
+                    }
+                };
+            } else
+            it = new FileInputIterator(file.getPath());
+
+            boolean inLicense = false;
+            boolean waitForSignature = false; // often the signature token and the actual singature or on different lines, try to fix this
+            while (it.hasNext()) {
+                final String aLine = it.next();
+
+                if (aLine.length() > 0 && !aLine.startsWith("#")) {
+                    if (!inLicense) {
+                        if (aLine.equals("License certificate:") || aLine.equals("Registration details:"))  // the latter for legacy
+                            inLicense = true;
+                    } else {
+                        final int pos = aLine.indexOf(':');
+                        if (pos != -1) {
+                            final String key = aLine.substring(0, pos).trim();
+                            final String value = pos + 1 < aLine.length() ? aLine.substring(pos + 1, aLine.length()).trim() : null;
+                            final Item item = Item.valueOf(key);
+                            item2value.put(item, value);
+                            if (item.equals(Item.Signature)) {
+                                if (value == null && it.hasNext()) // signature appears to be on next line
+                                {
+                                    item2value.put(item, it.next().trim());
+                                }
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception ex) {
+        } finally {
+            try {
+                if (it != null)
+                    it.close();
+            } catch (IOException e) {
+            }
+        }
+    }
+
+    /**
+     * verify that signature is valid
+     *
+     * @param signer
+     * @return true, if signature is valid
+     * @throws IOException
+     * @throws NoSuchProviderException
+     * @throws NoSuchAlgorithmException
+     * @throws InvalidKeySpecException
+     * @throws SignatureException
+     * @throws InvalidKeyException
+     */
+    public boolean verifySignature(Signer signer) {
+        try {
+            return signer.verifySignedData(getBytes(), Signer.signatureHexStringToBytes(get(Item.Signature)));
+        } catch (Exception e) {
+        }
+        return false;
+    }
+
+    /**
+     * gets licensed-to header string
+     *
+     * @return string
+     */
+    public String getLicensedTo() {
+        if (item2value.size() > 0) {
+            final String value = item2value.get(Item.LicenseType);
+            if (value != null) {
+                String typeName = Basic.getFirstWord(value);
+                switch (Type.valueOf(typeName)) {
+                    case Academic: {
+                        StringWriter w = new StringWriter();
+                        w.write("Licensed to user: " + item2value.get(Item.User) + ", ");
+                        w.write("Academic institution: " + item2value.get(Item.Organization) + ", ");
+                        w.write("Email: " + item2value.get(Item.Email));
+                        return w.toString();
+                    }
+                    case SingleUser: {
+                        StringWriter w = new StringWriter();
+                        w.write("Licensed to user: " + item2value.get(Item.User) + ", ");
+                        w.write("Organization: " + item2value.get(Item.Organization) + ", ");
+                        w.write("Email: " + item2value.get(Item.Email));
+                        return w.toString();
+                    }
+                    case Site: {
+                        StringWriter w = new StringWriter();
+                        w.write("Licensed to Site: " + item2value.get(Item.City) + ", ");
+                        w.write("Organization: " + item2value.get(Item.Organization) + ", ");
+                        w.write("Email: " + item2value.get(Item.Email));
+                        return w.toString();
+                    }
+                    case Company: {
+                        StringWriter w = new StringWriter();
+                        w.write("Licensed to company: " + item2value.get(Item.Organization) + ", ");
+                        w.write("Contact: " + item2value.get(Item.User) + ", ");
+                        w.write("Email: " + item2value.get(Item.Email));
+                        return w.toString();
+                    }
+                    case Temporary: {
+                        StringWriter w = new StringWriter();
+                        w.write("Temporary license ");
+                        w.write("User: " + item2value.get(Item.User) + ", ");
+                        w.write("Organization: " + item2value.get(Item.Organization));
+                        return w.toString();
+                    }
+                    case NonProfitResearchLab: {
+                        StringWriter w = new StringWriter();
+                        w.write("Non-profit license ");
+                        w.write("User: " + item2value.get(Item.User) + ", ");
+                        w.write("Organization: " + item2value.get(Item.Organization));
+                        return w.toString();
+                    }
+                }
+            }
+        }
+        return "No valid license - for evaluation only";
+    }
+}
diff --git a/src/jloda/util/ListOfLongs.java b/src/jloda/util/ListOfLongs.java
new file mode 100644
index 0000000..7624c33
--- /dev/null
+++ b/src/jloda/util/ListOfLongs.java
@@ -0,0 +1,69 @@
+/**
+ * ListOfLongs.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+/**
+ * a list of longs
+ * Created by huson on 5/16/14.
+ */
+public class ListOfLongs {
+    private long[] data;
+    private int size = 0;
+
+    public ListOfLongs() {
+        this(1024);
+    }
+
+    public ListOfLongs(int initialSize) {
+        data = new long[initialSize];
+    }
+
+    public void clear() {
+        size = 0;
+    }
+
+    public void add(long value) {
+        if (size == data.length) {
+            long[] tmp = new long[(int) Math.min(Integer.MAX_VALUE, 2l * data.length)];
+            System.arraycopy(data, 0, tmp, 0, data.length);
+            data = tmp;
+        }
+        data[size++] = value;
+    }
+
+    public int size() {
+        return size;
+    }
+
+    public long get(int i) {
+        return data[i];
+    }
+
+    public void addAll(ListOfLongs listOfLongs) {
+        long newSize = size + listOfLongs.size;
+        if (newSize >= data.length) {
+            long[] tmp = new long[(int) Math.min(Integer.MAX_VALUE, newSize)];
+            System.arraycopy(data, 0, tmp, 0, data.length);
+            data = tmp;
+        }
+        System.arraycopy(listOfLongs.data, 0, data, size, listOfLongs.size);
+        size += listOfLongs.size;
+    }
+}
diff --git a/src/jloda/util/MenuMnemonics.java b/src/jloda/util/MenuMnemonics.java
new file mode 100644
index 0000000..902257e
--- /dev/null
+++ b/src/jloda/util/MenuMnemonics.java
@@ -0,0 +1,92 @@
+/**
+ * MenuMnemonics.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import javax.swing.*;
+import java.util.BitSet;
+
+/**
+ * add mnemonics to menu, preserving any already set and fixing any broken ones
+ * Daniel Huson , 6.2005
+ */
+public class MenuMnemonics {
+    /**
+     * set the mnemonic for all items of a menu
+     *
+     * @param menu
+     */
+    public static void setMnemonics(JMenu menu) {
+        if (menu.getMnemonic() == 0) {
+            int menuMnemonic = menu.getText().charAt(0);
+            menu.setMnemonic(menuMnemonic);
+        }
+        final BitSet seen = new BitSet();
+
+        // use all mnemonics already set:
+        for (int itemNumber = 0; itemNumber < menu.getItemCount(); itemNumber++) {
+            if (menu.getItem(itemNumber) != null) {
+                final String text = menu.getItem(itemNumber).getText();
+                if (text != null) {
+                    final int m = Character.toLowerCase(menu.getItem(itemNumber).getMnemonic());
+                    if (m != 0) {
+                        if (!seen.get(m)) {
+                            seen.set(m);
+                            menu.getItem(itemNumber).setMnemonic(m);
+                        } else {
+                            menu.getItem(itemNumber).setMnemonic(0);
+                        }
+                    }
+                }
+            }
+        }
+        // add new mnemonics
+        for (int itemNumber = 0; itemNumber < menu.getItemCount(); itemNumber++) {
+            if (menu.getItem(itemNumber) != null) {
+                final String text = menu.getItem(itemNumber).getText();
+                if (text != null) {
+                    final JMenu subMenu;
+                    if (menu.getItem(itemNumber) instanceof JMenu)
+                        subMenu = (JMenu) menu.getItem(itemNumber);
+                    else
+                        subMenu = null;
+                    final int m = Character.toLowerCase(menu.getItem(itemNumber).getMnemonic());
+                    if (m == 0) // not set
+                    {
+                        for (int pos = 0; pos < text.length(); pos++) {
+                            final int letter = Character.toLowerCase(text.charAt(pos));
+                            if (Character.isLetter(letter)) {
+                                if (!seen.get(letter)) {
+                                    menu.getItem(itemNumber).setMnemonic(letter);
+                                    seen.set(letter);
+                                    if (subMenu !=null) {
+                                        subMenu.setMnemonic(letter);
+                                    }
+                                    break; // found a usable letter
+                                }
+                            }
+                        }
+                    }
+                    if (subMenu != null)
+                        setMnemonics(subMenu);
+                }
+            }
+        }
+    }
+}
diff --git a/src/jloda/util/MultiLineCellRenderer.java b/src/jloda/util/MultiLineCellRenderer.java
new file mode 100644
index 0000000..efcf268
--- /dev/null
+++ b/src/jloda/util/MultiLineCellRenderer.java
@@ -0,0 +1,82 @@
+/**
+ * MultiLineCellRenderer.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import javax.swing.*;
+import javax.swing.tree.TreeCellRenderer;
+import java.awt.*;
+
+/**
+ * MultiLine Cell Renderer for the JTree to display more than one line
+ *
+ * @author daniel huson, 2010
+ */
+public class MultiLineCellRenderer implements TreeCellRenderer {
+    public final static String GRAY = "<font color=#a0a0a0>";
+    public final static String RED = "<font color=#ff0000>";
+    public final static String BLUE = "<font color=#0000ff>";
+    public final static String GREEN = "<font color=#00ff00>";
+
+    /**
+     * create a multi-line renderer
+     */
+    public MultiLineCellRenderer() {
+    }
+
+    /**
+     * get the tree cell render component
+     *
+     * @param tree
+     * @param value
+     * @param isSelected
+     * @param expanded
+     * @param leaf
+     * @param row
+     * @param hasFocus
+     * @return component
+     */
+    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean isSelected, boolean expanded,
+                                                  boolean leaf, int row, boolean hasFocus) {
+        String stringValue = tree.convertValueToText(value, isSelected, expanded, leaf, row, hasFocus);
+
+        JEditorPane textPane = new JEditorPane();
+        textPane.setEditable(false);
+
+        textPane.setContentType("text/html");
+        String text = Basic.trimEmptyLines(stringValue);
+        if (text.contains("\n") && text.indexOf("\n") < text.length() - 1) // more than one line:
+        {
+            text = Basic.trimEmptyLines(text).replaceAll("\t", "	");
+            text = "<html><pre><font face=\"monospace\" size=\"3\">" + text + "</font></pre></html>";
+        } else // only one line:
+            text = "<html><font face=\"monospace\" size=\"3\">" + text + "</font></html>";
+
+        textPane.setText(text);
+        textPane.setEnabled(tree.isEnabled());
+        if (isSelected) {
+            textPane.setBackground(ProgramProperties.SELECTION_COLOR);
+        } else {
+            textPane.setBackground(UIManager.getColor("Tree.textBackground"));
+        }
+        textPane.revalidate();
+        return textPane;
+    }
+}
+
diff --git a/src/jloda/util/NexusFileFilter.java b/src/jloda/util/NexusFileFilter.java
new file mode 100644
index 0000000..42bedb4
--- /dev/null
+++ b/src/jloda/util/NexusFileFilter.java
@@ -0,0 +1,48 @@
+/**
+ * NexusFileFilter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.FilenameFilter;
+
+/**
+ * @author Daniel Huson
+ *         Nexus file filter
+ *         6.2010
+ */
+
+public class NexusFileFilter extends FileFilterBase implements FilenameFilter {
+    public NexusFileFilter() {
+        add("nex");
+        add("nxs");
+        add("nexus");
+    }
+
+    public NexusFileFilter(String additionalSuffix) {
+        this();
+        add(additionalSuffix);
+    }
+
+    /**
+     * @return description of file matching the filter
+     */
+    public String getBriefDescription() {
+        return "Nexus Files";
+    }
+}
diff --git a/src/jloda/util/NotOwnerException.java b/src/jloda/util/NotOwnerException.java
new file mode 100644
index 0000000..dbdd99e
--- /dev/null
+++ b/src/jloda/util/NotOwnerException.java
@@ -0,0 +1,58 @@
+/**
+ * NotOwnerException.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: NotOwnerException.java,v 1.7 2006-06-06 18:56:03 huson Exp $
+ *
+ * Exceptions for all of jloda
+ *
+ * @author Daniel Huson
+ */
+
+package jloda.util;
+//import java.lang.Exception;
+
+/**
+ * This exception indicates that a given node or edge is not owned by
+ * the given Graph, NodeArray or EdgeArray.
+ */
+public class NotOwnerException extends RuntimeException {
+    final Object obj;
+
+    /**
+     * Constructor of NotOwnerException
+     *
+     * @param obj Object
+     */
+    public NotOwnerException(Object obj) {
+        super("" + obj);
+        this.obj = obj;
+    }
+
+    /**
+     * Gets the object
+     *
+     * @return the Object
+     */
+    public Object getObject() {
+        return obj;
+    }
+}
+// EOF
+
diff --git a/src/jloda/util/Pair.java b/src/jloda/util/Pair.java
new file mode 100644
index 0000000..f0bfb2a
--- /dev/null
+++ b/src/jloda/util/Pair.java
@@ -0,0 +1,188 @@
+/**
+ * Pair.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.util.Comparator;
+
+/**
+ * a generic pair of objects
+ *
+ * @author huson
+ *         Date: 14-May-2004
+ */
+public class Pair<S, T> implements Comparable<Pair<S, T>>, Comparator<Pair<S, T>> {
+    S first;
+    T second;
+
+    public Pair() {
+
+    }
+
+    public Pair(S first, T second) {
+        setFirst(first);
+        setSecond(second);
+
+    }
+
+    public S getFirst() {
+        return first;
+    }
+
+    public void setFirst(S first) {
+        this.first = first;
+    }
+
+    public T getSecond() {
+        return second;
+    }
+
+    public void setSecond(T second) {
+        this.second = second;
+    }
+
+    public int getFirstInt() {
+        return ((Integer) first);
+    }
+
+
+    public int getSecondInt() {
+        return ((Integer) second);
+    }
+
+    public double getFirstDouble() {
+        return ((Double) first);
+    }
+
+    public long getFirstLong() {
+        return ((Long) first);
+    }
+
+
+    public long getSecondLong() {
+        return (Long) second;
+    }
+
+    public double getSecondDouble() {
+        return ((Double) second);
+    }
+
+    public float getFirstFloat() {
+        return ((Float) first);
+    }
+
+
+    public float getSecondFloat() {
+        return ((Float) second);
+    }
+
+    public String toString() {
+        return "[" + first.toString() + " ; " + second.toString() + "]";
+    }
+
+    public int hashCode() {
+        if (first == null || second == null)
+            return 0;
+        else
+            return first.hashCode() + 37 * second.hashCode();
+    }
+
+    public int compareTo(Pair<S, T> p) {
+        int value = ((Comparable<S>) this.getFirst()).compareTo(p.getFirst());
+        if (value != 0)
+            return value;
+        else
+            return ((Comparable<T>) this.getSecond()).compareTo(p.getSecond());
+    }
+
+    public boolean equals(Object other) {
+        boolean good = false;
+        if (other instanceof Pair) {
+            Pair p = (Pair) other;
+            if (first == null) {
+                good = (p.first == null);
+            } else {
+                good = first.equals(p.first);
+            }
+            if (good) {
+                if (second == null) {
+                    good = (p.second == null);
+                } else {
+                    good = second.equals(p.second);
+                }
+            }
+        }
+        return good;
+    }
+
+    /**
+     * Compare two pairs
+     * "Note: this comparator imposes orderings that are inconsistent with equals."
+     *
+     * @param p1 the first object to be compared.
+     * @param p2 the second object to be compared.
+     * @return a negative integer, zero, or a positive integer as the
+     *         first argument is less than, equal to, or greater than the
+     *         second.
+     * @throws ClassCastException if the arguments' types prevent them from
+     *                            being compared by this comparator.
+     */
+    public int compare(Pair<S, T> p1, Pair<S, T> p2) {
+        return p1.compareTo(p2);
+    }
+
+    /**
+     * clone this pair
+     *
+     * @return a shallow clone of this pair
+     */
+    public Object clone() {
+        try {
+            super.clone();
+        } catch (CloneNotSupportedException e) {
+            Basic.caught(e);
+        }
+        return new Pair<>(getFirst(), getSecond());
+    }
+
+    public void set(S first, T second) {
+        this.first = first;
+        this.second = second;
+    }
+
+    public S get1() {
+        return first;
+    }
+
+    public T get2() {
+        return second;
+    }
+
+    public void set1(S first) {
+        this.first = first;
+    }
+
+    public void set2(T second) {
+        this.second = second;
+    }
+
+    public boolean contains(Object x) {
+        return x.equals(first) || x.equals(second);
+    }
+}
diff --git a/src/jloda/util/PeakMemoryUsageMonitor.java b/src/jloda/util/PeakMemoryUsageMonitor.java
new file mode 100644
index 0000000..9983e6c
--- /dev/null
+++ b/src/jloda/util/PeakMemoryUsageMonitor.java
@@ -0,0 +1,87 @@
+/**
+ * PeakMemoryUsageMonitor.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.util.concurrent.Executors;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+
+/**
+ * this class records the peak memory usage of a program
+ * Daniel Huson, 5.2015
+ */
+public class PeakMemoryUsageMonitor {
+    private static PeakMemoryUsageMonitor instance;
+    private final long start;
+    private long peak = ((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1048576);
+
+    /**
+     * constructor
+     */
+    private PeakMemoryUsageMonitor() {
+        start = System.currentTimeMillis();
+        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable() {
+            public void run() {
+                long used = ((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1048576);
+                if (used > peak)
+                    peak = used;
+            }
+        }, 0, 5, SECONDS);
+    }
+
+    private static PeakMemoryUsageMonitor getInstance() {
+        if (instance == null) {
+            instance = new PeakMemoryUsageMonitor();
+        }
+        return instance;
+    }
+
+    /**
+     * start recording memory and time
+     */
+    public static void start() {
+        getInstance();
+    }
+
+    /**
+     * get peak usage string
+     *
+     * @return peak usage
+     */
+    public static String getPeakUsageString() {
+        long available = (Runtime.getRuntime().maxMemory() / 1048576);
+        if (available < 1024) {
+            return String.format("%d of %dM", getInstance().peak, available);
+        } else {
+            return String.format("%.1f of %.1fG", (double) getInstance().peak / 1024.0, (double) available / 1024.0);
+        }
+    }
+
+    /**
+     * get number of elapsed seconds since start
+     *
+     * @return seconds since start
+     */
+    public static String getSecondsSinceStartString() {
+        return ((System.currentTimeMillis() - getInstance().start) / 1000) + "s";
+    }
+
+}
diff --git a/src/jloda/util/PhylipUtils.java b/src/jloda/util/PhylipUtils.java
new file mode 100644
index 0000000..006a905
--- /dev/null
+++ b/src/jloda/util/PhylipUtils.java
@@ -0,0 +1,162 @@
+/**
+ * PhylipUtils.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.io.StreamTokenizer;
+
+/**
+ * stuff for phylip io
+ */
+public class PhylipUtils {
+    /**
+     * truncates or pads string to have exactly length len, used for PhylipSequences type io
+     *
+     * @param str a string
+     * @param len the max length
+     */
+    static public String padLabel(String str, int len) {
+        String result = "";
+
+        for (int i = 0; i < len; i++) {
+            if (i < str.length())
+                result += str.charAt(i);
+            else
+                result += ' ';
+        }
+        return result;
+    }
+
+
+    /**
+     * reads the dimensions sequences in PhylipSequences format. Expects first line to
+     * contain ntax and nchar,
+     *
+     * @param dimensions ntax and nchar
+     * @param r          the reader
+     */
+    public static void readDimensions(int[] dimensions, Reader r)
+            throws IOException {
+        StreamTokenizer st = new StreamTokenizer(r);
+        st.resetSyntax();
+        st.eolIsSignificant(false);
+        st.whitespaceChars(0, 32);
+        st.wordChars(33, 126);
+
+
+        st.nextToken();
+        dimensions[0] = (Integer.parseInt(st.sval));
+        st.nextToken();
+        dimensions[1] = Integer.parseInt(st.sval);
+    }
+
+    /**
+     * reads sequences in PhylipSequences format. To be precise, first expects ntax and nchar,
+     * then expects a taxon name followed by nchar symbols for its sequence
+     *
+     * @param dimensions array ntax and nchar
+     * @param data       array of names and sequences
+     * @param r          the reader
+     */
+    public static void read(int[] dimensions, String[][] data, Reader r) throws IOException {
+        StreamTokenizer st = new StreamTokenizer(r);
+        st.resetSyntax();
+        st.eolIsSignificant(false);
+        st.whitespaceChars(0, 32);
+        st.wordChars(33, 126);
+
+        st.nextToken();
+        int ntax = Integer.parseInt(st.sval);
+        String names[] = data[0] = new String[ntax + 1];
+        String sequences[] = data[1] = new String[ntax + 1];
+        st.nextToken();
+        int nchar = Integer.parseInt(st.sval);
+        dimensions[0] = ntax;
+        dimensions[1] = nchar;
+
+        for (int i = 1; i <= ntax; i++) {
+            st.nextToken();
+            if (st.sval.length() <= 10) {
+                names[i] = st.sval;
+                sequences[i] = "";
+            } else {
+                names[i] = st.sval.substring(0, 9);
+                sequences[i] = st.sval.substring(10);
+            }
+            while (sequences[i].length() < nchar) {
+                st.nextToken();
+                sequences[i] += st.sval;
+            }
+        }
+    }
+
+    /**
+     * reads sequences in PhylipSequences format. To be precise, first expects ntax and nchar,
+     * then expects a taxon name followed by nchar symbols for its sequence
+     *
+     * @param data array of names and sequences
+     * @param r    the reader
+     */
+    public static void read(String[][] data, Reader r)
+            throws IOException {
+        int[] dimensions = new int[2];
+        read(dimensions, data, r);
+    }
+
+    /**
+     * print a distance matrix in phylip format
+     *
+     * @param names taxon names 1..ntax
+     * @param dist  distances
+     * @param out   stream
+     */
+    public static void print(String[] names, float[][] dist, PrintStream out) {
+        int ntax = names.length - 1;
+        // Print phylip distance matrix
+        out.println("" + ntax);
+        for (int i = 1; i <= ntax; i++) {
+            out.print(PhylipUtils.padLabel(names[i], 10));
+            for (int j = 1; j <= ntax; j++) {
+                out.print(" " + dist[i][j]);
+            }
+            out.print("\n");
+        }
+
+    }
+
+    /**
+     * print a sequences in phylip format
+     *
+     * @param data
+     * @param os   stream
+     */
+    public static void print(String[][] data, PrintStream os) {
+        int ntax = data[0].length - 1;
+        int nchar = data[1][1].length();
+        // Print phylip sequences
+        os.println("" + ntax + " " + nchar);
+        for (int i = 1; i <= ntax; i++) {
+            os.print(PhylipUtils.padLabel(data[0][i], 10));
+            os.println(data[1][i]);
+        }
+    }
+}
diff --git a/src/jloda/util/PluginClassLoader.java b/src/jloda/util/PluginClassLoader.java
new file mode 100644
index 0000000..8977de4
--- /dev/null
+++ b/src/jloda/util/PluginClassLoader.java
@@ -0,0 +1,152 @@
+/**
+ * PluginClassLoader.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+import java.net.URLClassLoader;
+import java.util.*;
+
+/**
+ * Finds all classes in the named package, of the given type
+ *
+ * 2003
+ */
+public class PluginClassLoader {
+    // Extended the PluginClassLoader to include the Plugins from the pluginFolder
+    static private HashMap<String, URLClassLoader> pluginName2URLClassLoader = new HashMap<>();
+
+    /**
+     * get an instance of each class of the given type in the given package
+     *
+     * @param packageName
+     * @param clazz
+     * @return instances
+     */
+    public static List<Object> getInstances(String packageName, Class<jloda.gui.commands.ICommand> clazz) {
+        return getInstances(new String[]{packageName}, clazz);
+    }
+
+    /**
+     * get an instance of each class of the given type in the given packages
+     *
+     * @param packageNames
+     * @param clazz
+     * @return instances
+     */
+    public static List<Object> getInstances(String[] packageNames, Class clazz) {
+        final List<Object> plugins = new LinkedList<>();
+        final LinkedList<String> packageNameQueue = new LinkedList<>();
+        packageNameQueue.addAll(Arrays.asList(packageNames));
+        while (packageNameQueue.size() > 0) {
+            try {
+                final String packageName = packageNameQueue.removeFirst();
+                final String[] resources = Basic.fetchResources(packageName);
+
+                for (int i = 0; i != resources.length; ++i) {
+                    //System.err.println("Resource: " + resources[i]);
+                    if (resources[i].endsWith(".class")) {
+                        try {
+                            resources[i] = resources[i].substring(0, resources[i].length() - 6);
+                            Class c = Basic.classForName(packageName.concat(".").concat(resources[i]));
+                            if (!c.isInterface() && !Modifier.isAbstract(c.getModifiers()) && clazz.isAssignableFrom(c)) {
+                                try {
+                                    plugins.add(c.newInstance());
+                                } catch (InstantiationException ex) {
+                                    //Basic.caught(ex);
+                                }
+                            }
+                        } catch (Exception ex) {
+                            // Basic.caught(ex);
+                        }
+                    } else {
+                        try {
+                            final String newPackageName = resources[i];
+                            if (!newPackageName.equals(packageName)) {
+                                packageNameQueue.addLast(newPackageName);
+                                // System.err.println("Adding package name: " + newPackageName);
+                            }
+                        } catch (Exception ex) {
+                            // Basic.caught(ex);
+                        }
+                    }
+                }
+            } catch (IOException ex) {
+                Basic.caught(ex);
+            }
+        }
+        return plugins;
+    }
+
+    /**
+     * get an instance of each class of the given type in the given package, sorted
+     *
+     * @param packageName
+     * @param type
+     * @return instances
+     */
+    public static List getInstancesSorted(String packageName, Class<jloda.gui.commands.ICommand> type) {
+        List plugins = getInstances(packageName, type);
+
+        Object[] array = plugins.toArray();
+
+        Arrays.sort(array, new Comparator<Object>() {
+            public int compare(Object o1, Object o2) {
+                // First compare the interface... if equal, compare the name
+
+                Class[] int1 = o1.getClass().getInterfaces();
+                Class[] int2 = o2.getClass().getInterfaces();
+
+                if (int1.length == 0 || int2.length == 0) {
+                    if (int1.length == 0 && int2.length > 0)
+                        return 1;
+                    else if (int1.length > 0)
+                        return -1;
+                    else
+                        return o1.getClass().getName().compareTo(o2.getClass().getName());
+                }
+                String name1;
+                String name2;
+                if (int1[0] == int2[0]) {
+                    // Compare the names of the classes if the same interface
+                    name1 = o1.getClass().getName();
+                    name2 = o2.getClass().getName();
+                } else {
+                    // Compare the names of the interfaces if not the same
+                    name1 = int1[0].getName(); // Only look at the first it implements
+                    name2 = int2[0].getName();
+                }
+                return name1.compareTo(name2);
+
+            }
+        });
+        return Arrays.asList(array);
+    }
+
+    public static HashMap<String, URLClassLoader> getPluginName2URLClassLoader() {
+        return pluginName2URLClassLoader;
+    }
+
+    public static void setPluginName2URLClassLoader(HashMap<String, URLClassLoader> pluginClass2URLClassLoader) {
+        pluginName2URLClassLoader = pluginClass2URLClassLoader;
+    }
+
+}
+
diff --git a/src/jloda/util/PolygonDouble.java b/src/jloda/util/PolygonDouble.java
new file mode 100644
index 0000000..48d2e73
--- /dev/null
+++ b/src/jloda/util/PolygonDouble.java
@@ -0,0 +1,165 @@
+/**
+ * PolygonDouble.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * polygon with double coordinates
+ * Daniel Huson, 1.2007
+ */
+public class PolygonDouble {
+    public int npoints;
+    public double[] xpoints;
+    public double[] ypoints;
+
+    /**
+     * construct an empty polygon
+     */
+    public PolygonDouble() {
+        npoints = 0;
+        xpoints = new double[0];
+        ypoints = new double[0];
+    }
+
+    /**
+     * construct a polygon of size npoints
+     *
+     * @param npoints number of points
+     */
+    public PolygonDouble(int npoints) {
+        this.npoints = npoints;
+        xpoints = new double[npoints];
+        ypoints = new double[npoints];
+    }
+
+    /**
+     * construct a polygon and copy the given points
+     *
+     * @param npoints
+     * @param xpoints
+     * @param ypoints
+     */
+    public PolygonDouble(int npoints, double[] xpoints, double[] ypoints) {
+        this.npoints = npoints;
+        this.xpoints = new double[npoints];
+        this.ypoints = new double[npoints];
+        for (int i = 0; i < npoints; i++) {
+            this.xpoints[i] = xpoints[i];
+            this.ypoints[i] = ypoints[i];
+        }
+    }
+
+    /**
+     * construct a polygon from a rectangle
+     *
+     * @param box
+     */
+    public PolygonDouble(Rectangle2D box) {
+        this.npoints = 4;
+        this.xpoints = new double[npoints];
+        this.ypoints = new double[npoints];
+        this.xpoints[0] = box.getX();
+        this.ypoints[0] = box.getY();
+        this.xpoints[1] = box.getX();
+        this.ypoints[1] = box.getY() + box.getHeight();
+        this.xpoints[2] = box.getX() + box.getWidth();
+        this.ypoints[2] = box.getY() + box.getHeight();
+        this.xpoints[3] = box.getX() + box.getWidth();
+        this.ypoints[3] = box.getY();
+    }
+
+    /**
+     * construct a polygon and copy the given points
+     *
+     * @param npoints
+     * @param points
+     */
+    public PolygonDouble(int npoints, Point2D[] points) {
+        this.npoints = npoints;
+        this.xpoints = new double[npoints];
+        this.ypoints = new double[npoints];
+        for (int i = 0; i < npoints; i++) {
+            this.xpoints[i] = points[i].getX();
+            this.ypoints[i] = points[i].getY();
+        }
+    }
+
+    /**
+     * construct a polygon from a list of points
+     *
+     * @param points
+     */
+    public PolygonDouble(List points) {
+        this(points.size());
+        int i = 0;
+        for (Object point : points) {
+            Point2D aPt = (Point2D) point;
+            xpoints[i] = aPt.getX();
+            ypoints[i] = aPt.getY();
+            i++;
+        }
+    }
+
+    /**
+     * construct a polygon from a list of points
+     *
+     * @param a
+     * @param b
+     * @param points
+     * @param c
+     */
+    public PolygonDouble(Point2D a, Point2D b, List points, Point2D c) {
+        this(points.size() + 3);
+        int i = 0;
+        xpoints[i] = a.getX();
+        ypoints[i++] = a.getY();
+        xpoints[i] = b.getX();
+        ypoints[i++] = b.getY();
+        for (Object point : points) {
+            Point2D aPt = (Point2D) point;
+            xpoints[i] = aPt.getX();
+            ypoints[i] = aPt.getY();
+            i++;
+        }
+        xpoints[i] = c.getX();
+        ypoints[i++] = c.getY();
+    }
+
+    /**
+     * set the polygon from two lists of Double
+     *
+     * @param npoints
+     * @param xpoints
+     * @param ypoints
+     */
+    public void set(int npoints, ArrayList xpoints, ArrayList ypoints) {
+        this.npoints = npoints;
+        this.xpoints = new double[npoints];
+        this.ypoints = new double[npoints];
+        int i = 0;
+        for (Object xpoint : xpoints) this.xpoints[i++] = (Double) xpoint;
+        i = 0;
+        for (Object ypoint : ypoints) this.ypoints[i++] = (Double) ypoint;
+    }
+}
diff --git a/src/jloda/util/ProgramProperties.java b/src/jloda/util/ProgramProperties.java
new file mode 100644
index 0000000..0080118
--- /dev/null
+++ b/src/jloda/util/ProgramProperties.java
@@ -0,0 +1,549 @@
+/**
+ * ProgramProperties.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.print.PageFormat;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.StringTokenizer;
+
+/**
+ * track program properties
+ *
+ * @author huson
+ *         Date: 08-Nov-2004
+ */
+public class ProgramProperties {
+    static public final java.util.Properties props = new java.util.Properties();
+    public static Color SELECTION_COLOR = new Color(252, 208, 102);
+    public static Color SELECTION_COLOR_DARKER = new Color(210, 190, 95);
+    public static Color SELECTION_COLOR_ADDITIONAL_TEXT = new Color(93, 155, 206);
+
+    static private String defaultFileName = null;
+    static private String programName = "";
+    static private String programVersion = "";
+    static private String programTitle = "";
+    private static ImageIcon programIcon = null;
+    private static final boolean macOS = (System.getProperty("os.name") != null && System.getProperty("os.name").toLowerCase().startsWith("mac"));
+    private static boolean useGUI = false;
+    private static IStateChecker stateChecker = null;
+    public static final String OPENFILE = "OpenFile";
+    public static final String SAVEFILE = "SaveFile";
+    public static final String FINDFILE = "FindFile";
+    public static final String SAVEFORMAT = "SaveFormat";
+    public static final String EXPORTFILE = "ExportFile";
+    public static final String RECENTFILES = "RecentFiles";
+    public static final String MAXRECENTFILES = "MaxRecentFiles";
+    public static final String TOOLBARITEMS = "ToolbarItems";
+    public static final String SHOWTOOLBAR = "ShowToolbar";
+    public static final String EVOLVERTOOLBARITEMS = "EvolverToolbarItems";
+    public static final String SHOWEVOLVERTOOLBAR = "ShowEvolverToolbar";
+    public static final String SHOWVERSIONINTITLE = "VINT";
+    public static final String DRAWERKIND = "DrawerKind";
+    public static final String LASTCOMMAND = "LastCommand";
+    public static final String FINDSTRING = "FindString";
+    public static final String MAIN_WINDOW_GEOMETRY = "MainWindowGeometry";
+    public static final String MULTI_WINDOW_GEOMETRY = "MultiWindowGeometry";
+    public static PageFormat pageFormat = null;
+    public static final String DEFAULT_FONT = "DefaultFont";
+
+    /**
+     * load properties from default file
+     */
+    public static void load() {
+        try (FileInputStream fis = new FileInputStream(getDefaultFileName())) {
+            props.load(fis);
+            //System.err.println("Loaded properties from: " + getDefaultFileName());
+        } catch (Exception ex) {
+            //Basic.caught(ex);
+        }
+    }
+
+    /**
+     * load properties from specified file
+     */
+    public static void load(String fileName) {
+        setPropertiesFileName(fileName);
+        load();
+    }
+
+    /**
+     * save properties to default file
+     */
+    public static void store() {
+            try (OutputStream fos = new FileOutputStream(getDefaultFileName())) {
+                props.store(fos, programName);
+            //System.err.println("Stored properties to: " + getDefaultFileName());
+        } catch (Exception ex) {
+            //Basic.caught(ex);
+        }
+    }
+
+    /**
+     * save properties to specified file
+     */
+    public static void store(String fileName) {
+        setPropertiesFileName(fileName);
+        store();
+    }
+
+    /**
+     * gets a int property
+     * @return set property or default
+     */
+    public static int get(Object name, int def) {
+        String value = (String) props.get(name);
+        if (value == null)
+            return def;
+        else
+            return Integer.parseInt(value);
+    }
+
+    /**
+     * gets a int[] property
+     * @return set property or default
+     */
+    public static int[] get(Object name, int[] def) {
+        String value = (String) props.get(name);
+        if (value == null)
+            return def;
+        else {
+            try {
+                java.util.List<String> list = new LinkedList<>();
+                StringTokenizer tok = new StringTokenizer(value, ";");
+                while (tok.hasMoreTokens())
+                    list.add(tok.nextToken());
+                int[] result = new int[list.size()];
+                int i = 0;
+                for (String s : list) {
+                    result[i++] = Integer.parseInt(s);
+                }
+                if (def.length > 0 && result.length != def.length)
+                    return def;
+                else
+                    return result;
+            } catch (Exception ex) {
+                return def;
+            }
+        }
+    }
+
+    /**
+     * gets a color property
+     * @return set property or default
+     */
+    public static Color get(Object name, Color def) {
+        String value = (String) props.get(name);
+        if (value == null || value.equalsIgnoreCase("null"))
+            return def;
+        else
+            return Color.decode(value);
+    }
+
+
+    /**
+     * gets a double property
+     *
+     * @return set property or default
+     */
+    public static double get(Object name, double def) {
+        String value = (String) props.get(name);
+        if (value == null)
+            return def;
+        else
+            return Double.parseDouble(value);
+    }
+
+    /**
+     * gets a boolean property
+     *
+     * @return set property or default
+     */
+    public static boolean get(Object name, boolean def) {
+        String value = (String) props.get(name);
+        if (value == null)
+            return def;
+        else
+            return Boolean.valueOf(value);
+    }
+
+
+    /**
+     * gets a string property
+     *
+     * @return set property or default
+     */
+    public static String get(String name, String def) {
+        return props.getProperty(name, def);
+    }
+
+    /**
+     * gets a font property
+     *
+     * @return font or default
+     */
+    public static Font get(String name, Font def) {
+        String value = (String) props.get(name);
+        if (value == null)
+            return def;
+        else {
+            value = value.replaceAll(" ", "\\ ");
+            return Font.decode(value);
+        }
+    }
+
+    /**
+     * gets a list of string pairs
+     *
+     * @return list of string pairs
+     */
+    public static Collection<Pair<String, String>> get(String name, Collection<Pair<String, String>> def) {
+        String value = (String) props.get(name);
+        if (value == null)
+            return def;
+        else {
+            Collection<Pair<String, String>> list = new LinkedList<>();
+            String[] tokens = value.split("%%%");
+            for (int i = 0; i < tokens.length - 1; i += 2)
+                list.add(new Pair<>(tokens[i].trim(), tokens[i + 1].trim()));
+            return list;
+        }
+    }
+
+    /**
+     * gets a list of strings
+     *
+     * @return list of string pairs
+     */
+    public static String[] get(String name, String[] def) {
+        String value = (String) props.get(name);
+        if (value == null)
+            return def;
+        else {
+            Collection<String> list = new LinkedList<>();
+            String[] tokens = value.split("%%%");
+            for (String token : tokens) list.add(token.trim());
+            return list.toArray(new String[list.size()]);
+        }
+    }
+
+    /**
+     * get the default properties file name
+     *
+     * @return file name
+     */
+    public static String getDefaultFileName() {
+        return defaultFileName;
+    }
+
+    /**
+     * set the default properties file name
+     *
+     */
+    public static void setPropertiesFileName(String defaultFileName) {
+        ProgramProperties.defaultFileName = defaultFileName;
+    }
+
+    public static File getFile(String key) {
+        String fileName = props.getProperty(key);
+        if (fileName != null)
+            return new File(fileName);
+        return null;
+    }
+
+    /**
+     * remove a property
+     *
+     */
+    public static void remove(String key) {
+        props.remove(key);
+    }
+
+    /**
+     * put a property
+     *
+     */
+    public static void put(String key, int value) {
+        props.setProperty(key, "" + value);
+    }
+
+    /**
+     * put a property
+     *
+     */
+    public static void put(String key, int[] value) {
+        StringBuilder buf = new StringBuilder();
+        for (int aValue : value) buf.append(aValue).append(";");
+        props.setProperty(key, "" + buf.toString());
+    }
+
+    /**
+     * put a property
+     *
+     */
+    public static void put(String key, double value) {
+        props.setProperty(key, "" + value);
+    }
+
+    /**
+     * put a property
+     *
+     */
+    public static void put(String key, boolean value) {
+        props.setProperty(key, "" + value);
+    }
+
+    /**
+     * put a property
+     *
+     */
+    public static void put(String key, String value) {
+        props.setProperty(key, value);
+    }
+
+    /**
+     * put a file property
+     *
+     */
+    public static void put(String key, File value) {
+        props.setProperty(key, value.getAbsolutePath());
+    }
+
+
+    /**
+     * put a property
+     *
+     */
+    public static void put(String key, Color value) {
+        if (value == null)
+            props.setProperty(key, "null");
+        else
+            props.setProperty(key, "" + value.getRGB());
+    }
+
+
+    /**
+     * put a property
+     *
+     */
+    public static void put(String key, Font value) {
+        put(key, value.getFamily(), value.getStyle(), value.getSize());
+    }
+
+    /**
+     * put a property
+     */
+    public static void put(String key, String family, Integer style0, Integer size0) {
+        Font def = get(key, (Font) null);
+        String name;
+        if (family == null)
+            name = def.getFamily();
+        else
+            name = family;
+        int style;
+        if (style0 == null)
+            style = def.getStyle();
+        else
+            style = style0;
+        int size;
+        if (size0 == null)
+            size = def.getSize();
+        else
+            size = size0;
+
+        switch (style) {
+            case Font.BOLD + Font.ITALIC:
+                name += "-BOLDITALIC";
+                break;
+            case Font.BOLD:
+                name += "-BOLD";
+                break;
+            case Font.ITALIC:
+                name += "-ITALIC";
+                break;
+            default:
+            case Font.PLAIN:
+                name += "-PLAIN";
+                break;
+        }
+        name += "-" + size;
+        props.setProperty(key, name);
+    }
+
+    /**
+     * put a property
+     *
+     */
+    public static void put(String key, Collection<Pair<String, String>> value) {
+        StringBuilder buf = new StringBuilder();
+        for (Pair<String, String> pair : value) {
+            buf.append(pair.getFirst()).append("%%%");
+            buf.append(pair.getSecond()).append("%%%");
+        }
+        props.setProperty(key, buf.toString());
+    }
+
+    /**
+     * put a property
+     *
+     */
+    public static void put(String key, String[] value) {
+        StringBuilder buf = new StringBuilder();
+        boolean first = true;
+        for (String s : value) {
+            if (first)
+                first = false;
+            else
+                buf.append("%%%");
+            buf.append(s);
+        }
+        props.setProperty(key, buf.toString());
+    }
+
+
+    /**
+     * get a property
+     *
+     * @return property for key
+     */
+    public static String get(String key) {
+        return props.getProperty(key);
+    }
+
+    /**
+     * sets the name of the program generating these properties
+     *
+     */
+    public static void setProgramName(String programName) {
+        ProgramProperties.programName = programName;
+    }
+
+    /**
+     * gets the program name
+     *
+     * @return name
+     */
+    public static String getProgramName() {
+        return programName;
+    }
+
+    /**
+     * sets the program version string, if not already set...
+     *
+     */
+    public static void setProgramVersion(String version) {
+        if (programVersion == null || programVersion.length() == 0)
+            ProgramProperties.programVersion = version;
+    }
+
+    /**
+     * gets the program versions string
+     *
+     * @return version
+     */
+    public static String getProgramVersion() {
+        return programVersion;
+    }
+
+    /**
+     * sets the program title string
+     *
+     */
+    public static void setProgramTitle(String title) {
+        ProgramProperties.programTitle = title;
+    }
+
+    /**
+     * gets the program titles string
+     *
+     */
+    public static String getProgramTitle() {
+        return programTitle;
+    }
+
+    /**
+     * are we running on a mac?
+     *
+     * @return true, if os is mac
+     */
+    public static boolean isMacOS() {
+        return macOS;
+    }
+
+    /**
+     * gets the program icon
+     *
+     * @return program icon
+     */
+    public static ImageIcon getProgramIcon() {
+        return programIcon;
+    }
+
+    /**
+     * sets the program icon
+     *
+     */
+    public static void setProgramIcon(ImageIcon icon) {
+        ProgramProperties.programIcon = icon;
+    }
+
+    public static PageFormat getPageFormat() {
+        return pageFormat;
+    }
+
+    public static void setPageFormat(PageFormat pageFormat) {
+        ProgramProperties.pageFormat = pageFormat;
+    }
+
+    /**
+     * returns the given text, if the key has been set, otherwise returns ""
+     *
+     * @return text or ""
+     */
+    public static String getIfEnabled(String key, String text) {
+        if (get(key, false))
+            return text;
+        else
+            return "";
+    }
+
+    public static boolean isUseGUI() {
+        return useGUI;
+    }
+
+    public static void setUseGUI(boolean useGUI) {
+        ProgramProperties.useGUI = useGUI;
+    }
+
+    public static void checkState() {
+        if (stateChecker != null)
+            stateChecker.check();
+    }
+
+    public static void setStateChecker(IStateChecker stateChecker) {
+        ProgramProperties.stateChecker = stateChecker;
+    }
+}
diff --git a/src/jloda/util/ProgressCmdLine.java b/src/jloda/util/ProgressCmdLine.java
new file mode 100644
index 0000000..088a140
--- /dev/null
+++ b/src/jloda/util/ProgressCmdLine.java
@@ -0,0 +1,136 @@
+/**
+ * ProgressCmdLine.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+
+/**
+ * progress listener that writes only to the command line
+ *
+ * @author huson
+ *         Date: 26-Jun-2004
+ */
+public class ProgressCmdLine implements ProgressListener {
+    private long steps = 0;
+
+    /**
+     * constructor
+     */
+    public ProgressCmdLine() {
+    }
+
+    /**
+     * constructor
+     *
+     * @param taskName
+     * @param subtaskName
+     */
+    public ProgressCmdLine(final String taskName, final String subtaskName) {
+        setTasks(taskName, subtaskName);
+    }
+
+    /**
+     * sets the steps number of steps to be done. By default, the maximum is set to 100
+     *
+     * @param steps
+     */
+    public void setMaximum(final long steps) {
+
+    }
+
+    /**
+     * sets the progress
+     *
+     * @param steps
+     */
+    public void setProgress(final long steps) throws CanceledException {
+        this.steps = steps;
+    }
+
+    /**
+     * gets the current progress
+     *
+     * @return progress
+     */
+    public long getProgress() {
+        return steps;
+    }
+
+    /**
+     * closes the dialog.
+     */
+    public void close() {
+    }
+
+    /**
+     * has user canceled?
+     *
+     * @throws jloda.util.CanceledException
+     */
+    public void checkForCancel() throws CanceledException {
+    }
+
+    /**
+     * Sets the Task and subtask names, for use in progress bar displays
+     *
+     * @param taskName
+     * @param subtaskName
+     */
+    public void setTasks(String taskName, String subtaskName) {
+        System.err.println(taskName + (subtaskName != null ? (": " + subtaskName) : ""));
+    }
+
+    /**
+     * Sets just the subtask
+     *
+     * @param subtaskName
+     */
+    public void setSubtask(String subtaskName) {
+        if (subtaskName != null)
+            System.err.println(subtaskName);
+    }
+
+    public void setCancelable(boolean enabled) {
+
+    }
+
+    public boolean isUserCancelled() {
+        return false;
+    }
+
+    public void setUserCancelled(boolean userCancelled) {
+    }
+
+    public void incrementProgress() {
+
+    }
+
+    /**
+     * is user allowed to cancel
+     *
+     * @return cancelable?
+     */
+    public boolean isCancelable() {
+        return false;
+    }
+
+    public void setDebug(boolean debug) {
+    }
+}
+
diff --git a/src/jloda/util/ProgressListener.java b/src/jloda/util/ProgressListener.java
new file mode 100644
index 0000000..fabc77b
--- /dev/null
+++ b/src/jloda/util/ProgressListener.java
@@ -0,0 +1,102 @@
+/**
+ * ProgressListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+
+/**
+ * Progress listener interface
+ *
+ * @author huson
+ *         Date: 02-Dec-2003
+ */
+public interface ProgressListener extends AutoCloseable {
+    /**
+     * set the total number of steps to be done
+     *
+     * @param total
+     */
+    void setMaximum(long total);
+
+    /**
+     * set progress
+     *
+     * @param current step
+     */
+    void setProgress(long current) throws CanceledException;
+
+    /**
+     * gets the current progress
+     *
+     * @return progress
+     */
+    long getProgress();
+
+    void checkForCancel() throws CanceledException;
+
+    /**
+     * Sets the Task and subtask names, for use in progress bar displays
+     *
+     * @param taskName
+     * @param subtaskName
+     */
+    void setTasks(String taskName, String subtaskName);
+
+    /**
+     * Sets just the subtask
+     *
+     * @param subtaskName
+     */
+    void setSubtask(String subtaskName);
+
+    /**
+     * Enable the user to cancel during this operation.
+     *
+     * @param enabled
+     */
+    void setCancelable(boolean enabled);
+
+    boolean isUserCancelled();
+
+    void setUserCancelled(boolean userCancelled);
+
+    /**
+     * increment progress
+     */
+    void incrementProgress() throws CanceledException;
+
+    /**
+     * close the progress listener
+     */
+    void close();
+
+    /**
+     * is user allowed to cancel
+     *
+     * @return cancelable?
+     */
+    boolean isCancelable();
+
+    /**
+     * set the debug mode
+     *
+     * @param debug
+     */
+    void setDebug(boolean debug);
+}
diff --git a/src/jloda/util/ProgressPercentage.java b/src/jloda/util/ProgressPercentage.java
new file mode 100644
index 0000000..17f8e4f
--- /dev/null
+++ b/src/jloda/util/ProgressPercentage.java
@@ -0,0 +1,208 @@
+/**
+ * ProgressPercentage.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+
+/**
+ * progress listener that writes percentages to the command line
+ *
+ * @author huson
+ *         Date: 26-Jun-2004
+ */
+public class ProgressPercentage implements ProgressListener {
+    private long steps = 0;
+
+    private final boolean[] percentageReported = new boolean[11];
+    private int nextPercentageToReport;
+
+    private long nextThreshold = 0;
+    private long tenPercent = 0;
+    private long startTime = 0;
+
+    private boolean reportedCompleted = false;
+
+    /**
+     * constructor
+     */
+    public ProgressPercentage() {
+        this(0);
+    }
+
+    /**
+     * constructor
+     * @param maxSteps
+     */
+    public ProgressPercentage(long maxSteps) {
+        startTime = System.currentTimeMillis();
+        percentageReported[10] = true; // sentinel
+        setMaximum(maxSteps);
+    }
+
+    /**
+     * constructor
+     * @param taskName
+     */
+    public ProgressPercentage(final String taskName) {
+        this(0);
+        System.err.println(taskName);
+    }
+
+    /**
+     * constructor
+     * @param taskName
+     * @param maxSteps
+     */
+    public ProgressPercentage(final String taskName, long maxSteps) {
+        this(maxSteps);
+        System.err.println(taskName);
+    }
+
+    /**
+     * constructor
+     *
+     * @param taskName
+     * @param subtaskName
+     */
+    public ProgressPercentage(final String taskName, final String subtaskName) {
+        this(0);
+        System.err.println(taskName + (subtaskName != null ? " (" + subtaskName + ")" : ""));
+    }
+
+    /**
+     * sets the steps number of steps to be done. By default, the maximum is set to 100
+     *
+     * @param maxSteps
+     */
+    public void setMaximum(final long maxSteps) {
+        tenPercent = maxSteps / 10;
+        for (int i = 0; i < percentageReported.length - 1; i++) // not the last entry!
+            percentageReported[i] = false;
+        nextThreshold = tenPercent;
+        nextPercentageToReport = 1;
+    }
+
+    /**
+     * sets the progress
+     *
+     * @param steps
+     */
+    public void setProgress(final long steps) {
+        if (steps > nextThreshold && !percentageReported[nextPercentageToReport]) {
+            System.err.print((10 * nextPercentageToReport + "% "));
+            percentageReported[nextPercentageToReport] = true;
+            if (nextPercentageToReport < 10)
+                nextPercentageToReport++;
+            nextThreshold += tenPercent;
+        }
+        this.steps = steps;
+        if (reportedCompleted)
+            reportedCompleted = false;
+    }
+
+    /**
+     * gets the current progress
+     *
+     * @return progress
+     */
+    public long getProgress() {
+        return steps;
+    }
+
+    /**
+     * closes the dialog.
+     */
+    public void close() {
+        reportTaskCompleted();
+    }
+
+    /**
+     * report end of task
+     */
+    public void reportTaskCompleted() {
+        System.err.println("100% (" + getTimeString() + ")");
+        startTime = System.currentTimeMillis();
+        reportedCompleted = false;
+    }
+
+    /**
+     * report end of task
+     */
+    public String getTimeString() {
+        return String.format("%.1fs", (System.currentTimeMillis() - startTime) / 1000.0);
+    }
+
+    /**
+     * has user canceled?
+     *
+     * @throws CanceledException
+     */
+    public void checkForCancel() {
+    }
+
+    /**
+     * Sets the Task and subtask names, for use in progress bar displays
+     *
+     * @param taskName
+     * @param subtaskName
+     */
+    public void setTasks(String taskName, String subtaskName) {
+        // if (taskName != null)
+        //    System.err.println(taskName + (subtaskName != null ? (": " + subtaskName) : ""));
+    }
+
+    /**
+     * Sets just the subtask
+     *
+     * @param subtaskName
+     */
+    public void setSubtask(String subtaskName) {
+        if (!reportedCompleted && steps > 0)
+            reportTaskCompleted();
+        if (subtaskName != null)
+            System.err.println(subtaskName);
+    }
+
+    public void setCancelable(boolean enabled) {
+    }
+
+    public boolean isUserCancelled() {
+        return false;
+    }
+
+    public void setUserCancelled(boolean userCancelled) {
+    }
+
+    public void incrementProgress() {
+        setProgress(steps + 1);
+    }
+
+    /**
+     * is user allowed to cancel
+     *
+     * @return cancelable?
+     */
+    public boolean isCancelable() {
+        return false;
+    }
+
+    public void setDebug(boolean debug) {
+    }
+}
+
diff --git a/src/jloda/util/ProgressSilent.java b/src/jloda/util/ProgressSilent.java
new file mode 100644
index 0000000..c0fa181
--- /dev/null
+++ b/src/jloda/util/ProgressSilent.java
@@ -0,0 +1,127 @@
+/**
+ * ProgressSilent.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+/**
+ * silent progress listener
+ *
+ * @author huson
+ *         Date: 26-Jun-2004
+ */
+public class ProgressSilent implements ProgressListener {
+    /**
+     * constructor
+     */
+    public ProgressSilent() {
+    }
+
+    /**
+     * constructor
+     *
+     * @param taskName
+     * @param subtaskName
+     */
+    public ProgressSilent(final String taskName, final String subtaskName) {
+    }
+
+    /**
+     * sets the steps number of steps to be done. By default, the maximum is set to 100
+     *
+     * @param steps
+     */
+    public void setMaximum(final long steps) {
+    }
+
+    /**
+     * sets the progress
+     *
+     * @param steps
+     */
+    public void setProgress(final long steps) throws CanceledException {
+    }
+
+    /**
+     * gets the current progress
+     *
+     * @return progress
+     */
+    public long getProgress() {
+        return 0;
+    }
+
+    /**
+     * closes the dialog.
+     */
+    public void close() {
+    }
+
+    /**
+     * has user canceled?
+     *
+     * @throws CanceledException
+     */
+    public void checkForCancel() throws CanceledException {
+    }
+
+    /**
+     * Sets the Task and subtask names, for use in progress bar displays
+     *
+     * @param taskName
+     * @param subtaskName
+     */
+    public void setTasks(String taskName, String subtaskName) {
+    }
+
+    /**
+     * Sets just the subtask
+     *
+     * @param subtaskName
+     */
+    public void setSubtask(String subtaskName) {
+    }
+
+    public void setCancelable(boolean enabled) {
+
+    }
+
+    public boolean isUserCancelled() {
+        return false;
+    }
+
+    public void setUserCancelled(boolean userCancelled) {
+    }
+
+    public void incrementProgress() {
+
+    }
+
+    /**
+     * is user allowed to cancel
+     *
+     * @return cancelable?
+     */
+    public boolean isCancelable() {
+        return false;
+    }
+
+    public void setDebug(boolean debug) {
+    }
+
+}
diff --git a/src/jloda/util/PropertiesListListener.java b/src/jloda/util/PropertiesListListener.java
new file mode 100644
index 0000000..69813a5
--- /dev/null
+++ b/src/jloda/util/PropertiesListListener.java
@@ -0,0 +1,45 @@
+/**
+ * PropertiesListListener.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.util.List;
+
+/**
+ * listen for changes to list of values of a property
+ *
+ * @author huson
+ *         Date: 11-Dec-2004
+ */
+public interface PropertiesListListener {
+    /**
+     * gets the new list of values after the change
+     *
+     * @param values
+     */
+    void hasChanged(List<String> values);
+
+    /**
+     * is this listener interesed in the named property?
+     *
+     * @param name
+     * @return is interested
+     */
+    boolean isInterested(String name);
+}
diff --git a/src/jloda/util/ProteinComplexityMeasure.java b/src/jloda/util/ProteinComplexityMeasure.java
new file mode 100644
index 0000000..f29e381
--- /dev/null
+++ b/src/jloda/util/ProteinComplexityMeasure.java
@@ -0,0 +1,158 @@
+/**
+ * ProteinComplexityMeasure.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+/**
+ * computes the minimum complexity encountered in a protein string
+ * Daniel Huson, 10.2012
+ */
+public class ProteinComplexityMeasure {
+    private static final int N = 20; // alphabet size
+    private static final int L = 16;  // window size
+    private static final double LFactorial = 20922789888000.0;
+    private static double[] factorial = null;
+
+    /**
+     * uses Wootten and Federhen to compute the complexity of a sequence
+     *
+     * @param sequence
+     * @return average complexity
+     */
+    public static float getMinimumProteinComplexityWoottenFederhen(byte[] sequence, int length) {
+        if (sequence == null || length < L)
+            return 0;
+
+        int[] counts = new int[N];
+
+        for (int pos = 0; pos < L; pos++) // first 12 values
+        {
+            counts[getIndex(sequence[pos])]++;
+        }
+        double minComplexity = 1;
+
+        // System.err.print("Values: ");
+        for (int pos = L; pos < length - L; pos += L) {
+            double product = computeProductOfFactorials(counts);
+            double K = 1.0 / L * Math.log(LFactorial / product) / Math.log(N);
+            counts[getIndex(sequence[pos - L])]--;
+            counts[getIndex(sequence[pos])]++;
+            // System.err.print(" "+K);
+            if (K < minComplexity)
+                minComplexity = K;
+        }
+        // System.err.println("minComplexity="+minComplexity+", sequence: "+s);
+        return (float) Math.max(0.0001, minComplexity);   // MEGAN interprets 0 as being turned off...
+    }
+
+    /**
+     * computes the produce of factorials (of values up to L)
+     *
+     * @param counts
+     * @return produce of factorials
+     */
+    private static double computeProductOfFactorials(int[] counts) {
+        if (factorial == null) {
+            factorial = new double[L + 1];
+            double value = 1.0;
+            for (int i = 0; i <= L; i++) {
+                if (i > 0)
+                    value *= i;
+                factorial[i] = value;
+            }
+        }
+        double result = 1.0;
+        for (int count : counts) {
+            result *= factorial[count];
+        }
+        return result;
+    }
+
+    /**
+     * gets the index
+     *
+     * @param c
+     * @return index
+     */
+    private static int getIndex(byte c) {
+        switch (c) {
+            default:
+            case 'a':
+            case 'A':
+                return 0;
+            case 'r':
+            case 'R':
+                return 1;
+            case 'n':
+            case 'N':
+                return 2;
+            case 'd':
+            case 'D':
+                return 3;
+            case 'c':
+            case 'C':
+                return 4;
+            case 'e':
+            case 'E':
+                return 5;
+            case 'q':
+            case 'Q':
+                return 6;
+            case 'g':
+            case 'G':
+                return 7;
+            case 'h':
+            case 'H':
+                return 8;
+            case 'i':
+            case 'I':
+                return 9;
+            case 'l':
+            case 'L':
+                return 10;
+            case 'k':
+            case 'K':
+                return 11;
+            case 'm':
+            case 'M':
+                return 12;
+            case 'f':
+            case 'F':
+                return 13;
+            case 'p':
+            case 'P':
+                return 14;
+            case 's':
+            case 'S':
+                return 15;
+            case 't':
+            case 'T':
+                return 16;
+            case 'w':
+            case 'W':
+                return 17;
+            case 'y':
+            case 'Y':
+                return 18;
+            case 'v':
+            case 'V':
+                return 19;
+        }
+    }
+}
diff --git a/src/jloda/util/RTFFileFilter.java b/src/jloda/util/RTFFileFilter.java
new file mode 100644
index 0000000..cabbe66
--- /dev/null
+++ b/src/jloda/util/RTFFileFilter.java
@@ -0,0 +1,95 @@
+/**
+ * RTFFileFilter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * RTF file filter
+ * daniel huson, 2015
+ */
+public class RTFFileFilter implements FilenameFilter {
+    private static RTFFileFilter instance;
+
+    /**
+     * get an instance of this file filter
+     *
+     * @return instance
+     */
+    public static RTFFileFilter getInstance() {
+        if (instance == null)
+            instance = new RTFFileFilter();
+        return instance;
+    }
+
+    /**
+     * Tests if a specified file should be included in a file list.
+     *
+     * @param dir  the directory in which the file was found.
+     * @param name the name of the file.
+     * @return <code>true</code> if and only if the name should be
+     * included in the file list; <code>false</code> otherwise.
+     */
+    @Override
+    public boolean accept(File dir, String name) {
+        FileInputIterator it;
+        try {
+            it = new FileInputIterator(new File(dir, name));
+            try {
+                if (it.next().startsWith("{\\rtf"))
+                    return true;
+            } finally {
+                it.close();
+            }
+        } catch (IOException e) {
+        }
+        return false;
+    }
+
+    /**
+     * returns all stripped lines from an rtf file
+     *
+     * @param file
+     * @return stripped files
+     */
+    public static String[] getStrippedLines(File file) {
+        if (getInstance().accept(file.getParentFile(), file.getName())) {
+            try {
+                List<String> lines = new LinkedList<>();
+                try (FileInputIterator it = new FileInputIterator(file)) {
+                    while (it.hasNext()) {
+                        String aLine = it.next().replaceAll("\\{\\*?\\\\[^{}]+}|[{}]|\\\\\\n?[A-Za-z]+\\n?(?:-?\\d+)?[ ]?", "").replaceAll("\\\\", "").trim();
+                        if (aLine.contains("Email:") && aLine.contains("mailto:"))
+                            aLine = aLine.replaceAll(".*mailto:", "Email: ").replaceAll("\"", "").trim();
+                        if (aLine.length() > 0)
+                            lines.add(aLine);
+                    }
+                    return lines.toArray(new String[lines.size()]);
+                }
+            } catch (Exception e) {
+            }
+        }
+        return new String[0];
+    }
+}
diff --git a/src/jloda/util/RTree.java b/src/jloda/util/RTree.java
new file mode 100644
index 0000000..a436f45
--- /dev/null
+++ b/src/jloda/util/RTree.java
@@ -0,0 +1,487 @@
+/**
+ * RTree.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.geom.Rectangle2D;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Random;
+
+/**
+ * two-dimensional R-tree
+ * Daniel Huson, 7.2012
+ */
+public class RTree<T> {
+    final static private int MAX_NUMBER_CHILDREN = 3;
+    private RNode root;
+    private int size;
+    private RNode head;
+    private RNode tail;
+    private RNode lastHit;
+    private int numberOfComparisons = 0;
+
+    /**
+     * constructor
+     */
+    public RTree() {
+        root = null;
+        size = 0;
+        lastHit = null;
+    }
+
+    /**
+     * add a rectangle and associated data to the RTree
+     *
+     * @param rect
+     * @param data
+     */
+    public void add(Rectangle2D rect, T data) {
+        if (root == null) {
+            root = new RNode(rect);
+        }
+        RNode v = new RNode(rect, data);
+        if (head == null)
+            head = v;
+        if (tail != null)
+            tail.setNext(v);
+        tail = v;
+        RNode split = addBelowRec(root, v);
+        if (split != null) {
+            Rectangle2D newRect = (Rectangle2D) root.getRect().clone();
+            newRect.add(rect);
+            RNode newRoot = new RNode(newRect);
+            newRoot.addChild(root);
+            newRoot.addChild(split);
+            root = newRoot;
+        }
+        size++;
+    }
+
+    /**
+     * add data as close as possible to the given location without overlapping an data already contained in the RTree
+     *
+     * @param location
+     * @param dimension
+     * @param data
+     */
+    public Point addCloseTo(int seed, Point location, int minDx, int minDy, boolean left, Dimension dimension, T data) {
+        int x = location.x;
+        int y = location.y;
+
+
+        Rectangle bbox = new Rectangle();
+        bbox.setSize(dimension);
+
+        if (size() == 0) {
+            if (!overlaps(bbox)) {
+                bbox.setLocation(x, y);
+                add(bbox, data);
+                return new Point(x, y);
+            }
+        }
+
+        Random rand = new Random(seed);
+        boolean upDown = rand.nextBoolean();
+        boolean leftRight = rand.nextBoolean();
+
+        int direction = 3;
+        for (int k = 1; true; k++) { // number steps in a direction
+            for (int i = 0; i < 2; i++) {  // two different directions
+                if (direction == 3)
+                    direction = 0;
+                else
+                    direction++;
+                for (int j = 0; j <= k; j++) {  // the steps in the direction
+                    switch (direction) {
+                        case 0:
+                            if (left)
+                                x = location.x + (leftRight ? 1 : -1) * (minDx + k * 2);
+                            else
+                                x = location.x - (leftRight ? 1 : -1) * (minDx + k * 2);
+                            break;
+                        case 1:
+                            if (!left)
+                                y = location.y + (upDown ? 1 : -1) * (minDy + k * 2);
+                            else
+                                y = location.y - (upDown ? 1 : -1) * (minDy + k * 2);
+                            break;
+                        case 2:
+                            if (!left)
+                                y = location.y - (upDown ? 1 : -1) * (minDy + k * 2);
+                            else
+                                y = location.y + (upDown ? 1 : -1) * (minDy - k * 2);
+                            break;
+                    }
+                    bbox.setLocation((x >= location.x ? x : x - dimension.width), y);
+                    if (!overlaps(bbox)) {
+                        add(bbox, data);
+                        return new Point(bbox.x, bbox.y);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * add the node v below the given node
+     *
+     * @param parent
+     * @param v
+     */
+    private RNode addBelowRec(RNode parent, RNode v) {
+        parent.rect.add(v.rect);
+        if (parent.data != null)
+            System.err.println("Entered addBelowRec with leaf node: " + parent);
+        RNode child = parent.chooseOverlappingInternalChild(v);
+        if (child != null) {
+            return addBelowRec(child, v);
+        } else {
+            if (parent.addChild(v)) {
+                return null;
+            } else {
+                return parent.split(v);
+            }
+        }
+    }
+
+    /**
+     * get a hit data item, if one exists
+     *
+     * @param rect
+     * @return data item
+     */
+    public T getHitData(Rectangle2D rect) {
+        numberOfComparisons = 0;
+        if (root == null)
+            return null;
+        if (lastHit != null && rect.intersects(lastHit.getRect())) {
+            numberOfComparisons++;
+            return lastHit.getData();
+        }
+        RNode node = getHitRec(root, rect);
+        if (node == null)
+            return null;
+        else {
+            lastHit = node;
+            return node.getData();
+        }
+    }
+
+    /**
+     * determines whether given rect overlaps with any of the contained rectangles
+     *
+     * @param rect
+     * @return true, if an overlap was detected
+     */
+    public boolean overlaps(Rectangle2D rect) {
+        return getHitData(rect) != null;
+    }
+
+    public int getNumberOfComparisonsUsedOnLastQuery() {
+        return numberOfComparisons;
+    }
+
+    /**
+     * recursively do the work
+     *
+     * @param node
+     * @param rect
+     * @return hit data item or null
+     */
+    private RNode getHitRec(RNode node, Rectangle2D rect) {
+        if (node.rect.intersects(rect)) {
+            numberOfComparisons++;
+            if (node.data != null)
+                return node;
+            else {
+                for (int i = 0; i < node.getNumberOfChildren(); i++) {
+                    RNode result = getHitRec(node.getChild(i), rect);
+                    if (result != null)
+                        return result;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * erase
+     */
+    public void clear() {
+        root = null;
+        size = 0;
+        lastHit = null;
+        head = null;
+        tail = null;
+    }
+
+    /**
+     * size
+     *
+     * @return size
+     */
+    public int size() {
+        return size;
+    }
+
+    /**
+     * gets the bounding box
+     *
+     * @param bbox bounding box
+     */
+    public void getBoundingBox(Rectangle2D bbox) {
+        if (root == null)
+            bbox.setRect(0, 0, 0, 0);
+        else
+            bbox.setRect(root.rect);
+    }
+
+    public Iterator<Pair<Rectangle2D, T>> iterator() {
+        return new Iterator<Pair<Rectangle2D, T>>() {
+            RNode node = head;
+
+            public boolean hasNext() {
+                return node != null;
+            }
+
+            public Pair<Rectangle2D, T> next() {
+                if (node == null)
+                    return null;
+                Pair<Rectangle2D, T> result = new Pair<>(node.getRect(), node.getData());
+                node = node.getNext();
+                return result;
+            }
+
+            public void remove() {
+            }
+        };
+    }
+
+    public void draw(Graphics gc) {
+        if (root != null)
+            drawRec(root, gc, 1);
+    }
+
+    private void drawRec(RNode v, Graphics gc, int depth) {
+        for (int i = 0; i < v.getNumberOfChildren(); i++)
+            drawRec(v.getChild(i), gc, depth + 1);
+        if (v.isLeaf()) {
+            gc.setColor(new Color(0, 255, 0, 140));
+            gc.drawString(v.getData().toString() + " (@ " + depth + ")", (int) v.getRect().getX(), (int) v.getRect().getY());
+            ((Graphics2D) gc).draw(v.getRect());
+        } else {
+            Random random = new Random(depth);
+            gc.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255), 140));
+            Rectangle2D rect = (Rectangle2D) v.getRect().clone();
+            rect.setRect(rect.getX() - depth, rect.getY() - depth, rect.getWidth() + 2 * depth, rect.getHeight() + 2 * depth);
+            gc.drawString("" + depth, (int) v.getRect().getX() + 10 * depth, (int) v.getRect().getY());
+            ((Graphics2D) gc).draw(rect);
+
+        }
+    }
+
+    public abstract class RTreeVisitor {
+        public void visit(Rectangle2D rect, T data) {
+        }
+    }
+
+    /**
+     * node in Rtree
+     */
+    private class RNode {
+        private Rectangle2D rect;
+        private final RNode[] children;
+        private RNode next;
+        private int numberOfChildren;
+        private T data;
+
+        RNode(Rectangle2D rect) {
+            this.rect = (Rectangle2D) rect.clone();
+            children = (RNode[]) Array.newInstance(RNode.class, MAX_NUMBER_CHILDREN);
+        }
+
+        RNode(Rectangle2D rect, T data) {
+            this.rect = (Rectangle2D) rect.clone();
+            this.data = data;
+            children = null;
+        }
+
+        int getNumberOfChildren() {
+            return numberOfChildren;
+        }
+
+        Rectangle2D getRect() {
+            return rect;
+        }
+
+        T getData() {
+            return data;
+        }
+
+        RNode getChild(int i) {
+            return children[i];
+        }
+
+        boolean addChild(RNode node) {
+            if (numberOfChildren < MAX_NUMBER_CHILDREN) {
+                if (rect == null)
+                    rect = (Rectangle2D) node.rect.clone();
+                else
+                    rect.add(node.rect);
+                children[numberOfChildren++] = node;
+                return true;
+            } else
+                return false;
+        }
+
+        RNode chooseOverlappingInternalChild(RNode node) {
+            if (numberOfChildren == 0)
+                return null;
+            double bestArea = Double.MAX_VALUE;
+            int bestI = -1;
+            for (int i = 0; i < numberOfChildren; i++) {
+                if (children[i].numberOfChildren > 0) {
+                    double area = computeAreaOfUnion(children[i].rect, node.rect);
+                    if (area < bestArea) {
+                        bestArea = area;
+                        bestI = i;
+                    }
+                }
+            }
+            if (bestI >= 0)
+                return children[bestI];
+            else
+                return null;
+        }
+
+        /**
+         * split a node and return the part to be reinserted below parent node
+         *
+         * @return split off part
+         */
+        RNode split(RNode v) {
+            if (children == null)
+                System.err.println("Splitting leaf: " + this);
+
+            RNode[] all = Arrays.copyOf(children, numberOfChildren + 1);
+            all[all.length - 1] = v;
+
+            int worstI = -1;
+            int worstJ = -1;
+            double worstArea = -1;
+
+            for (int i = 0; i < all.length; i++) {
+                for (int j = i + 1; j < all.length; j++) {
+                    double area = computeAreaOfUnion(all[i].rect, all[j].rect);
+                    if (area > worstArea) {
+                        worstArea = area;
+                        worstI = i;
+                        worstJ = j;
+                    }
+                }
+            }
+            RNode a = this;
+            a.clearChildren();
+            a.setRect(all[worstI].getRect());
+            a.addChild(all[worstI]);
+
+            RNode b = new RNode(all[worstJ].getRect());
+            b.addChild(all[worstJ]);
+
+            for (int i = 0; i < all.length; i++) {
+                if (i != worstI && i != worstJ) {
+                    double aArea = computeAreaOfUnion(a.rect, all[i].rect);
+                    double bArea = computeAreaOfUnion(b.rect, all[i].rect);
+                    if (aArea <= bArea)
+                        a.addChild(all[i]);
+                    else
+                        b.addChild(all[i]);
+                }
+            }
+            return b;
+        }
+
+        boolean isLeaf() {
+            return data != null;
+        }
+
+        void clearChildren() {
+            for (int i = 0; i < numberOfChildren; i++)
+                children[i] = null;
+            numberOfChildren = 0;
+        }
+
+        void setRect(Rectangle2D rect) {
+            this.rect = (Rectangle2D) rect.clone();
+        }
+
+        double computeAreaOfUnion(Rectangle2D rect1, Rectangle2D rect2) {
+            return (Math.max(rect1.getMaxX(), rect2.getMaxX()) - Math.min(rect1.getMinX(), rect2.getMinX()))
+                    * (Math.max(rect1.getMaxY(), rect2.getMaxY()) - Math.min(rect1.getMinY(), rect2.getMinY()));
+        }
+
+        RNode getNext() {
+            return next;
+        }
+
+        void setNext(RNode next) {
+            this.next = next;
+        }
+    }
+
+    public static void main(String[] args) {
+        final RTree<Integer> rtree = new RTree<>();
+
+        int numberOfComparisons = 0;
+        int numberOfTries = 0;
+
+        Random random = new Random(666);
+
+        for (int i = 0; i < 50; i++) {
+            int x = random.nextInt(1000);
+            int y = random.nextInt(800);
+            int width = random.nextInt(1000 - x);
+            int height = random.nextInt(Math.min(64, 800 - y));
+            Rectangle2D rect = new Rectangle(x, y, width, height);
+            if (rtree.getHitData(rect) == null) {
+                rtree.add(rect, i);
+            } else i--;
+            numberOfComparisons += rtree.numberOfComparisons;
+            numberOfTries++;
+            // System.err.println("getHitData comparisons: "+rtree.numberOfComparisons);
+        }
+
+        System.err.println("Average number of comparisons: " + ((float) numberOfComparisons / (float) numberOfTries));
+
+        JPanel panel = new JPanel() {
+            public void paint(Graphics graphics) {
+                super.paint(graphics);
+                rtree.draw(graphics);
+            }
+        };
+        JFrame frame = new JFrame();
+        frame.setSize(1100, 900);
+        frame.add(panel);
+        frame.setVisible(true);
+    }
+}
diff --git a/src/jloda/util/RandomGaussian.java b/src/jloda/util/RandomGaussian.java
new file mode 100644
index 0000000..c85ee69
--- /dev/null
+++ b/src/jloda/util/RandomGaussian.java
@@ -0,0 +1,160 @@
+/**
+ * RandomGaussian.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * Random numbers from Gaussian distribution
+ * @version $Id: RandomGaussian.java,v 1.3 2006-06-06 18:56:04 huson Exp $
+ * @author Daniel Huson
+ *        9.2003
+ */
+package jloda.util;
+
+import java.util.Random;
+
+/**
+ * Random numbers for Gaussian distribution
+ */
+public class RandomGaussian {
+    private double mean;
+    private double stdDev;
+    private final Random rand;
+
+    /**
+     * construct Gaussian random source with mean 0, std deviation 1
+     */
+    public RandomGaussian() {
+        mean = 0;
+        stdDev = 1;
+        rand = new Random();
+    }
+
+    /**
+     * construct Gaussian random source with given mean and std deviation
+     *
+     * @param mean
+     * @param stdDev
+     */
+    public RandomGaussian(double mean, double stdDev) {
+        this.mean = mean;
+        this.stdDev = stdDev;
+        rand = new Random();
+    }
+
+    /**
+     * construct Gaussian random source with mean 0, std deviation 1
+     */
+    public RandomGaussian(int seed) {
+        mean = 0;
+        stdDev = 1;
+        rand = new Random(seed);
+    }
+
+    /**
+     * construct Gaussian random source with given mean and std deviation
+     *
+     * @param mean
+     * @param stdDev
+     */
+    public RandomGaussian(double mean, double stdDev, int seed) {
+        this.mean = mean;
+        this.stdDev = stdDev;
+        rand = new Random(seed);
+    }
+
+    /**
+     * get mean
+     *
+     * @return mean
+     */
+    public double getMean() {
+        return mean;
+    }
+
+    /**
+     * sets the mean
+     *
+     * @param mean
+     */
+    public void setMean(double mean) {
+        this.mean = mean;
+    }
+
+    /**
+     * gets the set standard deviation
+     *
+     * @return standard deviation
+     */
+    public double getStdDev() {
+        return stdDev;
+    }
+
+    /**
+     * sets the standard deviation
+     *
+     * @param stdDev
+     */
+    public void setStdDev(double stdDev) {
+        this.stdDev = stdDev;
+    }
+
+    /**
+     * gets the next gaussian value
+     *
+     * @return next value
+     */
+    public double nextDouble() {
+        return mean + (rand.nextGaussian() * stdDev);
+    }
+
+    /**
+     * gets the next gaussian value
+     *
+     * @return next value
+     */
+    public float nextFloat() {
+        return (float) nextDouble();
+    }
+
+    /**
+     * gets the next gaussian value
+     *
+     * @return next value
+     */
+    public int nextInt() {
+        return Math.round(nextFloat());
+    }
+
+    /**
+     * gets the next gaussian value
+     *
+     * @return next value
+     */
+    public long nextLong() {
+        return Math.round(nextDouble());
+    }
+
+    /**
+     * sets the seed
+     *
+     * @param seed
+     */
+    public void setSeed(long seed) {
+        rand.setSeed(seed);
+    }
+}
diff --git a/src/jloda/util/RememberingComboBox.java b/src/jloda/util/RememberingComboBox.java
new file mode 100644
index 0000000..cdf21d8
--- /dev/null
+++ b/src/jloda/util/RememberingComboBox.java
@@ -0,0 +1,181 @@
+/**
+ * RememberingComboBox.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import javax.swing.*;
+import javax.swing.plaf.basic.BasicComboBoxEditor;
+import java.awt.*;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * remembering combo box
+ * bryant, huson
+ * Date: Feb 1, 2006
+ */
+public class RememberingComboBox extends JComboBox<String> {
+    private final BasicComboBoxEditor editor;
+
+    /**
+     * constructor
+     */
+    public RememberingComboBox() {
+        super();
+        setEnabled(true);
+        setEditable(true);
+        //clearHistory();
+
+        editor = new BasicComboBoxEditor();
+        setEditor(editor);
+
+        editor.getEditorComponent().addKeyListener(new KeyAdapter() {
+            public void keyTyped(KeyEvent keyEvent) {
+                if (!RememberingComboBox.this.getBackground().equals(Color.WHITE))
+                    RememberingComboBox.this.setBackground(Color.WHITE);
+            }
+        });
+    }
+
+    public Color getBackground() {
+        if (editor != null)
+            return editor.getEditorComponent().getBackground();
+        else
+            return super.getBackground();
+    }
+
+    public void setBackground(Color background) {
+        if (editor != null)
+            editor.getEditorComponent().setBackground(background);
+        else
+            super.setBackground(background);
+    }
+
+    public Color getForeground() {
+        if (editor != null)
+            return editor.getEditorComponent().getForeground();
+        else
+            return super.getForeground();
+    }
+
+    public void setForeground(Color foreground) {
+        if (editor != null)
+            editor.getEditorComponent().setForeground(foreground);
+        else
+            super.setForeground(foreground);
+    }
+
+    /**
+     * Gets the current typed text. If save is true, then this is inserted into the list, after removing any
+     * duplicate entries.
+     *
+     * @param save
+     * @return current text
+     */
+    public String getCurrentText(boolean save) {
+        String newEntry = null;
+        if (getSelectedItem() != null)
+            newEntry = getSelectedItem().toString();
+
+        if (newEntry == null)
+            newEntry = "";
+        if (save && newEntry.length() > 0) {
+            //Check to see if it already appears. If it does, remove, as well as any null entires. Then insert item at start of list.
+            int index = 0;
+            while (index < getItemCount()) {
+                String thisEntry = getItemAt(index);
+                if (thisEntry.length() == 0)
+                    removeItemAt(index);
+                else if (thisEntry.equals(newEntry))
+                    removeItemAt(index);
+                else
+                    index++;
+            }
+
+            insertItemAt(newEntry, 0);
+            setSelectedIndex(0);
+        }
+        return newEntry;
+    }
+
+    /**
+     * Removes all entries and sets current text to ""
+     */
+    public void clearHistory() {
+        removeAllItems();
+        addItem("");
+    }
+
+    public void addItems(Collection<String> items) {
+        for (String item : items) addItem(item);
+    }
+
+    /**
+     * gets the list of items
+     *
+     * @param maxNumber
+     * @return first maxNumber items
+     */
+    public List<String> getItems(int maxNumber) {
+        maxNumber = Math.min(maxNumber, this.getItemCount());
+
+        final List<String> list = new ArrayList<>(maxNumber);
+        for (int i = 0; i < maxNumber; i++) {
+            list.add(getItemAt(i));
+        }
+        return list;
+    }
+
+    /**
+     * add a list of sep-separated items
+     *
+     * @param str
+     * @param sep
+     */
+    public void addItemsFromString(String str, String sep) {
+        if (str != null && str.length() > 0)
+            for (StringTokenizer tok = new StringTokenizer(str, sep); tok.hasMoreElements(); ) {
+                String string = tok.nextToken();
+                if (string.length() > 0)
+                    addItem(string);
+            }
+        //  if (getItemCount() > 0)
+        //    setSelectedIndex(0);
+    }
+
+    /**
+     * gets the list of items as a sep-separated string
+     *
+     * @param maxNumber
+     * @param sep
+     * @return first maxNumber items
+     */
+    public String getItemsAsString(int maxNumber, String sep) {
+        StringBuilder buf = new StringBuilder();
+        for (int i = 0; i < Math.min(maxNumber, this.getItemCount()); i++) {
+            buf.append(getItemAt(i));
+            buf.append(sep);
+        }
+        return buf.toString();
+    }
+}
diff --git a/src/jloda/util/ResourceManager.java b/src/jloda/util/ResourceManager.java
new file mode 100644
index 0000000..a0536f6
--- /dev/null
+++ b/src/jloda/util/ResourceManager.java
@@ -0,0 +1,417 @@
+/**
+ * ResourceManager.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.HashMap;
+
+
+/**
+ * get icons and  cursors from resources
+ */
+public class ResourceManager {
+
+    /**
+     * Specifies the path where to look for icon files in a jar archive.
+     */
+    public static final String iconPackagePath;
+
+    /**
+     * Specifies the path where to look for cursor files in a jar archive.
+     */
+    public static final String cursorPackagePath;
+    /**
+     * Specifies the path where to look for data files in a jar archive.
+     */
+    public static final String filePackagePath;
+
+    public static final String cssPackagePath;
+
+
+    /**
+     * Maps icon names to initialized icons that are reachable by a getter.
+     */
+    private static final HashMap<String, ImageIcon> iconMap;
+
+    /**
+     * Maps cursor names to initialized icons that are reachable by a getter.
+     */
+    private static final HashMap<String, Cursor> cursorMap;
+
+    /**
+     * Maps file names to initialized files that are reachable by a getter.
+     */
+    private static final HashMap<String, File> dataMap;
+
+    /**
+     * Static constructor.
+     */
+    static {
+        iconPackagePath = "resources.icons";
+        cursorPackagePath = "resources.cursors";
+        filePackagePath = "resources.files";
+        cssPackagePath = "resources.css";
+
+        iconMap = new HashMap<>();
+        cursorMap = new HashMap<>();
+        dataMap = new HashMap<>();
+    }
+
+    private static boolean warningMissingIcon;
+
+    /**
+     * get the icon package path
+     *
+     * @return icon package path
+     */
+    public static String getIconPackagePath() {
+        return iconPackagePath;
+    }
+
+    /**
+     * get the cursor package path
+     *
+     * @return path
+     */
+    public static String getCursorPackagePath() {
+        return cursorPackagePath;
+    }
+
+
+    public static String getFilePackagePath() {
+        return filePackagePath;
+    }
+
+    /**
+     * Returns the icon with name specified by the parameter, or <code>null</code> if there is none.
+     */
+    public static ImageIcon getIcon(String name) {
+        if (!iconMap.containsKey(name)) {
+            Image iconImage = getImageResource(iconPackagePath, name);
+            if (iconImage != null) {
+                ImageIcon icon = new ImageIcon(iconImage);
+                iconMap.put(name, icon);
+            } else {
+                if (Basic.getDebugMode() && warningMissingIcon)
+                    System.err.println("ICON NOT FOUND: " + name + ", path: " + iconPackagePath);
+                Image image = getImageResource(iconPackagePath, "sun/toolbarButtonGraphics/general/Help16.gif");
+                if (image != null)
+                    return new ImageIcon(image);
+                else
+                    return null;
+            }
+        }
+        return iconMap.get(name);
+    }
+
+    /**
+     * Returns the file with name specified by the parameter, or <code>null</code> if there is none.
+     */
+    public static File getFile(String name) {
+        if (!dataMap.containsKey(name)) {
+            File data = getFileResource(filePackagePath, name);
+            dataMap.put(name, data);
+        }
+        return dataMap.get(name);
+    }
+
+    /**
+     * Returns the file with name specified by the parameter, or <code>null</code> if there is none.
+     */
+    public static URL getCssURL(String name) {
+        return getFileURL(cssPackagePath, name);
+    }
+
+    /**
+     * Returns the path with name specified by the parameter, or just the name, else
+     */
+    public static String getFileName(String name) {
+        if (!dataMap.containsKey(name)) {
+            File data = getFileResource(filePackagePath, name);
+            dataMap.put(name, data);
+        }
+        File file = dataMap.get(name);
+        if (file != null)
+            return file.getPath().replaceAll("%20", " ");
+        else
+            return "File " + name + ": Path not found";
+    }
+
+    /**
+     * Gets stream from package resources.files, else attempts to open
+     * stream from named file in file system
+     *
+     * @param fileName the name of the file
+     */
+    public static InputStream getFileAsStream(String fileName) {
+        if (fileName == null)
+            return null;
+        fileName = fileName.trim();
+        if (fileName.length() == 0)
+            return null;
+        return getFileAsStream(filePackagePath, fileName);
+    }
+
+    /**
+     * Returns file resource as stream, unless the string contains a slash, in which case returns Stream from the file system
+     *
+     * @param filePackage the package containing file
+     * @param fileName       the name of the file
+     */
+    public static InputStream getFileAsStream(String filePackage, String fileName) {
+        if (fileName.contains("/") || fileName.contains("\\")) {
+            File file = new File(fileName);
+            try {
+                return Basic.getInputStreamPossiblyZIPorGZIP(file.getPath());
+            } catch (IOException e) {
+                if (!fileName.endsWith(".info")) // don't complain about missing info files
+                    System.err.println(e.getMessage());
+                return null;
+            }
+        } else
+            return getFileResourceAsStream(filePackage, fileName);
+
+    }
+
+    /**
+     * Returns the cursor with name specified by the parameter, or <code>null</code> if there is none.
+     */
+    public static Cursor getCursor(String name) {
+        if (!cursorMap.containsKey(name)) {
+            final Toolkit toolkit = Toolkit.getDefaultToolkit();
+            final Dimension dim = toolkit.getBestCursorSize(20, 20);
+            final int x = dim.width / 2;
+            final int y = dim.height / 2;
+
+            Image image = getImageResource(cursorPackagePath, name);
+            if ((new ImageIcon(image)).getImageLoadStatus() == MediaTracker.COMPLETE) {
+                Cursor cursor = toolkit.createCustomCursor(image, new Point(x, y), name);
+                cursorMap.put(name, cursor);
+
+            }
+        }
+        return cursorMap.get(name);
+    }
+
+    /**
+     * Returns an Image (icon) with specified file name at the location specified by <code>packageName</code>.
+     *
+     * @param packageName the path through a package (the name of the subpackage) where to look for the icon
+     * @param fileName       the name of the icon file
+     */
+    public static Image getImageResource(String packageName, String fileName) {
+        Image ret = null;
+        try {
+            String resname = "/" + packageName.replace('.', '/') + "/" + fileName;
+            resname = resname.replaceAll(" ", "\\ ");
+            InputStream is = ResourceManager.class.getResourceAsStream(resname);
+            if (is != null) {
+                byte[] buffer = new byte[0];
+                byte[] tmpbuf = new byte[1024];
+                while (true) {
+                    int len = is.read(tmpbuf);
+                    if (len <= 0)
+                        break;
+                    byte[] newbuf = new byte[buffer.length + len];
+                    System.arraycopy(buffer, 0, newbuf, 0, buffer.length);
+                    System.arraycopy(tmpbuf, 0, newbuf, buffer.length, len);
+                    buffer = newbuf;
+                }
+                ret = Toolkit.getDefaultToolkit().createImage(buffer);
+                is.close();
+            }
+        } catch (Exception exc) {
+            Basic.caught(exc);
+        }
+        return ret;
+    }
+
+    /**
+     * Returns an Image (icon) with specified file name at the location specified by <code>packageName</code>.
+     *
+     * @param packageName the path through a package (the name of the subpackage) where to look for the icon
+     * @param fileName       the name of the icon file
+     */
+    public static BufferedImage getBufferedImageResource(String packageName, String fileName) {
+        BufferedImage bufferedImage = null;
+        try {
+            final String resourceName = ("/" + packageName.replace('.', '/') + "/" + fileName).replaceAll(" ", "\\ ");
+            final InputStream is = ResourceManager.class.getResourceAsStream(resourceName);
+            if (is != null) {
+                bufferedImage = ImageIO.read(is);
+                is.close();
+            }
+        } catch (Exception exc) {
+            Basic.caught(exc);
+        }
+        return bufferedImage;
+    }
+
+
+    /**
+     * Returns File with specified file name at the location specified by <code>packageName</code>.
+     *
+     * @param packageName the path through a package (the name of the subpackage) where to look for the icon
+     * @param fileName       the name of the file
+     */
+    public static File getFileResource(String packageName, String fileName) {
+        try {
+            final String resourceName = ("/" + packageName.replace('.', '/') + "/" + fileName).replaceAll(" ", "\\ ");
+            final URL url = ResourceManager.class.getResource(resourceName);
+            return new File(url.getFile());
+        } catch (Exception exc) {
+        }
+        return null;
+    }
+
+    /**
+     * Returns URL with specified file name at the location specified by <code>packageName</code>.
+     *
+     * @param packageName the path through a package (the name of the subpackage) where to look for the icon
+     * @param fileName    the name of the file
+     */
+    public static URL getFileURL(String packageName, String fileName) {
+        try {
+            final String resourceName = ("/" + packageName.replace('.', '/') + "/" + fileName).replaceAll(" ", "\\ ");
+            return ResourceManager.class.getResource(resourceName);
+        } catch (Exception exc) {
+        }
+        return null;
+    }
+
+    /**
+     * Returns file resource as stream
+     *
+     * @param packageName the path through a package (the name of the subpackage) where to look for the icon
+     * @param fileName       the name of the file
+     */
+    public static InputStream getFileResourceAsStream(String packageName, String fileName) {
+        try {
+            final String resourceName = ("/" + packageName.replace('.', '/') + "/" + fileName).replace(" ", "\\ ");
+            return ResourceManager.class.getResourceAsStream(resourceName);
+        } catch (Exception ex) {
+            Basic.caught(ex);
+        }
+        return null;
+    }
+
+    /**
+     * does resource file exist?
+     *
+     * @param fileName
+     * @return true if file exists
+     */
+    public static boolean resourceFileExists(String fileName) {
+        try {
+            final InputStream ins = ResourceManager.class.getResourceAsStream("/resources/files/" + fileName);
+            if (ins != null) {
+                ins.close();
+                return true;
+            }
+        } catch (Exception e) {
+        }
+        return false;
+    }
+
+    /**
+     * gets an image from the named package
+     *
+     * @param packageName
+     * @param fileName
+     * @return image
+     * @throws IOException
+     */
+    public static Image getImage(String packageName, String fileName) throws IOException {
+        return getImageResource(packageName, fileName);
+    }
+
+    /**
+     * does the named file exist as a resource or file?
+     *
+     * @param name
+     * @return true if named file exists as a resource or file
+     */
+    public static boolean fileExists(String name) {
+        if (name == null || name.length() == 0)
+            return false;
+
+        InputStream ins = null;
+        try {
+            ins = getFileAsStream(name);
+            return (ins != null);
+        } catch (Exception ex) {
+        } finally {
+            if (ins != null)
+                try {
+                    ins.close();
+                } catch (IOException e) {
+                }
+        }
+        return false;
+    }
+
+    /**
+     * does the named file exist as a resource ?
+     *
+     * @param packageName
+     * @param name
+     * @return true if named file exists as a resource
+     */
+    public static boolean fileResourceExists(String packageName, String name) {
+        if (name == null || name.length() == 0)
+            return false;
+
+        InputStream ins = null;
+        boolean existsAsStream = false;
+        try {
+            ins = getFileResourceAsStream(packageName, name);
+            existsAsStream = (ins != null);
+        } catch (Exception ex) {
+        } finally {
+            if (ins != null)
+                try {
+                    ins.close();
+                } catch (IOException e) {
+                }
+        }
+        return existsAsStream || (new File(name)).canRead();
+    }
+
+    public static void setWarningMissingIcon(boolean warningMissingIcon) {
+        ResourceManager.warningMissingIcon = warningMissingIcon;
+    }
+
+    public static boolean isWarningMissingIcon() {
+        return warningMissingIcon;
+    }
+
+    public static HashMap<String, ImageIcon> getIconMap() {
+        return iconMap;
+    }
+}
+
+
diff --git a/src/jloda/util/RunLater.java b/src/jloda/util/RunLater.java
new file mode 100644
index 0000000..53f340a
--- /dev/null
+++ b/src/jloda/util/RunLater.java
@@ -0,0 +1,63 @@
+/**
+ * RunLater.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+/**
+ * run a method later
+ * Daniel Huson, 5.2011
+ */
+public class RunLater {
+    /**
+     * wait the given amount of milli-seconds and then call the run method of the runnable object
+     *
+     * @param waitMilliSeconds
+     * @param runnable
+     */
+    public void apply(final long waitMilliSeconds, final Runnable runnable) {
+
+        System.err.println("In:");
+        Runnable myRunnable = new Runnable() {
+            public void run() {
+                long startTime = System.currentTimeMillis();
+                long endTime = startTime + waitMilliSeconds;
+
+                while (System.currentTimeMillis() < endTime) {
+                    // Still within time threshold, wait a little longer
+                    try {
+                        Thread.sleep(100L);  // Sleep 100 milliseconds
+                        if (!Thread.currentThread().isAlive()) {
+                            break;
+                        }
+                    } catch (InterruptedException e) {
+                        // Someone woke us up during sleep, that's OK
+                    }
+                }
+                System.err.println("Run:");
+                runnable.run();
+            }
+        };
+        Thread worker = new Thread(myRunnable);
+        worker.setDaemon(true);
+        worker.setPriority(Thread.currentThread().getPriority() - 1);
+        worker.start();
+
+        System.err.println("Out:");
+    }
+}
diff --git a/src/jloda/util/SequenceUtils.java b/src/jloda/util/SequenceUtils.java
new file mode 100644
index 0000000..6e9d6ad
--- /dev/null
+++ b/src/jloda/util/SequenceUtils.java
@@ -0,0 +1,574 @@
+/**
+ * SequenceUtils.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.StringWriter;
+
+/**
+ * some utilities for DNA and amino acid sequences
+ * Daniel Huson, 9.2011
+ */
+public class SequenceUtils {
+    private final static byte[][][] codon2aminoAcid = new byte[127][127][127];
+
+    static {
+        // initialize the codon2aminoAcid table
+        String nucleotides = "actgACGTuUN-";
+        for (int i = 0; i < nucleotides.length(); i++) {
+            char a = nucleotides.charAt(i);
+            for (int j = 0; j < nucleotides.length(); j++) {
+                char b = nucleotides.charAt(j);
+                for (int k = 0; k < nucleotides.length(); k++) {
+                    char c = nucleotides.charAt(k);
+                    codon2aminoAcid[(int) a][(int) b][(int) c] = getAminoAcidInit(a, b, c);
+                }
+            }
+        }
+    }
+
+    /**
+     * translate DNA into amino acids
+     *
+     * @param c1
+     * @param c2
+     * @param c3
+     * @return amino acid
+     */
+    static private byte getAminoAcidInit(int c1, int c2, int c3) {
+        c1 = Character.toUpperCase(c1);
+        if (c1 == 'T')
+            c1 = 'U';
+        c2 = Character.toUpperCase(c2);
+        if (c2 == 'T')
+            c2 = 'U';
+        c3 = Character.toUpperCase(c3);
+        if (c3 == 'T')
+            c3 = 'U';
+
+        if (c1 == '-' || c2 == '-' || c3 == '-')
+            return '-';
+
+        switch (c1) {
+            case 'U':
+                switch (c2) {
+                    case 'U':
+                        switch (c3) {
+                            case 'U':
+                                return 'F';
+                            case 'C':
+                                return 'F';
+                            case 'A':
+                                return 'L';
+                            case 'G':
+                                return 'L';
+                            default:
+                                return 'X';
+                        }
+                    case 'C':
+                        switch (c3) {
+                            case 'U':
+                                return 'S';
+                            case 'C':
+                                return 'S';
+                            case 'A':
+                                return 'S';
+                            case 'G':
+                                return 'S';
+                            default:
+                                return 'S';
+                        }
+                    case 'A':
+                        switch (c3) {
+                            case 'U':
+                                return 'Y';
+                            case 'C':
+                                return 'Y';
+                            case 'A':
+                                return '*';
+                            case 'G':
+                                return '*';
+                            default:
+                                return 'X';
+                        }
+                    case 'G':
+                        switch (c3) {
+                            case 'U':
+                                return 'C';
+                            case 'C':
+                                return 'C';
+                            case 'A':
+                                return '*';
+                            case 'G':
+                                return 'W';
+                            default:
+                                return 'X';
+                        }
+                    default:
+                        return 'X';
+                }
+            case 'C':
+                switch (c2) {
+                    case 'U':
+                        switch (c3) {
+                            case 'U':
+                                return 'L';
+                            case 'C':
+                                return 'L';
+                            case 'A':
+                                return 'L';
+                            case 'G':
+                                return 'L';
+                            default:
+                                return 'L';
+                        }
+                    case 'C':
+                        switch (c3) {
+                            case 'U':
+                                return 'P';
+                            case 'C':
+                                return 'P';
+                            case 'A':
+                                return 'P';
+                            case 'G':
+                                return 'P';
+                            default:
+                                return 'P';
+                        }
+                    case 'A':
+                        switch (c3) {
+                            case 'U':
+                                return 'H';
+                            case 'C':
+                                return 'H';
+                            case 'A':
+                                return 'Q';
+                            case 'G':
+                                return 'Q';
+                            default:
+                                return 'X';
+                        }
+                    case 'G':
+                        switch (c3) {
+                            case 'U':
+                                return 'R';
+                            case 'C':
+                                return 'R';
+                            case 'A':
+                                return 'R';
+                            case 'G':
+                                return 'R';
+                            default:
+                                return 'R';
+                        }
+                    default:
+                        return 'X';
+                }
+            case 'A':
+                switch (c2) {
+                    case 'U':
+                        switch (c3) {
+                            case 'U':
+                                return 'I';
+                            case 'C':
+                                return 'I';
+                            case 'A':
+                                return 'I';
+                            case 'G':
+                                return 'M';
+                            default:
+                                return 'X';
+                        }
+                    case 'C':
+                        switch (c3) {
+                            case 'U':
+                                return 'T';
+                            case 'C':
+                                return 'T';
+                            case 'A':
+                                return 'T';
+                            case 'G':
+                                return 'T';
+                            default:
+                                return 'T';
+                        }
+                    case 'A':
+                        switch (c3) {
+                            case 'U':
+                                return 'N';
+                            case 'C':
+                                return 'N';
+                            case 'A':
+                                return 'K';
+                            case 'G':
+                                return 'K';
+                            default:
+                                return 'X';
+                        }
+                    case 'G':
+                        switch (c3) {
+                            case 'U':
+                                return 'S';
+                            case 'C':
+                                return 'S';
+                            case 'A':
+                                return 'R';
+                            case 'G':
+                                return 'R';
+                            default:
+                                return 'X';
+                        }
+                    default:
+                        return 'X';
+                }
+            case 'G':
+                switch (c2) {
+                    case 'U':
+                        switch (c3) {
+                            case 'U':
+                                return 'V';
+                            case 'C':
+                                return 'V';
+                            case 'A':
+                                return 'V';
+                            case 'G':
+                                return 'V';
+                            default:
+                                return 'V';
+                        }
+                    case 'C':
+                        switch (c3) {
+                            case 'U':
+                                return 'A';
+                            case 'C':
+                                return 'A';
+                            case 'A':
+                                return 'A';
+                            case 'G':
+                                return 'A';
+                            default:
+                                return 'A';
+                        }
+                    case 'A':
+                        switch (c3) {
+                            case 'U':
+                                return 'D';
+                            case 'C':
+                                return 'D';
+                            case 'A':
+                                return 'E';
+                            case 'G':
+                                return 'E';
+                            default:
+                                return 'X';
+                        }
+                    case 'G':
+                        switch (c3) {
+                            case 'U':
+                                return 'G';
+                            case 'C':
+                                return 'G';
+                            case 'A':
+                                return 'G';
+                            case 'G':
+                                return 'G';
+                            default:
+                                return 'G';
+                        }
+                    default:
+                        return 'X';
+                }
+            default:
+                return 'X';
+        }
+    }
+
+    /**
+     * gets the amino acid for the codon starting the given position
+     *
+     * @param sequence
+     * @param pos
+     * @return amino acid
+     */
+    static public byte getAminoAcid(byte[] sequence, int pos) {
+        /*
+        byte result=getAminoAcid(sequence[pos], sequence[pos + 1], sequence[pos + 2]);
+        System.err.println(String.format("%c%c%c -> %c",sequence[pos],sequence[pos+1],sequence[pos+2],result));
+        return result;
+        */
+        return getAminoAcid(sequence[pos], sequence[pos + 1], sequence[pos + 2]);
+    }
+
+    /**
+     * gets the amino acid for the codon starting at the given position in the reverse strand.
+     * To get the amino acid sequence of the reverse strand of DNA using this method,
+     * start at beginning of leading strand, calling this method repeatedly, building the protein sequence from the end to the beginning
+     *
+     * @param sequence
+     * @param pos
+     * @return amino acid
+     */
+    static public byte getAminoAcidReverse(byte[] sequence, int pos) {
+        return getAminoAcid(getComplement(sequence[pos + 2]), getComplement(sequence[pos + 1]), getComplement(sequence[pos]));
+    }
+
+    /**
+     * gets the amino acid for the codon a,b,cin the reverse strand.
+     * To get the amino acid sequence of the reverse strand of DNA using this method,
+     * start at the end of the leading strand and repeatedly call this method with letters at positions pos, pos-1, pos-2
+     *
+     * @param a
+     * @param b
+     * param c
+     * @return amino acid
+     */
+    static public byte getAminoAcidReverse(byte a, byte b, byte c) {
+        return getAminoAcid(getComplement(a), getComplement(b), getComplement(c));
+    }
+
+    /**
+     * gets the amino acid for the codon starting the given position
+     *
+     * @param sequence
+     * @param pos
+     * @return amino acid
+     */
+    static public byte getAminoAcid(String sequence, int pos) {
+        return getAminoAcid(sequence.charAt(pos), sequence.charAt(++pos), sequence.charAt(++pos));
+    }
+
+    /**
+     * gets the amino acid for the codon starting at the given position in the reverse strand.
+     * To get the amino acid sequence of the reverse strand of DNA using this method,
+     * start at beginning of leading strand, calling this method repeatedly, building the protein sequence from the end to the beginning
+     *
+     * @param sequence
+     * @param pos
+     * @return amino acid
+     */
+    static public byte getAminoAcidReverse(String sequence, int pos) {
+        return getAminoAcid(getComplement((byte) sequence.charAt(pos + 2)), getComplement((byte) sequence.charAt(pos + 1)), getComplement((byte) sequence.charAt(pos)));
+    }
+
+    /**
+     * translate DNA into amino acids
+     *
+     * @param c1
+     * @param c2
+     * @param c3
+     * @return amino acid
+     */
+    static public byte getAminoAcid(int c1, int c2, int c3) {
+        try {
+            byte aa = codon2aminoAcid[c1][c2][c3];
+            if (aa != 0) {
+                return aa;
+            }
+        } catch (Exception ex) {
+        }
+        return 'X';
+    }
+
+    /**
+     * is this a valid nucleotide (with ambiguity codes)
+     *
+     * @param ch
+     * @return true, if nucleotide
+     */
+    public static boolean isNucleotide(int ch) {
+        return "atugckmryswbvhdxn".indexOf(Character.toLowerCase(ch)) != -1;
+    }
+
+    /**
+     * reverse complement of string
+     *
+     * @param readSequence
+     * @return reverse complement
+     */
+    public static String getReverseComplement(String readSequence) {
+        StringBuilder buf = new StringBuilder();
+        for (int i = readSequence.length() - 1; i >= 0; i--) {
+            buf.append((char) getComplement((byte) readSequence.charAt(i)));
+        }
+        return buf.toString();
+    }
+
+    /**
+     * gets the  complement of a nucleotide. Returns ambiguity codes unaltered.
+     *
+     * @param nucleotide
+     * @return reverse complement
+     */
+    public static byte getComplement(byte nucleotide) {
+        switch (nucleotide) {
+            case 'a':
+                return 't';
+            case 'A':
+                return 'T';
+            case 'c':
+                return 'g';
+            case 'C':
+                return 'G';
+            case 'g':
+                return 'c';
+            case 'G':
+                return 'C';
+            case 't':
+                return 'a';
+            case 'T':
+                return 'A';
+            default:
+                return nucleotide;
+        }
+    }
+
+    /**
+     * reverse (but do NOT complement) a sequence
+     *
+     * @param sequence
+     * @return reverse string (but not complemented
+     */
+    public static String getReverse(String sequence) {
+        StringWriter w = new StringWriter();
+        for (int i = sequence.length() - 1; i >= 0; i--) {
+            w.write(sequence.charAt(i));
+        }
+        return w.toString();
+    }
+
+    /**
+     * reverse (but do NOT complement) a sequence
+     *
+     * @param sequence
+     * @return reverse string (but not complemented
+     */
+    public static byte[] getReverse(byte[] sequence) {
+        byte[] result = new byte[sequence.length];
+        for (int i = 0; i < sequence.length; i++) {
+            result[i] = sequence[sequence.length - 1 - i];
+        }
+        return result;
+    }
+
+
+    /**
+     * translate a DNA sequence into protein
+     *
+     * @param reverse
+     * @param sequence
+     * @return
+     */
+    public static byte[] translate(boolean reverse, int shift, String sequence) {
+        byte[] result = new byte[(sequence.length() - shift) / 3];
+        if (!reverse) {
+            int pos = 0;
+            for (int i = shift; i <= sequence.length() - 3; i += 3) {
+                result[pos++] = getAminoAcid(sequence, i);
+            }
+        } else // reverse complement
+        {
+            int pos = 0;
+            for (int i = sequence.length() - 1 - shift; i >= 2; i -= 3) {
+                result[pos++] = getAminoAcidReverse(sequence, i);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * copies a string to a byte array, 0 terminated.
+     * Note that the length of bytes is usually larger than the string length
+     *
+     * @param string
+     * @param bytes
+     * @return 0-terminated bytes
+     */
+    public static byte[] getBytes0Terminated(String string, byte[] bytes) {
+        if (bytes.length < string.length() + 1)
+            bytes = new byte[2 * string.length() + 1];
+        for (int i = 0; i < string.length(); i++)
+            bytes[i] = (byte) string.charAt(i);
+        bytes[string.length()] = 0;
+        return bytes;
+
+    }
+
+    /**
+     * convert 0 terminated bytes to string
+     *
+     * @param bytes
+     * @return string
+     */
+    public static String getStringFromBytes0Terminated(byte[] bytes) {
+        StringBuilder buf = new StringBuilder();
+        for (byte aByte : bytes) {
+            if (aByte == 0)
+                break;
+            buf.append((char) aByte);
+        }
+        return buf.toString();
+    }
+
+    /**
+     * counts how many times each of the given symbols have been used
+     *
+     * @param sequence
+     * @param symbols
+     * @return usage
+     */
+    public static int[] computeUsageCounts(byte[] sequence, byte[] symbols) {
+        int[] counts = new int[symbols.length];
+
+        for (byte b : sequence) {
+            for (int j = 0; j < symbols.length; j++) {
+                if (symbols[j] == b)
+                    counts[j]++;
+            }
+        }
+        return counts;
+    }
+
+    /**
+     * count the number of gaps ('-') in a sequence
+     *
+     * @param sequence
+     * @return number of gaps
+     */
+    public static int countGaps(String sequence) {
+        int count = 0;
+        for (int i = 0; i < sequence.length(); i++)
+            if (sequence.charAt(i) == '-')
+                count++;
+        return count;
+    }
+
+    /**
+     * count the number of gaps ('-') in a sequence
+     *
+     * @param sequence
+     * @return number of gaps
+     */
+    public static int countGaps(byte[] sequence) {
+        int count = 0;
+        for (byte aSequence : sequence)
+            if (aSequence == '-')
+                count++;
+        return count;
+    }
+}
diff --git a/src/jloda/util/Signer.java b/src/jloda/util/Signer.java
new file mode 100644
index 0000000..92885ea
--- /dev/null
+++ b/src/jloda/util/Signer.java
@@ -0,0 +1,244 @@
+/**
+ * Signer.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.NoSuchElementException;
+
+/**
+ * methods for signing licenses
+ * Daniel Huson, 4.2013
+ */
+public class Signer {
+    private PrivateKey privateKey;
+    private PublicKey publicKey;
+
+    /**
+     * constructor
+     */
+    public Signer() {
+    }
+
+    /**
+     * Construct a signer and generate a private key and public key
+     *
+     * @param seed
+     * @throws NoSuchProviderException
+     * @throws NoSuchAlgorithmException
+     */
+    public Signer(int seed) throws NoSuchProviderException, NoSuchAlgorithmException {
+        SecureRandom random = SecureRandom.getInstance("SHA1PRNG", "SUN");
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DSA", "SUN");
+        keyGen.initialize(1024, random);
+        //Note: The SecureRandom implementation attempts to completely randomize the internal state of the generator itself unless the caller
+        // follows the call to the getInstance method with a call to the setSeed method.
+        // So if you had a specific seed value that you wanted used, you would call the following prior to the initialize call:
+        random.setSeed(seed);
+
+        KeyPair pair = keyGen.generateKeyPair();
+        privateKey = pair.getPrivate();
+        publicKey = pair.getPublic();
+    }
+
+    /**
+     * load public key from a file
+     *
+     * @param fileName
+     * @return public key
+     * @throws IOException
+     * @throws InvalidKeySpecException
+     * @throws NoSuchProviderException
+     * @throws NoSuchAlgorithmException
+     */
+    public void loadPublicKey(String fileName) throws IOException, InvalidKeySpecException, NoSuchProviderException, NoSuchAlgorithmException {
+        InputStream fs = ResourceManager.getFileAsStream(fileName);
+        byte[] encKey = new byte[fs.available()];
+        int total = fs.available();
+        int count = 0;
+        while (count < total) {
+            count += fs.read(encKey, count, total - count);
+        }
+        fs.close();
+
+        X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encKey);
+        KeyFactory keyFactory = KeyFactory.getInstance("DSA", "SUN");
+        publicKey = keyFactory.generatePublic(pubKeySpec);
+    }
+
+    /**
+     * save the public key
+     *
+     * @param fileName
+     * @throws IOException
+     */
+    public void savePublicKey(String fileName) throws IOException {
+        if (publicKey == null)
+            throw new NoSuchElementException("publicKey");
+
+        byte[] bytes = publicKey.getEncoded();
+        FileOutputStream fs = new FileOutputStream(fileName);
+        fs.write(bytes);
+        fs.close();
+
+    }
+
+    /**
+     * save the public key
+     *
+     * @param fileName
+     * @throws IOException
+     */
+    public void savePrivateKey(String fileName) throws IOException {
+        if (privateKey == null)
+            throw new NoSuchElementException("privateKey");
+        byte[] bytes = privateKey.getEncoded();
+        FileOutputStream fs = new FileOutputStream(fileName);
+        fs.write(bytes);
+        fs.close();
+
+    }
+
+    /**
+     * generate a signature
+     *
+     * @param data
+     * @return signature
+     * @throws NoSuchProviderException
+     * @throws NoSuchAlgorithmException
+     * @throws InvalidKeyException
+     * @throws SignatureException
+     */
+    public byte[] generateSignature(String data) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+        return generateSignature(data.getBytes());
+    }
+
+    /**
+     * generate a signature
+     *
+     * @param data
+     * @return signature
+     * @throws NoSuchProviderException
+     * @throws NoSuchAlgorithmException
+     * @throws InvalidKeyException
+     * @throws SignatureException
+     */
+    public byte[] generateSignature(byte[] data) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+        if (privateKey == null)
+            throw new NoSuchElementException("privateKey");
+        Signature dsa = Signature.getInstance("SHA1withDSA", "SUN");
+        dsa.initSign(privateKey);
+        dsa.update(data);
+        return dsa.sign();
+    }
+
+    /**
+     * verify that signature matches data
+     *
+     * @param data
+     * @param signature
+     * @return true, if signature is valid for data
+     * @throws NoSuchProviderException
+     * @throws NoSuchAlgorithmException
+     * @throws InvalidKeySpecException
+     * @throws InvalidKeyException
+     * @throws SignatureException
+     */
+    public boolean verifySignedData(byte[] data, byte[] signature) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException {
+        if (publicKey == null)
+            throw new NoSuchElementException("publicKey");
+        Signature sig = Signature.getInstance("SHA1withDSA", "SUN");
+        sig.initVerify(publicKey);
+        sig.update(data);
+        return sig.verify(signature);
+    }
+
+    /**
+     * convert signature bytes to hex string
+     *
+     * @param signature
+     * @return hex string
+     */
+    public static String signatureBytesToHexString(byte[] signature) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : signature) {
+            sb.append(String.format("%02X", b));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * converts signature hex string to bytes
+     *
+     * @param signature
+     * @return bytes
+     */
+    public static byte[] signatureHexStringToBytes(String signature) {
+        byte[] buf = new byte[signature.length() / 2];
+        int i = 0;
+        for (char c : signature.toCharArray()) {
+            byte b = Byte.parseByte(String.valueOf(c), 16);
+            buf[i / 2] |= (b << (((i % 2) == 0) ? 4 : 0));
+            i++;
+        }
+
+        return buf;
+    }
+
+    /**
+     * get public key
+     *
+     * @return public key
+     */
+    public PublicKey getPublicKey() {
+        return publicKey;
+    }
+
+    /**
+     * set public key
+     *
+     * @param publicKey
+     */
+    public void setPublicKey(PublicKey publicKey) {
+        this.publicKey = publicKey;
+    }
+
+    /**
+     * get private key
+     *
+     * @return private key
+     */
+    public PrivateKey getPrivateKey() {
+        return privateKey;
+    }
+
+    /**
+     * set private key
+     *
+     * @param privateKey
+     */
+    public void setPrivateKey(PrivateKey privateKey) {
+        this.privateKey = privateKey;
+    }
+}
diff --git a/src/jloda/util/Single.java b/src/jloda/util/Single.java
new file mode 100644
index 0000000..e99033c
--- /dev/null
+++ b/src/jloda/util/Single.java
@@ -0,0 +1,107 @@
+/**
+ * Single.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.util.Comparator;
+
+/**
+ * a mutable object
+ *
+ * @author huson
+ *         Date: 14-May-2004
+ */
+public class Single<S> implements Comparable<Single<S>>, Comparator<Single<S>> {
+    S value;
+
+    public Single() {
+
+    }
+
+    public Single(S value) {
+        set(value);
+    }
+
+    public S get() {
+        return value;
+    }
+
+    public void set(S s) {
+        this.value = s;
+    }
+
+    public String toString() {
+        return value.toString();
+    }
+
+    public int hashCode() {
+        return value.hashCode();
+    }
+
+    public int compareTo(Single<S> p) {
+        int value = ((Comparable<S>) this.get()).compareTo(p.get());
+        if (value != 0)
+            return value;
+        else
+            return ((Comparable<S>) this.get()).compareTo(p.get());
+    }
+
+    public boolean equals(Object other) {
+        boolean good = false;
+        if (other instanceof Single) {
+            Single p = (Single) other;
+            if (value == null) {
+                good = (p.value == null);
+            } else {
+                good = value.equals(p.value);
+            }
+        }
+        return good;
+    }
+
+    /**
+     * Compare
+     * "Note: this comparator imposes orderings that are inconsistent with equals."
+     *
+     * @param p1 the first object to be compared.
+     * @param p2 the second object to be compared.
+     * @return a negative integer, zero, or a positive integer as the
+     *         first argument is less than, equal to, or greater than the
+     *         second.
+     * @throws ClassCastException if the arguments' types prevent them from
+     *                            being compared by this comparator.
+     */
+    public int compare(Single<S> p1, Single<S> p2) {
+        return p1.compareTo(p2);
+    }
+
+    /**
+     * clone this pair
+     *
+     * @return a shallow clone of this pair
+     */
+    public Object clone() {
+        try {
+            super.clone();
+        } catch (CloneNotSupportedException e) {
+            Basic.caught(e);
+        }
+        return new Single<>(get());
+    }
+}
diff --git a/src/jloda/util/State.java b/src/jloda/util/State.java
new file mode 100644
index 0000000..1a2a945
--- /dev/null
+++ b/src/jloda/util/State.java
@@ -0,0 +1,28 @@
+/**
+ * State.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+/**
+ * one method returning true or false
+ * Daniel Huson
+ */
+public interface State {
+    boolean get();
+}
diff --git a/src/jloda/util/Statistics.java b/src/jloda/util/Statistics.java
new file mode 100644
index 0000000..a7ffae2
--- /dev/null
+++ b/src/jloda/util/Statistics.java
@@ -0,0 +1,107 @@
+/**
+ * Statistics.java
+ * Copyright (C) 2016 Daniel H. Huson
+ * <p>
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package jloda.util;
+
+import java.util.Collection;
+
+/**
+ * calculates basic statistics
+ * Daniel Huson, 5.2006
+ */
+public class Statistics {
+    private double mean;
+    private final int count;
+    private double sum;
+    private double stdDev;
+    private double min = Double.MAX_VALUE;
+    private double max = Double.MIN_VALUE;
+
+    /**
+     * computes simple statistics for given collection of numbers
+     *
+     * @param data
+     */
+    public Statistics(Collection<? extends Number> data) {
+        count = data.size();
+        if (count > 0) {
+            for (Number number : data) {
+                double value = number.doubleValue();
+                sum += value;
+                if (value < min)
+                    min = value;
+                if (value > max)
+                    max = value;
+            }
+            mean = sum / count;
+            if (count > 1) {
+                double sum2 = 0;
+                for (Number number : data) {
+                    double value = number.doubleValue();
+                    sum2 += (value - mean) * (value - mean);
+                }
+                stdDev = Math.sqrt(sum2 / count);
+            }
+        }
+    }
+
+    /**
+     * gets string representation of stats
+     *
+     * @return string
+     */
+    public String toString() {
+        return "n=" + count + " mean=" + (float) mean + " stdDev=" + (float) stdDev + " min=" + (float) min + " max=" + (float) max;
+    }
+
+    public double getMean() {
+        return mean;
+    }
+
+    public int getCount() {
+        return count;
+    }
+
+    public double getSum() {
+        return sum;
+    }
+
+    public double getStdDev() {
+        return stdDev;
+    }
+
+    public double getMin() {
+        return min;
+    }
+
+    public double getMax() {
+        return max;
+    }
+
+    public double getNormalized(double value) {
+        if (stdDev > 0)
+            return (value - mean) / stdDev;
+        else
+            return value;
+    }
+
+    public double getZScore(double value) {
+        return (stdDev > 0 ? (value - mean) / stdDev : 0);
+    }
+}
diff --git a/src/jloda/util/StreamGobbler.java b/src/jloda/util/StreamGobbler.java
new file mode 100644
index 0000000..5a01d9e
--- /dev/null
+++ b/src/jloda/util/StreamGobbler.java
@@ -0,0 +1,75 @@
+/**
+ * StreamGobbler.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+/**
+ * Use this to monitor processes run with Runtime.exec()
+ * Date: 21-Nov-2004
+ */
+public class StreamGobbler extends Thread {
+    boolean stopped = false;
+    final InputStream inputStream;
+    final String prompt;
+
+    /**
+     * construct a gobbler
+     *
+     * @param inputStream input stream to monitor
+     * @param prompt      label to put in front of output, or null
+     */
+    public StreamGobbler(InputStream inputStream, String prompt) {
+        this.inputStream = inputStream;
+        this.prompt = prompt;
+    }
+
+    /**
+     * the run method
+     */
+    public void run() {
+        try {
+            InputStreamReader isr = new InputStreamReader(inputStream);
+            BufferedReader br = new BufferedReader(isr);
+            String line = null;
+            while ((line = br.readLine()) != null) {
+                if (prompt == null)
+                    System.err.println(line);
+                else
+                    System.err.println(prompt + "> " + line);
+                if (stopped)
+                    break;
+            }
+        } catch (IOException ex) {
+            Basic.caught(ex);
+        }
+    }
+
+    /**
+     * finish gobbling
+     */
+    public void finish() {
+        stopped = true;
+    }
+}
+
diff --git a/src/jloda/util/StringParser.java b/src/jloda/util/StringParser.java
new file mode 100644
index 0000000..e22bf10
--- /dev/null
+++ b/src/jloda/util/StringParser.java
@@ -0,0 +1,161 @@
+/**
+ * StringParser.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.awt.*;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.StringTokenizer;
+
+/**
+ * parses strings in format label=value
+ * Daniel Huson   , 1.2006
+ */
+public class StringParser implements Iterator {
+    String pushedBack = null;
+    final StringTokenizer strTok;
+
+    public StringParser(final String input) {
+        this.strTok = new StringTokenizer(input);
+    }
+
+    /**
+     * has another token
+     *
+     * @return true, if has another token
+     */
+    public boolean hasNext() {
+        return strTok.hasMoreTokens();
+    }
+
+    /**
+     * assumes the next entry is label=int
+     *
+     * @param label
+     * @return
+     * @throws IOException
+     */
+    public int getInt(final String label) throws IOException {
+        matchLabel(label);
+        try {
+            return (Integer.parseInt(nextToken()));
+        } catch (Exception ex) {
+            throw new IOException("Integer expected");
+        }
+    }
+
+    /**
+     * assumes the next entry is label=byte
+     *
+     * @param label
+     * @return
+     * @throws IOException
+     */
+    public byte getByte(final String label) throws IOException {
+        matchLabel(label);
+        try {
+            return (Byte.parseByte(nextToken()));
+        } catch (Exception ex) {
+            throw new IOException("Byte expected");
+        }
+    }
+
+    /**
+     * assumes the next entry is label=byte
+     *
+     * @param label
+     * @return
+     * @throws IOException
+     */
+    public double getDouble(final String label) throws IOException {
+        matchLabel(label);
+        try {
+            return (Double.parseDouble(nextToken()));
+        } catch (Exception ex) {
+            throw new IOException("Double expected");
+        }
+    }
+
+    /**
+     * assumes the next entry is label=color
+     *
+     * @param label
+     * @return
+     * @throws IOException
+     */
+    public Color getColor(final String label) throws IOException {
+        matchLabel(label);
+        try {
+            return Color.decode(nextToken());
+        } catch (Exception ex) {
+            throw new IOException("Color expected");
+        }
+    }
+
+    public String getString(final String label) throws IOException {
+        matchLabel(label);
+        try {
+            return nextToken();
+        } catch (Exception ex) {
+            throw new IOException("String expected");
+        }
+    }
+
+    public Object next() {
+        return nextToken();
+    }
+
+    /**
+     * gets the next token
+     *
+     * @return next token
+     */
+    public String nextToken() {
+        if (pushedBack != null) {
+            String result = pushedBack;
+            pushedBack = null;
+            return result;
+        } else
+            return strTok.nextToken();
+    }
+
+    public String peek() {
+        if (pushedBack != null)
+            return pushedBack;
+        else {
+            if (!strTok.hasMoreTokens())
+                return null;
+            pushedBack = strTok.nextToken();
+            return pushedBack;
+        }
+    }
+
+    private void matchLabel(final String label) throws IOException {
+        String str = nextToken();
+        if (!(str.equals(label + "=") || (str + nextToken()).equals(label + "=")))
+            throw new IOException("Expected '" + label + "=', got: " + str);
+    }
+
+    /**
+     * need this for the iterator interface
+     */
+    public void remove() {
+    }
+}
diff --git a/src/jloda/util/Task.java b/src/jloda/util/Task.java
new file mode 100644
index 0000000..3156bf1
--- /dev/null
+++ b/src/jloda/util/Task.java
@@ -0,0 +1,103 @@
+/**
+ * Task.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+/**
+ * tasks to be run in parallel on a pool of threads.
+ * Use this when you want to run one of the tasks submitted to a ScheduledThreadPoolExecutor
+ * in a different thread.
+ * Simply call the run method. It will only start running the given runnable if it is not already running.
+ * Daniel Huson, 7.2011
+ */
+public class Task implements Runnable {
+    public enum Status {
+        PENDING, RUNNING, DONE
+    }
+
+    private Status status = Status.PENDING;
+    private Runnable runnable;
+
+    /**
+     * construct a new task
+     */
+    public Task() {
+    }
+
+    /**
+     * set the runnable
+     *
+     * @param runnable
+     */
+    public void setRunnable(Runnable runnable) {
+        this.runnable = runnable;
+    }
+
+    /**
+     * try to run the task. It will only be executed, if it has status pending.
+     * If run, once completed, status is set to done.
+     */
+    public void run() {
+        if (setStatusRun() && runnable != null) {
+            try {
+                runnable.run();
+            } finally {
+                setStatusDone();
+            }
+        }
+    }
+
+    /**
+     * returns true, if this task has already been completed
+     *
+     * @return true, if done
+     */
+    public boolean isDone() {
+        return status == Status.DONE;
+    }
+
+
+    /**
+     * try to set the status to run
+     *
+     * @return true, if status was pending, else false
+     */
+    private boolean setStatusRun() {
+        synchronized (this) {
+            if (status != Status.PENDING)
+                return false;
+            status = Status.RUNNING;
+            return true;
+        }
+    }
+
+    /**
+     * try to set the status to done
+     *
+     * @return true, if status was running
+     */
+    private boolean setStatusDone() {
+        synchronized (this) {
+            if (status != Status.RUNNING)
+                return false;
+            status = Status.DONE;
+            return true;
+        }
+    }
+}
diff --git a/src/jloda/util/TemporaryFileSet.java b/src/jloda/util/TemporaryFileSet.java
new file mode 100644
index 0000000..0b6db29
--- /dev/null
+++ b/src/jloda/util/TemporaryFileSet.java
@@ -0,0 +1,70 @@
+/**
+ * TemporaryFileSet.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.File;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * provides a set of temporary files
+ * Daniel Huson, 6.2010
+ */
+public class TemporaryFileSet {
+    private final String dir = System.getProperty("user.home") + "/tmp";
+    private final long fileSetId = (new Date()).getTime();
+    private final Set<String> fileNames = new HashSet<>();
+
+
+    public TemporaryFileSet() {
+        File tmpDir = new File(dir);
+        if (!tmpDir.exists()) {
+            System.err.println("Creating temporary directory: " + dir);
+            tmpDir.mkdir();
+        }
+    }
+
+    /**
+     * returns the path name of a temporary file. Path names of different files sets are unique
+     *
+     * @param name
+     * @param suffix
+     * @return path name
+     */
+    public String getTemporaryFile(String name, String suffix) {
+        File file = new File(dir, fileSetId + name + suffix);
+        file.deleteOnExit();
+        fileNames.add(file.getPath());
+        System.err.println("Temp file: " + file.getPath());
+        return file.getPath();
+    }
+
+    /**
+     * delete all files in this file set
+     */
+    public void deleteAllFiles() {
+        for (String fileName : fileNames) {
+            File file = new File(fileName);
+            if (file.exists() && !file.delete())
+                System.err.println("Warning: failed to delete temporary file: " + file.getPath());
+        }
+    }
+}
diff --git a/src/jloda/util/TextFileFilter.java b/src/jloda/util/TextFileFilter.java
new file mode 100644
index 0000000..3be6871
--- /dev/null
+++ b/src/jloda/util/TextFileFilter.java
@@ -0,0 +1,61 @@
+/**
+ * TextFileFilter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.io.FilenameFilter;
+
+/**
+ * @author Daniel Huson
+ *         text file filter
+ *         12.03
+ */
+
+public class TextFileFilter extends FileFilterBase implements FilenameFilter {
+    public TextFileFilter() {
+        this(new String[0], false);
+    }
+
+    public TextFileFilter(String additionalSuffix) {
+        this(additionalSuffix, false);
+    }
+
+    public TextFileFilter(String[] additionalSuffixes) {
+        this(additionalSuffixes, false);
+    }
+
+    public TextFileFilter(String additionalSuffix, boolean allowGZip) {
+        this(new String[]{additionalSuffix}, allowGZip);
+    }
+
+    public TextFileFilter(String[] additionalSuffixes, boolean allowGZip) {
+        add("txt");
+        add("text");
+        for (String s : additionalSuffixes)
+            add(s);
+        setAllowGZipped(allowGZip);
+    }
+
+    /**
+     * @return description of file matching the filter
+     */
+    public String getBriefDescription() {
+        return "Text Files";
+    }
+}
diff --git a/src/jloda/util/TextPrinter.java b/src/jloda/util/TextPrinter.java
new file mode 100644
index 0000000..1bda842
--- /dev/null
+++ b/src/jloda/util/TextPrinter.java
@@ -0,0 +1,131 @@
+/**
+ * TextPrinter.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import java.awt.*;
+import java.awt.font.LineBreakMeasurer;
+import java.awt.font.TextAttribute;
+import java.awt.font.TextLayout;
+import java.awt.print.PageFormat;
+import java.awt.print.Printable;
+import java.awt.print.PrinterException;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+
+/**
+ * @author Daniel Huson, Michael Schr�der
+ * @version $Id: TextPrinter.java,v 1.1 2005-12-09 15:51:18 huson Exp $
+ */
+public class TextPrinter implements Printable {
+
+    // Constants for font name, size, style and line spacing
+    public static final float LINESPACEFACTOR = 1.1f;
+    Vector lines;        // The text to be printed, broken into lines
+    final Font font;           // The font to print with
+    float linespacing;   // How much space between lines
+    int linesPerPage;    // How many lines fit on a page
+    int numPages = 1;    // How many pages required to print all lines
+    int baseline = -1;   // The baseline position of the font
+    final String text;         // the text to be printed
+
+    /**
+     * Constructs a TextPrinter
+     *
+     * @param text the text to be printed
+     * @param font the font to print with
+     */
+    public TextPrinter(String text, Font font) {
+        this.text = text;
+        this.font = font;
+    }
+
+    public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
+
+        Graphics2D g = (Graphics2D) graphics;
+        g.setColor(Color.black);
+
+        if (baseline == -1) { // init printing
+            FontMetrics fm = g.getFontMetrics(font);
+            baseline = fm.getAscent();
+            linespacing = LINESPACEFACTOR * fm.getHeight();
+            linesPerPage = (int) Math.floor(pageFormat.getImageableHeight() / linespacing);
+            lines = new Vector();
+            BufferedReader buf = new BufferedReader(new StringReader(text));
+
+            float wrapWidth = (float) pageFormat.getImageableWidth();
+
+            Map textAttributes = new HashMap();
+            textAttributes.put(TextAttribute.FONT, font);
+            textAttributes.put(TextAttribute.SIZE, font.getSize2D());
+            textAttributes.put(TextAttribute.FOREGROUND, Color.black);
+
+            String line;
+            try {
+                while ((line = buf.readLine()) != null) {
+
+                    if (line.length() > 0) {
+                        AttributedString styledText = new AttributedString(line, textAttributes);
+                        AttributedCharacterIterator charIt = styledText.getIterator();
+                        LineBreakMeasurer measurer = new LineBreakMeasurer(charIt, g.getFontRenderContext());
+                        while (measurer.getPosition() < charIt.getEndIndex()) {
+                            TextLayout layout = measurer.nextLayout(wrapWidth);
+                            lines.add(layout);
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                Basic.caught(e);
+            }
+
+            numPages = lines.size() / linesPerPage;
+
+        } // end init printing
+
+        if (pageIndex > numPages) {
+            return NO_SUCH_PAGE;
+        } else {
+            int startLine = pageIndex * linesPerPage;
+            int endLine = startLine + linesPerPage - 1;
+            if (endLine >= lines.size())
+                endLine = lines.size() - 1;
+
+            float x0 = (float) pageFormat.getImageableX();
+            float y0 = (float) pageFormat.getImageableY() + baseline;
+
+            for (int i = startLine; i <= endLine; i++) {
+
+                TextLayout line = (TextLayout) lines.elementAt(i);
+                line.draw(g, x0, y0);
+
+                y0 += linespacing;
+            }
+
+            return PAGE_EXISTS;
+        }
+
+    }
+
+}
diff --git a/src/jloda/util/TextWindow.java b/src/jloda/util/TextWindow.java
new file mode 100644
index 0000000..14cb957
--- /dev/null
+++ b/src/jloda/util/TextWindow.java
@@ -0,0 +1,329 @@
+/**
+ * TextWindow.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import javax.swing.*;
+import javax.swing.text.DefaultEditorKit;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.Writer;
+
+/**
+ * a simple text window
+ *
+ * @author markus franz and daniel huson
+ *         Date: 23-Mar-2004
+ */
+public class TextWindow extends JFrame {
+    protected final JTextArea textArea = new JTextArea();
+    protected final JScrollPane sp = new JScrollPane(textArea);
+    protected boolean showing = true;
+    AbstractAction saveAction;
+    File lastSaveFile;
+    AbstractAction closeAction;
+    AbstractAction quitAction;
+    AbstractAction fontSizeAction;
+    private AbstractAction clear; // erase the document
+    // need an instance to get default textComponent Actions
+    private DefaultEditorKit kit;
+    private Action cut;
+    private Action copy;
+    private Action paste;
+    private Action selectAll;
+    // private static boolean macOS = System.getProperty("mrj.version") != null;
+
+    /**
+     * constructor
+     *
+     * @param name name of window
+     */
+    public TextWindow(String name) {
+        this(name, true);
+    }
+
+    /**
+     * constructor
+     *
+     * @param name name of window
+     */
+    public TextWindow(String name, boolean withMenubar) {
+        super(name);
+        setSize(600, 210);
+        textArea.setFont(new Font("Courier", Font.PLAIN, 12));
+
+
+        getContentPane().add(sp);
+        if (withMenubar)
+            addMenus();
+    }
+
+    /**
+     * gets the text area
+     *
+     * @return the text area
+     */
+    public JTextArea getTextArea() {
+        return textArea;
+    }
+
+    protected void addMenus() {
+        JMenuBar menuBar = new JMenuBar();
+
+        JMenu menu = new JMenu("File");
+        menu.setMnemonic('F');
+        menu.add(new JMenuItem(getSaveAction()));
+        menu.addSeparator();
+        menu.add(new JMenuItem(getCloseAction()));
+        menu.addSeparator();
+        menu.add(new JMenuItem(getQuitAction()));
+        menuBar.add(menu);
+
+        menu = new JMenu("Edit");
+        menu.setMnemonic('E');
+        menu.addSeparator();
+        JMenuItem item = new JMenuItem(getCutAction());
+        item.setText("Cut");
+        menu.add(item);
+        item = new JMenuItem(getCopyAction());
+        item.setText("Copy");
+        menu.add(item);
+        item = new JMenuItem(getPasteAction());
+        item.setText("Paste");
+        menu.add(item);
+        menu.addSeparator();
+        menu.add(new JMenuItem(getClearAction()));
+        menu.addSeparator();
+        item = new JMenuItem(getSelectAllAction());
+        item.setText("Select All");
+        menu.add(item);
+        menu.addSeparator();
+
+        menu.add(new JMenuItem(getFontSizeAction()));
+        menuBar.add(menu);
+
+        setJMenuBar(menuBar);
+    }
+
+    public AbstractAction getSaveAction() {
+        AbstractAction action = saveAction;
+        if (action != null)
+            return action;
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                JFileChooser chooser = new JFileChooser(lastSaveFile);
+                if (chooser.showSaveDialog(null)
+                        == JFileChooser.APPROVE_OPTION) {
+                    File file = chooser.getSelectedFile();
+
+                    if (file.exists() &&
+                            JOptionPane.showConfirmDialog(null,
+                                    "This file already exists. " +
+                                            "Would you like to overwrite the existing file?",
+                                    "Save File",
+                                    JOptionPane.YES_NO_OPTION) == 1)
+                        return; // overwrite canceled
+
+                    try {
+                        Writer w = new FileWriter(file);
+                        String text = textArea.getText();
+                        w.write(text);
+                        w.close();
+                    } catch (Exception ex) {
+                        System.err.println("Save failed: " + ex);
+                    }
+                    lastSaveFile = file;
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Save");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Save messages");
+        action.putValue(AbstractAction.MNEMONIC_KEY, new Integer('S'));
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke('S', InputEvent.CTRL_MASK));
+
+        return saveAction = action;
+    }
+
+    public AbstractAction getCloseAction() {
+        AbstractAction action = closeAction;
+        if (action != null)
+            return action;
+
+        final TextWindow me = this;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                me.setVisible(false);
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Close");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Close messages");
+        action.putValue(AbstractAction.MNEMONIC_KEY, new Integer('C'));
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke('W', InputEvent.CTRL_MASK));
+        return closeAction = action;
+    }
+
+    public AbstractAction getQuitAction() {
+        AbstractAction action = quitAction;
+        if (action != null)
+            return action;
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                System.exit(0);
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Quit");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Quit");
+        action.putValue(AbstractAction.MNEMONIC_KEY, new Integer('Q'));
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke('Q', InputEvent.CTRL_MASK));
+
+        return quitAction = action;
+    }
+
+    public AbstractAction getFontSizeAction() {
+        AbstractAction action = fontSizeAction;
+        if (action != null)
+            return action;
+        final TextWindow me = this;
+
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                Object[] possibleValues = {"6", "7", "8", "9", "10", "12", "14", "16", "24", "32"};
+                String def = "" + me.getFont().getSize();
+                Object selectedValue = JOptionPane.showInputDialog(null,
+                        "Font Size...", "Input",
+                        JOptionPane.INFORMATION_MESSAGE, null,
+                        possibleValues, def);
+                if (selectedValue != null && !selectedValue.equals(def)) {
+                    textArea.setFont(Font.decode("Monospaced-NORMAL-" + selectedValue));
+                    textArea.repaint();
+                }
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Font Size");
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Choose Font Size");
+        action.putValue(AbstractAction.MNEMONIC_KEY, new Integer('F'));
+        action.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke('F', InputEvent.CTRL_MASK));
+
+        return fontSizeAction = action;
+    }
+
+    public AbstractAction getClearAction() {
+        AbstractAction action = clear;
+        if (action != null) return action;
+
+        // Clear the document
+        action = new AbstractAction() {
+            public void actionPerformed(ActionEvent event) {
+                textArea.setText("");
+            }
+        };
+        action.putValue(AbstractAction.NAME, "Clear");
+        // erase.putValue(AbstractAction.SMALL_ICON, ResourceManager.getIcon("quit"));
+        action.putValue(AbstractAction.SHORT_DESCRIPTION, "Clear document");
+        return clear = action;
+    }
+
+    public Action getCutAction() {
+        Action action = cut;
+        if (action != null) return action;
+
+        if (kit == null) kit = new DefaultEditorKit();
+
+        Action[] defActions = kit.getActions();
+        for (Action defAction : defActions) {
+            if (defAction.getValue(Action.NAME) == DefaultEditorKit.cutAction) {
+                action = defAction;
+            }
+        }
+
+        if (action != null) {
+            action.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_X, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+            action.putValue(Action.SHORT_DESCRIPTION, "Cut");
+        }
+        return cut = action;
+    }
+
+    public Action getCopyAction() {
+        Action action = copy;
+        if (action != null) return action;
+
+        if (kit == null) kit = new DefaultEditorKit();
+
+        Action[] defActions = kit.getActions();
+        for (Action defAction : defActions) {
+
+            if ((defAction.getValue(Action.NAME)).equals(DefaultEditorKit.copyAction)) {
+                action = defAction;
+            }
+        }
+
+        if (action != null) {
+            action.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+            action.putValue(Action.SHORT_DESCRIPTION, "Copy");
+        }
+
+        return copy = action;
+    }
+
+    public Action getPasteAction() {
+        Action action = paste;
+        if (action != null) return action;
+
+        if (kit == null) kit = new DefaultEditorKit();
+
+        Action[] defActions = kit.getActions();
+        for (Action defAction : defActions) {
+            if (defAction.getValue(Action.NAME) == DefaultEditorKit.pasteAction) {
+                action = defAction;
+            }
+        }
+
+        if (action != null) {
+            action.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_V, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+            action.putValue(Action.SHORT_DESCRIPTION, "Paste");
+        }
+
+        return paste = action;
+    }
+
+    public Action getSelectAllAction() {
+        Action action = selectAll;
+        if (action != null) return action;
+
+        if (kit == null) kit = new DefaultEditorKit();
+
+        Action[] defActions = kit.getActions();
+        for (Action defAction : defActions) {
+            if (defAction.getValue(Action.NAME) == DefaultEditorKit.selectAllAction) {
+                action = defAction;
+            }
+        }
+
+        if (action != null) {
+            action.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_A, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
+            action.putValue(Action.SHORT_DESCRIPTION, "Select All");
+        }
+        return selectAll = action;
+    }
+}
diff --git a/src/jloda/util/TimeStamp.java b/src/jloda/util/TimeStamp.java
new file mode 100644
index 0000000..19d7f40
--- /dev/null
+++ b/src/jloda/util/TimeStamp.java
@@ -0,0 +1,39 @@
+/**
+ * TimeStamp.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/** A unique time stamp at every call
+ * @version $Id: TimeStamp.java,v 1.2 2006-06-06 18:56:04 huson Exp $
+ * @author Daniel Huson
+ * 5.03
+ */
+
+package jloda.util;
+
+public class TimeStamp {
+    private static long prevTimeStamp = 0;
+
+    /**
+     * Returns the next tick of the timestamp clock
+     *
+     * @return the next tick of the timestamp clock
+     */
+    public static long get() {
+        return ++prevTimeStamp;
+    }
+}
diff --git a/src/jloda/util/ToolTipHelper.java b/src/jloda/util/ToolTipHelper.java
new file mode 100644
index 0000000..a3cfe6d
--- /dev/null
+++ b/src/jloda/util/ToolTipHelper.java
@@ -0,0 +1,85 @@
+/**
+ * ToolTipHelper.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+ * use an additional thread to compute and set the tool tip of a component
+ * Daniel Huson, 5.2012
+ */
+public abstract class ToolTipHelper {
+    private final ExecutorService executorService = Executors.newFixedThreadPool(1);
+    private final JComponent component;
+    private Future future;
+
+    /**
+     * constructor
+     *
+     * @param component component to receive tooltip text
+     */
+    public ToolTipHelper(JComponent component) {
+        this.component = component;
+    }
+
+    /**
+     * override this with code for computing the tool tip text
+     *
+     * @return tool tip text
+     */
+    public abstract String computeToolTip(Point mousePosition);
+
+    /**
+     * call this whenever mouse has moved
+     *
+     * @param newMousePosition
+     */
+    public void mouseMoved(final Point newMousePosition) {
+        if (future != null) {
+            future.cancel(true);
+            future = null;
+        }
+        future = executorService.submit(new Runnable() {
+            public void run() {
+                try {
+                    final String toolTipText = computeToolTip(newMousePosition);
+                    SwingUtilities.invokeAndWait(new Runnable() {
+                        public void run() {
+                            component.setToolTipText(toolTipText);
+                        }
+                    });
+
+                } catch (Exception e) {
+                }
+            }
+        });
+    }
+
+    /**
+     * shut down this service
+     */
+    public void shutdownNow() {
+        executorService.shutdownNow();
+    }
+}
diff --git a/src/jloda/util/Triplet.java b/src/jloda/util/Triplet.java
new file mode 100644
index 0000000..23de798
--- /dev/null
+++ b/src/jloda/util/Triplet.java
@@ -0,0 +1,150 @@
+/**
+ * Triplet.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util;
+
+public class Triplet<T1, T2, T3> implements Comparable<Triplet<T1, T2, T3>> {
+    private T1 first;
+    private T2 second;
+    private T3 third;
+
+    public Triplet() {
+    }
+
+    public Triplet(T1 first, T2 second, T3 third) {
+        this.first = first;
+        this.second = second;
+        this.third = third;
+    }
+
+    public T1 getFirst() {
+        return first;
+    }
+
+    public T2 getSecond() {
+        return second;
+    }
+
+    public T3 getThird() {
+        return third;
+    }
+
+    public void setFirst(T1 first) {
+        this.first = first;
+    }
+
+    public void setSecond(T2 second) {
+        this.second = second;
+    }
+
+    public void setThird(T3 third) {
+        this.third = third;
+    }
+
+    @Override
+    public int hashCode() {
+        return first.hashCode() + second.hashCode() + third.hashCode();
+    }
+
+    public String toString() {
+        return first + ", " + second + ", " + third;
+    }
+
+    public int compareTo(Triplet<T1, T2, T3> p) {
+        int value = ((Comparable<T1>) this.getFirst()).compareTo(p.getFirst());
+        if (value != 0)
+            return value;
+        else
+            value = ((Comparable<T2>) this.getSecond()).compareTo(p.getSecond());
+        if (value != 0)
+            return value;
+        else
+            return ((Comparable<T3>) this.getThird()).compareTo(p.getThird());
+    }
+
+    public boolean equals(Object other) {
+        boolean good = false;
+        if (other instanceof Triplet) {
+            Triplet p = (Triplet) other;
+            if (first == null) {
+                good = (p.first == null);
+            } else {
+                good = first.equals(p.first);
+            }
+            if (good) {
+                if (second == null) {
+                    good = (p.second == null);
+                } else {
+                    good = second.equals(p.second);
+                }
+            }
+            if (good) {
+                if (third == null) {
+                    good = (p.third == null);
+                } else {
+                    good = third.equals(p.third);
+                }
+            }
+
+        }
+        return good;
+    }
+
+    /**
+     * Compare two Triplets
+     * "Note: this comparator imposes orderings that are inconsistent with equals."
+     *
+     * @param p1 the first object to be compared.
+     * @param p2 the second object to be compared.
+     * @return a negative integer, zero, or a positive integer as the
+     *         first argument is less than, equal to, or greater than the
+     *         second.
+     * @throws ClassCastException if the arguments' types prevent them from
+     *                            being compared by this comparator.
+     */
+    public int compare(Triplet<T1, T2, T3> p1, Triplet<T1, T2, T3> p2) {
+        return p1.compareTo(p2);
+    }
+
+    /**
+     * clone this Triplet
+     *
+     * @return a shallow clone of this Triplet
+     */
+    public Object clone() {
+        try {
+            super.clone();
+        } catch (CloneNotSupportedException e) {
+            Basic.caught(e);
+        }
+        return new Triplet<>(getFirst(), getSecond(), getThird());
+    }
+
+    public T1 get1() {
+        return first;
+    }
+
+    public T2 get2() {
+        return second;
+    }
+
+    public T3 get3() {
+        return third;
+    }
+}
diff --git a/src/jloda/util/UsageException.java b/src/jloda/util/UsageException.java
new file mode 100644
index 0000000..68faef4
--- /dev/null
+++ b/src/jloda/util/UsageException.java
@@ -0,0 +1,43 @@
+/**
+ * UsageException.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * @version $Id: UsageException.java,v 1.3 2006-06-06 18:56:04 huson Exp $
+ *
+ * Command line options usage exception
+ *
+ * @author Daniel Huson
+ */
+package jloda.util;
+
+/**
+ * Command line options usage exception
+ */
+public class UsageException extends Exception {
+    /**
+     * constructor of UsageException
+     *
+     * @param str String
+     */
+    public UsageException(String str) {
+        super(str + ", use option '-h' for help");
+    }
+}
+// EOF
+
diff --git a/src/jloda/util/lang/Language.java b/src/jloda/util/lang/Language.java
new file mode 100644
index 0000000..21fde54
--- /dev/null
+++ b/src/jloda/util/lang/Language.java
@@ -0,0 +1,98 @@
+/**
+ * Language.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util.lang;
+
+import jloda.util.Alert;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+/**
+ * base class for language support
+ * Daniel Huson, 1.2009
+ */
+public class Language {
+    protected final Map map;
+
+    /**
+     * constructor
+     */
+    public Language() {
+        map = new HashMap();
+        init();
+
+    }
+
+    /**
+     * gets the map
+     *
+     * @return map
+     */
+    public Map getMap() {
+        return map;
+    }
+
+    /**
+     * initializes the translation
+     */
+    protected void init() {
+    }
+
+    /**
+     * load translations from a file.
+     * Format: each line contains a pair of the form English:Translation
+     *
+     * @param file
+     * @throws java.io.IOException
+     */
+    public void load(File file) {
+        try {
+            System.err.println("Loading file: " + file);
+            BufferedReader reader = new BufferedReader(new FileReader(file));
+
+            String aLine;
+            while ((aLine = reader.readLine()) != null) {
+                if (aLine.length() > 0 && !aLine.startsWith("#")) {
+                    StringTokenizer st = new StringTokenizer(aLine, ":");
+                    if (st.countTokens() == 2) {
+                        map.put(st.nextToken(), st.nextToken());
+                    }
+                }
+            }
+            reader.close();
+        } catch (Exception e) {
+            new Alert("Warning: Language file not found: " + file);
+        }
+    }
+
+    /**
+     * set a pair of original and translated strings
+     *
+     * @param original
+     * @param translated
+     */
+    public void put(String original, String translated) {
+        map.put(original, translated);
+    }
+}
diff --git a/src/jloda/util/lang/Translator.java b/src/jloda/util/lang/Translator.java
new file mode 100644
index 0000000..378c19c
--- /dev/null
+++ b/src/jloda/util/lang/Translator.java
@@ -0,0 +1,148 @@
+/**
+ * Translator.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util.lang;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * translate all english text into another language
+ * Daniel Huson, 11.2008
+ */
+public class Translator {
+    private final Map map = new HashMap();
+    private static Translator instance;
+    private boolean doTranslation = false;
+
+    private File logFile;
+    private final Set unresolved = new HashSet();
+
+    /**
+     * get the one instance of the translator
+     *
+     * @return translator instance
+     */
+    public static Translator getInstance() {
+        if (instance == null)
+            instance = new Translator();
+        return instance;
+    }
+
+    /**
+     * load translation from a language object
+     *
+     * @param language
+     */
+    public void load(Language language) {
+        Map lmap = language.getMap();
+        for (Object obj : lmap.keySet()) {
+            if (lmap.get(obj) != null)
+                map.put(obj, lmap.get(obj));
+        }
+    }
+
+    /**
+     * get the translation for a string, if translation is on
+     *
+     * @param original
+     * @param log      log missing translations?
+     * @return translation
+     */
+    public String getTranslation(String original, boolean log) {
+        if (doTranslation) {
+            String translated = (String) map.get(original);
+            if (translated == null) {
+                if (!unresolved.contains(original) && log) {
+                    unresolved.add(original);
+                }
+                return original;
+            } else
+                return translated.trim();
+        } else
+            return original;
+    }
+
+    /**
+     * translate a string
+     *
+     * @param original
+     * @return translation
+     */
+    public static String get(String original) {
+        return getInstance().getTranslation(original, true);
+    }
+
+    /**
+     * translate a string
+     *
+     * @param original
+     * @param log      log missing translations?
+     * @return translation
+     */
+    public static String get(String original, boolean log) {
+        return getInstance().getTranslation(original, log);
+    }
+
+    /**
+     * is translation on?
+     *
+     * @return true, if translation on
+     */
+    public boolean isDoTranslation() {
+        return doTranslation;
+    }
+
+    /**
+     * do translation?
+     *
+     * @param doTranslation
+     */
+    public void setDoTranslation(boolean doTranslation) {
+        this.doTranslation = doTranslation;
+    }
+
+    /**
+     * dump all defined and undefined translations to a file
+     *
+     * @param file
+     * @throws IOException
+     */
+    public void dumpToFile(File file) throws IOException {
+        BufferedWriter w = new BufferedWriter(new FileWriter(file));
+
+        for (Object o : map.keySet()) {
+            String label = (String) o;
+            w.write("\tput(\"" + label + "\",\"" + map.get(label) + "\");\n");
+        }
+        w.write("\t// Unresolved:\n");
+        for (Object anUnresolved : unresolved) {
+            String label = (String) anUnresolved;
+            w.write("\tput(\"" + label + "\",null);\n");
+        }
+        w.flush();
+        w.close();
+    }
+}
diff --git a/src/jloda/util/parse/NexusStreamParser.java b/src/jloda/util/parse/NexusStreamParser.java
new file mode 100644
index 0000000..47225f9
--- /dev/null
+++ b/src/jloda/util/parse/NexusStreamParser.java
@@ -0,0 +1,1500 @@
+/**
+ * NexusStreamParser.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util.parse;
+
+/**
+ * @version $Id: NexusStreamParser.java,v 1.16 2010-05-31 04:27:41 huson Exp $
+ *
+ * @author Daniel Huson
+ *
+ */
+
+
+import jloda.util.Basic;
+import jloda.util.Colors;
+
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.*;
+import java.util.List;
+
+/**
+ * Parser for NexusBlock files
+ */
+public class NexusStreamParser extends NexusStreamTokenizer {
+    /**
+     * Construct a new NexusStreamParser object
+     *
+     * @param r the corresponding reader
+     */
+    public NexusStreamParser(Reader r) {
+        super(r);
+    }
+
+    /**
+     * Match the tokens in the string with the one obtained from the reader
+     *
+     * @param str string of tokens
+     */
+    public void matchIgnoreCase(String str) throws IOException {
+        NexusStreamTokenizer sst = new NexusStreamTokenizer(new StringReader(str));
+        sst.setSquareBracketsSurroundComments(isSquareBracketsSurroundComments());
+        while (sst.nextToken() != NexusStreamParser.TT_EOF) {
+            nextToken();
+            if (!toString().equalsIgnoreCase(sst.toString())) {
+                throw new IOException("Line " + lineno() + ": '" + sst.toString() + "' expected, got: '" + toString() + "'");
+            }
+        }
+    }
+
+    /**
+     * Match the tokens in the string with the one obtained from the reader
+     *
+     * @param str string of tokens
+     */
+    public void matchRespectCase(String str) throws IOException {
+        NexusStreamTokenizer sst = new NexusStreamTokenizer(new StringReader(str));
+        sst.setSquareBracketsSurroundComments(isSquareBracketsSurroundComments());
+
+        while (sst.nextToken() != NexusStreamParser.TT_EOF) {
+            nextToken();
+            if (!toString().equals(sst.toString())) {
+                throw new IOException("Line " + lineno() +
+                        ": '" + sst.toString() + "' expected, got: '" + toString() + "'");
+            }
+        }
+    }
+
+    /**
+     * Match the given word in the string with the next one obtained from the
+     * reader
+     *
+     * @param str string containing one word
+     */
+    public void matchWordIgnoreCase(String str) throws IOException {
+        nextToken();
+        if (!toString().equalsIgnoreCase(str)) {
+            throw new IOException("Line " + lineno() +
+                    ": '" + str + "' expected, got: '" + toString() + "'");
+        }
+    }
+
+    /**
+     * Match the given word in the string with the next one obtained from the
+     * reader
+     *
+     * @param str string containing one word
+     */
+    public void matchWordRespectCase(String str) throws IOException {
+        nextToken();
+        if (!toString().equals(str)) {
+            throw new IOException("Line " + lineno() +
+                    ": '" + str + "' expected, got: '" + toString() + "'");
+        }
+    }
+
+    /**
+     * match next token with 'begin NAME;' or 'beginblock NAME;'
+     *
+     * @param blockName name of block
+     * @throws IOException
+     */
+    public void matchBeginBlock(String blockName) throws IOException {
+        matchAnyTokenIgnoreCase("begin beginblock");
+        matchIgnoreCase(blockName + ";");
+    }
+
+    /**
+     * match next token with 'end;' or 'endblock;'
+     *
+     * @throws IOException
+     */
+    public void matchEndBlock() throws IOException {
+        matchAnyTokenIgnoreCase("end endblock");
+        matchRespectCase(";");
+    }
+
+    /**
+     * Match the given label in the string with the next one obtained from the
+     * reader
+     *
+     * @param str string containing one word
+     */
+    public void matchLabelIgnoreCase(String str) throws IOException {
+        pushPunctuationCharacters(LABEL_PUNCTUATION);
+        try {
+            nextToken();
+            if (!toString().equalsIgnoreCase(str)) {
+                throw new IOException("Line " + lineno() +
+                        ": '" + str + "' expected, got: '" + toString() + "'");
+            }
+        } finally {
+            popPunctuationCharacters();
+        }
+    }
+
+    /**
+     * Match the given label in the string with the next one obtained from the
+     * reader
+     *
+     * @param str string containing one word
+     */
+    public void matchLabelRespectCase(String str) throws IOException {
+        pushPunctuationCharacters(LABEL_PUNCTUATION);
+        try {
+            nextToken();
+            if (!toString().equals(str)) {
+                throw new IOException("Line " + lineno() +
+                        ": '" + str + "' expected, got: '" + toString() + "'");
+            }
+        } finally {
+            popPunctuationCharacters();
+        }
+    }
+
+    /**
+     * Compares the given string of tokens with the next tokens in the
+     * stream
+     *
+     * @param s string of tokens to be compared with the tokens in the input stream
+     * @return true, if all tokens match
+     */
+    public boolean peekMatchIgnoreCase(String s) {
+        NexusStreamTokenizer sst = new NexusStreamTokenizer(new StringReader(s));
+        sst.setSquareBracketsSurroundComments(isSquareBracketsSurroundComments());
+
+        LinkedList<Double> nvals = new LinkedList<>();
+        LinkedList<String> svals = new LinkedList<>();
+        LinkedList<Integer> ttypes = new LinkedList<>();
+        LinkedList<Integer> lines = new LinkedList<>();
+
+        svals.add(sval);
+        nvals.add(nval);
+        ttypes.add(ttype);
+        lines.add(lineno());
+
+        boolean flag = true;
+        try {
+            while (sst.nextToken() != NexusStreamParser.TT_EOF) {
+                nextToken();
+                svals.add(sval);
+                nvals.add(nval);
+                ttypes.add(ttype);
+                lines.add(lineno());
+
+                flag = toString().equalsIgnoreCase(sst.toString());
+                if (!flag)
+                    break;
+            }
+            pushBack(svals, nvals, ttypes, lines);
+            nextToken();
+        } catch (IOException ex) {
+            return false;
+        }
+        return flag;
+    }
+
+    /**
+     * Compares the given string of tokens with the next tokens in the
+     * stream
+     *
+     * @param s string of tokens to be compared with the tokens in the input stream
+     * @return true, if all tokens match
+     */
+    public boolean peekMatchRespectCase(String s) {
+        final NexusStreamTokenizer sst = new NexusStreamTokenizer(new StringReader(s));
+        sst.setSquareBracketsSurroundComments(isSquareBracketsSurroundComments());
+
+        final LinkedList<Double> nvals = new LinkedList<>();
+        final LinkedList<String> svals = new LinkedList<>();
+        final LinkedList<Integer> ttypes = new LinkedList<>();
+        final LinkedList<Integer> lines = new LinkedList<>();
+
+        svals.add(sval);
+        nvals.add(nval);
+        ttypes.add(ttype);
+        lines.add(lineno());
+
+        boolean flag = true;
+        try {
+            while (sst.nextToken() != NexusStreamParser.TT_EOF) {
+                nextToken();
+                svals.add(sval);
+                nvals.add(nval);
+                ttypes.add(ttype);
+                lines.add(lineno());
+                flag = toString().equals(sst.toString());
+                if (!flag)
+                    break;
+            }
+            pushBack(svals, nvals, ttypes, lines);
+            nextToken();
+        } catch (IOException ex) {
+            return false;
+        }
+        return flag;
+    }
+
+    /**
+     * peeks at the next word
+     *
+     * @return next word
+     */
+    public String peekNextWord() {
+        final LinkedList<Double> nvals = new LinkedList<>();
+        final LinkedList<String> svals = new LinkedList<>();
+        final LinkedList<Integer> ttypes = new LinkedList<>();
+        final LinkedList<Integer> lines = new LinkedList<>();
+
+        svals.add(sval);
+        nvals.add(nval);
+        ttypes.add(ttype);
+        lines.add(lineno());
+
+        String result = null;
+        try {
+            nextToken();
+            svals.add(sval);
+            nvals.add(nval);
+            ttypes.add(ttype);
+            lines.add(lineno());
+            result = toString();
+            pushBack(svals, nvals, ttypes, lines);
+            nextToken();
+        } catch (IOException ex) {
+        }
+        return result;
+    }
+
+
+    /**
+     * do the next tokens match 'begin NAME;'
+     *
+     * @param blockName
+     * @return true, if begin of named block
+     */
+    public boolean peekMatchBeginBlock(String blockName) {
+        return peekMatchIgnoreCase("begin " + blockName + ";")
+                || peekMatchIgnoreCase("Beginblock " + blockName + ";");
+    }
+
+    /**
+     * do the next tokens match 'END;'
+     *
+     * @return true, if end of block
+     */
+    public boolean peekMatchEndBlock() {
+        return peekMatchIgnoreCase("end;") || peekMatchIgnoreCase("endblock;");
+    }
+
+    /**
+     * Returns a list of strings containing all tokens between
+     * 'first' and 'last' so that the next call of nextToken will return
+     * the token after 'last'
+     *
+     * @param first the current token must match this, or null
+     * @param last  all tokens before this one are returned, or null, to read to the end of the stream
+     * @return a list of strings containing all tokens between 'first'
+     * and 'last'
+     */
+    public List<String> getTokensLowerCase(String first, String last) throws IOException {
+        if (first != null)
+            matchIgnoreCase(first);
+        final LinkedList<String> list = new LinkedList<>();
+        nextToken();
+        while (last == null || !toString().equals(last)) {
+            if (ttype == TT_EOF) {
+                if (last == null)
+                    break;
+                throw new IOException("Line " + lineno() + ": '" + last +
+                        "' expected, got EOF");
+            }
+            list.add(toString().toLowerCase());
+            nextToken();
+        }
+        return list;
+    }
+
+    /**
+     * Returns a list of strings containing all tokens between
+     * 'first' and 'last' so that the next call of nextToken will return
+     * the token after 'last'
+     *
+     * @param first the current token must match this, or null
+     * @param last  all tokens before this one are returned
+     * @return a list of strings containing all tokens between 'first'
+     * and 'last'
+     */
+    public List<String> getTokensRespectCase(String first, String last) throws IOException {
+        if (first != null)
+            matchIgnoreCase(first);
+        final LinkedList<String> list = new LinkedList<>();
+        nextToken();
+        while (last == null || !toString().equals(last)) {
+            if (ttype == TT_EOF) {
+                if (last == null)
+                    break;
+                throw new IOException("Line " + lineno() + ": '" + last +
+                        "' expected, got EOF");
+            }
+            list.add(toString());
+            nextToken();
+        }
+        return list;
+
+    }
+
+    /**
+     * Gets the next token as a word using ';' as punctuation character
+     *
+     * @return the next token as a word
+     */
+    public String getWordFileNamePunctuation() throws IOException {
+        pushPunctuationCharacters(SEMICOLON_PUNCTUATION);
+        try {
+            nextToken();
+        } finally {
+            popPunctuationCharacters();
+        }
+        return toString();
+    }
+
+
+    /**
+     * Gets the next token as an absolute file name. If word is relative file name, prepends current user.dir
+     *
+     * @return the next token as a word
+     */
+    public String getAbsoluteFileName() throws IOException {
+        String fileName = getWordFileNamePunctuation();
+        if (fileName != null && fileName.length() > 0) {
+            File file = new File(fileName);
+            if ((file.getParent() == null || file.getParent().length() == 0) && !file.getPath().startsWith("DB:")
+                    && !file.getPath().startsWith("WS:") && !file.getPath().startsWith("http") && !file.getPath().contains("::"))
+                fileName = (new File(System.getProperty("user.dir"), file.getName())).getPath();
+        }
+        return fileName;
+    }
+
+
+    /**
+     * Returns a string containing all tokens between
+     * 'first' and 'last' separated by blanks.
+     * The next call of nextToken will return
+     * the token after 'last'
+     *
+     * @param first the current token must match this
+     * @param last  all tokens before this one are returned
+     * @return a string consisting of all tokens found
+     * and 'last'
+     */
+    public String getTokensFileNamePunctuation(String first, String last) throws IOException {
+        pushPunctuationCharacters(SEMICOLON_PUNCTUATION);
+
+        String result = "";
+        try {
+            if (first != null)
+                matchIgnoreCase(first);
+            nextToken();
+            while (!toString().equals(last)) {
+                result += " " + toString();
+                if (ttype == TT_EOF)
+                    throw new IOException("Line " + lineno() + ": '" + last + "' expected, got EOF");
+                nextToken();
+            }
+
+        } finally {
+            popPunctuationCharacters();
+        }
+        if (result.equals(""))
+            return null;
+        return result;
+    }
+
+    /**
+     * Returns a string containing all tokens between
+     * 'first' and 'last' separated by blanks.
+     * The next call of nextToken will return
+     * the token after 'last'.
+     *
+     * @param first the current token must match this
+     * @param last  all tokens before this one are returned
+     * @return a string consisting of all tokens found
+     * and 'last', all in single quotes
+     */
+    public String getQuotedTokensRespectCase(String first, String last) throws IOException {
+        final StringBuilder buf = new StringBuilder();
+        if (first != null)
+            matchIgnoreCase(first);
+        nextToken();
+        while (!toString().equals(last)) {
+            buf.append("'").append(toString()).append("'");
+            if (ttype == TT_EOF)
+                throw new IOException("Line " + lineno() + ": '" + last + "' expected, got EOF");
+            nextToken();
+        }
+
+        if (buf.length() == 0)
+            return null;
+        return buf.toString();
+    }
+
+    /**
+     * Returns a string containing all tokens between
+     * 'first' and 'last' separated by blanks.
+     * The next call of nextToken will return
+     * the token after 'last'
+     *
+     * @param first the current token must match this
+     * @param last  all tokens before this one are returned
+     * @return a string consisting of all tokens found
+     * and 'last'
+     */
+    public String getTokensStringLowerCase(String first, String last) throws IOException {
+        matchIgnoreCase(first);
+        return getTokensStringLowerCase(last);
+    }
+
+    /**
+     * Returns a string containing all tokens before
+     * 'last' separated by blanks.
+     * The next call of nextToken will return
+     * the token after 'last'
+     *
+     * @param last all tokens before this one are returned
+     * @return a string consisting of all tokens found
+     * and 'last'
+     */
+    public String getTokensStringLowerCase(String last) throws IOException {
+        String result = "";
+        nextToken();
+        while (!toString().equalsIgnoreCase(last)) {
+            result += " " + toString().toLowerCase();
+            if (ttype == TT_EOF)
+                throw new IOException("Line " + lineno() + ": '" + last + "' expected, got EOF");
+            nextToken();
+        }
+        if (result.equals(""))
+            return null;
+        return result;
+    }
+
+
+    /**
+     * Returns a string containing all tokens before
+     * 'last' separated by blanks.
+     * The next call of nextToken will return
+     * the token after 'last'
+     *
+     * @param last all tokens before this one are returned
+     * @return a string consisting of all tokens found
+     * and 'last'
+     */
+    public String getTokensStringRespectCase(String last) throws IOException {
+        String result = "";
+        nextToken();
+        while (!toString().equals(last)) {
+            result += " " + toString();
+            if (ttype == TT_EOF)
+                throw new IOException("Line " + lineno() + ": '" + last +
+                        "' expected, got EOF");
+            nextToken();
+        }
+        if (result.equals(""))
+            return null;
+        return result.trim();
+    }
+
+    /**
+     * Searches for an occurrence of `token1 token2 token3 ...'
+     * in a list of tokens, returns value, if found and defaultValue, if not.
+     * If tokens are found, then they are removed from tokens
+     *
+     * @param tokens a list of tokens
+     * @param query  the list of tokens to be found in tokens
+     * @param value    the return value, if first the list of tokens is found
+     * @param defaultValue the value to be returned, if the list of tokens is
+     *               not found
+     * @return value
+     */
+    public boolean findIgnoreCase(List<String> tokens, String query, boolean value, boolean defaultValue) throws IOException {
+        if (tokens.size() == 0)
+            return defaultValue;
+
+        boolean result = defaultValue;
+        boolean found = false;
+        String str = List2String(tokens);
+        final NexusStreamParser s = new NexusStreamParser(new StringReader(str));
+        tokens.clear();
+
+        while (s.ttype != NexusStreamParser.TT_EOF) {
+            if (!found && s.peekMatchIgnoreCase(query)) {
+                result = value;
+                found = true;
+                s.matchIgnoreCase(query);
+            } else {
+                if (s.nextToken() != NexusStreamParser.TT_EOF)
+                    tokens.add(s.toString());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Searches for an occurrence of "token1 token2 etc [number]", ie a
+     * list of tokens followed by an optional number, returns -1 of only the
+     * tokens are found, 0, if the tokens are not found and n otherwise,
+     * where n is the number read
+     * in a list of tokens, returns value, if found and defaultValue, if not.
+     * If tokens and the number are found, then they are removed from tokens
+     *
+     * @param tokens a list of tokens
+     * @param query  the list of tokens to be found in tokens
+     * @param defaultValue the value to be returned, if the list of tokens is
+     *               not found
+     * @return value
+     */
+    public float findIgnoreCase(List<String> tokens, String query, float defaultValue) throws IOException {
+        if (tokens.size() == 0)
+            return defaultValue;
+
+        float result = defaultValue;
+        boolean found = false;
+        final NexusStreamParser s = new NexusStreamParser(new StringReader(List2String(tokens)));
+        tokens.clear();
+
+        while (s.ttype != NexusStreamParser.TT_EOF) {
+            if (!found && s.peekMatchIgnoreCase(query)) {
+                found = true;
+                s.matchIgnoreCase(query);
+
+                String str = s.getWordRespectCase();
+                try {
+                    result = Float.parseFloat(str);
+                } catch (NumberFormatException ex) {
+                    throw new IOException("Number expected, got: '" + str + "'");
+                }
+            } else // copy unused tokens back to token list
+            {
+                if (s.nextToken() != NexusStreamParser.TT_EOF)
+                    tokens.add(s.toString());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Searches for an occurrence of `token leftDelimiter value value ... rightDelimiter',
+     * where each value is a word.
+     * Returns a string containing all values, or
+     * a default value, if token does not occur
+     *
+     * @param tokens       the list of tokens
+     * @param token          the token to look for
+     * @param leftDelimiter  the left delimiter
+     * @param rightDelimiter the left delimiter
+     * @param defaultValue       the return value, if token not found
+     * @return the value
+     */
+    public String findIgnoreCase(List<String> tokens, String token, String leftDelimiter, String rightDelimiter, String defaultValue) throws IOException {
+        if (tokens.size() == 0)
+            return defaultValue;
+
+        boolean found = false;
+        // The following line seems to be a bug - have replaced it
+        //String result = defaultValue;
+        String result = "";
+
+        NexusStreamParser s =
+                new NexusStreamParser(new StringReader(List2String(tokens)));
+        tokens.clear();
+
+        while (s.ttype != NexusStreamParser.TT_EOF) {
+            if (!found && s.peekMatchIgnoreCase(token)) {
+                s.matchIgnoreCase(token + leftDelimiter);
+                while (true) {
+                    s.nextToken();
+                    String word = s.toString();
+                    found = true;
+
+                    if (word.equalsIgnoreCase(rightDelimiter))
+                        break;
+                    if (!result.equals(""))
+                        result += " ";
+                    result += word;
+                }
+            } else {
+                if (s.nextToken() != NexusStreamParser.TT_EOF)
+                    tokens.add(s.toString());
+            }
+        }
+        if (result.equals(""))
+            result = defaultValue;
+        return result;
+    }
+
+    /**
+     * Searches for an occurrence of `token value', where value is a
+     * string occuring in legalValues, returning value, if found, or
+     * a default value, if token does not occur
+     *
+     * @param tokens    the list of tokens
+     * @param token       the token to look for
+     * @param legalValues if not null, string containing all legal values of the token
+     * @param defaultValue    the return value, if token not found
+     * @return the value
+     */
+    public String findIgnoreCase(List<String> tokens, String token, String legalValues, String defaultValue) throws IOException {
+        if (tokens.size() == 0)
+            return defaultValue;
+
+        boolean found = false;
+        String result = defaultValue;
+        final NexusStreamParser s = new NexusStreamParser(new StringReader(List2String(tokens)));
+        tokens.clear();
+
+        while (s.ttype != NexusStreamParser.TT_EOF) {
+            if (!found && s.peekMatchIgnoreCase(token)) {
+                s.matchIgnoreCase(token);
+                s.nextToken();
+                result = s.toString();
+                found = true;
+                if (legalValues != null && !findIgnoreCase(legalValues, result))
+                    throw new IOException("Line " + lineno() + ": " + token + " '" + result + "': illegal value");
+            } else {
+                if (s.nextToken() != NexusStreamParser.TT_EOF)
+                    tokens.add(s.toString());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Searches for an occurrence of `token value', where value is a
+     * character occuring in legalValues, returning value, if found, or
+     * a default value, if token does not occur
+     *
+     * @param tokens    the list of tokens
+     * @param token       the token to look for
+     * @param legalValues if not null, string containing all legal values of the
+     *                  character
+     * @param defaultValue    the return value, if token not found
+     * @return the value
+     */
+    public char findIgnoreCase(List<String> tokens, String token, String legalValues, char defaultValue) throws IOException {
+        if (tokens.size() == 0)
+            return defaultValue;
+
+        boolean found = false;
+        char result = defaultValue;
+        final NexusStreamParser s = new NexusStreamParser(new StringReader(List2String(tokens)));
+        tokens.clear();
+
+        while (s.ttype != NexusStreamParser.TT_EOF) {
+            if (!found && s.peekMatchIgnoreCase(token)) {
+                s.matchIgnoreCase(token);
+                s.nextToken();
+                String str = s.toString();
+                if (str.length() > 1)
+                    throw new IOException
+                            ("Line " + lineno() + ": " + token + " '" + result + "': char expected");
+                result = str.charAt(0);
+                found = true;
+                if (legalValues != null && legalValues.indexOf((int) result) == -1)
+                    throw new IOException
+                            ("Line " + lineno() + ": " + token + " '" + result + "': illegal value");
+            } else {
+                if (s.nextToken() != NexusStreamParser.TT_EOF)
+                    tokens.add(s.toString());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Searches for an occurrence of a token=value, where value is a double
+     * between minValue and maxValue
+     *
+     * @param tokens   the list of tokens
+     * @param token      the token to look for
+     * @param minValue the minimal value
+     * @param maxValue the maximal value
+     * @param defaultValue   the return value, if token not found
+     * @return the value
+     */
+    public double findIgnoreCase(List<String> tokens, String token, double minValue, double maxValue, double defaultValue) throws IOException {
+        if (tokens.size() == 0)
+            return defaultValue;
+
+        boolean found = false;
+        double result = defaultValue;
+        final NexusStreamParser s = new NexusStreamParser(new StringReader(List2String(tokens)));
+        tokens.clear();
+
+        while (s.ttype != NexusStreamParser.TT_EOF) {
+            if (!found && s.peekMatchIgnoreCase(token)) {
+                s.matchIgnoreCase(token);
+                s.nextToken();
+                try {
+                    result = Double.parseDouble(s.sval);
+                } catch (Exception e) {
+                    throw new IOException
+                            ("Line " + lineno() + ": " + token + " '" + result + "': number expected");
+                }
+                if (result < minValue || result > maxValue)
+                    throw new IOException
+                            ("Line " + lineno() + ": " + token + " '" + result + "': out of range: "
+                                    + minValue + " - " + maxValue);
+                found = true;
+            } else {
+                if (s.nextToken() != NexusStreamParser.TT_EOF)
+                    tokens.add(s.toString());
+            }
+        }
+        return result;
+    }
+
+
+    /**
+     * Searches for an occurrence of a token=value, where value is a double
+     * between minValue and maxValue
+     *
+     * @param tokens   the list of tokens
+     * @param token      the token to look for
+     * @param minValue the minimal value
+     * @param maxValue the maximal value
+     * @param defaultValue   the return value, if token not found
+     * @return the value
+     */
+    public int findIgnoreCase(List<String> tokens, String token, int minValue, int maxValue, int defaultValue) throws IOException {
+        if (tokens.size() == 0)
+            return defaultValue;
+
+        boolean found = false;
+        int result = defaultValue;
+        final NexusStreamParser s = new NexusStreamParser(new StringReader(List2String(tokens)));
+        tokens.clear();
+
+        while (s.ttype != NexusStreamParser.TT_EOF) {
+            if (!found && s.peekMatchIgnoreCase(token)) {
+                s.matchIgnoreCase(token);
+                s.nextToken();
+                try {
+                    result = Integer.parseInt(s.sval);
+                } catch (Exception e) {
+                    throw new IOException
+                            ("Line " + lineno() + ": " + token + " '" + result + "': number expected");
+                }
+                if (result < minValue || result > maxValue)
+                    throw new IOException
+                            ("Line " + lineno() + ": " + token + " '" + result + "': out of range: "
+                                    + minValue + " - " + maxValue);
+                found = true;
+            } else {
+                if (s.nextToken() != NexusStreamParser.TT_EOF)
+                    tokens.add(s.toString());
+            }
+        }
+        return result;
+    }
+
+
+    /**
+     * Searches for an occurrence of a token value, where value is a color
+     *
+     * @param tokens the list of tokens
+     * @param token    the token to look for
+     * @param defaultValue the return value, if token not found
+     * @return the value
+     */
+    public Color findIgnoreCase(List<String> tokens, String token, Color defaultValue) throws IOException {
+        if (tokens.size() == 0)
+            return defaultValue;
+
+        boolean found = false;
+        Color result = defaultValue;
+        final NexusStreamParser s = new NexusStreamParser(new StringReader(List2String(tokens)));
+        tokens.clear();
+
+        while (s.ttype != NexusStreamParser.TT_EOF) {
+            if (!found && s.peekMatchIgnoreCase(token)) {
+                s.matchIgnoreCase(token);
+                try {
+                    result = s.getColor();
+                    found = true;
+                } catch (Exception e) {
+                    throw new IOException("Line " + lineno() + ": " + token + ": color or null expected");
+                }
+            } else {
+                if (s.nextToken() != NexusStreamParser.TT_EOF)
+                    tokens.add(s.toString());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Determines whether a given token occurs anywhere in a list of tokens
+     *
+     * @param tokens list of tokens
+     * @param token    the token to find
+     * @return true, if token contained in values
+     */
+    public boolean findIgnoreCase(List<String> tokens, String token) throws IOException {
+        if (tokens.size() == 0)
+            return false;
+
+        boolean result = false;
+        final NexusStreamParser s = new NexusStreamParser(new StringReader(List2String(tokens)));
+        tokens.clear();
+
+        while (s.ttype != NexusStreamParser.TT_EOF) {
+            if (s.peekMatchIgnoreCase(token)) {
+                result = true;
+                s.matchIgnoreCase(token);
+            } else {
+                if (s.nextToken() != NexusStreamParser.TT_EOF)
+                    tokens.add(s.toString());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Determines whether a given token occurs anywhere in a string
+     * containing tokens
+     *
+     * @param vals string of tokens
+     * @param token  the token to find
+     * @return true, if token contained in values
+     */
+    public boolean findIgnoreCase(String vals, String token) throws IOException {
+        final NexusStreamParser s = new NexusStreamParser(new StringReader(vals));
+        while (s.ttype != NexusStreamParser.TT_EOF) {
+            if (s.peekMatchIgnoreCase(token))
+                return true;
+            s.nextToken();
+        }
+        return false;
+    }
+
+    /**
+     * check that find has exhausted all tokens
+     *
+     * @param tokens
+     * @throws IOException
+     */
+    public void checkFindDone(List<String> tokens) throws IOException {
+        if (tokens.size() != 0)
+            throw new IOException("Line " + lineno() + ": unexpected tokens: " + tokens);
+    }
+
+    /**
+     * Get an integer from the reader
+     *
+     * @return integer read
+     */
+    public int getInt() throws IOException {
+        pushPunctuationCharacters(NEGATIVE_INTEGER_PUNCTUATION);
+        try {
+            nextToken();
+            nval = Integer.valueOf(sval);
+        } catch (Exception ex) {
+            popPunctuationCharacters();
+            throw new IOException("Line " + lineno() +
+                    ": INTEGER expected, got: '" + sval + "'");
+        }
+        popPunctuationCharacters();
+        return (int) nval;
+    }
+
+    /**
+     * Get an integer from the reader
+     *
+     * @param low  smallest legal value
+     * @param high highest legal value
+     * @return integer read
+     */
+    public int getInt(int low, int high) throws IOException {
+        int result = getInt();
+
+        if (result < low || result > high) {
+            if (low > Integer.MIN_VALUE && high == Integer.MAX_VALUE)
+                throw new IOException("Line " + lineno() +
+                        ":  value " + result + " smaller than minimum: " + low);
+            else if (low == Integer.MIN_VALUE && high < Integer.MAX_VALUE)
+                throw new IOException("Line " + lineno() +
+                        ":  value " + result + " larger than maximum: " + high);
+            else
+                throw new IOException("Line " + lineno() +
+                        ":  value " + result + " out of range: " + low + " - " + high);
+        }
+        return result;
+    }
+
+
+    /**
+     * Get a double from the reader
+     *
+     * @return double read
+     */
+    public double getDouble() throws IOException {
+        pushPunctuationCharacters(LABEL_PUNCTUATION);
+        try {
+            nextToken();
+            nval = Double.valueOf(sval);
+        } catch (Exception ex) {
+            popPunctuationCharacters();
+            throw new IOException("Line " + lineno() +
+                    ": DOUBLE expected, got: '" + sval + "'");
+        }
+        popPunctuationCharacters();
+        return nval;
+        /* The code below doesn't work when number contains E-4 etc: */
+        /*
+        setParseNumbers(true);
+        if(nextToken()!=TT_NUMBER)
+        {
+            setParseNumbers(false);
+            throw new IOException("Line "+lineno()+
+                ": DOUBLE expected, got: '"+toString()+"'");
+        }
+        setParseNumbers(false);
+        return nval;
+        */
+    }
+
+    /**
+     * Get an integer from the reader
+     *
+     * @param low  smallest legal value
+     * @param high highest legal value
+     * @return integer read
+     */
+    public double getDouble(double low, double high) throws IOException {
+        double result = getDouble();
+
+        if (result < low || result > high) {
+            if (low > Double.MIN_VALUE && high == Double.MAX_VALUE)
+                throw new IOException("Line " + lineno() +
+                        ":  value " + result + " smaller than minimum: " + low);
+            else if (low == Double.MIN_VALUE && high < Double.MAX_VALUE)
+                throw new IOException("Line " + lineno() +
+                        ":  value " + result + " larger than maximum: " + high);
+            else
+                throw new IOException("Line " + lineno() +
+                        ":  value " + result + " out of range: " + low + " - " + high);
+        }
+        return result;
+    }
+
+
+    /**
+     * Get a boolean from the reader
+     *
+     * @return boolean read
+     */
+    public boolean getBoolean() throws IOException {
+        pushPunctuationCharacters(LABEL_PUNCTUATION);
+        boolean value;
+        try {
+            nextToken();
+            if (sval.equalsIgnoreCase("true"))
+                value = true;
+            else if (sval.equalsIgnoreCase("false"))
+                value = false;
+            else
+                throw new IOException("Not a boolean: " + sval);
+        } catch (Exception ex) {
+            popPunctuationCharacters();
+            throw new IOException("Line " + lineno() + ": Boolean expected, got: '" + sval + "'");
+        }
+        popPunctuationCharacters();
+        return value;
+    }
+
+    /**
+     * Get a word from the reader
+     *
+     * @return word read
+     */
+    public String getWordRespectCase() throws IOException {
+        nextToken();
+        return toString();
+    }
+
+    /**
+     * gets a taxon or set label
+     *
+     * @return a label
+     */
+    public String getLabelRespectCase() throws IOException {
+        pushPunctuationCharacters(LABEL_PUNCTUATION);
+        String result;
+        try {
+            result = getWordRespectCase();
+        } finally {
+            popPunctuationCharacters();
+        }
+        return result;
+    }
+
+    /**
+     * Convert a list of tokens to a string
+     *
+     * @param tokens list of tokens
+     * @return string representation of list of tokens
+     */
+    static String List2String(List tokens) {
+        StringBuilder sb = new StringBuilder();
+
+        ListIterator it = tokens.listIterator();
+
+        boolean first = true;
+        while (it.hasNext()) {
+            if (first) {
+                sb.append("'").append(it.next()).append("'");
+                first = false;
+            } else
+                sb.append(" '").append(it.next()).append("'");
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Convert a abbreviated block into a full block.
+     * For example, convert "assume disttransform=NJ;"
+     * into "begin st_assumptions; distransform=NJ;end;"
+     * Uses only ';' as punctuation character
+     *
+     * @param firstSourceLabel the first source label such as "assume"
+     * @param lastSourceLabel  the last source label such as ";"
+     * @param blockName        the name of the block
+     * @return the full block
+     */
+    public String convertToBlock(String firstSourceLabel, String lastSourceLabel, String blockName) throws Exception {
+        pushPunctuationCharacters(SEMICOLON_PUNCTUATION);
+        String str = "begin " + blockName + ";";
+        try {
+            List<String> tokens = getTokensRespectCase(firstSourceLabel, lastSourceLabel);
+            for (String token : tokens) str += " " + token;
+
+            str += ";end;";
+        } finally {
+            popPunctuationCharacters();
+        }
+        return str;
+    }
+
+    /**
+     * Peeks at the next token and attempts to match it to any of the tokens
+     * present in str
+     *
+     * @param s a string of tokens
+     */
+    public boolean peekMatchAnyTokenIgnoreCase(String s) {
+        try {
+            final NexusStreamTokenizer sst = new NexusStreamTokenizer(new StringReader(s));
+            sst.setSquareBracketsSurroundComments(isSquareBracketsSurroundComments());
+
+            while (sst.nextToken() != NexusStreamParser.TT_EOF) {
+                if (peekMatchIgnoreCase(sst.toString()))
+                    return true;
+            }
+        } catch (IOException ex) {
+            jloda.util.Basic.caught(ex);
+        }
+        return false;
+    }
+
+    /**
+     * Peeks at the next token and attempts to match it to any of the tokens
+     * present in str
+     *
+     * @param s a string of tokens
+     */
+    public void matchAnyTokenIgnoreCase(String s) throws IOException {
+        try {
+            final NexusStreamTokenizer sst = new NexusStreamTokenizer(new StringReader(s));
+            sst.setSquareBracketsSurroundComments(isSquareBracketsSurroundComments());
+
+            while (sst.nextToken() != NexusStreamParser.TT_EOF) {
+                if (peekMatchIgnoreCase(sst.toString())) {
+                    matchIgnoreCase(sst.toString());
+                    return;
+                }
+            }
+        } catch (IOException ex) {
+            jloda.util.Basic.caught(ex);
+        }
+        throw new IOException("Line " + lineno() + ": any of '" + s.toLowerCase() + "' expected");
+    }
+
+
+    /**
+     * Peeks at the next token and attempts to match it to any of the tokens
+     * present in str
+     *
+     * @param s a string of tokens
+     */
+    public boolean peekMatchAnyTokenRespectCase(String s) {
+        try {
+            final NexusStreamTokenizer sst = new NexusStreamTokenizer(new StringReader(s));
+            sst.setSquareBracketsSurroundComments(isSquareBracketsSurroundComments());
+
+            while (sst.nextToken() != NexusStreamParser.TT_EOF) {
+                if (peekMatchRespectCase(sst.toString()))
+                    return true;
+            }
+        } catch (IOException ex) {
+            jloda.util.Basic.caught(ex);
+        }
+        return false;
+    }
+
+
+    /**
+     * returns all words between first and last using ';' as punctuation character
+     *
+     * @param first
+     * @param last
+     * @return all words between first and last token
+     */
+    public List<String> getWordsRespectCase(String first, String last) throws IOException {
+        pushPunctuationCharacters(SEMICOLON_PUNCTUATION);
+
+        LinkedList<String> list = new LinkedList<>();
+        try {
+            if (first != null)
+                matchIgnoreCase(first);
+            nextToken();
+            while (!toString().equals(last)) {
+                list.add(toString());
+                if (ttype == TT_EOF)
+                    throw new IOException("Line " + lineno() + ": '" + last +
+                            "' expected, got EOF");
+                nextToken();
+            }
+        } catch (IOException ex) {
+            popPunctuationCharacters();
+            throw ex;
+        }
+        popPunctuationCharacters();
+        return list;
+    }
+
+    /**
+     * returns the next n words
+     *
+     * @param n number of wordxs
+     * @return next n words
+     */
+    public List getWordsRespectCase(int n) throws IOException {
+        pushPunctuationCharacters(SEMICOLON_PUNCTUATION);
+        LinkedList<String> list = new LinkedList<>();
+
+        try {
+            for (int i = 0; i < n; i++)
+                list.add(getWordRespectCase());
+
+        } catch (IOException ex) {
+            popPunctuationCharacters();
+            throw ex;
+        }
+        popPunctuationCharacters();
+        return list;
+    }
+
+    /**
+     * returns the list of all positive integers found between first and last token.
+     * Integers are separated by commas. A range of positive integers is specified as i - j
+     *
+     * @param firstToken
+     * @param lastToken
+     * @return all integers
+     */
+    public List<Integer> getIntegerList(String firstToken, String lastToken) throws IOException {
+
+        List<String> tokens;
+        try {
+            pushPunctuationCharacters(STRICT_PUNCTUATION);
+            tokens = getTokensLowerCase(firstToken, lastToken);
+        } finally {
+            popPunctuationCharacters();
+        }
+
+        List<Integer> result = new LinkedList<>();
+        BitSet seen = new BitSet();
+
+        int inState = 0; // 0: expecting first number, 1: expecting new number or -2: expecting second number
+        int firstNumber = 0;
+        int secondNumber;
+        final Iterator<String> it = tokens.listIterator();
+        while (it.hasNext()) {
+            String label = it.next();
+
+            if (label.equalsIgnoreCase("none")) {
+                if (!it.hasNext())
+                    throw new IOException("line " + lineno() + ": unexcepted: " + label);
+                return result; // return empty list
+            }
+
+            switch (inState) {
+
+                case 1:      // expecting number or -
+                    if (label.equals("-")) {
+                        inState = 2;
+                        break; // end of case 1
+                    }
+                    if (!seen.get(firstNumber)) {
+                        result.add(firstNumber);
+                        seen.set(firstNumber);
+                    }
+                    // fall through to case 0:
+                case 0: // expecting first number
+                    try {
+                        firstNumber = Integer.parseInt(label);
+
+                    } catch (Exception ex) {
+                        throw new IOException
+                                ("line " + lineno() + ": number expected: " + label);
+                    }
+                    inState = 1;
+                    break;
+                case 2: // expecting second number
+                    try {
+                        secondNumber = Integer.parseInt(label);
+                    } catch (Exception ex) {
+                        throw new IOException
+                                ("line " + lineno() + ": number expected: " + label);
+                    }
+
+                    int imin = Math.min(firstNumber, secondNumber);
+                    int imax = Math.max(firstNumber, secondNumber);
+                    for (int i = imin; i <= imax; i++) {
+                        if (!seen.get(i)) {
+                            result.add(i);
+                            seen.set(i);
+                        }
+                    }
+                    inState = 0;
+                    break;
+                default:
+                    break;
+            }
+        }
+        switch (inState) {
+            case 1:
+                if (!seen.get(firstNumber)) {
+                    result.add(firstNumber);
+                    seen.set(firstNumber);
+                }
+                break;
+            case 2:
+                throw new IOException("line " + lineno() + ": second number expected");
+            default:
+                break;
+
+        }
+        return result;
+    }
+
+    /**
+     * get the line number mentioned in an exception or 0
+     *
+     * @param ex exception
+     * @return line number or 0
+     */
+    static public int getLineNumber(Exception ex) {
+        try {
+            final NexusStreamParser np = new NexusStreamParser(new StringReader(ex.toString()));
+            while (np.peekNextToken() != NexusStreamParser.TT_EOF
+                    && !np.peekMatchIgnoreCase("line"))
+                np.getWordRespectCase();
+            np.getWordRespectCase();
+            return np.getInt();
+        } catch (Exception ex2) {
+        }
+        return 0;
+    }
+
+    /**
+     * gets the legal token matched by next word in stream
+     *
+     * @param legalTokens
+     * @return matched token
+     * @throws IOException
+     */
+    public String getWordMatchesIgnoringCase(String legalTokens) throws IOException {
+        final String word = getWordRespectCase();
+        final NexusStreamParser np = new NexusStreamParser(new StringReader(legalTokens));
+        while (np.peekNextToken() != NexusStreamParser.TT_EOF) {
+            if (np.peekMatchIgnoreCase(word))
+                return np.getWordRespectCase();
+            else
+                np.getWordRespectCase();
+        }
+        throw new IOException("line " + lineno() + ": input '" + word + "' does not match any of legal tokens: " + legalTokens);
+    }
+
+    /**
+     * gets the legal token matched by next word in stream
+     *
+     * @param legalTokens
+     * @return matched token
+     * @throws IOException
+     */
+    public String getWordMatchesRespectingCase(String legalTokens) throws IOException {
+        final String word = getWordRespectCase();
+        final NexusStreamParser np = new NexusStreamParser(new StringReader(legalTokens));
+        while (np.peekNextToken() != NexusStreamParser.TT_EOF) {
+            if (np.peekMatchRespectCase(word))
+                return np.getWordRespectCase();
+            else
+                np.getWordRespectCase();
+        }
+        throw new IOException("line " + lineno() + ": input '" + word + "' does not match any of legal tokens: " + legalTokens);
+    }
+
+    /**
+     * gets the legal token matched by next word in stream
+     *
+     * @param legalTokens
+     * @return matched token
+     * @throws IOException
+     */
+    public String getWordMatchesRespectingCase(String[] legalTokens) throws IOException {
+        final String word = getWordRespectCase();
+        for (String legalToken : legalTokens)
+            if (word.equals(legalToken))
+                return legalToken;
+        throw new IOException("line " + lineno() + ": input '" + word + "' does not match any of legal tokens: " + legalTokens);
+
+    }
+
+    /**
+     * gets the legal token matched by next word in stream
+     *
+     * @param legalTokens
+     * @return matched token
+     * @throws IOException
+     */
+    public String getWordMatchesIgnoringCase(String[] legalTokens) throws IOException {
+        String word = getWordRespectCase();
+        for (String legalToken : legalTokens)
+            if (word.equalsIgnoreCase(legalToken))
+                return legalToken;
+        throw new IOException("line " + lineno() + ": input '" + word + "' does not match any of legal tokens: " + Basic.toString(legalTokens, ", "));
+    }
+
+    /**
+     * get a color, either from a name or from r g b
+     *
+     * @return color
+     * @throws IOException
+     */
+    public Color getColor() throws IOException {
+        try {
+            int r = 0, g = 0, b = 0, a = 0;
+            for (int i = 0; i < 4; i++) {
+                String word = getWordRespectCase();
+                switch (i) {
+                    case 0:
+                        if (word.equals("null"))
+                            return null;
+                        if (isHexInt(word)) {
+                            r = parseHexInt(word);
+                            return new Color(r);
+                        } else if (isInteger(word)) {
+                            r = Integer.parseInt(word);
+                        } else {
+                            return Colors.parseColor(word);
+                        }
+                        break;
+                    case 1:
+                        g = Integer.parseInt(word);
+                        break;
+                    case 2:
+                        b = Integer.parseInt(word);
+                        if (!isInteger(peekNextWord())) {
+                            return new Color(r, g, b);
+                        }
+                        break;
+                    case 3:
+                        a = Integer.parseInt(word);
+                        break;
+                }
+            }
+            return new Color(r, g, b, a);
+        } catch (Exception ex) {
+            throw new IOException("line " + lineno() + ": color expected, either X11-name or value (c, r g b, or r g b a)");
+        }
+    }
+
+    public static boolean isBoolean(String value) {
+        return value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false");
+    }
+
+    public static boolean isInteger(String value) {
+        try {
+            if (value.startsWith("0x"))
+                Integer.parseInt(value.substring(2), 16);
+            else
+                Integer.parseInt(value);
+            return true;
+        } catch (Exception ex) {
+            return false;
+        }
+    }
+
+    public static boolean isFloat(String value) {
+        try {
+            Float.parseFloat(value);
+            return true;
+        } catch (Exception ex) {
+            return false;
+        }
+    }
+
+    public static boolean isHexInt(String value) {
+        try {
+            if (value.startsWith("0x"))
+                Integer.parseInt(value.substring(2), 16);
+            else
+                return false;
+            return true;
+        } catch (Exception ex) {
+            return false;
+        }
+    }
+
+    public int parseHexInt(String value) {
+        if (value.startsWith("0x"))
+            return Integer.parseInt(value.substring(2), 16);
+        else
+            throw new NumberFormatException("Not hex: " + value);
+
+    }
+}
+
+// EOF
diff --git a/src/jloda/util/parse/NexusStreamTokenizer.java b/src/jloda/util/parse/NexusStreamTokenizer.java
new file mode 100644
index 0000000..a3d42a4
--- /dev/null
+++ b/src/jloda/util/parse/NexusStreamTokenizer.java
@@ -0,0 +1,447 @@
+/**
+ * NexusStreamTokenizer.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+/**
+ * tokenizer for nexus streams and similar input
+ *
+ * @author Daniel Huson, 2002
+ *
+ */
+
+package jloda.util.parse;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StreamTokenizer;
+import java.util.Collection;
+import java.util.EmptyStackException;
+import java.util.LinkedList;
+import java.util.Stack;
+
+/**
+ * Tokenizer for NexusBlock input stream
+ */
+public class NexusStreamTokenizer extends StreamTokenizer {
+    final public static String STRICT_PUNCTUATION = "(){}/\\,;:=*\"`+-<>";
+    final public static String NEGATIVE_INTEGER_PUNCTUATION = "(){}/\\,;:=*\"`+<>";
+    final public static String LABEL_PUNCTUATION = "(),;:=\"`{}";
+    final public static String ASSIGNMENT_PUNCTUATION = "=;";
+    final public static String SEMICOLON_PUNCTUATION = ";";
+    final public static String EOL_SPACE = "\f\n\r";
+    final public static String SPACE = " \f\n\r\t";
+    final public static String ILLEGAL_CHARS = "\f\n\r\t()[]{}/\\,;:=*'\"`<>";
+
+    private boolean parsenumbers = false;
+
+    private boolean squareBracketsSurroundComments = true;
+
+    private String punctchars = NEGATIVE_INTEGER_PUNCTUATION;
+    private final Stack<String> punctCharsStack = new Stack<>();
+
+    private String spaceChars = SPACE;
+    private final Stack<String> spaceCharsStack = new Stack<>();
+    private boolean eolsignificant = false;
+
+    public double nval = 0;
+    public String sval = "";
+    public int ttype = 0;
+    private int line = 0;
+
+    private boolean collectAllComments = false;
+    private String comment = null;
+
+    // we need these so that we can peek ahead as far as we like
+    private final LinkedList<Double> nvals = new LinkedList<>();
+    private final LinkedList<String> svals = new LinkedList<>();
+    private final LinkedList<Integer> ttypes = new LinkedList<>();
+    private final LinkedList<Integer> lines = new LinkedList<>();
+
+    /**
+     * Construct a new NexusBlock object for the specified reader
+     */
+    public NexusStreamTokenizer(Reader r) {
+        super(r);
+        setSyntax();
+    }
+
+
+    /**
+     * Get the next token and returns its type.
+     *
+     * @return the type of the token
+     */
+    public int nextToken() throws java.io.IOException {
+        int tt;
+
+        if (ttypes.size() > 0) {
+            if (nvals.getFirst() == null) {
+                nval = 0;
+                nvals.removeFirst();
+            } else
+                nval = nvals.removeFirst();
+            sval = svals.removeFirst();
+            ttype = ttypes.removeFirst();
+            line = lines.removeFirst();
+            return ttype;
+        } else {
+            tt = super.nextToken();
+            sval = super.sval;
+            nval = super.nval;
+            ttype = super.ttype;
+            line = super.lineno();
+        }
+        // The following lines skip comments of the form enclosed by [ and ]
+        // Comments enclosed by [! and ] are printed to standard err
+        if (squareBracketsSurroundComments) {
+            while (tt == (int) '[') // start of comment
+            {
+                int cline = lineno();
+                boolean verbose = false;
+
+                setCommentSyntax();
+                tt = super.nextToken();
+                sval = super.sval;
+
+// Set the comment String
+
+                if (sval != null) {
+                    if (collectAllComments && comment != null)
+                        comment += "\n" + (sval.startsWith("!") ? sval.substring(1) : sval);
+                    else
+                        comment = (sval.startsWith("!") ? sval.substring(1) : sval);
+                }
+                nval = super.nval;
+                ttype = super.ttype;
+                line = super.lineno();
+                if (ttype == TT_WORD && sval.charAt(0) == '!') {
+                    verbose = true;
+                    System.err.print("[");
+                }
+                while (tt != (int) ']') {
+                    if (tt == TT_EOF) {
+                        setSyntax();
+                        throw new java.io.IOException
+                                ("Line " + cline + ": start of unterminated comment");
+                    }
+                    if (verbose && ttype == TT_WORD)
+                        System.err.println(sval);
+                    tt = super.nextToken();
+                    sval = super.sval;
+                    if (sval != null) {
+                        if (comment == null)
+                            comment = (sval.startsWith("!") ? sval.substring(1) : sval);
+                        else
+                            comment += "\n" + (sval.startsWith("!") ? sval.substring(1) : sval);
+                    }
+                    nval = super.nval;
+                    ttype = super.ttype;
+                    line = super.lineno();
+                }
+                if (verbose)
+                    System.err.println("]");
+                setSyntax();
+                tt = super.nextToken();
+                sval = super.sval;
+                nval = super.nval;
+                ttype = super.ttype;
+                line = super.lineno();
+            }
+        }
+        return tt;
+    }
+
+    /**
+     * Gets all comments since last call of getComment
+     *
+     * @return comments
+     */
+    public String getComment() {
+        String result = comment;
+        comment = null;
+        return result;
+    }
+
+    /**
+     * Push the current token onto the token stream
+     */
+    public void pushBack() {
+        svals.add(0, sval);
+        nvals.add(0, nval);
+        ttypes.add(0, ttype);
+        lines.add(0, lineno());
+    }
+
+    /**
+     * Push the given token onto the token stream
+     *
+     * @param sval  the string value
+     * @param nval  the number value
+     * @param ttype the token type
+     * @param line  the line number
+     */
+    public void pushBack(String sval, double nval, int ttype, int line) {
+        svals.add(0, sval);
+        nvals.add(0, nval);
+        ttypes.add(0, ttype);
+        lines.add(0, line);
+    }
+
+    /**
+     * Push the given tokens onto the token stream
+     *
+     * @param svals  a collection of string values
+     * @param nvals  a collection of number values
+     * @param ttypes a collection of token types
+     * @param lines  a collection of line numbers
+     */
+    public void pushBack(Collection<String> svals, Collection<Double> nvals, Collection<Integer> ttypes, Collection<Integer> lines) {
+        this.svals.addAll(0, svals);
+        this.nvals.addAll(0, nvals);
+        this.ttypes.addAll(0, ttypes);
+        this.lines.addAll(0, lines);
+    }
+
+    /**
+     * Peeks at the next token
+     *
+     * @return ttype of next token
+     */
+    public int peekNextToken() throws IOException {
+        int tt = nextToken();
+        pushBack();
+        pushBack(); //TODO: I don't understand whats going on here! - David.
+        nextToken();
+        return tt;
+    }
+
+
+    /**
+     * Set the current punctuation characters
+     *
+     * @param s string of punctuation characters
+     */
+    public void setPunctuationCharacters(String s) {
+        punctchars = s;
+        setSyntax();
+    }
+
+    /**
+     * Get the current punctuation characters
+     *
+     * @return string of punctuation characters
+     */
+    public String getPunctuationCharacters() {
+        return punctchars;
+    }
+
+    /**
+     * Push the current punctuation characters
+     *
+     * @param s string of punctuation characters
+     */
+    public void pushPunctuationCharacters(String s) {
+        punctCharsStack.push(punctchars);
+        setPunctuationCharacters(s);
+    }
+
+    /**
+     * Pop the current punctuation characters
+     */
+    public void popPunctuationCharacters() throws EmptyStackException {
+        setPunctuationCharacters(punctCharsStack.pop());
+    }
+
+    /**
+     * Set the current space characters
+     *
+     * @param s string of space characters
+     */
+    public void setSpaceCharacters(String s) {
+        spaceChars = s;
+        setSyntax();
+    }
+
+    /**
+     * Push the current space characters
+     *
+     * @param s string of space characters
+     */
+    public void pushSpaceCharacters(String s) {
+        spaceCharsStack.push(spaceChars);
+        setSpaceCharacters(s);
+    }
+
+    /**
+     * Pop the current space characters
+     */
+    public void popSpaceCharacters() throws EmptyStackException {
+        setSpaceCharacters(spaceCharsStack.pop());
+    }
+
+
+    /**
+     * Parse numbers or not
+     *
+     * @param flag parse numbers or not
+     */
+    public void setParseNumbers(boolean flag) {
+        parsenumbers = flag;
+        setSyntax();
+    }
+
+    /**
+     * End-of-line is significant or not?
+     *
+     * @return boolean true if eoln returned as separate token
+     */
+    public boolean isEolSignificant() {
+        return eolsignificant;
+    }
+
+    /**
+     * End-of-line is significant or not?
+     *
+     * @param flag significant or not
+     */
+    public void setEolIsSignificant(boolean flag) {
+        eolsignificant = flag;
+        setSyntax();
+    }
+
+    /**
+     * Reset the syntax using current settings
+     */
+    public void setSyntax() {
+        resetSyntax();
+        if (parsenumbers)
+            parseNumbers();
+        eolIsSignificant(eolsignificant);
+        lowerCaseMode(false);
+        wordChars(33, 126);
+
+        for (int i = 0; i < punctchars.length(); i++)
+            ordinaryChar(punctchars.charAt(i));
+        ordinaryChar('['); // always need this to identify comments
+        ordinaryChar(']'); // always need this to identify comments
+        for (int i = 0; i < spaceChars.length(); i++)
+            whitespaceChars(spaceChars.charAt(i), spaceChars.charAt(i));
+        quoteChar('\'');
+    }
+
+    /**
+     * sets the syntax without "'" as quote character.
+     */
+    public void setSyntaxNoQuote() {
+        resetSyntax();
+        if (parsenumbers)
+            parseNumbers();
+        eolIsSignificant(eolsignificant);
+        lowerCaseMode(false);
+        wordChars(33, 126);
+        for (int i = 0; i < punctchars.length(); i++)
+            ordinaryChar(punctchars.charAt(i));
+        for (int i = 0; i < spaceChars.length(); i++)
+            whitespaceChars(spaceChars.charAt(i), spaceChars.charAt(i));
+    }
+
+    /**
+     * Sets the syntax so that all characters upto the end of the comment
+     * are returned as one token
+     */
+    void setCommentSyntax() {
+        resetSyntax();
+        wordChars(1, 126);
+        eolIsSignificant(true);
+        ordinaryChar(']');
+        whitespaceChars('\n', '\n');
+    }
+
+    /**
+     * Returns the current token as a string
+     *
+     * @return current token as a string
+     */
+    public String toString() {
+        if (ttype == TT_WORD || ttype == (int) '\'')
+            return sval;
+        else if (ttype == TT_NUMBER)
+            return String.valueOf(nval);
+        else
+            return "" + (char) ttype;
+    }
+
+    /**
+     * Returns the current line number
+     *
+     * @return current line number
+     */
+    public int lineno() {
+        return line;
+    }
+
+    /**
+     * Is given character a label punctuation character?
+     *
+     * @param ch a character
+     * @return true, if ch is contained in LABEL_PUNCTUATION
+     */
+    static public boolean isLabelPunctuation(char ch) {
+        return LABEL_PUNCTUATION.indexOf(ch) != -1;
+    }
+
+    /**
+     * Is given character a space character?
+     *
+     * @param ch a character
+     * @return true, if ch is contained in SPACE
+     */
+    static public boolean isSpace(char ch) {
+        return SPACE.indexOf(ch) != -1;
+    }
+
+    /**
+     * if set, getComment will return all comments encountered since last call of getComment, otherwise
+     * will only return last comment
+     *
+     * @return true, if all comments are to be collected
+     */
+    public boolean isCollectAllComments() {
+        return collectAllComments;
+    }
+
+    /**
+     * if set, getComment will return all comments encountered since last call of getComment, otherwise
+     * will only return last comment
+     *
+     * @param collectAllComments
+     */
+    public void setCollectAllComments(boolean collectAllComments) {
+        this.collectAllComments = collectAllComments;
+    }
+
+    public boolean isSquareBracketsSurroundComments() {
+        return squareBracketsSurroundComments;
+    }
+
+    public void setSquareBracketsSurroundComments(boolean squareBracketsSurroundComments) {
+        this.squareBracketsSurroundComments = squareBracketsSurroundComments;
+    }
+}
+
+// EOF
diff --git a/src/jloda/util/shapes/TaxaSetShape.java b/src/jloda/util/shapes/TaxaSetShape.java
new file mode 100644
index 0000000..2d6374e
--- /dev/null
+++ b/src/jloda/util/shapes/TaxaSetShape.java
@@ -0,0 +1,76 @@
+/**
+ * TaxaSetShape.java 
+ * Copyright (C) 2016 Daniel H. Huson
+ *
+ * (Some files contain contributions from other authors, who are then mentioned separately.)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package jloda.util.shapes;
+
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * Created by IntelliJ IDEA.
+ * User: kloepper
+ * Date: 18.02.2007
+ * Time: 21:12:56
+ * To change this template use File | Settings | File Templates.
+ */
+public class TaxaSetShape implements Shape {
+
+    public boolean contains(double x, double y) {
+        return false;  //To change body of implemented methods use File | Settings | File Templates.
+    }
+
+    public boolean contains(double x, double y, double w, double h) {
+        return false;  //To change body of implemented methods use File | Settings | File Templates.
+    }
+
+    public boolean intersects(double x, double y, double w, double h) {
+        return false;  //To change body of implemented methods use File | Settings | File Templates.
+    }
+
+    public Rectangle getBounds() {
+        return null;  //To change body of implemented methods use File | Settings | File Templates.
+    }
+
+    public boolean contains(Point2D p) {
+        return false;  //To change body of implemented methods use File | Settings | File Templates.
+    }
+
+    public Rectangle2D getBounds2D() {
+        return null;  //To change body of implemented methods use File | Settings | File Templates.
+    }
+
+    public boolean contains(Rectangle2D r) {
+        return false;  //To change body of implemented methods use File | Settings | File Templates.
+    }
+
+    public boolean intersects(Rectangle2D r) {
+        return false;  //To change body of implemented methods use File | Settings | File Templates.
+    }
+
+    public PathIterator getPathIterator(AffineTransform at) {
+        return null;  //To change body of implemented methods use File | Settings | File Templates.
+    }
+
+    public PathIterator getPathIterator(AffineTransform at, double flatness) {
+        return null;  //To change body of implemented methods use File | Settings | File Templates.
+    }
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/libjloda-java.git



More information about the debian-med-commit mailing list