[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