Merge branch 'alpha/JAL-3362_Jalview_212_alpha' into alpha/merge_212_JalviewJS_2112
authorJim Procter <jprocter@issues.jalview.org>
Mon, 18 May 2020 14:26:30 +0000 (15:26 +0100)
committerJim Procter <jprocter@issues.jalview.org>
Mon, 18 May 2020 14:26:30 +0000 (15:26 +0100)
resolved conflicts and adapted to some upstream changes. no detailed functional testing as yet (see next commit :) )

 Conflicts:
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/api/AlignViewportI.java
src/jalview/bin/Jalview.java
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceI.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/JvSwingUtils.java
src/jalview/gui/OverviewPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/SplitFrame.java
src/jalview/gui/WsJobParameters.java
src/jalview/io/AlignmentFileReaderI.java
src/jalview/io/FileLoader.java
src/jalview/io/StockholmFile.java
src/jalview/jbgui/GAlignFrame.java
src/jalview/jbgui/GPreferences.java
src/jalview/renderer/AnnotationRenderer.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/ws/jws2/Jws2Discoverer.java
test/jalview/analysis/AAFrequencyTest.java
test/jalview/gui/AlignFrameTest.java
test/jalview/gui/AlignViewportTest.java

75 files changed:
1  2 
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/analysis/AAFrequency.java
src/jalview/analysis/AlignmentSorter.java
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/Dna.java
src/jalview/api/AlignViewportI.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/AlignViewport.java
src/jalview/appletgui/AlignmentPanel.java
src/jalview/appletgui/AnnotationPanel.java
src/jalview/appletgui/OverviewPanel.java
src/jalview/appletgui/TitledPanel.java
src/jalview/bin/Jalview.java
src/jalview/datamodel/Alignment.java
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceGroup.java
src/jalview/datamodel/SequenceI.java
src/jalview/ext/ensembl/EnsemblFeatures.java
src/jalview/ext/ensembl/EnsemblInfo.java
src/jalview/ext/ensembl/EnsemblXref.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/CalculationChooser.java
src/jalview/gui/Desktop.java
src/jalview/gui/JvSwingUtils.java
src/jalview/gui/OverviewPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/RestServiceEditorPane.java
src/jalview/gui/SliderPanel.java
src/jalview/gui/SplitFrame.java
src/jalview/gui/WsJobParameters.java
src/jalview/gui/WsParamSetManager.java
src/jalview/hmmer/HmmerCommand.java
src/jalview/io/AlignFile.java
src/jalview/io/AlignmentFileWriterI.java
src/jalview/io/FileFormat.java
src/jalview/io/FileLoader.java
src/jalview/io/HMMFile.java
src/jalview/io/IdentifyFile.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/StockholmFile.java
src/jalview/io/packed/ParsePackedSet.java
src/jalview/jbgui/GAlignFrame.java
src/jalview/jbgui/GPreferences.java
src/jalview/jbgui/GUserDefinedColours.java
src/jalview/project/Jalview2XML.java
src/jalview/renderer/AnnotationRenderer.java
src/jalview/schemes/ResidueProperties.java
src/jalview/util/Platform.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/workers/ConsensusThread.java
src/jalview/ws/dbsources/EmblXmlSource.java
src/jalview/ws/dbsources/Uniprot.java
src/jalview/ws/jws2/JabaParamStore.java
src/jalview/ws/jws2/Jws2Discoverer.java
src/jalview/ws/jws2/MsaWSClient.java
src/jalview/ws/jws2/SeqAnnotationServiceCalcWorker.java
src/org/json/JSONObject.java
test/jalview/analysis/AAFrequencyTest.java
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/analysis/CrossRefTest.java
test/jalview/datamodel/AlignmentTest.java
test/jalview/datamodel/SequenceTest.java
test/jalview/fts/service/pdb/PDBFTSPanelTest.java
test/jalview/gui/AlignFrameTest.java
test/jalview/gui/AlignViewportTest.java
test/jalview/gui/PopupMenuTest.java
test/jalview/gui/StructureChooserTest.java
test/jalview/io/FileFormatsTest.java
test/jalview/io/StockholmFileTest.java
test/jalview/project/Jalview2xmlTests.java
test/jalview/schemes/PIDColourSchemeTest.java

@@@ -1334,13 -1352,79 +1346,86 @@@ label.most_bound_molecules = Most Boun
  label.most_polymer_residues = Most Polymer Residues
  label.cached_structures = Cached Structures
  label.free_text_search = Free Text Search
 +label.annotation_name = Annotation Name
 +label.annotation_description = Annotation Description 
 +label.edit_annotation_name_description = Edit Annotation Name/Description
 +label.alignment = alignment
 +label.pca = PCA
 +label.create_image_of = Create {0} image of {1}
 +label.click_to_edit = Click to edit, right-click for menu
+ label.hmmalign = hmmalign
+ label.use_hmm = HMM profile to use
+ label.use_sequence = Sequence to use
+ label.hmmbuild = hmmbuild
+ label.hmmsearch = hmmsearch
+ label.jackhmmer = jackhmmer
+ label.installation = Installation
+ label.hmmer_location = HMMER Binaries Installation Location
+ label.cygwin_location = Cygwin Binaries Installation Location (Windows)
+ label.information_annotation = Information Annotation
+ label.ignore_below_background_frequency = Ignore Below Background Frequency
+ label.information_description = Information content, measured in bits
+ warn.no_hmm = No Hidden Markov model found.\nRun hmmbuild or load an HMM file first.
+ label.no_sequences_found = No matching sequences, or an error occurred.
+ label.hmmer = HMMER
+ label.trim_termini = Trim Non-Matching Termini
+ label.trim_termini_desc = If true, non-matching regions on either end of the resulting alignment are removed.
+ label.no_of_sequences = Number of sequences returned
+ label.reporting_cutoff = Reporting Cut-off
+ label.inclusion_threshold = Inlcusion Threshold
+ label.freq_alignment = Use alignment background frequencies
+ label.freq_uniprot = Use Uniprot background frequencies
+ label.hmmalign_options = hmmalign options
+ label.hmmsearch_options = hmmsearch options
+ label.jackhmmer_options = jackhmmer options
+ label.executable_not_found = The ''{0}'' executable file was not found
+ warn.command_failed = {0} failed
+ label.invalid_folder = Invalid Folder
+ label.number_of_results = Number of Results to Return
+ label.number_of_iterations = Number of jackhmmer Iterations
+ label.auto_align_seqs = Automatically Align Fetched Sequences
+ label.new_returned = new sequences returned
+ label.use_accessions = Return Accessions
+ label.check_for_new_sequences = Return Number of New Sequences
+ label.evalue = E-Value
+ label.reporting_seq_evalue = Reporting Sequence E-value Cut-off
+ label.reporting_seq_score = Reporting Sequence Score Threshold
+ label.reporting_dom_evalue = Reporting Domain E-value Cut-off
+ label.reporting_dom_score = Reporting Domain Score Threshold
+ label.inclusion_seq_evalue = Inclusion Sequence E-value Cut-off
+ label.inclusion_seq_score = Inclusion Sequence Score Threshold
+ label.inclusion_dom_evalue = Inclusion Domain E-value Cut-off
+ label.inclusion_dom_score = Inclusion Domain Score Threshold
+ label.number_of_results_desc = The maximum number of hmmsearch results to display
+ label.number_of_iterations_desc = The number of iterations jackhmmer will complete when searching for new sequences
+ label.auto_align_seqs_desc = If true, all fetched sequences will be aligned to the hidden Markov model with which the search was performed
+ label.check_for_new_sequences_desc = Display number of new sequences returned from hmmsearch compared to the previous alignment 
+ label.use_accessions_desc = If true, the accession number of each sequence is returned, rather than that sequence's name
+ label.reporting_seq_e_value_desc = The E-value cutoff for returned sequences 
+ label.reporting_seq_score_desc = The score threshold for returned sequences 
+ label.reporting_dom_e_value_desc = The E-value cutoff for returned domains 
+ label.reporting_dom_score_desc = The score threshold for returned domains 
+ label.inclusion_seq_e_value_desc = Sequences with an E-value less than this cut-off are classed as significant
+ label.inclusion_seq_score_desc = Sequences with a bit score greater than this threshold are classed as significant
+ label.inclusion_dom_e_value_desc = Domains with an E-value less than this cut-off are classed as significant
+ label.inclusion_dom_score_desc = Domains with a bit score greater than this threshold are classed as significant
+ label.add_database = Add Database
+ label.this_alignment = This alignment
+ warn.invalid_format = This is not a valid database file format. The current supported formats are Fasta, Stockholm and Pfam.
+ label.database_for_hmmsearch = The database hmmsearch will search through
+ label.use_reference = Use Reference Annotation
+ label.use_reference_desc = If true, hmmbuild will keep all columns defined as a reference position by the reference annotation
+ label.hmm_name = Alignment HMM Name
+ label.hmm_name_desc = The name given to the HMM for the alignment
+ warn.no_reference_annotation = No reference annotation found
+ label.hmmbuild_for = Build HMM for
+ label.hmmbuild_for_desc = Build an HMM for the selected sets of sequences
+ label.alignment = Alignment
+ label.groups_and_alignment = All groups and alignment
+ label.groups = All groups
+ label.selected_group = Selected group
+ label.use_info_for_height = Use Information Content as Letter Height
+ action.search = Search
  label.backupfiles_confirm_delete = Confirm delete
  label.backupfiles_confirm_delete_old_files = Delete the following older backup files? (see the Backups tab in Preferences for more options)
  label.backupfiles_confirm_save_file = Confirm save file
@@@ -1330,13 -1334,7 +1329,14 @@@ label.most_bound_molecules = Más Molécu
  label.most_polymer_residues = Más Residuos de Polímeros
  label.cached_structures = Estructuras en Caché
  label.free_text_search = Búsqueda de texto libre
 +label.annotation_name = Nombre de la anotación
 +label.annotation_description = Descripción de la anotación 
 +label.edit_annotation_name_description = Editar el nombre/descripción de la anotación
 +label.alignment = alineamiento
 +label.pca = ACP
 +label.create_image_of = Crear imagen {0} de {1}
 +label.click_to_edit = Haga clic para editar, clic en el botón derecho para ver el menú  
+ action.search = Buscar
  label.backupfiles_confirm_delete = Confirmar borrar
  label.backupfiles_confirm_delete_old_files = Â¿Borrar los siguientes archivos? (ver la pestaña 'Copias' de la ventana de Preferencias para más opciones)
  label.backupfiles_confirm_save_file = Confirmar guardar archivo
Simple merge
@@@ -28,7 -28,7 +28,6 @@@ import jalview.datamodel.AlignmentAnnot
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.Annotation;
  import jalview.datamodel.DBRefEntry;
--import jalview.datamodel.DBRefSource;
  import jalview.datamodel.FeatureProperties;
  import jalview.datamodel.GraphLine;
  import jalview.datamodel.Mapping;
@@@ -499,19 -505,24 +507,33 @@@ public interface AlignViewportI extend
    @Override
    void setProteinFontAsCdna(boolean b);
  
-   TreeModel getCurrentTree();
+   void setHmmProfiles(ProfilesI info);
  
-   void setCurrentTree(TreeModel tree);
+   ProfilesI getHmmProfiles();
+   /**
+    * Registers and starts a worker thread to calculate Information Content
+    * annotation, if it is not already registered
+    * 
+    * @param ap
+    */
+   void initInformationWorker(AlignmentViewPanel ap);
+   boolean isInfoLetterHeight();
 -  abstract TreeModel getCurrentTree();
++  public abstract TreeModel getCurrentTree();
  
 -  abstract void setCurrentTree(TreeModel tree);
 +  /**
 +   * Answers a data bean containing data for export as configured by the
 +   * supplied options
 +   * 
 +   * @param options
 +   * @return
 +   */
 +  AlignmentExportData getAlignExportData(AlignExportSettingsI options);
 +
++  public abstract void setCurrentTree(TreeModel tree);
    /**
     * @param update
     *          - set the flag for updating structures on next repaint
Simple merge
@@@ -52,8 -52,16 +52,14 @@@ public class AlignViewport extends Alig
  
    public jalview.bin.JalviewLite applet;
  
 -  boolean MAC = false;
 -
    private AnnotationColumnChooser annotationColumnSelectionState;
  
+   java.awt.Frame nullFrame;
+   protected FeatureSettings featureSettings = null;
+   private float heightScale = 1, widthScale = 1;
    public AlignViewport(AlignmentI al, JalviewLite applet)
    {
      super(al);
@@@ -28,7 -28,7 +28,6 @@@ import jalview.renderer.AwtRenderPanelI
  import jalview.schemes.ResidueProperties;
  import jalview.util.Comparison;
  import jalview.util.MessageManager;
--import jalview.util.Platform;
  import jalview.viewmodel.ViewportListenerI;
  import jalview.viewmodel.ViewportRanges;
  
   */
  package jalview.appletgui;
  
--import java.awt.Frame;
  import java.awt.Graphics;
  import java.awt.Insets;
--import java.awt.Label;
  import java.awt.Panel;
--import java.awt.event.WindowAdapter;
--import java.awt.event.WindowEvent;
++
  
  public class TitledPanel extends Panel
  {
@@@ -104,30 -97,22 +104,30 @@@ public class Jalvie
  
    static
    {
 -    // grab all the rights we can the JVM
 -    Policy.setPolicy(new Policy()
 +    if (!Platform.isJS())
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
      {
 -      @Override
 -      public PermissionCollection getPermissions(CodeSource codesource)
 -      {
 -        Permissions perms = new Permissions();
 -        perms.add(new AllPermission());
 -        return (perms);
 -      }
 -    
 -      @Override
 -      public void refresh()
 +      // grab all the rights we can for the JVM
-           Policy.setPolicy(new Policy()
-           {
-             @Override
-             public PermissionCollection getPermissions(CodeSource codesource)
-             {
-               Permissions perms = new Permissions();
-               perms.add(new AllPermission());
-               return (perms);
-             }
-       
-             @Override
-             public void refresh()
-             {
-             }
-           });
++      Policy.setPolicy(new Policy()
+       {
 -      }
 -    });
++        @Override
++        public PermissionCollection getPermissions(CodeSource codesource)
++        {
++          Permissions perms = new Permissions();
++          perms.add(new AllPermission());
++          return (perms);
++        }
++  
++        @Override
++        public void refresh()
++        {
++        }
++      });
 +    }
    }
  
    /**
     */
    public static void main(String[] args)
    {
- //    setLogging(); // BH - for event debugging in JavaScript
++//  setLogging(); // BH - for event debugging in JavaScript
      instance = new Jalview();
      instance.doMain(args);
 +}
 +
 +  private static void logClass(String name) 
-   {   
-         // BH - for event debugging in JavaScript
++  {  
++    // BH - for event debugging in JavaScript
 +      ConsoleHandler consoleHandler = new ConsoleHandler();
 +      consoleHandler.setLevel(Level.ALL);
 +      Logger logger = Logger.getLogger(name);
 +      logger.setLevel(Level.ALL);
 +      logger.addHandler(consoleHandler);
    }
  
 +  @SuppressWarnings("unused")
 +  private static void setLogging() 
 +  {
 +
 +    /**
 +     * @j2sIgnore
 +     * 
 +     */
 +    {
 +      System.out.println("not in js");
 +    }
 +
-         // BH - for event debugging in JavaScript (Java mode only)
++    // BH - for event debugging in JavaScript (Java mode only)
 +    if (!Platform.isJS())
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
-       {
-               Logger.getLogger("").setLevel(Level.ALL);
++    {
++      Logger.getLogger("").setLevel(Level.ALL);
 +        logClass("java.awt.EventDispatchThread");
 +        logClass("java.awt.EventQueue");
 +        logClass("java.awt.Component");
 +        logClass("java.awt.focus.Component");
 +        logClass("java.awt.focus.DefaultKeyboardFocusManager"); 
-       }       
++    }  
 +
 +  }
 +  
 +
 +  
 +
    /**
     * @param args
     */
Simple merge
@@@ -72,26 -54,23 +73,30 @@@ public class Sequence extends ASequenc
  
    private char[] sequence;
  
 -  String description;
 +  private String description;
 +
 +  private int start;
 +
 +  private int end;
  
 -  int start;
 +  private Vector<PDBEntry> pdbIds;
  
 -  int end;
 +  private String vamsasId;
  
+   HiddenMarkovModel hmm;
+   boolean isHMMConsensusSequence = false;
 -  Vector<PDBEntry> pdbIds;
 +  private DBModList<DBRefEntry> dbrefs; // controlled access
  
 -  String vamsasId;
 -
 -  DBRefEntry[] dbrefs;
 +  /**
 +   * a flag to let us know that elements have changed in dbrefs
 +   * 
 +   * @author Bob Hanson
 +   */
 +  private int refModCount = 0;
  
 -  RNA rna;
 +  private RNA rna;
  
    /**
     * This annotation is displayed below the alignment but the positions are tied
@@@ -582,8 -587,12 +592,15 @@@ public interface SequenceI extends ASeq
     *          iterator over regions
     * @return first residue not contained in regions
     */
 -  int firstResidueOutsideIterator(Iterator<int[]> it);
++
 +  public int firstResidueOutsideIterator(Iterator<int[]> it);
 +
  
+   /**
+    * Answers true if this sequence has an associated Hidden Markov Model
+    * 
+    * @return
+    */
+   boolean hasHMMProfile();
  }
 +
@@@ -27,9 -27,8 +27,7 @@@ import jalview.datamodel.SequenceFeatur
  import jalview.datamodel.SequenceI;
  import jalview.io.gff.SequenceOntologyI;
  import jalview.util.JSONUtils;
- import jalview.util.Platform;
  
--import java.io.BufferedReader;
  import java.io.IOException;
  import java.net.MalformedURLException;
  import java.net.URL;
@@@ -22,9 -22,8 +22,7 @@@ package jalview.ext.ensembl
  
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.DBRefSource;
- import jalview.util.JSONUtils;
  
--import java.io.BufferedReader;
  import java.io.IOException;
  import java.net.MalformedURLException;
  import java.net.URL;
@@@ -23,9 -23,8 +23,7 @@@ package jalview.ext.ensembl
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.DBRefEntry;
  import jalview.util.DBRefUtils;
- import jalview.util.JSONUtils;
  
--import java.io.BufferedReader;
  import java.io.IOException;
  import java.net.MalformedURLException;
  import java.net.URL;
   */
  package jalview.gui;
  
 +import java.awt.BorderLayout;
 +import java.awt.Color;
 +import java.awt.Component;
 +import java.awt.Rectangle;
 +import java.awt.Toolkit;
 +import java.awt.datatransfer.Clipboard;
 +import java.awt.datatransfer.DataFlavor;
 +import java.awt.datatransfer.StringSelection;
 +import java.awt.datatransfer.Transferable;
 +import java.awt.dnd.DnDConstants;
 +import java.awt.dnd.DropTargetDragEvent;
 +import java.awt.dnd.DropTargetDropEvent;
 +import java.awt.dnd.DropTargetEvent;
 +import java.awt.dnd.DropTargetListener;
 +import java.awt.event.ActionEvent;
 +import java.awt.event.ActionListener;
 +import java.awt.event.FocusAdapter;
 +import java.awt.event.FocusEvent;
 +import java.awt.event.ItemEvent;
 +import java.awt.event.ItemListener;
 +import java.awt.event.KeyAdapter;
 +import java.awt.event.KeyEvent;
 +import java.awt.event.MouseEvent;
 +import java.awt.print.PageFormat;
 +import java.awt.print.PrinterJob;
 +import java.beans.PropertyChangeEvent;
 +import java.io.File;
 +import java.io.FileWriter;
 +import java.io.PrintWriter;
 +import java.net.URL;
 +import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.Deque;
- import java.util.Enumeration;
- import java.util.Hashtable;
 +import java.util.List;
 +import java.util.Vector;
 +
 +import javax.swing.ButtonGroup;
 +import javax.swing.JCheckBoxMenuItem;
 +import javax.swing.JComponent;
 +import javax.swing.JEditorPane;
 +import javax.swing.JInternalFrame;
 +import javax.swing.JLabel;
 +import javax.swing.JLayeredPane;
 +import javax.swing.JMenu;
 +import javax.swing.JMenuItem;
 +import javax.swing.JPanel;
 +import javax.swing.JScrollPane;
 +import javax.swing.SwingUtilities;
 +
 +import ext.vamsas.ServiceHandle;
  import jalview.analysis.AlignmentSorter;
  import jalview.analysis.AlignmentUtils;
  import jalview.analysis.CrossRef;
@@@ -150,10 -102,64 +155,20 @@@ import jalview.viewmodel.AlignmentViewp
  import jalview.viewmodel.ViewportRanges;
  import jalview.ws.DBRefFetcher;
  import jalview.ws.DBRefFetcher.FetchFinishedListenerI;
+ import jalview.ws.api.ServiceWithParameters;
  import jalview.ws.jws1.Discoverer;
  import jalview.ws.jws2.Jws2Discoverer;
- import jalview.ws.jws2.jabaws2.Jws2Instance;
+ import jalview.ws.params.ArgumentI;
+ import jalview.ws.params.ParamDatastoreI;
+ import jalview.ws.params.WsParamSetI;
  import jalview.ws.seqfetcher.DbSourceProxy;
+ import jalview.ws.slivkaws.SlivkaWSDiscoverer;
 -
 -import java.awt.BorderLayout;
 -import java.awt.Component;
 -import java.awt.Rectangle;
 -import java.awt.Toolkit;
 -import java.awt.datatransfer.Clipboard;
 -import java.awt.datatransfer.DataFlavor;
 -import java.awt.datatransfer.StringSelection;
 -import java.awt.datatransfer.Transferable;
 -import java.awt.dnd.DnDConstants;
 -import java.awt.dnd.DropTargetDragEvent;
 -import java.awt.dnd.DropTargetDropEvent;
 -import java.awt.dnd.DropTargetEvent;
 -import java.awt.dnd.DropTargetListener;
 -import java.awt.event.ActionEvent;
 -import java.awt.event.ActionListener;
 -import java.awt.event.FocusAdapter;
 -import java.awt.event.FocusEvent;
 -import java.awt.event.ItemEvent;
 -import java.awt.event.ItemListener;
 -import java.awt.event.KeyAdapter;
 -import java.awt.event.KeyEvent;
 -import java.awt.event.MouseEvent;
 -import java.awt.print.PageFormat;
 -import java.awt.print.PrinterJob;
 -import java.beans.PropertyChangeEvent;
 -import java.io.File;
 -import java.io.FileWriter;
+ import java.io.IOException;
 -import java.io.PrintWriter;
 -import java.net.URL;
 -import java.util.ArrayList;
 -import java.util.Arrays;
 -import java.util.Deque;
+ import java.util.HashSet;
 -import java.util.List;
+ import java.util.Set;
 -import java.util.Vector;
 -import javax.swing.ButtonGroup;
 -import javax.swing.JCheckBoxMenuItem;
 -import javax.swing.JEditorPane;
+ import javax.swing.JFileChooser;
 -import javax.swing.JInternalFrame;
 -import javax.swing.JLayeredPane;
 -import javax.swing.JMenu;
 -import javax.swing.JMenuItem;
+ import javax.swing.JOptionPane;
 -import javax.swing.JScrollPane;
 -import javax.swing.SwingUtilities;
  
  /**
   * DOCUMENT ME!
@@@ -191,7 -195,6 +205,10 @@@ public class AlignFrame extends GAlignF
     */
    String fileName = null;
  
++  /**
++       * TODO: remove reference to 'FileObject' in AlignFrame - not correct mapping
++       */
 +  File fileObject;
  
    /**
     * Creates a new AlignFrame object with specific width and height.
  
    @Override
    public void associatedData_actionPerformed(ActionEvent e)
+           throws IOException, InterruptedException
    {
 -    // Pick the tree file
 -    JalviewFileChooser chooser = new JalviewFileChooser(
 +    final JalviewFileChooser chooser = new JalviewFileChooser(
              jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
      chooser.setFileView(new JalviewFileView());
 -    chooser.setDialogTitle(
 -            MessageManager.getString("label.load_jalview_annotations"));
 -    chooser.setToolTipText(
 -            MessageManager.getString("label.load_jalview_annotations"));
 -
 -    int value = chooser.showOpenDialog(null);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    String tooltip = MessageManager.getString("label.load_jalview_annotations");
 +    chooser.setDialogTitle(tooltip);
 +    chooser.setToolTipText(tooltip);
 +    chooser.setResponseHandler(0, new Runnable()
      {
 -      String choice = chooser.getSelectedFile().getPath();
 -      jalview.bin.Cache.setProperty("LAST_DIRECTORY", choice);
 -      loadJalviewDataFile(choice, null, null, null);
 -    }
 +      @Override
 +      public void run()
 +      {
 +        String choice = chooser.getSelectedFile().getPath();
 +        jalview.bin.Cache.setProperty("LAST_DIRECTORY", choice);
 +        loadJalviewDataFile(chooser.getSelectedFile(), null, null, null);
 +      }
 +    });
  
 +    chooser.showOpenDialog(this);
    }
  
    /**
     * 
     * @param file
     *          either a filename or a URL string.
+    * @throws InterruptedException
+    * @throws IOException
     */
 -  public void loadJalviewDataFile(String file, DataSourceType sourceType,
 +  public void loadJalviewDataFile(Object file, DataSourceType sourceType,
            FileFormatI format, SequenceI assocSeq)
    {
 +    // BH 2018 was String file
      try
      {
        if (sourceType == null)
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
@@@ -68,7 -65,7 +69,8 @@@ import jalview.datamodel.SequenceFeatur
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  import jalview.gui.ColourMenuHelper.ColourChangeListener;
 +import jalview.gui.JalviewColourChooser.ColourChooserListener;
+ import jalview.io.CountReader;
  import jalview.io.FileFormatI;
  import jalview.io.FileFormats;
  import jalview.io.FormatAdapter;
Simple merge
@@@ -26,7 -26,7 +26,6 @@@ import jalview.jbgui.GSliderPanel
  import jalview.renderer.ResidueShaderI;
  import jalview.util.MessageManager;
  
--import java.awt.event.ActionEvent;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.beans.PropertyVetoException;
@@@ -1100,4 -1101,4 +1101,4 @@@ public class SplitFrame extends GSplitF
    {
      return featureSettingsUI != null && !featureSettingsUI.isClosed();
    }
--}
++}
@@@ -953,244 -906,7 +906,6 @@@ public class WsJobParameters extends JP
      paramPane.revalidate();
      revalidate();
    }
--
-   /**
-    * testing method - grab a service and parameter set and show the window
-    * 
-    * @param args
-    * @j2sIgnore
-    */
-   public static void main(String[] args)
-   {
-     jalview.ws.jws2.Jws2Discoverer disc = jalview.ws.jws2.Jws2Discoverer
-             .getDiscoverer();
-     int p = 0;
-     if (args.length > 0)
-     {
-       Vector<String> services = new Vector<>();
-       services.addElement(args[p++]);
-       Jws2Discoverer.getDiscoverer().setServiceUrls(services);
-     }
-     try
-     {
-       disc.run();
-     } catch (Exception e)
-     {
-       System.err.println("Aborting. Problem discovering services.");
-       e.printStackTrace();
-       return;
-     }
-     Jws2Instance lastserv = null;
-     for (Jws2Instance service : disc.getServices())
-     {
-       lastserv = service;
-       if (p >= args.length || service.serviceType.equalsIgnoreCase(args[p]))
-       {
-         if (lastserv != null)
-         {
-           List<Preset> prl = null;
-           Preset pr = null;
-           if (++p < args.length)
-           {
-             PresetManager prman = lastserv.getPresets();
-             if (prman != null)
-             {
-               pr = prman.getPresetByName(args[p]);
-               if (pr == null)
-               {
-                 // just grab the last preset.
-                 prl = prman.getPresets();
-               }
-             }
-           }
-           else
-           {
-             PresetManager prman = lastserv.getPresets();
-             if (prman != null)
-             {
-               prl = prman.getPresets();
-             }
-           }
-           Iterator<Preset> en = (prl == null) ? null : prl.iterator();
-           while (en != null && en.hasNext())
-           {
-             if (en != null)
-             {
-               if (!en.hasNext())
-               {
-                 en = prl.iterator();
-               }
-               pr = en.next();
-             }
-             {
-               System.out.println("Testing opts dupes for "
-                       + lastserv.getUri() + " : " + lastserv.getActionText()
-                       + ":" + pr.getName());
-               List<Option> rg = lastserv.getRunnerConfig().getOptions();
-               for (Option o : rg)
-               {
-                 try
-                 {
-                   Option cpy = jalview.ws.jws2.ParameterUtils.copyOption(o);
-                 } catch (Exception e)
-                 {
-                   System.err.println("Failed to copy " + o.getName());
-                   e.printStackTrace();
-                 } catch (Error e)
-                 {
-                   System.err.println("Failed to copy " + o.getName());
-                   e.printStackTrace();
-                 }
-               }
-             }
-             {
-               System.out.println("Testing param dupes:");
-               List<Parameter> rg = lastserv.getRunnerConfig()
-                       .getParameters();
-               for (Parameter o : rg)
-               {
-                 try
-                 {
-                   Parameter cpy = jalview.ws.jws2.ParameterUtils
-                           .copyParameter(o);
-                 } catch (Exception e)
-                 {
-                   System.err.println("Failed to copy " + o.getName());
-                   e.printStackTrace();
-                 } catch (Error e)
-                 {
-                   System.err.println("Failed to copy " + o.getName());
-                   e.printStackTrace();
-                 }
-               }
-             }
-             {
-               System.out.println("Testing param write:");
-               List<String> writeparam = null, readparam = null;
-               try
-               {
-                 writeparam = jalview.ws.jws2.ParameterUtils
-                         .writeParameterSet(
-                                 pr.getArguments(lastserv.getRunnerConfig()),
-                                 " ");
-                 System.out.println("Testing param read :");
-                 List<Option> pset = jalview.ws.jws2.ParameterUtils
-                         .processParameters(writeparam,
-                                 lastserv.getRunnerConfig(), " ");
-                 readparam = jalview.ws.jws2.ParameterUtils
-                         .writeParameterSet(pset, " ");
-                 Iterator<String> o = pr.getOptions().iterator(),
-                         s = writeparam.iterator(), t = readparam.iterator();
-                 boolean failed = false;
-                 while (s.hasNext() && t.hasNext())
-                 {
-                   String on = o.next(), sn = s.next(), st = t.next();
-                   if (!sn.equals(st))
-                   {
-                     System.out.println(
-                             "Original was " + on + " Phase 1 wrote " + sn
-                                     + "\tPhase 2 wrote " + st);
-                     failed = true;
-                   }
-                 }
-                 if (failed)
-                 {
-                   System.out.println(
-                           "Original parameters:\n" + pr.getOptions());
-                   System.out.println(
-                           "Wrote parameters in first set:\n" + writeparam);
-                   System.out.println(
-                           "Wrote parameters in second set:\n" + readparam);
-                 }
-               } catch (Exception e)
-               {
-                 e.printStackTrace();
-               }
-             }
-             WsJobParameters pgui = new WsJobParameters(lastserv,
-                     new JabaPreset(lastserv, pr));
-             JFrame jf = new JFrame(MessageManager
-                     .formatMessage("label.ws_parameters_for", new String[]
-                     { lastserv.getActionText() }));
-             JPanel cont = new JPanel(new BorderLayout());
-             pgui.validate();
-             cont.setPreferredSize(pgui.getPreferredSize());
-             cont.add(pgui, BorderLayout.CENTER);
-             jf.setLayout(new BorderLayout());
-             jf.add(cont, BorderLayout.CENTER);
-             jf.validate();
-             final Thread thr = Thread.currentThread();
-             jf.addWindowListener(new WindowListener()
-             {
-               @Override
-               public void windowActivated(WindowEvent e)
-               {
-                 // TODO Auto-generated method stub
-               }
-               @Override
-               public void windowClosed(WindowEvent e)
-               {
-               }
-               @Override
-               public void windowClosing(WindowEvent e)
-               {
-                 thr.interrupt();
-               }
-               @Override
-               public void windowDeactivated(WindowEvent e)
-               {
-                 // TODO Auto-generated method stub
-               }
-               @Override
-               public void windowDeiconified(WindowEvent e)
-               {
-                 // TODO Auto-generated method stub
-               }
-               @Override
-               public void windowIconified(WindowEvent e)
-               {
-                 // TODO Auto-generated method stub
-               }
-               @Override
-               public void windowOpened(WindowEvent e)
-               {
-                 // TODO Auto-generated method stub
-               }
-             });
-             jf.setVisible(true);
-             boolean inter = false;
-             while (!inter)
-             {
-               try
-               {
-                 Thread.sleep(10000);
-               } catch (Exception e)
-               {
-                 inter = true;
-               }
-             }
-             jf.dispose();
-           }
-         }
-       }
-     }
-   }
    public boolean isServiceDefaults()
    {
      return (!isModified()
index 0000000,b311b76..0240352
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,536 +1,536 @@@
+ package jalview.hmmer;
+ import jalview.analysis.SeqsetUtils;
+ import jalview.bin.Cache;
+ import jalview.datamodel.Alignment;
+ import jalview.datamodel.AlignmentAnnotation;
+ import jalview.datamodel.AlignmentI;
+ import jalview.datamodel.AnnotatedCollectionI;
+ import jalview.datamodel.Annotation;
+ import jalview.datamodel.HiddenMarkovModel;
+ import jalview.datamodel.SequenceGroup;
+ import jalview.datamodel.SequenceI;
+ import jalview.gui.AlignFrame;
+ import jalview.gui.JvOptionPane;
+ import jalview.gui.Preferences;
+ import jalview.io.FastaFile;
+ import jalview.io.HMMFile;
+ import jalview.io.StockholmFile;
+ import jalview.util.FileUtils;
+ import jalview.util.MessageManager;
+ import jalview.util.Platform;
+ import jalview.ws.params.ArgumentI;
+ import java.io.BufferedReader;
+ import java.io.File;
+ import java.io.IOException;
+ import java.io.InputStreamReader;
+ import java.io.PrintWriter;
+ import java.nio.file.Paths;
+ import java.util.ArrayList;
+ import java.util.Hashtable;
+ import java.util.List;
+ /**
+  * Base class for hmmbuild, hmmalign and hmmsearch
+  * 
+  * @author TZVanaalten
+  *
+  */
+ public abstract class HmmerCommand implements Runnable
+ {
+   public static final String HMMBUILD = "hmmbuild";
+   protected final AlignFrame af;
+   protected final AlignmentI alignment;
+   protected final List<ArgumentI> params;
+   /*
+    * constants for i18n lookup of passed parameter names
+    */
+   static final String DATABASE_KEY = "label.database";
+   static final String THIS_ALIGNMENT_KEY = "label.this_alignment";
+   static final String USE_ACCESSIONS_KEY = "label.use_accessions";
+   static final String AUTO_ALIGN_SEQS_KEY = "label.auto_align_seqs";
+   static final String NUMBER_OF_RESULTS_KEY = "label.number_of_results";
+   static final String NUMBER_OF_ITERATIONS = "label.number_of_iterations";
+   static final String TRIM_TERMINI_KEY = "label.trim_termini";
+   static final String RETURN_N_NEW_SEQ = "label.check_for_new_sequences";
+   static final String REPORTING_CUTOFF_KEY = "label.reporting_cutoff";
+   static final String CUTOFF_NONE = "label.default";
+   static final String CUTOFF_SCORE = "label.score";
+   static final String CUTOFF_EVALUE = "label.evalue";
+   static final String REPORTING_SEQ_EVALUE_KEY = "label.reporting_seq_evalue";
+   static final String REPORTING_DOM_EVALUE_KEY = "label.reporting_dom_evalue";
+   static final String REPORTING_SEQ_SCORE_KEY = "label.reporting_seq_score";
+   static final String REPORTING_DOM_SCORE_KEY = "label.reporting_dom_score";
+   static final String INCLUSION_SEQ_EVALUE_KEY = "label.inclusion_seq_evalue";
+   static final String INCLUSION_DOM_EVALUE_KEY = "label.inclusion_dom_evalue";
+   static final String INCLUSION_SEQ_SCORE_KEY = "label.inclusion_seq_score";
+   static final String INCLUSION_DOM_SCORE_KEY = "label.inclusion_dom_score";
+   static final String ARG_TRIM = "--trim";
+   static final String INCLUSION_THRESHOLD_KEY = "label.inclusion_threshold";
+   /**
+    * Constructor
+    * 
+    * @param alignFrame
+    * @param args
+    */
+   public HmmerCommand(AlignFrame alignFrame, List<ArgumentI> args)
+   {
+     af = alignFrame;
+     alignment = af.getViewport().getAlignment();
+     params = args;
+   }
+   /**
+    * Answers true if preference HMMER_PATH is set, and its value is the path to
+    * a directory that contains an executable <code>hmmbuild</code> or
+    * <code>hmmbuild.exe</code>, else false
+    * 
+    * @return
+    */
+   public static boolean isHmmerAvailable()
+   {
+     File exec = FileUtils.getExecutable(HMMBUILD,
+             Cache.getProperty(Preferences.HMMER_PATH));
+     return exec != null;
+   }
+   /**
+    * Uniquifies the sequences when exporting and stores their details in a
+    * hashtable
+    * 
+    * @param seqs
+    */
+   protected Hashtable stashSequences(SequenceI[] seqs)
+   {
+     return SeqsetUtils.uniquify(seqs, true);
+   }
+   /**
+    * Restores the sequence data lost by uniquifying
+    * 
+    * @param hashtable
+    * @param seqs
+    */
+   protected void recoverSequences(Hashtable hashtable, SequenceI[] seqs)
+   {
+     SeqsetUtils.deuniquify(hashtable, seqs);
+   }
+   /**
+    * Runs a command as a separate process and waits for it to complete. Answers
+    * true if the process return status is zero, else false.
+    * 
+    * @param commands
+    *          the executable command and any arguments to it
+    * @throws IOException
+    */
+   public boolean runCommand(List<String> commands)
+           throws IOException
+   {
 -    List<String> args = Platform.isWindows() ? wrapWithCygwin(commands)
++    List<String> args = Platform.isWindowsAndNotJS() ? wrapWithCygwin(commands)
+             : commands;
+     try
+     {
+       ProcessBuilder pb = new ProcessBuilder(args);
+       pb.redirectErrorStream(true); // merge syserr to sysout
 -      if (Platform.isWindows())
++      if (Platform.isWindowsAndNotJS())
+       {
+         String path = pb.environment().get("Path");
+         path = jalview.bin.Cache.getProperty("CYGWIN_PATH") + ";" + path;
+         pb.environment().put("Path", path);
+       }
+       final Process p = pb.start();
+       new Thread(new Runnable()
+       {
+         @Override
+         public void run()
+         {
+           BufferedReader input = new BufferedReader(
+                   new InputStreamReader(p.getInputStream()));
+           try
+           {
+             String line = input.readLine();
+             while (line != null)
+             {
+               System.out.println(line);
+               line = input.readLine();
+             }
+           } catch (IOException e)
+           {
+             e.printStackTrace();
+           }
+         }
+       }).start();
+       p.waitFor();
+       int exitValue = p.exitValue();
+       if (exitValue != 0)
+       {
+         Cache.log.error("Command failed, return code = " + exitValue);
+         Cache.log.error("Command/args were: " + args.toString());
+       }
+       return exitValue == 0; // 0 is success, by convention
+     } catch (Exception e)
+     {
+       e.printStackTrace();
+       return false;
+     }
+   }
+   /**
+    * Converts the given command to a Cygwin "bash" command wrapper. The hmmer
+    * command and any arguments to it are converted into a single parameter to the
+    * bash command.
+    * 
+    * @param commands
+    */
+   protected List<String> wrapWithCygwin(List<String> commands)
+   {
+     File bash = FileUtils.getExecutable("bash",
+             Cache.getProperty(Preferences.CYGWIN_PATH));
+     if (bash == null)
+     {
+       Cache.log.error("Cygwin shell not found");
+       return commands;
+     }
+     List<String> wrapped = new ArrayList<>();
+     // wrapped.add("C:\Users\tva\run");
+     wrapped.add(bash.getAbsolutePath());
+     wrapped.add("-c");
+     /*
+      * combine hmmbuild/search/align and arguments to a single string
+      */
+     StringBuilder sb = new StringBuilder();
+     for (String cmd : commands)
+     {
+       sb.append(" ").append(cmd);
+     }
+     wrapped.add(sb.toString());
+     return wrapped;
+   }
+   /**
+    * Exports an alignment, and reference (RF) annotation if present, to the
+    * specified file, in Stockholm format, removing all HMM sequences
+    * 
+    * @param seqs
+    * @param toFile
+    * @param annotated
+    * @throws IOException
+    */
+   public void exportStockholm(SequenceI[] seqs, File toFile,
+           AnnotatedCollectionI annotated)
+           throws IOException
+   {
+     if (seqs == null)
+     {
+       return;
+     }
+     AlignmentI newAl = new Alignment(seqs);
+     if (!newAl.isAligned())
+     {
+       newAl.padGaps();
+     }
+     if (toFile != null && annotated != null)
+     {
+       AlignmentAnnotation[] annots = annotated.getAlignmentAnnotation();
+       if (annots != null)
+       {
+         for (AlignmentAnnotation annot : annots)
+         {
+           if (annot.label.contains("Reference") || "RF".equals(annot.label))
+           {
+             AlignmentAnnotation newRF;
+             if (annot.annotations.length > newAl.getWidth())
+             {
+               Annotation[] rfAnnots = new Annotation[newAl.getWidth()];
+               System.arraycopy(annot.annotations, 0, rfAnnots, 0,
+                       rfAnnots.length);
+               newRF = new AlignmentAnnotation("RF", "Reference Positions",
+                       rfAnnots);
+             }
+             else
+             {
+               newRF = new AlignmentAnnotation(annot);
+             }
+             newAl.addAnnotation(newRF);
+           }
+         }
+       }
+     }
+     for (SequenceI seq : newAl.getSequencesArray())
+     {
+       if (seq.getAnnotation() != null)
+       {
+         for (AlignmentAnnotation ann : seq.getAnnotation())
+         {
+           seq.removeAlignmentAnnotation(ann);
+         }
+       }
+     }
+     StockholmFile file = new StockholmFile(newAl);
+     String output = file.print(seqs, false);
+     PrintWriter writer = new PrintWriter(toFile);
+     writer.println(output);
+     writer.close();
+   }
+   /**
+    * Answers the full path to the given hmmer executable, or null if file cannot
+    * be found or is not executable
+    * 
+    * @param cmd
+    *          command short name e.g. hmmalign
+    * @return
+    * @throws IOException
+    */
+   protected String getCommandPath(String cmd)
+           throws IOException
+   {
+     String binariesFolder = Cache.getProperty(Preferences.HMMER_PATH);
+     // ensure any symlink to the directory is resolved:
+     binariesFolder = Paths.get(binariesFolder).toRealPath().toString();
+     File file = FileUtils.getExecutable(cmd, binariesFolder);
+     if (file == null && af != null)
+     {
+       JvOptionPane.showInternalMessageDialog(af, MessageManager
+               .formatMessage("label.executable_not_found", cmd));
+     }
+     return file == null ? null : getFilePath(file, true);
+   }
+   /**
+    * Exports an HMM to the specified file
+    * 
+    * @param hmm
+    * @param hmmFile
+    * @throws IOException
+    */
+   public void exportHmm(HiddenMarkovModel hmm, File hmmFile)
+           throws IOException
+   {
+     if (hmm != null)
+     {
+       HMMFile file = new HMMFile(hmm);
+       PrintWriter writer = new PrintWriter(hmmFile);
+       writer.print(file.print());
+       writer.close();
+     }
+   }
+   // TODO is needed?
+   /**
+    * Exports a sequence to the specified file
+    * 
+    * @param hmm
+    * @param hmmFile
+    * @throws IOException
+    */
+   public void exportSequence(SequenceI seq, File seqFile) throws IOException
+   {
+     if (seq != null)
+     {
+       FastaFile file = new FastaFile();
+       PrintWriter writer = new PrintWriter(seqFile);
+       writer.print(file.print(new SequenceI[] { seq }, false));
+       writer.close();
+     }
+   }
+   /**
+    * Answers the HMM profile for the profile sequence the user selected (default
+    * is just the first HMM sequence in the alignment)
+    * 
+    * @return
+    */
+   protected HiddenMarkovModel getHmmProfile()
+   {
+     String alignToParamName = MessageManager.getString("label.use_hmm");
+     for (ArgumentI arg : params)
+     {
+       String name = arg.getName();
+       if (name.equals(alignToParamName))
+       {
+         String seqName = arg.getValue();
+         SequenceI hmmSeq = alignment.findName(seqName);
+         if (hmmSeq.hasHMMProfile())
+         {
+           return hmmSeq.getHMM();
+         }
+       }
+     }
+     return null;
+   }
+   /**
+    * Answers the query sequence the user selected (default is just the first
+    * sequence in the alignment)
+    * 
+    * @return
+    */
+   protected SequenceI getSequence()
+   {
+     String alignToParamName = MessageManager
+             .getString("label.use_sequence");
+     for (ArgumentI arg : params)
+     {
+       String name = arg.getName();
+       if (name.equals(alignToParamName))
+       {
+         String seqName = arg.getValue();
+         SequenceI seq = alignment.findName(seqName);
+         return seq;
+       }
+     }
+     return null;
+   }
+   /**
+    * Answers an absolute path to the given file, in a format suitable for
+    * processing by a hmmer command. On a Windows platform, the native Windows file
+    * path is converted to Cygwin format, by replacing '\'with '/' and drive letter
+    * X with /cygdrive/x.
+    * 
+    * @param resultFile
+    * @param isInCygwin
+    *                     True if file is to be read/written from within the Cygwin
+    *                     shell. Should be false for any imports.
+    * @return
+    */
+   protected String getFilePath(File resultFile, boolean isInCygwin)
+   {
+     String path = resultFile.getAbsolutePath();
 -    if (Platform.isWindows() && isInCygwin)
++    if (Platform.isWindowsAndNotJS() && isInCygwin)
+     {
+       // the first backslash escapes '\' for the regular expression argument
+       path = path.replaceAll("\\" + File.separator, "/");
+       int colon = path.indexOf(':');
+       if (colon > 0)
+       {
+         String drive = path.substring(0, colon);
+         path = path.replaceAll(drive + ":", "/cygdrive/" + drive);
+       }
+     }
+     return path;
+   }
+   /**
+    * A helper method that deletes any HMM consensus sequence from the given
+    * collection, and from the parent alignment if <code>ac</code> is a subgroup
+    * 
+    * @param ac
+    */
+   void deleteHmmSequences(AnnotatedCollectionI ac)
+   {
+     List<SequenceI> hmmSeqs = ac.getHmmSequences();
+     for (SequenceI hmmSeq : hmmSeqs)
+     {
+       if (ac instanceof SequenceGroup)
+       {
+         ((SequenceGroup) ac).deleteSequence(hmmSeq, false);
+         AnnotatedCollectionI context = ac.getContext();
+         if (context != null && context instanceof AlignmentI)
+         {
+           ((AlignmentI) context).deleteSequence(hmmSeq);
+         }
+       }
+       else
+       {
+         ((AlignmentI) ac).deleteSequence(hmmSeq);
+       }
+     }
+   }
+   /**
+    * Sets the names of any duplicates within the given sequences to include their
+    * respective lengths. Deletes any duplicates that have the same name after this
+    * step
+    * 
+    * @param seqs
+    */
+   void renameDuplicates(AlignmentI al)
+   {
+     SequenceI[] seqs = al.getSequencesArray();
+     List<Boolean> wasRenamed = new ArrayList<>();
+     for (SequenceI seq : seqs)
+     {
+       wasRenamed.add(false);
+     }
+     for (int i = 0; i < seqs.length; i++)
+     {
+       for (int j = 0; j < seqs.length; j++)
+       {
+         if (seqs[i].getName().equals(seqs[j].getName()) && i != j
+                 && !wasRenamed.get(j))
+         {
+           wasRenamed.set(i, true);
+           String range = "/" + seqs[j].getStart() + "-" + seqs[j].getEnd();
+           // setting sequence name to include range - to differentiate between
+           // sequences of the same name. Currently have to include the range twice
+           // because the range is removed (once) when setting the name
+           // TODO come up with a better way of doing this
+           seqs[j].setName(seqs[j].getName() + range + range);
+         }
+       }
+       if (wasRenamed.get(i))
+       {
+         String range = "/" + seqs[i].getStart() + "-" + seqs[i].getEnd();
+         seqs[i].setName(seqs[i].getName() + range + range);
+       }
+     }
+     for (int i = 0; i < seqs.length; i++)
+     {
+       for (int j = 0; j < seqs.length; j++)
+       {
+         if (seqs[i].getName().equals(seqs[j].getName()) && i != j)
+         {
+           al.deleteSequence(j);
+         }
+       }
+     }
+   }
+ }
Simple merge
   */
  package jalview.io;
  
 -import jalview.api.AlignExportSettingI;
 +import jalview.api.AlignExportSettingsI;
  import jalview.api.AlignmentViewPanel;
- import jalview.api.FeatureSettingsModelI;
- import jalview.datamodel.AlignmentI;
  import jalview.datamodel.SequenceI;
  
  public interface AlignmentFileWriterI
@@@ -371,10 -371,26 +371,26 @@@ public enum FileFormat implements FileF
      @Override
      public boolean isIdentifiable()
      {
 -      return false;
 +      return true;
      }
+   },
+   HMMER3("HMMER3", "hmm", true, true)
+   {
+     @Override
+     public AlignmentFileReaderI getReader(FileParse source)
+             throws IOException
+     {
+       return new HMMFile(source);
+     }
+     @Override
+     public AlignmentFileWriterI getWriter(AlignmentI al)
+     {
+       return new HMMFile();
+     }
    };
  
    private boolean writable;
  
    private boolean readable;
@@@ -48,8 -41,18 +48,13 @@@ import jalview.structure.StructureSelec
  import jalview.util.MessageManager;
  import jalview.ws.utils.UrlDownloadClient;
  
 -import java.io.File;
 -import java.io.IOException;
+ import java.util.ArrayList;
+ import java.util.List;
 -import java.util.StringTokenizer;
 -
 -import javax.swing.SwingUtilities;
  public class FileLoader implements Runnable
  {
+   private static final String TAB = "\t";
    String file;
  
    DataSourceType protocol;
      return alignFrame;
    }
  
-   public void updateRecentlyOpened()
+   public void LoadFileOntoAlignmentWaitTillLoaded(AlignViewport viewport,
+           String file, DataSourceType sourceType, FileFormatI format)
    {
 +    Vector<String> recent = new Vector<>();
 +    if (protocol == DataSourceType.PASTE)
+     this.viewport = viewport;
+     this.file = file;
+     this.protocol = sourceType;
+     this.format = format;
 -    _LoadAlignmentFileWaitTillLoaded();
++    _LoadFileWaitTillLoaded();
+   }
 -  protected void _LoadAlignmentFileWaitTillLoaded()
 -  {
 -    Thread loader = new Thread(this);
 -    loader.start();
 -
 -    while (loader.isAlive())
 -    {
 -      try
 -      {
 -        Thread.sleep(500);
 -      } catch (Exception ex)
 -      {
 -      }
 -    }
 -  }
+   /**
+    * Updates (or creates) the tab-separated list of recently opened files held
+    * under the given property name by inserting the filePath at the front of the
+    * list. Duplicates are removed, and the list is limited to 11 entries. The
+    * method returns the updated value of the property.
+    * 
+    * @param filePath
+    * @param sourceType
+    */
+   public static String updateRecentlyOpened(String filePath,
+           DataSourceType sourceType)
+   {
+     if (sourceType != DataSourceType.FILE
+             && sourceType != DataSourceType.URL)
      {
-       // do nothing if the file was pasted in as text... there is no filename to
-       // refer to it as.
-       return;
+       return null;
      }
-     if (file != null
-             && file.indexOf(System.getProperty("java.io.tmpdir")) > -1)
+     String propertyName = sourceType == DataSourceType.FILE ? "RECENT_FILE"
+             : "RECENT_URL";
+     String historyItems = Cache.getProperty(propertyName);
+     if (filePath != null
+             && filePath.indexOf(System.getProperty("java.io.tmpdir")) > -1)
      {
        // ignore files loaded from the system's temporary directory
-       return;
+       return null;
      }
-     String type = protocol == DataSourceType.FILE ? "RECENT_FILE"
-             : "RECENT_URL";
  
-     String historyItems = Cache.getProperty(type);
-     StringTokenizer st;
+     List<String> recent = new ArrayList<>();
  
      if (historyItems != null)
      {
  
        while (st.hasMoreTokens())
        {
-         recent.addElement(st.nextToken().trim());
+         String trimmed = st.nextToken().trim();
 -        if (!recent.contains(trimmed))
 -        {
 -          recent.add(trimmed);
 -        }
++      recent.add(trimmed);
        }
      }
  
index 0000000,07f29c8..2fce4cc
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,716 +1,716 @@@
+ package jalview.io;
 -import jalview.api.AlignExportSettingI;
++import jalview.api.AlignExportSettingsI;
+ import jalview.api.AlignmentViewPanel;
+ import jalview.datamodel.HMMNode;
+ import jalview.datamodel.HiddenMarkovModel;
+ import jalview.datamodel.SequenceI;
+ import java.io.BufferedReader;
+ import java.io.IOException;
+ import java.util.ArrayList;
+ import java.util.List;
+ import java.util.Scanner;
+ /**
+  * Adds capability to read in and write out HMMER3 files. .
+  * 
+  * 
+  * @author TZVanaalten
+  *
+  */
+ public class HMMFile extends AlignFile
+         implements AlignmentFileReaderI, AlignmentFileWriterI
+ {
+   private static final String TERMINATOR = "//";
+   /*
+    * keys to data in HMM file, used to store as properties of the HiddenMarkovModel
+    */
+   public static final String HMM = "HMM";
+   public static final String NAME = "NAME";
+   public static final String ACCESSION_NUMBER = "ACC";
+   public static final String DESCRIPTION = "DESC";
+   public static final String LENGTH = "LENG";
+   public static final String MAX_LENGTH = "MAXL";
+   public static final String ALPHABET = "ALPH";
+   public static final String DATE = "DATE";
+   public static final String COMMAND_LOG = "COM";
+   public static final String NUMBER_OF_SEQUENCES = "NSEQ";
+   public static final String EFF_NUMBER_OF_SEQUENCES = "EFFN";
+   public static final String CHECK_SUM = "CKSUM";
+   public static final String STATISTICS = "STATS";
+   public static final String COMPO = "COMPO";
+   public static final String GATHERING_THRESHOLD = "GA";
+   public static final String TRUSTED_CUTOFF = "TC";
+   public static final String NOISE_CUTOFF = "NC";
+   public static final String VITERBI = "VITERBI";
+   public static final String MSV = "MSV";
+   public static final String FORWARD = "FORWARD";
+   public static final String MAP = "MAP";
+   public static final String REFERENCE_ANNOTATION = "RF";
+   public static final String CONSENSUS_RESIDUE = "CONS";
+   public static final String CONSENSUS_STRUCTURE = "CS";
+   public static final String MASKED_VALUE = "MM";
+   private static final String ALPH_AMINO = "amino";
+   private static final String ALPH_DNA = "DNA";
+   private static final String ALPH_RNA = "RNA";
+   private static final String ALPHABET_AMINO = "ACDEFGHIKLMNPQRSTVWY";
+   private static final String ALPHABET_DNA = "ACGT";
+   private static final String ALPHABET_RNA = "ACGU";
+   private static final int NUMBER_OF_TRANSITIONS = 7;
+   private static final String SPACE = " ";
+   /*
+    * optional guide line added to an output HMMER file, purely for readability
+    */
+   private static final String TRANSITIONTYPELINE = "            m->m     m->i     m->d     i->m     i->i     d->m     d->d";
+   private static String NL = System.lineSeparator();
+   private HiddenMarkovModel hmm;
+   // number of symbols in the alphabet used in the hidden Markov model
+   private int numberOfSymbols;
+   /**
+    * Constructor that parses immediately
+    * 
+    * @param inFile
+    * @param type
+    * @throws IOException
+    */
+   public HMMFile(String inFile, DataSourceType type) throws IOException
+   {
+     super(inFile, type);
+   }
+   /**
+    * Constructor that parses immediately
+    * 
+    * @param source
+    * @throws IOException
+    */
+   public HMMFile(FileParse source) throws IOException
+   {
+     super(source);
+   }
+   /**
+    * Default constructor
+    */
+   public HMMFile()
+   {
+   }
+   /**
+    * Constructor for HMMFile used for exporting
+    * 
+    * @param hmm
+    */
+   public HMMFile(HiddenMarkovModel markov)
+   {
+     hmm = markov;
+   }
+   /**
+    * Returns the HMM produced by parsing a HMMER3 file
+    * 
+    * @return
+    */
+   public HiddenMarkovModel getHMM()
+   {
+     return hmm;
+   }
+   /**
+    * Gets the name of the hidden Markov model
+    * 
+    * @return
+    */
+   public String getName()
+   {
+     return hmm.getName();
+   }
+   /**
+    * Reads the data from HMM file into the HMM model
+    */
+   @Override
+   public void parse()
+   {
+     try
+     {
+       hmm = new HiddenMarkovModel();
+       parseHeaderLines(dataIn);
+       parseModel(dataIn);
+     } catch (Exception e)
+     {
+       e.printStackTrace();
+     }
+   }
+   /**
+    * Reads the header properties from a HMMER3 file and saves them in the
+    * HiddeMarkovModel. This method exits after reading the next line after the
+    * HMM line.
+    * 
+    * @param input
+    * @throws IOException
+    */
+   void parseHeaderLines(BufferedReader input) throws IOException
+   {
+     boolean readingHeaders = true;
+     hmm.setFileHeader(input.readLine());
+     String line = input.readLine();
+     while (readingHeaders && line != null)
+     {
+       Scanner parser = new Scanner(line);
+       String next = parser.next();
+       if (ALPHABET.equals(next))
+       {
+         String alphabetType = parser.next();
+         hmm.setProperty(ALPHABET, alphabetType);
+         String alphabet = ALPH_DNA.equalsIgnoreCase(alphabetType)
+                 ? ALPHABET_DNA
+                 : (ALPH_RNA.equalsIgnoreCase(alphabetType) ? ALPHABET_RNA
+                         : ALPHABET_AMINO);
+         numberOfSymbols = hmm.setAlphabet(alphabet);
+       }
+       else if (HMM.equals(next))
+       {
+         readingHeaders = false;
+         String symbols = line.substring(line.indexOf(HMM) + HMM.length());
+         numberOfSymbols = hmm.setAlphabet(symbols);
+       }
+       else if (STATISTICS.equals(next))
+       {
+         parser.next();
+         String key;
+         String value;
+         key = parser.next();
+         value = parser.next() + SPACE + SPACE + parser.next();
+         hmm.setProperty(key, value);
+       }
+       else
+       {
+         String key = next;
+         String value = parser.next();
+         while (parser.hasNext())
+         {
+           value = value + SPACE + parser.next();
+         }
+         hmm.setProperty(key, value);
+       }
+       parser.close();
+       line = input.readLine();
+     }
+   }
+   /**
+    * Parses the model data from the HMMER3 file. The input buffer should be
+    * positioned at the (optional) COMPO line if there is one, else at the insert
+    * emissions line for the BEGIN node of the model.
+    * 
+    * @param input
+    * @throws IOException
+    */
+   void parseModel(BufferedReader input) throws IOException
+   {
+     /*
+      * specification says there must always be an HMM header (already read)
+      * and one more header (guide headings) which is skipped here
+      */
+     int nodeNo = 0;
+     String line = input.readLine();
+     List<HMMNode> nodes = new ArrayList<>();
+     while (line != null && !TERMINATOR.equals(line))
+     {
+       HMMNode node = new HMMNode();
+       nodes.add(node);
+       Scanner scanner = new Scanner(line);
+       String next = scanner.next();
+       /*
+        * expect COMPO (optional) for average match emissions
+        * or a node number followed by node's match emissions
+        */
+       if (COMPO.equals(next) || nodeNo > 0)
+       {
+         /*
+          * parse match emissions
+          */
+         double[] matches = parseDoubles(scanner, numberOfSymbols);
+         node.setMatchEmissions(matches);
+         if (!COMPO.equals(next))
+         {
+           int resNo = parseAnnotations(scanner, node);
+           if (resNo == 0)
+           {
+             /*
+              * no MAP annotation provided, just number off from 0 (begin node)
+              */
+             resNo = nodeNo;
+           }
+           node.setResidueNumber(resNo);
+         }
+         line = input.readLine();
+       }
+       scanner.close();
+       /*
+        * parse insert emissions
+        */
+       scanner = new Scanner(line);
+       double[] inserts = parseDoubles(scanner, numberOfSymbols);
+       node.setInsertEmissions(inserts);
+       scanner.close();
+       /*
+        * parse state transitions
+        */
+       line = input.readLine();
+       scanner = new Scanner(line);
+       double[] transitions = parseDoubles(scanner,
+               NUMBER_OF_TRANSITIONS);
+       node.setStateTransitions(transitions);
+       scanner.close();
+       line = input.readLine();
+       nodeNo++;
+     }
+     hmm.setNodes(nodes);
+   }
+   /**
+    * Parses the annotations on the match emission line and add them to the node.
+    * (See p109 of the HMMER User Guide (V3.1b2) for the specification.) Returns
+    * the residue position that the node maps to, if provided, else zero.
+    * 
+    * @param scanner
+    * @param node
+    */
+   int parseAnnotations(Scanner scanner, HMMNode node)
+   {
+     int mapTo = 0;
+     /*
+      * map from hmm node to sequence position, if provided
+      */
+     if (scanner.hasNext())
+     {
+       String value = scanner.next();
+       if (!"-".equals(value))
+       {
+         try
+         {
+           mapTo = Integer.parseInt(value);
+           node.setResidueNumber(mapTo);
+         } catch (NumberFormatException e)
+         {
+           // ignore
+         }
+       }
+     }
+     /*
+      * hmm consensus residue if provided, else '-'
+      */
+     if (scanner.hasNext())
+     {
+       node.setConsensusResidue(scanner.next().charAt(0));
+     }
+     /*
+      * RF reference annotation, if provided, else '-'
+      */
+     if (scanner.hasNext())
+     {
+       node.setReferenceAnnotation(scanner.next().charAt(0));
+     }
+     /*
+      * 'm' for masked position, if provided, else '-'
+      */
+     if (scanner.hasNext())
+     {
+       node.setMaskValue(scanner.next().charAt(0));
+     }
+     /*
+      * structure consensus symbol, if provided, else '-'
+      */
+     if (scanner.hasNext())
+     {
+       node.setConsensusStructure(scanner.next().charAt(0));
+     }
+     return mapTo;
+   }
+   /**
+    * Fills an array of doubles parsed from an input line
+    * 
+    * @param input
+    * @param numberOfElements
+    * @return
+    * @throws IOException
+    */
+   static double[] parseDoubles(Scanner input,
+           int numberOfElements) throws IOException
+   {
+     double[] values = new double[numberOfElements];
+     for (int i = 0; i < numberOfElements; i++)
+     {
+       if (!input.hasNext())
+       {
+         throw new IOException("Incomplete data");
+       }
+       String next = input.next();
+       if (next.contains("*"))
+       {
+         values[i] = Double.NEGATIVE_INFINITY;
+       }
+       else
+       {
+         double prob = Double.valueOf(next);
+         prob = Math.pow(Math.E, -prob);
+         values[i] = prob;
+       }
+     }
+     return values;
+   }
+   /**
+    * Returns a string to be added to the StringBuilder containing the entire
+    * output String.
+    * 
+    * @param initialColumnSeparation
+    *          The initial whitespace separation between the left side of the
+    *          file and first character.
+    * @param columnSeparation
+    *          The separation between subsequent data entries.
+    * @param data
+    *          The list of data to be added to the String.
+    * @return
+    */
+   String addData(int initialColumnSeparation,
+           int columnSeparation, List<String> data)
+   {
+     String line = "";
+     boolean first = true;
+     for (String value : data)
+     {
+       int sep = first ? initialColumnSeparation : columnSeparation;
+       line += String.format("%" + sep + "s", value);
+       first = false;
+     }
+     return line;
+   }
+   /**
+    * Converts list of characters into a list of Strings.
+    * 
+    * @param list
+    * @return Returns the list of Strings.
+    */
+   List<String> charListToStringList(List<Character> list)
+   {
+     List<String> strList = new ArrayList<>();
+     for (char value : list)
+     {
+       String strValue = Character.toString(value);
+       strList.add(strValue);
+     }
+     return strList;
+   }
+   /**
+    * Converts an array of doubles into a list of Strings, rounded to the nearest
+    * 5th decimal place
+    * 
+    * @param doubles
+    * @param noOfDecimals
+    * @return
+    */
+   List<String> doublesToStringList(double[] doubles)
+   {
+     List<String> strList = new ArrayList<>();
+     for (double value : doubles)
+     {
+       String strValue;
+       if (value > 0)
+       {
+         strValue = String.format("%.5f", value);
+       }
+       else if (value == -0.00000d)
+       {
+         strValue = "0.00000";
+       }
+       else
+       {
+         strValue = "*";
+       }
+       strList.add(strValue);
+     }
+     return strList;
+   }
+   /**
+    * Appends model data in string format to the string builder
+    * 
+    * @param output
+    */
+   void appendModelAsString(StringBuilder output)
+   {
+     output.append(HMM).append("  ");
+     String charSymbols = hmm.getSymbols();
+     for (char c : charSymbols.toCharArray())
+     {
+       output.append(String.format("%9s", c));
+     }
+     output.append(NL).append(TRANSITIONTYPELINE);
+     int length = hmm.getLength();
+     for (int nodeNo = 0; nodeNo <= length; nodeNo++)
+     {
+       String matchLine = String.format("%7s",
+               nodeNo == 0 ? COMPO : Integer.toString(nodeNo));
+       double[] doubleMatches = convertToLogSpace(
+               hmm.getNode(nodeNo).getMatchEmissions());
+       List<String> strMatches = doublesToStringList(doubleMatches);
+       matchLine += addData(10, 9, strMatches);
+       if (nodeNo != 0)
+       {
+         matchLine += SPACE + (hmm.getNodeMapPosition(nodeNo));
+         matchLine += SPACE + hmm.getConsensusResidue(nodeNo);
+         matchLine += SPACE + hmm.getReferenceAnnotation(nodeNo);
+         if (hmm.getFileHeader().contains("HMMER3/f"))
+         {
+           matchLine += SPACE + hmm.getMaskedValue(nodeNo);
+           matchLine += SPACE + hmm.getConsensusStructure(nodeNo);
+         }
+       }
+       output.append(NL).append(matchLine);
+       
+       String insertLine = "";
+       double[] doubleInserts = convertToLogSpace(
+               hmm.getNode(nodeNo).getInsertEmissions());
+       List<String> strInserts = doublesToStringList(doubleInserts);
+       insertLine += addData(17, 9, strInserts);
+       output.append(NL).append(insertLine);
+       String transitionLine = "";
+       double[] doubleTransitions = convertToLogSpace(
+               hmm.getNode(nodeNo).getStateTransitions());
+       List<String> strTransitions = doublesToStringList(
+               doubleTransitions);
+       transitionLine += addData(17, 9, strTransitions);
+       output.append(NL).append(transitionLine);
+     }
+   }
+   /**
+    * Appends formatted HMM file properties to the string builder
+    * 
+    * @param output
+    */
+   void appendProperties(StringBuilder output)
+   {
+     output.append(hmm.getFileHeader());
+     String format = "%n%-5s %1s";
+     appendProperty(output, format, NAME);
+     appendProperty(output, format, ACCESSION_NUMBER);
+     appendProperty(output, format, DESCRIPTION);
+     appendProperty(output, format, LENGTH);
+     appendProperty(output, format, MAX_LENGTH);
+     appendProperty(output, format, ALPHABET);
+     appendBooleanProperty(output, format, REFERENCE_ANNOTATION);
+     appendBooleanProperty(output, format, MASKED_VALUE);
+     appendBooleanProperty(output, format, CONSENSUS_RESIDUE);
+     appendBooleanProperty(output, format, CONSENSUS_STRUCTURE);
+     appendBooleanProperty(output, format, MAP);
+     appendProperty(output, format, DATE);
+     appendProperty(output, format, NUMBER_OF_SEQUENCES);
+     appendProperty(output, format, EFF_NUMBER_OF_SEQUENCES);
+     appendProperty(output, format, CHECK_SUM);
+     appendProperty(output, format, GATHERING_THRESHOLD);
+     appendProperty(output, format, TRUSTED_CUTOFF);
+     appendProperty(output, format, NOISE_CUTOFF);
+     if (hmm.getMSV() != null)
+     {
+       format = "%n%-19s %18s";
+       output.append(String.format(format, "STATS LOCAL MSV", hmm.getMSV()));
+       output.append(String.format(format, "STATS LOCAL VITERBI",
+               hmm.getViterbi()));
+       output.append(String.format(format, "STATS LOCAL FORWARD",
+               hmm.getForward()));
+     }
+   }
+   /**
+    * Appends 'yes' or 'no' for the given property, according to whether or not
+    * it is set in the HMM
+    * 
+    * @param output
+    * @param format
+    * @param propertyName
+    */
+   private void appendBooleanProperty(StringBuilder output, String format,
+           String propertyName)
+   {
+     boolean set = hmm.getBooleanProperty(propertyName);
+     output.append(String.format(format, propertyName,
+             set ? HiddenMarkovModel.YES : HiddenMarkovModel.NO));
+   }
+   /**
+    * Appends the value of the given property to the output, if not null
+    * 
+    * @param output
+    * @param format
+    * @param propertyName
+    */
+   private void appendProperty(StringBuilder output, String format,
+           String propertyName)
+   {
+     String value = hmm.getProperty(propertyName);
+     if (value != null)
+     {
+       output.append(String.format(format, propertyName, value));
+     }
+   }
+   @Override
+   public String print(SequenceI[] sequences, boolean jvsuffix)
+   {
+     if (sequences[0].getHMM() != null)
+     {
+       hmm = sequences[0].getHMM();
+     }
+     return print();
+   }
+   /**
+    * Prints the .hmm file to a String.
+    * 
+    * @return
+    */
+   public String print()
+   {
+     StringBuilder output = new StringBuilder();
+     appendProperties(output);
+     output.append(NL);
+     appendModelAsString(output);
+     output.append(NL).append(TERMINATOR).append(NL);
+     return output.toString();
+   }
+   /**
+    * Converts the probabilities contained in an array into log space
+    * 
+    * @param ds
+    */
+   double[] convertToLogSpace(double[] ds)
+   {
+     double[] converted = new double[ds.length];
+     for (int i = 0; i < ds.length; i++)
+     {
+       double prob = ds[i];
+       double logProb = -1 * Math.log(prob);
+       converted[i] = logProb;
+     }
+     return converted;
+   }
+   /**
+    * Returns the HMM sequence produced by reading a .hmm file.
+    */
+   @Override
+   public SequenceI[] getSeqsAsArray()
+   {
+     SequenceI hmmSeq = hmm.getConsensusSequence();
+     SequenceI[] seq = new SequenceI[1];
+     seq[0] = hmmSeq;
+     return seq;
+   }
+   @Override
+   public void setNewlineString(String newLine)
+   {
+     NL = newLine;
+   }
+   @Override
 -  public void setExportSettings(AlignExportSettingI exportSettings)
++  public void setExportSettings(AlignExportSettingsI exportSettings)
+   {
+   }
+   @Override
+   public void configureForView(AlignmentViewPanel viewpanel)
+   {
+   }
+   @Override
+   public boolean hasWarningMessage()
+   {
+     return false;
+   }
+   @Override
+   public String getWarningMessage()
+   {
+     return "warning message";
+   }
+ }
Simple merge
@@@ -77,13 -78,18 +77,8 @@@ public class StockholmFile extends Alig
  {
    private static final String ANNOTATION = "annotation";
  
- //  private static final Regex OPEN_PAREN = new Regex("(<|\\[)", "(");
- //
- //  private static final Regex CLOSE_PAREN = new Regex("(>|\\])", ")");
-   public static final Regex DETECT_BRACKETS = new Regex(
-           "(<|>|\\[|\\]|\\(|\\)|\\{|\\})");
+   private static final char UNDERSCORE = '_';
+   
 -  private static final Regex OPEN_PAREN = new Regex("(<|\\[)", "(");
 -
 -  private static final Regex CLOSE_PAREN = new Regex("(>|\\])", ")");
 -
 -  // private static final Regex OPEN_PAREN = new Regex("(<|\\[)", "(");
 -  // private static final Regex CLOSE_PAREN = new Regex("(>|\\])", ")");
 -
 -  public static final Regex DETECT_BRACKETS = new Regex(
 -          "(<|>|\\[|\\]|\\(|\\)|\\{|\\})");
 -
    // WUSS extended symbols. Avoid ambiguity with protein SS annotations by using NOT_RNASS first.
    public static final String RNASS_BRACKETS = "<>[](){}AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz";
  
        if (alAnot != null)
        {
          Annotation[] ann;
-         for (int j = 0, nj = alAnot.length; j < nj; j++)
 -
+         for (int j = 0; j < alAnot.length; j++)
          {
-           String key = type2id(alAnot[j].label);
-           boolean isrna = alAnot[j].isValidStruc();
-           if (isrna)
-           {
-             // hardwire to secondary structure if there is RNA secondary
-             // structure on the annotation
-             key = "SS";
-           }
-           if (key == null)
+           if (alAnot[j].annotations != null)
            {
+             String key = type2id(alAnot[j].label);
+             boolean isrna = alAnot[j].isValidStruc();
  
-             continue;
-           }
+             if (isrna)
+             {
+               // hardwire to secondary structure if there is RNA secondary
+               // structure on the annotation
+               key = "SS";
+             }
+             if (key == null)
+             {
 -
+               continue;
+             }
  
-           // out.append("#=GR ");
-           out.append(new Format("%-" + maxid + "s").form(
-                   "#=GR " + printId(seq, jvSuffix) + " " + key + " "));
-           ann = alAnot[j].annotations;
-           String sseq = "";
-           for (int k = 0, nk = ann.length; k < nk; k++)
-           {
-             sseq += outputCharacter(key, k, isrna, ann, seq);
-           }
-           out.append(sseq);
-           out.append(newline);
+             // out.append("#=GR ");
+             out.append(new Format("%-" + maxid + "s").form(
+                     "#=GR " + printId(s[i], jvSuffix) + " " + key + " "));
+             ann = alAnot[j].annotations;
 -            String seq = "";
++            String sseq = "";
+             for (int k = 0; k < ann.length; k++)
+             {
 -              seq += outputCharacter(key, k, isrna, ann, s[i]);
++              sseq += outputCharacter(key, k, isrna, ann, s[i]);
+             }
 -            out.append(seq);
++            out.append(sseq);
+             out.append(newline);
 -          }
++        }
          }
        }
  
        out.append(new Format("%-" + maxid + "s")
@@@ -20,7 -20,7 +20,6 @@@
   */
  package jalview.io.packed;
  
--import jalview.api.FeatureColourI;
  import jalview.datamodel.AlignmentI;
  import jalview.io.AppletFormatAdapter;
  import jalview.io.FileFormatI;
@@@ -68,11 -70,11 +71,13 @@@ public class GAlignFrame extends JInter
  
    protected JMenuItem closeMenuItem = new JMenuItem();
  
 -  protected JMenu webService = new JMenu();
 +  public JMenu webService = new JMenu();// BH 2019 was protected, but not
 +                                        // sufficient for AlignFrame thread run
++    // JBP - followed suite for these other service related GUI elements.
++    // TODO: check we really need these to be public
++  public JMenu hmmerMenu = new JMenu();
  
-   public JMenuItem webServiceNoServices;// BH 2019 was protected, but not
-                                         // sufficient for AlignFrame thread run
 -  protected JMenu hmmerMenu = new JMenu();
 -
 -  protected JMenuItem webServiceNoServices;
++  public JMenuItem webServiceNoServices;
  
    protected JCheckBoxMenuItem viewBoxesMenuItem = new JCheckBoxMenuItem();
  
  
    protected JCheckBoxMenuItem normaliseSequenceLogo = new JCheckBoxMenuItem();
  
+   protected JCheckBoxMenuItem showInformationHistogram = new JCheckBoxMenuItem();
+   protected JCheckBoxMenuItem showHMMSequenceLogo = new JCheckBoxMenuItem();
+   protected JCheckBoxMenuItem normaliseHMMSequenceLogo = new JCheckBoxMenuItem();
    protected JCheckBoxMenuItem applyAutoAnnotationSettings = new JCheckBoxMenuItem();
  
 +  protected JMenuItem openFeatureSettings;
 +
    private SequenceAnnotationOrder annotationSortOrder;
  
    private boolean showAutoCalculatedAbove = false;
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        saveAs_actionPerformed(e);
 +        saveAs_actionPerformed();
        }
      };
+   
      // FIXME getDefaultToolkit throws an exception in Headless mode
      KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S,
              jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx()
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        delete_actionPerformed(e);
 +        delete_actionPerformed();
        }
      });
+   
      pasteMenu.setText(MessageManager.getString("action.paste"));
      JMenuItem pasteNew = new JMenuItem(
              MessageManager.getString("label.to_new_alignment"));
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        fetchSequence_actionPerformed(e);
 +        fetchSequence_actionPerformed();
        }
      });
+   
      JMenuItem associatedData = new JMenuItem(
              MessageManager.getString("label.load_features_annotations"));
      associatedData.addActionListener(new ActionListener()
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         associatedData_actionPerformed(e);
+         try
+         {
+           associatedData_actionPerformed(e);
+         } catch (IOException | InterruptedException e1)
+         {
+           // TODO Auto-generated catch block
+           e1.printStackTrace();
+         }
        }
      });
 -    loadVcf = new JMenuItem(MessageManager.getString("label.load_vcf_file"));
 +    loadVcf = new JMenuItem(
 +            MessageManager.getString("label.load_vcf_file"));
      loadVcf.setToolTipText(MessageManager.getString("label.load_vcf"));
      loadVcf.addActionListener(new ActionListener()
      {
      alignFrameMenuBar.add(formatMenu);
      alignFrameMenuBar.add(colourMenu);
      alignFrameMenuBar.add(calculateMenu);
 -    alignFrameMenuBar.add(webService);
 -    alignFrameMenuBar.add(hmmerMenu);
 +    if (!Platform.isJS())
 +    {
 +      alignFrameMenuBar.add(webService);
++      alignFrameMenuBar.add(hmmerMenu);
 +    }
+   
      fileMenu.add(fetchSequence);
      fileMenu.add(addSequenceMenu);
      fileMenu.add(reload);
      fileMenu.add(exportAnnotations);
      fileMenu.add(loadTreeMenuItem);
      fileMenu.add(associatedData);
 -    fileMenu.add(loadVcf);
 +    if (!Platform.isJS())
 +    {
 +      fileMenu.add(loadVcf);
 +    }
      fileMenu.addSeparator();
      fileMenu.add(closeMenuItem);
+   
      pasteMenu.add(pasteNew);
      pasteMenu.add(pasteThis);
      editMenu.add(undoMenuItem);
      calculateMenu.addSeparator();
      calculateMenu.add(expandAlignment);
      calculateMenu.add(extractScores);
 -    calculateMenu.addSeparator();
 -    calculateMenu.add(runGroovy);
 -  
 +    if (!Platform.isJS())
 +    {
 +      calculateMenu.addSeparator();
 +      calculateMenu.add(runGroovy);
 +    }
      webServiceNoServices = new JMenuItem(
              MessageManager.getString("label.no_services"));
      webService.add(webServiceNoServices);
@@@ -460,7 -478,135 +488,135 @@@ public class GPreferences extends JPane
    }
  
    /**
-    * Initialises the Output tab
+    * Initialises the hmmer tabbed panel
+    * 
+    * @return
+    */
+   private JPanel initHMMERTab()
+   {
+     hmmerTab = new JPanel();
+     hmmerTab.setLayout(new BoxLayout(hmmerTab, BoxLayout.Y_AXIS));
+     hmmerTab.setLayout(new MigLayout("flowy"));
+     /*
+      * path to hmmer binaries folder
+      */
+     JPanel installationPanel = new JPanel(new MigLayout("flowy"));
+     // new FlowLayout(FlowLayout.LEFT));
+     JvSwingUtils.createTitledBorder(installationPanel,
+             MessageManager.getString("label.installation"), true);
+     hmmerTab.add(installationPanel);
+     JLabel hmmerLocation = new JLabel(
+             MessageManager.getString("label.hmmer_location"));
+     hmmerLocation.setFont(LABEL_FONT);
+     final int pathFieldLength = 40;
+     hmmerPath = new JTextField(pathFieldLength);
+     hmmerPath.addMouseListener(new MouseAdapter()
+     {
+       @Override
+       public void mouseClicked(MouseEvent e)
+       {
+         if (e.getClickCount() == 2)
+         {
+           String chosen = openFileChooser(true);
+           if (chosen != null)
+           {
+             hmmerPath.setText(chosen);
+             validateHmmerPath();
+           }
+         }
+       }
+     });
+     installationPanel.add(hmmerLocation);
+     installationPanel.add(hmmerPath);
+     /*
+      * path to Cygwin binaries folder (for Windows)
+      */
 -    if (Platform.isWindows())
++    if (Platform.isWindowsAndNotJS())
+     {
+       JLabel cygwinLocation = new JLabel(
+               MessageManager.getString("label.cygwin_location"));
+       cygwinLocation.setFont(LABEL_FONT);
+       cygwinPath = new JTextField(pathFieldLength);
+       cygwinPath.addMouseListener(new MouseAdapter()
+       {
+         @Override
+         public void mouseClicked(MouseEvent e)
+         {
+           if (e.getClickCount() == 2)
+           {
+             String chosen = openFileChooser(true);
+             if (chosen != null)
+             {
+               cygwinPath.setText(chosen);
+               validateCygwinPath();
+             }
+           }
+         }
+       });
+       installationPanel.add(cygwinLocation);
+       installationPanel.add(cygwinPath);
+     }
+     /*
+      * preferences for hmmalign
+      */
+     JPanel alignOptionsPanel = new JPanel(new MigLayout());
+     // new FlowLayout(FlowLayout.LEFT));
+     JvSwingUtils.createTitledBorder(alignOptionsPanel,
+             MessageManager.getString("label.hmmalign_options"), true);
+     hmmerTab.add(alignOptionsPanel);
+     hmmrTrimTermini = new JCheckBox();
+     hmmrTrimTermini.setFont(LABEL_FONT);
+     hmmrTrimTermini.setText(MessageManager.getString("label.trim_termini"));
+     alignOptionsPanel.add(hmmrTrimTermini);
+     /*
+      * preferences for hmmsearch
+      */
+     JPanel searchOptions = new JPanel(new MigLayout());
+     // FlowLayout(FlowLayout.LEFT));
+     JvSwingUtils.createTitledBorder(searchOptions,
+             MessageManager.getString("label.hmmsearch_options"), true);
+     hmmerTab.add(searchOptions);
+     JLabel sequencesToKeep = new JLabel(
+             MessageManager.getString("label.no_of_sequences"));
+     sequencesToKeep.setFont(LABEL_FONT);
+     searchOptions.add(sequencesToKeep);
+     hmmerSequenceCount = new JTextField(5);
+     searchOptions.add(hmmerSequenceCount);
+     /*
+      * preferences for Information Content annotation
+      */
+     // JPanel dummy = new JPanel(new FlowLayout(FlowLayout.LEFT));
+     JPanel annotationOptions = new JPanel(new MigLayout("left"));
+     JvSwingUtils.createTitledBorder(annotationOptions,
+             MessageManager.getString("label.information_annotation"), true);
+     // dummy.add(annotationOptions);
+     hmmerTab.add(annotationOptions);
+     ButtonGroup backgroundOptions = new ButtonGroup();
+     hmmerBackgroundUniprot = new JRadioButton(
+             MessageManager.getString("label.freq_uniprot"));
+     hmmerBackgroundUniprot.setFont(LABEL_FONT);
+     hmmerBackgroundAlignment = new JRadioButton(
+             MessageManager.getString("label.freq_alignment"));
+     hmmerBackgroundAlignment.setFont(LABEL_FONT);
+     backgroundOptions.add(hmmerBackgroundUniprot);
+     backgroundOptions.add(hmmerBackgroundAlignment);
+     backgroundOptions.setSelected(hmmerBackgroundUniprot.getModel(), true);
+     // disable buttons for now as annotation only uses Uniprot background
+     hmmerBackgroundAlignment.setEnabled(false);
+     hmmerBackgroundUniprot.setEnabled(false);
+     annotationOptions.add(hmmerBackgroundUniprot, "wrap");
+     annotationOptions.add(hmmerBackgroundAlignment);
+     return hmmerTab;
+   }
+   /**
+    * Initialises the Output tabbed panel.
     * 
     * @return
     */
Simple merge
@@@ -67,10 -70,16 +69,16 @@@ public class AnnotationRendere
  
    private FontMetrics fm;
  
 -  private final boolean MAC = Platform.isAMac();
 +  private final boolean USE_FILL_ROUND_RECT = Platform.isAMacAndNotJS();
  
-   boolean av_renderHistogram = true, av_renderProfile = true,
-           av_normaliseProfile = false;
+   // todo remove these flags, read from group/viewport where needed
+   boolean av_renderHistogram = true;
+   boolean av_renderProfile = true;
+   boolean av_normaliseProfile = false;
+   boolean av_infoHeight = false;
  
    ResidueShaderI profcolour = null;
  
      for (int i = 0; i < aa.length; i++)
      {
        AlignmentAnnotation row = aa[i];
 -      isRNA = row.isRNA();
 +      boolean renderHistogram = true;
 +      boolean renderProfile = false;
 +      boolean normaliseProfile = false;
 +      boolean isRNA = row.isRNA();
 +
 +      // check if this is a consensus annotation row and set the display
 +      // settings appropriately
 +      // TODO: generalise this to have render styles for consensus/profile
 +      // data
 +      if (row.groupRef != null && row == row.groupRef.getConsensus())
        {
 -        // check if this is a consensus annotation row and set the display
 -        // settings appropriately
 -        // TODO: generalise this to have render styles for consensus/profile
 -        // data
 -        if (row.groupRef != null && row == row.groupRef.getConsensus())
 -        {
 -          renderHistogram = row.groupRef.isShowConsensusHistogram();
 -          renderProfile = row.groupRef.isShowSequenceLogo();
 -          normaliseProfile = row.groupRef.isNormaliseSequenceLogo();
 -        }
 -        else if (row == consensusAnnot || row == structConsensusAnnot
 -                || row == complementConsensusAnnot)
 -        {
 -          renderHistogram = av_renderHistogram;
 -          renderProfile = av_renderProfile;
 -          normaliseProfile = av_normaliseProfile;
 -        }
 -        else if (InformationThread.HMM_CALC_ID.equals(row.getCalcId()))
 +        renderHistogram = row.groupRef.isShowConsensusHistogram();
 +        renderProfile = row.groupRef.isShowSequenceLogo();
 +        normaliseProfile = row.groupRef.isNormaliseSequenceLogo();
 +      }
 +      else if (row == consensusAnnot || row == structConsensusAnnot
 +              || row == complementConsensusAnnot)
 +      {
 +        renderHistogram = av_renderHistogram;
 +        renderProfile = av_renderProfile;
 +        normaliseProfile = av_normaliseProfile;
 +      }
++      else if (InformationThread.HMM_CALC_ID.equals(row.getCalcId()))
++      {
++        if (row.groupRef != null)
+         {
 -          if (row.groupRef != null)
 -          {
 -            renderHistogram = row.groupRef.isShowInformationHistogram();
 -            renderProfile = row.groupRef.isShowHMMSequenceLogo();
 -            normaliseProfile = row.groupRef.isNormaliseHMMSequenceLogo();
 -          }
 -          else
 -          {
 -            renderHistogram = av.isShowInformationHistogram();
 -            renderProfile = av.isShowHMMSequenceLogo();
 -            normaliseProfile = av.isNormaliseHMMSequenceLogo();
 -          }
++          renderHistogram = row.groupRef.isShowInformationHistogram();
++          renderProfile = row.groupRef.isShowHMMSequenceLogo();
++          normaliseProfile = row.groupRef.isNormaliseHMMSequenceLogo();
+         }
+         else
+         {
 -          renderHistogram = true;
 -          // don't need to set render/normaliseProfile since they are not
 -          // currently used in any other annotation track renderer
++          renderHistogram = av.isShowInformationHistogram();
++          renderProfile = av.isShowHMMSequenceLogo();
++          normaliseProfile = av.isNormaliseHMMSequenceLogo();
+         }
+       }
++      else if (row == consensusAnnot || row == structConsensusAnnot
++              || row == complementConsensusAnnot)
++      {
++        renderHistogram = av_renderHistogram;
++        renderProfile = av_renderProfile;
++        normaliseProfile = av_normaliseProfile;
++      }
 +
        Annotation[] row_annotations = row.annotations;
        if (!row.visible)
        {
Simple merge
@@@ -717,10 -737,10 +739,10 @@@ public abstract class AlignmentViewpor
     * results of secondary structure base pair consensus for visible portion of
     * view
     */
 -  protected Hashtable[] hStrucConsensus = null;
 +  protected Hashtable<String, Object>[] hStrucConsensus = null;
  
    protected Conservation hconservation = null;
+   
    @Override
    public void setConservation(Conservation cons)
    {
    }
  
    @Override
+   public void setHmmProfiles(ProfilesI info)
+   {
+     hmmProfiles = info;
+   }
+   @Override
+   public ProfilesI getHmmProfiles()
+   {
+     return hmmProfiles;
+   }
+   @Override
 -  public Hashtable[] getComplementConsensusHash()
 +  public Hashtable<String, Object>[] getComplementConsensusHash()
    {
      return hcomplementConsensus;
    }
    }
  
    @Override
 +  public AlignmentExportData getAlignExportData(AlignExportSettingsI options)
 +  {
 +    AlignmentI alignmentToExport = null;
 +    String[] omitHidden = null;
 +    alignmentToExport = null;
 +
 +    if (hasHiddenColumns() && !options.isExportHiddenColumns())
 +    {
 +      omitHidden = getViewAsString(false,
 +              options.isExportHiddenSequences());
 +    }
 +
 +    int[] alignmentStartEnd = new int[2];
 +    if (hasHiddenRows() && options.isExportHiddenSequences())
 +    {
 +      alignmentToExport = getAlignment().getHiddenSequences()
 +              .getFullAlignment();
 +    }
 +    else
 +    {
 +      alignmentToExport = getAlignment();
 +    }
 +    alignmentStartEnd = getAlignment().getHiddenColumns()
 +            .getVisibleStartAndEndIndex(alignmentToExport.getWidth());
 +    AlignmentExportData ed = new AlignmentExportData(alignmentToExport,
 +            omitHidden, alignmentStartEnd);
 +    return ed;
 +  }
 +  
++  @Override
+   public boolean isNormaliseSequenceLogo()
+   {
+     return normaliseSequenceLogo;
+   }
+   public void setNormaliseSequenceLogo(boolean state)
+   {
+     normaliseSequenceLogo = state;
+   }
+   @Override
+   public boolean isNormaliseHMMSequenceLogo()
+   {
+     return hmmNormaliseSequenceLogo;
+   }
+   public void setNormaliseHMMSequenceLogo(boolean state)
+   {
+     hmmNormaliseSequenceLogo = state;
+   }
 -
    /**
     * flag set to indicate if structure views might be out of sync with sequences
     * in the alignment
@@@ -40,8 -40,6 +40,7 @@@ import jalview.ws.ebi.EBIFetchClient
  import jalview.xml.binding.embl.EntryType;
  import jalview.xml.binding.embl.EntryType.Feature;
  import jalview.xml.binding.embl.EntryType.Feature.Qualifier;
- import jalview.xml.binding.jalview.JalviewModel;
 +import jalview.xml.binding.embl.ROOT;
  import jalview.xml.binding.embl.XrefType;
  
  import java.io.File;
Simple merge
@@@ -98,10 -98,10 +98,10 @@@ public class JabaParamStore implements 
    @Override
    public List<WsParamSetI> getPresets()
    {
 -    List<WsParamSetI> prefs = new ArrayList();
 +    List<WsParamSetI> prefs = new ArrayList<>();
      if (servicePresets == null)
      {
-       servicePresets = new Hashtable<String, JabaPreset>();
+       servicePresets = new Hashtable<>();
        PresetManager prman;
        if ((prman = service.getPresets()) != null)
        {
@@@ -347,240 -343,12 +342,17 @@@ public class Jws2Discoverer implements 
    private void populateWSMenuEntry(JMenu jws2al,
            final AlignFrame alignFrame, String typeFilter)
    {
-     if (running || services == null || services.size() == 0)
-     {
-       return;
-     }
-     /**
-      * eventually, JWS2 services will appear under the same align/etc submenus.
-      * for moment we keep them separate.
-      */
-     JMenu atpoint;
-     List<Jws2Instance> enumerableServices = new ArrayList<>();
-     // jws2al.removeAll();
-     Map<String, Jws2Instance> preferredHosts = new HashMap<>();
-     Map<String, List<Jws2Instance>> alternates = new HashMap<>();
-     for (Jws2Instance service : services.toArray(new Jws2Instance[0]))
-     {
-       if (!isRecalculable(service.action))
-       {
-         // add 'one shot' services to be displayed using the classic menu
-         // structure
-         enumerableServices.add(service);
-       }
-       else
-       {
-         if (!preferredHosts.containsKey(service.serviceType))
-         {
-           Jws2Instance preferredInstance = getPreferredServiceFor(
-                   alignFrame, service.serviceType);
-           if (preferredInstance != null)
-           {
-             preferredHosts.put(service.serviceType, preferredInstance);
-           }
-           else
-           {
-             preferredHosts.put(service.serviceType, service);
-           }
-         }
-         List<Jws2Instance> ph = alternates.get(service.serviceType);
-         if (preferredHosts.get(service.serviceType) != service)
-         {
-           if (ph == null)
-           {
-             ph = new ArrayList<>();
-           }
-           ph.add(service);
-           alternates.put(service.serviceType, ph);
-         }
-       }
-     }
-     // create GUI element for classic services
-     addEnumeratedServices(jws2al, alignFrame, enumerableServices);
-     // and the instantaneous services
-     for (final Jws2Instance service : preferredHosts.values())
-     {
-       atpoint = JvSwingUtils.findOrCreateMenu(jws2al, service.action);
-       JMenuItem hitm;
-       if (atpoint.getItemCount() > 1)
-       {
-         // previous service of this type already present
-         atpoint.addSeparator();
-       }
-       atpoint.add(hitm = new JMenuItem(service.getHost()));
-       hitm.setForeground(Color.blue);
-       hitm.addActionListener(new ActionListener()
-       {
-         @Override
-         public void actionPerformed(ActionEvent e)
-         {
-           Desktop.showUrl(service.getHost());
-         }
-       });
-       hitm.setToolTipText(JvSwingUtils.wrapTooltip(false,
-               MessageManager.getString("label.open_jabaws_web_page")));
-       service.attachWSMenuEntry(atpoint, alignFrame);
-       if (alternates.containsKey(service.serviceType))
-       {
-         atpoint.add(hitm = new JMenu(
-                 MessageManager.getString("label.switch_server")));
-         hitm.setToolTipText(JvSwingUtils.wrapTooltip(false,
-                 MessageManager.getString("label.choose_jabaws_server")));
-         for (final Jws2Instance sv : alternates.get(service.serviceType))
-         {
-           JMenuItem itm;
-           hitm.add(itm = new JMenuItem(sv.getHost()));
-           itm.setForeground(Color.blue);
-           itm.addActionListener(new ActionListener()
-           {
-             @Override
-             public void actionPerformed(ActionEvent arg0)
-             {
-               new Thread(new Runnable()
-               {
-                 @Override
-                 public void run()
-                 {
-                   setPreferredServiceFor(alignFrame, sv.serviceType,
-                           sv.action, sv);
-                   changeSupport.firePropertyChange("services",
-                           new Vector<Jws2Instance>(), services);
-                 }
-               }).start();
-             }
-           });
-         }
-       }
-     }
-   }
-   /**
-    * add services using the Java 2.5/2.6/2.7 system which optionally creates
-    * submenus to index by host and service program type
-    */
-   private void addEnumeratedServices(final JMenu jws2al,
-           final AlignFrame alignFrame,
-           List<Jws2Instance> enumerableServices)
-   {
-     boolean byhost = Cache.getDefault("WSMENU_BYHOST", false),
-             bytype = Cache.getDefault("WSMENU_BYTYPE", false);
-     /**
-      * eventually, JWS2 services will appear under the same align/etc submenus.
-      * for moment we keep them separate.
-      */
-     JMenu atpoint;
-     List<String> hostLabels = new ArrayList<>();
-     Hashtable<String, String> lasthostFor = new Hashtable<>();
-     Hashtable<String, ArrayList<Jws2Instance>> hosts = new Hashtable<>();
-     ArrayList<String> hostlist = new ArrayList<>();
-     for (Jws2Instance service : enumerableServices)
-     {
-       ArrayList<Jws2Instance> hostservices = hosts.get(service.getHost());
-       if (hostservices == null)
-       {
-         hosts.put(service.getHost(),
-                 hostservices = new ArrayList<>());
-         hostlist.add(service.getHost());
-       }
-       hostservices.add(service);
-     }
-     // now add hosts in order of the given array
-     for (String host : hostlist)
-     {
-       Jws2Instance orderedsvcs[] = hosts.get(host)
-               .toArray(new Jws2Instance[1]);
-       String sortbytype[] = new String[orderedsvcs.length];
-       for (int i = 0; i < sortbytype.length; i++)
-       {
-         sortbytype[i] = orderedsvcs[i].serviceType;
-       }
-       jalview.util.QuickSort.sort(sortbytype, orderedsvcs);
-       for (final Jws2Instance service : orderedsvcs)
-       {
-         atpoint = JvSwingUtils.findOrCreateMenu(jws2al, service.action);
-         String type = service.serviceType;
-         if (byhost)
-         {
-           atpoint = JvSwingUtils.findOrCreateMenu(atpoint, host);
-           if (atpoint.getToolTipText() == null)
-           {
-             atpoint.setToolTipText(MessageManager
-                     .formatMessage("label.services_at", new String[]
-                     { host }));
-           }
-         }
-         if (bytype)
-         {
-           atpoint = JvSwingUtils.findOrCreateMenu(atpoint, type);
-           if (atpoint.getToolTipText() == null)
-           {
-             atpoint.setToolTipText(service.getActionText());
-           }
-         }
-         if (!byhost && !hostLabels.contains(
-                 host + service.serviceType + service.getActionText()))
-         // !hostLabels.contains(host + (bytype ?
-         // service.serviceType+service.getActionText() : "")))
-         {
-           // add a marker indicating where this service is hosted
-           // relies on services from the same host being listed in a
-           // contiguous
-           // group
-           JMenuItem hitm;
-           if (hostLabels.contains(host))
-           {
-             atpoint.addSeparator();
-           }
-           else
-           {
-             hostLabels.add(host);
-           }
-           if (lasthostFor.get(service.action) == null
-                   || !lasthostFor.get(service.action).equals(host))
-           {
-             atpoint.add(hitm = new JMenuItem(host));
-             hitm.setForeground(Color.blue);
-             hitm.addActionListener(new ActionListener()
-             {
-               @Override
-               public void actionPerformed(ActionEvent e)
-               {
-                 Desktop.showUrl(service.getHost());
-               }
-             });
-             hitm.setToolTipText(
-                     JvSwingUtils.wrapTooltip(true, MessageManager
-                             .getString("label.open_jabaws_web_page")));
-             lasthostFor.put(service.action, host);
-           }
-           hostLabels.add(
-                   host + service.serviceType + service.getActionText());
-         }
-         service.attachWSMenuEntry(atpoint, alignFrame);
-       }
-     }
+     PreferredServiceRegistry.getRegistry().populateWSMenuEntry(
+             getServices(),
+             changeSupport, jws2al,
+             alignFrame, typeFilter);
    }
  
 +  /**
 +   * 
 +   * @param args
 +   * @j2sIgnore
 +   */
    public static void main(String[] args)
    {
      if (args.length > 0)
Simple merge
index 0000000,81dfa30..132408b
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,966 +1,966 @@@
+ /*
+  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+  * Copyright (C) $$Year-Rel$$ The Jalview Authors
+  * 
+  * This file is part of Jalview.
+  * 
+  * Jalview is free software: you can redistribute it and/or
+  * modify it under the terms of the GNU General Public License 
+  * as published by the Free Software Foundation, either version 3
+  * of the License, or (at your option) any later version.
+  *  
+  * Jalview is distributed in the hope that it will be useful, but 
+  * WITHOUT ANY WARRANTY; without even the implied warranty 
+  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+  * PURPOSE.  See the GNU General Public License for more details.
+  * 
+  * You should have received a copy of the GNU General Public License
+  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+  * The Jalview Authors are detailed in the 'AUTHORS' file.
+  */
+ package jalview.ws.jws2;
+ import jalview.analysis.AlignSeq;
+ import jalview.analysis.AlignmentAnnotationUtils;
+ import jalview.analysis.SeqsetUtils;
+ import jalview.api.AlignViewportI;
+ import jalview.api.AlignmentViewPanel;
+ import jalview.api.FeatureColourI;
+ import jalview.bin.Cache;
+ import jalview.datamodel.AlignmentAnnotation;
+ import jalview.datamodel.AlignmentI;
+ import jalview.datamodel.AnnotatedCollectionI;
+ import jalview.datamodel.Annotation;
+ import jalview.datamodel.ContiguousI;
+ import jalview.datamodel.Mapping;
+ import jalview.datamodel.SequenceI;
+ import jalview.datamodel.features.FeatureMatcherSetI;
+ import jalview.gui.AlignFrame;
+ import jalview.gui.Desktop;
+ import jalview.gui.IProgressIndicator;
+ import jalview.gui.IProgressIndicatorHandler;
+ import jalview.gui.JvOptionPane;
+ import jalview.gui.WebserviceInfo;
+ import jalview.schemes.FeatureSettingsAdapter;
+ import jalview.schemes.ResidueProperties;
+ import jalview.util.MapList;
+ import jalview.util.MessageManager;
+ import jalview.workers.AlignCalcWorker;
+ import jalview.ws.JobStateSummary;
+ import jalview.ws.api.CancellableI;
+ import jalview.ws.api.JalviewServiceEndpointProviderI;
+ import jalview.ws.api.JobId;
+ import jalview.ws.api.SequenceAnnotationServiceI;
+ import jalview.ws.api.ServiceWithParameters;
+ import jalview.ws.api.WSAnnotationCalcManagerI;
+ import jalview.ws.gui.AnnotationWsJob;
+ import jalview.ws.jws2.dm.AAConSettings;
+ import jalview.ws.params.ArgumentI;
+ import jalview.ws.params.WsParamSetI;
+ import java.util.ArrayList;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.Map;
+ public class SeqAnnotationServiceCalcWorker extends AlignCalcWorker
+         implements WSAnnotationCalcManagerI
+ {
+   protected ServiceWithParameters service;
+   protected WsParamSetI preset;
+   protected List<ArgumentI> arguments;
+   protected IProgressIndicator guiProgress;
+   protected boolean submitGaps = true;
+   /**
+    * by default, we filter out non-standard residues before submission
+    */
+   protected boolean filterNonStandardResidues = true;
+   /**
+    * Recover any existing parameters for this service
+    */
+   protected void initViewportParams()
+   {
+     if (getCalcId() != null)
+     {
+       ((jalview.gui.AlignViewport) alignViewport).setCalcIdSettingsFor(
+               getCalcId(),
+               new AAConSettings(true, service, this.preset, arguments),
+               true);
+     }
+   }
+   /**
+    * 
+    * @return null or a string used to recover all annotation generated by this
+    *         worker
+    */
+   public String getCalcId()
+   {
+     return service.getAlignAnalysisUI() == null ? null
+             : service.getAlignAnalysisUI().getCalcId();
+   }
+   public WsParamSetI getPreset()
+   {
+     return preset;
+   }
+   public List<ArgumentI> getArguments()
+   {
+     return arguments;
+   }
+   /**
+    * reconfigure and restart the AAConClient. This method will spawn a new
+    * thread that will wait until any current jobs are finished, modify the
+    * parameters and restart the conservation calculation with the new values.
+    * 
+    * @param newpreset
+    * @param newarguments
+    */
+   public void updateParameters(final WsParamSetI newpreset,
+           final List<ArgumentI> newarguments)
+   {
+     preset = newpreset;
+     arguments = newarguments;
+     calcMan.startWorker(this);
+     initViewportParams();
+   }
+   protected boolean alignedSeqs = true;
+   protected boolean nucleotidesAllowed = false;
+   protected boolean proteinAllowed = false;
+   /**
+    * record sequences for mapping result back to afterwards
+    */
+   protected boolean bySequence = false;
+   protected Map<String, SequenceI> seqNames;
+   // TODO: convert to bitset
+   protected boolean[] gapMap;
+   int realw;
+   protected int start;
+   int end;
+   private AlignFrame alignFrame;
+   public boolean[] getGapMap()
+   {
+     return gapMap;
+   }
+   public SeqAnnotationServiceCalcWorker(AlignViewportI alignViewport,
+           AlignmentViewPanel alignPanel)
+   {
+     super(alignViewport, alignPanel);
+   }
+   public SeqAnnotationServiceCalcWorker(ServiceWithParameters service,
+           AlignFrame alignFrame,
+           WsParamSetI preset, List<ArgumentI> paramset)
+   {
+     this(alignFrame.getCurrentView(), alignFrame.alignPanel);
+     // TODO: both these fields needed ?
+     this.alignFrame = alignFrame;
+     this.guiProgress = alignFrame;
+     this.preset = preset;
+     this.arguments = paramset;
+     this.service = service;
+     try
+     {
+       annotService = (jalview.ws.api.SequenceAnnotationServiceI) ((JalviewServiceEndpointProviderI) service)
+               .getEndpoint();
+     } catch (ClassCastException cce)
+     {
+       JvOptionPane.showMessageDialog(Desktop.desktop,
+               MessageManager.formatMessage(
+                       "label.service_called_is_not_an_annotation_service",
+                       new String[]
+                       { service.getName() }),
+               MessageManager.getString("label.internal_jalview_error"),
+               JvOptionPane.WARNING_MESSAGE);
+     }
+     // configure submission flags
+     proteinAllowed = service.isProteinService();
+     nucleotidesAllowed = service.isNucleotideService();
+     alignedSeqs = service.isNeedsAlignedSequences();
+     bySequence = !service.isAlignmentAnalysis();
+     filterNonStandardResidues = service.isFilterSymbols();
+     min_valid_seqs = service.getMinimumInputSequences();
+     submitGaps = service.isAlignmentAnalysis();
+     if (service.isInteractiveUpdate())
+     {
+       initViewportParams();
+     }
+   }
+   /**
+    * 
+    * @return true if the submission thread should attempt to submit data
+    */
+   public boolean hasService()
+   {
+     return annotService != null;
+   }
+   protected jalview.ws.api.SequenceAnnotationServiceI annotService = null;
+   volatile JobId rslt = null;
+   AnnotationWsJob running = null;
+   private int min_valid_seqs;
+   @Override
+   public void run()
+   {
+     if (checkDone())
+     {
+       return;
+     }
+     if (!hasService())
+     {
+       calcMan.workerComplete(this);
+       return;
+     }
+     long progressId = -1;
+     int serverErrorsLeft = 3;
+     final boolean cancellable = CancellableI.class
+             .isAssignableFrom(annotService.getClass());
+     StringBuffer msg = new StringBuffer();
+     JobStateSummary job = new JobStateSummary();
+     WebserviceInfo info = new WebserviceInfo("foo", "bar", false);
+     try
+     {
+       List<SequenceI> seqs = getInputSequences(
+               alignViewport.getAlignment(),
+               bySequence ? alignViewport.getSelectionGroup() : null);
+       if (seqs == null || !checkValidInputSeqs(seqs))
+       {
+         jalview.bin.Cache.log.debug(
+                 "Sequences for analysis service were null or not valid");
+         calcMan.workerComplete(this);
+         return;
+       }
+       if (guiProgress != null)
+       {
+         guiProgress.setProgressBar(service.getActionText(),
+                 progressId = System.currentTimeMillis());
+       }
+       jalview.bin.Cache.log.debug("submitted " + seqs.size()
+               + " sequences to " + service.getActionText());
+       rslt = annotService.submitToService(seqs, getPreset(),
+               getArguments());
+       if (rslt == null)
+       {
+         return;
+       }
+       // TODO: handle job submission error reporting here.
+       Cache.log.debug("Service " + service.getUri() + "\nSubmitted job ID: "
+               + rslt);
+       ;
+       // ///
+       // otherwise, construct WsJob and any UI handlers
+       running = new AnnotationWsJob();
+       running.setJobHandle(rslt);
+       running.setSeqNames(seqNames);
+       running.setStartPos(start);
+       running.setSeqs(seqs);
+       job.updateJobPanelState(info, "", running);
+       if (guiProgress != null)
+       {
+         guiProgress.registerHandler(progressId,
+                 new IProgressIndicatorHandler()
+                 {
+                   @Override
+                   public boolean cancelActivity(long id)
+                   {
+                     ((CancellableI) annotService).cancel(running);
+                     return true;
+                   }
+                   @Override
+                   public boolean canCancel()
+                   {
+                     return cancellable;
+                   }
+                 });
+       }
+       
+       // ///
+       // and poll for updates until job finishes, fails or becomes stale
+       
+       boolean finished = false;
+       do
+       {
+         Cache.log.debug("Updating status for annotation service.");
+         annotService.updateStatus(running);
+         job.updateJobPanelState(info, "", running);
+         if (running.isSubjobComplete())
+         {
+           Cache.log.debug(
+                   "Finished polling analysis service job: status reported is "
+                           + running.getState());
+           finished = true;
+         }
+         else
+         {
+           Cache.log.debug("Status now " + running.getState());
+         }
+         if (calcMan.isPending(this) && isInteractiveUpdate())
+         {
+           Cache.log.debug("Analysis service job is stale. aborting.");
+           // job has become stale.
+           if (!finished) {
+             finished = true;
+             // cancel this job and yield to the new job
+             try
+             {
+               if (cancellable
+                         && ((CancellableI) annotService).cancel(running))
+               {
+                 System.err.println("Cancelled job: " + rslt);
+               }
+               else
+               {
+                 System.err.println("FAILED TO CANCEL job: " + rslt);
+               }
+   
+             } catch (Exception x)
+             {
+   
+             }
+           }
+           rslt = running.getJobHandle();
+           return;
+         }
+         // pull any stats - some services need to flush log output before
+         // results are available
+         Cache.log.debug("Updating progress log for annotation service.");
+         try
+         {
+         annotService.updateJobProgress(running);
+         } catch (Throwable thr)
+         {
+           Cache.log.debug("Ignoring exception during progress update.",
+                   thr);
+         }
+         Cache.log.trace("Result of poll: " + running.getStatus());
+         if (!finished && !running.isFailed())
+         {
+           try
+           {
+             Cache.log.debug("Analysis service job thread sleeping.");
+             Thread.sleep(200);
+             Cache.log.debug("Analysis service job thread woke.");
+           } catch (InterruptedException x)
+           {
+           }
+           ;
+         }
+       } while (!finished);
+       Cache.log.debug("Job poll loop exited. Job is " + running.getState());
+       // TODO: need to poll/retry
+       if (serverErrorsLeft > 0)
+       {
+         try
+         {
+           Thread.sleep(200);
+         } catch (InterruptedException x)
+         {
+         }
+       }
+       if (running.isFinished())
+       {
+         // expect there to be results to collect
+         // configure job with the associated view's feature renderer, if one
+         // exists.
+         // TODO: here one would also grab the 'master feature renderer' in order
+         // to enable/disable
+         // features automatically according to user preferences
+         running.setFeatureRenderer(
+                 ((jalview.gui.AlignmentPanel) ap).cloneFeatureRenderer());
+         Cache.log.debug("retrieving job results.");
+         final Map<String, FeatureColourI> featureColours = new HashMap<>();
+         final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
+         List<AlignmentAnnotation> returnedAnnot = annotService
+                 .getAnnotationResult(running.getJobHandle(), seqs,
+                         featureColours, featureFilters);
+         Cache.log.debug("Obtained " + (returnedAnnot == null ? "no rows"
+                 : ("" + returnedAnnot.size())));
+         Cache.log.debug("There were " + featureColours.size()
+                 + " feature colours and " + featureFilters.size()
+                 + " filters defined.");
+         // TODO
+         // copy over each annotation row reurned and also defined on each
+         // sequence, excluding regions not annotated due to gapMap/column
+         // visibility
+         // update calcId if it is not already set on returned annotation
+         if (returnedAnnot != null)
+         {
+           for (AlignmentAnnotation aa : returnedAnnot)
+           {
+             // assume that any CalcIds already set
+             if (getCalcId() != null && aa.getCalcId() == null
+                     || "".equals(aa.getCalcId()))
+             {
+               aa.setCalcId(getCalcId());
+             }
+             // autocalculated annotation are created by interactive alignment
+             // analysis services
+             aa.autoCalculated = service.isAlignmentAnalysis()
+                     && service.isInteractiveUpdate();
+           }
+         }
+         running.setAnnotation(returnedAnnot);
+         if (running.hasResults())
+         {
+           jalview.bin.Cache.log.debug("Updating result annotation from Job "
+                   + rslt + " at " + service.getUri());
+           updateResultAnnotation(true);
+           if (running.isTransferSequenceFeatures())
+           {
+             // TODO
+             // look at each sequence and lift over any features, excluding
+             // regions
+             // not annotated due to gapMap/column visibility
+             jalview.bin.Cache.log.debug(
+                     "Updating feature display settings and transferring features from Job "
+                             + rslt + " at " + service.getUri());
+             // TODO: consider merge rather than apply here
+             alignViewport.applyFeaturesStyle(new FeatureSettingsAdapter()
+             {
+               @Override
+               public FeatureColourI getFeatureColour(String type)
+               {
+                 return featureColours.get(type);
+               }
+               @Override
+               public FeatureMatcherSetI getFeatureFilters(String type)
+               {
+                 return featureFilters.get(type);
+               }
+               @Override
+               public boolean isFeatureDisplayed(String type)
+               {
+                 return featureColours.containsKey(type);
+               }
+             });
+             // TODO: JAL-1150 - create sequence feature settings API for
+             // defining
+             // styles and enabling/disabling feature overlay on alignment panel
+             if (alignFrame.alignPanel == ap)
+             {
+               alignViewport.setShowSequenceFeatures(true);
+               alignFrame.setMenusForViewport();
+             }
+           }
+           ap.adjustAnnotationHeight();
+         }
+       }
+       Cache.log.debug("Annotation Service Worker thread finished.");
+     }
+ // TODO: use service specitic exception handlers
+ //    catch (JobSubmissionException x)
+ //    {
+ //
+ //      System.err.println(
+ //              "submission error with " + getServiceActionText() + " :");
+ //      x.printStackTrace();
+ //      calcMan.disableWorker(this);
+ //    } catch (ResultNotAvailableException x)
+ //    {
+ //      System.err.println("collection error:\nJob ID: " + rslt);
+ //      x.printStackTrace();
+ //      calcMan.disableWorker(this);
+ //
+ //    } catch (OutOfMemoryError error)
+ //    {
+ //      calcMan.disableWorker(this);
+ //
+ //      ap.raiseOOMWarning(getServiceActionText(), error);
+ //    } 
+     catch (Throwable x)
+     {
+       calcMan.disableWorker(this);
+       System.err
+               .println("Blacklisting worker due to unexpected exception:");
+       x.printStackTrace();
+     } finally
+     {
+       calcMan.workerComplete(this);
+       if (ap != null)
+       {
+         if (guiProgress != null && progressId != -1)
+         {
+           guiProgress.setProgressBar("", progressId);
+         }
+         // TODO: may not need to paintAlignment again !
+         ap.paintAlignment(false, false);
+       }
+       if (msg.length() > 0)
+       {
+         // TODO: stash message somewhere in annotation or alignment view.
+         // code below shows result in a text box popup
+         /*
+          * jalview.gui.CutAndPasteTransfer cap = new
+          * jalview.gui.CutAndPasteTransfer(); cap.setText(msg.toString());
+          * jalview.gui.Desktop.addInternalFrame(cap,
+          * "Job Status for "+getServiceActionText(), 600, 400);
+          */
+       }
+     }
+   }
+   /**
+    * validate input for dynamic/non-dynamic update context TODO: move to
+    * analysis interface ?
+    * @param seqs
+    * 
+    * @return true if input is valid
+    */
+   boolean checkValidInputSeqs(List<SequenceI> seqs)
+   {
+     int nvalid = 0;
+     for (SequenceI sq : seqs)
+     {
+       if (sq.getStart() <= sq.getEnd()
+               && (sq.isProtein() ? proteinAllowed : nucleotidesAllowed))
+       {
+         if (submitGaps
+                 || sq.getLength() == (sq.getEnd() - sq.getStart() + 1))
+         {
+           nvalid++;
+         }
+       }
+     }
+     return nvalid >= min_valid_seqs;
+   }
+   public void cancelCurrentJob()
+   {
+     try
+     {
+       String id = running.getJobId();
+       if (((CancellableI) annotService).cancel(running))
+       {
+         System.err.println("Cancelled job " + id);
+       }
+       else
+       {
+         System.err.println("Job " + id + " couldn't be cancelled.");
+       }
+     } catch (Exception q)
+     {
+       q.printStackTrace();
+     }
+   }
+   /**
+    * Interactive updating. Analysis calculations that work on the currently
+    * displayed alignment data should cancel existing jobs when the input data
+    * has changed.
+    * 
+    * @return true if a running job should be cancelled because new input data is
+    *         available for analysis
+    */
+   boolean isInteractiveUpdate()
+   {
+     return service.isInteractiveUpdate();
+   }
+   /**
+    * decide what sequences will be analysed TODO: refactor to generate
+    * List<SequenceI> for submission to service interface
+    * 
+    * @param alignment
+    * @param inputSeqs
+    * @return
+    */
+   public List<SequenceI> getInputSequences(AlignmentI alignment,
+           AnnotatedCollectionI inputSeqs)
+   {
+     if (alignment == null || alignment.getWidth() <= 0
+             || alignment.getSequences() == null || alignment.isNucleotide()
+                     ? !nucleotidesAllowed
+                     : !proteinAllowed)
+     {
+       return null;
+     }
+     if (inputSeqs == null || inputSeqs.getWidth() <= 0
+             || inputSeqs.getSequences() == null
+             || inputSeqs.getSequences().size() < 1)
+     {
+       inputSeqs = alignment;
+     }
+     List<SequenceI> seqs = new ArrayList<>();
+     int minlen = 10;
+     int ln = -1;
+     if (bySequence)
+     {
+       seqNames = new HashMap<>();
+     }
+     gapMap = new boolean[0];
+     start = inputSeqs.getStartRes();
+     end = inputSeqs.getEndRes();
+     // TODO: URGENT! unify with JPred / MSA code to handle hidden regions
+     // correctly
+     // TODO: push attributes into WsJob instance (so they can be safely
+     // persisted/restored
+     for (SequenceI sq : (inputSeqs.getSequences()))
+     {
+       if (bySequence
+               ? sq.findPosition(end + 1)
+                       - sq.findPosition(start + 1) > minlen - 1
+               : sq.getEnd() - sq.getStart() > minlen - 1)
+       {
+         String newname = SeqsetUtils.unique_name(seqs.size() + 1);
+         // make new input sequence with or without gaps
+         if (seqNames != null)
+         {
+           seqNames.put(newname, sq);
+         }
+         SequenceI seq;
+         if (submitGaps)
+         {
+           seqs.add(seq = new jalview.datamodel.Sequence(newname,
+                   sq.getSequenceAsString()));
+           if (gapMap == null || gapMap.length < seq.getLength())
+           {
+             boolean[] tg = gapMap;
+             gapMap = new boolean[seq.getLength()];
+             System.arraycopy(tg, 0, gapMap, 0, tg.length);
+             for (int p = tg.length; p < gapMap.length; p++)
+             {
+               gapMap[p] = false; // init as a gap
+             }
+           }
+           for (int apos : sq.gapMap())
+           {
+             char sqc = sq.getCharAt(apos);
+             if (!filterNonStandardResidues
+                     || (sq.isProtein() ? ResidueProperties.aaIndex[sqc] < 20
+                             : ResidueProperties.nucleotideIndex[sqc] < 5))
+             {
+               gapMap[apos] = true; // aligned and real amino acid residue
+             }
+             ;
+           }
+         }
+         else
+         {
+           // TODO: add ability to exclude hidden regions
+           seqs.add(seq = new jalview.datamodel.Sequence(newname,
+                   AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
+                           sq.getSequenceAsString(start, end + 1))));
+           // for annotation need to also record map to sequence start/end
+           // position in range
+           // then transfer back to original sequence on return.
+         }
+         if (seq.getLength() > ln)
+         {
+           ln = seq.getLength();
+         }
+       }
+     }
+     if (alignedSeqs && submitGaps)
+     {
+       realw = 0;
+       for (int i = 0; i < gapMap.length; i++)
+       {
+         if (gapMap[i])
+         {
+           realw++;
+         }
+       }
+       // try real hard to return something submittable
+       // TODO: some of AAcon measures need a minimum of two or three amino
+       // acids at each position, and AAcon doesn't gracefully degrade.
+       for (int p = 0; p < seqs.size(); p++)
+       {
+         SequenceI sq = seqs.get(p);
+         // strip gapped columns
+         char[] padded = new char[realw],
+                 orig = sq.getSequence();
+         for (int i = 0, pp = 0; i < realw; pp++)
+         {
+           if (gapMap[pp])
+           {
+             if (orig.length > pp)
+             {
+               padded[i++] = orig[pp];
+             }
+             else
+             {
+               padded[i++] = '-';
+             }
+           }
+         }
+         seqs.set(p, new jalview.datamodel.Sequence(sq.getName(),
+                 new String(padded)));
+       }
+     }
+     return seqs;
+   }
+   @Override
+   public void updateAnnotation()
+   {
+     updateResultAnnotation(false);
+   }
+   public void updateResultAnnotation(boolean immediate)
+   {
+     if ((immediate || !calcMan.isWorking(this)) && running != null
+             && running.hasResults())
+     {
+       List<AlignmentAnnotation> ourAnnot = running.getAnnotation(),
+               newAnnots = new ArrayList<>();
+       //
+       // update graphGroup for all annotation
+       //
+       /**
+        * find a graphGroup greater than any existing ones this could be a method
+        * provided by alignment Alignment.getNewGraphGroup() - returns next
+        * unused graph group
+        */
+       int graphGroup = 1;
+       if (alignViewport.getAlignment().getAlignmentAnnotation() != null)
+       {
+         for (AlignmentAnnotation ala : alignViewport.getAlignment()
+                 .getAlignmentAnnotation())
+         {
+           if (ala.graphGroup > graphGroup)
+           {
+             graphGroup = ala.graphGroup;
+           }
+         }
+       }
+       /**
+        * update graphGroup in the annotation rows returned from service
+        */
+       // TODO: look at sequence annotation rows and update graph groups in the
+       // case of reference annotation.
+       for (AlignmentAnnotation ala : ourAnnot)
+       {
+         if (ala.graphGroup > 0)
+         {
+           ala.graphGroup += graphGroup;
+         }
+         SequenceI aseq = null;
+         /**
+          * transfer sequence refs and adjust gapmap
+          */
+         if (ala.sequenceRef != null)
+         {
+           SequenceI seq = running.getSeqNames()
+                   .get(ala.sequenceRef.getName());
+           aseq = seq;
+           while (seq.getDatasetSequence() != null)
+           {
+             seq = seq.getDatasetSequence();
+           }
+         }
+         Annotation[] resAnnot = ala.annotations,
+                 gappedAnnot = new Annotation[Math.max(
+                         alignViewport.getAlignment().getWidth(),
+                         gapMap.length)];
+         for (int p = 0, ap = start; ap < gappedAnnot.length; ap++)
+         {
+           if (gapMap != null && gapMap.length > ap && !gapMap[ap])
+           {
+             gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
+           }
+           else if (p < resAnnot.length)
+           {
+             gappedAnnot[ap] = resAnnot[p++];
+           }
+         }
+         ala.sequenceRef = aseq;
+         ala.annotations = gappedAnnot;
+         AlignmentAnnotation newAnnot = getAlignViewport().getAlignment()
+                 .updateFromOrCopyAnnotation(ala);
+         if (aseq != null)
+         {
+           aseq.addAlignmentAnnotation(newAnnot);
+           newAnnot.adjustForAlignment();
+           AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
+                   newAnnot, newAnnot.label, newAnnot.getCalcId());
+         }
+         newAnnots.add(newAnnot);
+       }
+       for (SequenceI sq : running.getSeqs())
+       {
+         if (!sq.getFeatures().hasFeatures()
 -                && (sq.getDBRefs() == null || sq.getDBRefs().length == 0))
++                && (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
+         {
+           continue;
+         }
+         running.setTransferSequenceFeatures(true);
+         SequenceI seq = running.getSeqNames().get(sq.getName());
+         SequenceI dseq;
+         ContiguousI seqRange = seq.findPositions(start, end);
+         while ((dseq = seq).getDatasetSequence() != null)
+         {
+           seq = seq.getDatasetSequence();
+         }
+         List<ContiguousI> sourceRange = new ArrayList();
+         if (gapMap != null && gapMap.length >= end)
+         {
+           int lastcol = start, col = start;
+           do
+           {
+             if (col == end || !gapMap[col])
+             {
+               if (lastcol <= (col - 1))
+               {
+                 seqRange = seq.findPositions(lastcol, col);
+                 sourceRange.add(seqRange);
+               }
+               lastcol = col + 1;
+             }
+           } while (++col <= end);
+         }
+         else
+         {
+           sourceRange.add(seq.findPositions(start, end));
+         }
+         int i = 0;
+         int source_startend[] = new int[sourceRange.size() * 2];
+         for (ContiguousI range : sourceRange)
+         {
+           source_startend[i++] = range.getBegin();
+           source_startend[i++] = range.getEnd();
+         }
+         Mapping mp = new Mapping(
+                 new MapList(source_startend, new int[]
+                 { seq.getStart(), seq.getEnd() }, 1, 1));
+         dseq.transferAnnotation(sq, mp);
+       }
+       updateOurAnnots(newAnnots);
+     }
+   }
+   /**
+    * notify manager that we have started, and wait for a free calculation slot
+    * 
+    * @return true if slot is obtained and work still valid, false if another
+    *         thread has done our work for us.
+    */
+   protected boolean checkDone()
+   {
+     calcMan.notifyStart(this);
+     ap.paintAlignment(false, false);
+     while (!calcMan.notifyWorking(this))
+     {
+       if (calcMan.isWorking(this))
+       {
+         return true;
+       }
+       try
+       {
+         if (ap != null)
+         {
+           ap.paintAlignment(false, false);
+         }
+         Thread.sleep(200);
+       } catch (Exception ex)
+       {
+         ex.printStackTrace();
+       }
+     }
+     if (alignViewport.isClosed())
+     {
+       abortAndDestroy();
+       return true;
+     }
+     return false;
+   }
+   protected void updateOurAnnots(List<AlignmentAnnotation> ourAnnot)
+   {
+     List<AlignmentAnnotation> our = ourAnnots;
+     ourAnnots = ourAnnot;
+     AlignmentI alignment = alignViewport.getAlignment();
+     if (our != null)
+     {
+       if (our.size() > 0)
+       {
+         for (AlignmentAnnotation an : our)
+         {
+           if (!ourAnnots.contains(an))
+           {
+             // remove the old annotation
+             alignment.deleteAnnotation(an);
+           }
+         }
+       }
+       our.clear();
+     }
+     // validate rows and update Alignmment state
+     for (AlignmentAnnotation an : ourAnnots)
+     {
+       alignViewport.getAlignment().validateAnnotation(an);
+     }
+     // TODO: may need a menu refresh after this
+     // af.setMenusForViewport();
+     ap.adjustAnnotationHeight();
+   }
+   public SequenceAnnotationServiceI getService()
+   {
+     return annotService;
+   }
+ }
index 42d34de,0000000..f0b5512
mode 100644,000000..100644
--- /dev/null
@@@ -1,2411 -1,0 +1,2410 @@@
 +package org.json;
 +
 +import java.io.Closeable;
 +
 +/*
 + * 
 + * Note: This file has been adapted for SwingJS by Bob Hanson hansonr@stolaf.edu 
 + * 
 + Copyright (c) 2002 JSON.org
 +
 + Permission is hereby granted, free of charge, to any person obtaining a copy
 + of this software and associated documentation files (the "Software"), to deal
 + in the Software without restriction, including without limitation the rights
 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 + copies of the Software, and to permit persons to whom the Software is
 + furnished to do so, subject to the following conditions:
 +
 + The above copyright notice and this permission notice shall be included in all
 + copies or substantial portions of the Software.
 +
 + The Software shall be used for Good, not Evil.
 +
 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 + SOFTWARE.
 + */
 +
 +import java.io.IOException;
 +import java.io.StringWriter;
 +import java.io.Writer;
- import java.lang.annotation.Annotation;
 +import java.lang.reflect.Field;
 +import java.lang.reflect.InvocationTargetException;
 +import java.lang.reflect.Method;
 +import java.lang.reflect.Modifier;
 +import java.math.BigDecimal;
 +import java.math.BigInteger;
 +import java.util.Collection;
 +import java.util.Enumeration;
 +import java.util.HashMap;
 +import java.util.Iterator;
 +import java.util.Locale;
 +import java.util.Map;
 +import java.util.Map.Entry;
 +import java.util.ResourceBundle;
 +import java.util.Set;
 +
 +/**
 + * A JSONObject is an unordered collection of name/value pairs. Its external
 + * form is a string wrapped in curly braces with colons between the names and
 + * values, and commas between the values and names. The internal form is an
 + * object having <code>get</code> and <code>opt</code> methods for accessing the
 + * values by name, and <code>put</code> methods for adding or replacing values
 + * by name. The values can be any of these types: <code>Boolean</code>,
 + * <code>JSONArray</code>, <code>JSONObject</code>, <code>Number</code>,
 + * <code>String</code>, or the <code>JSONObject.NULL</code> object. A JSONObject
 + * constructor can be used to convert an external form JSON text into an
 + * internal form whose values can be retrieved with the <code>get</code> and
 + * <code>opt</code> methods, or to convert values into a JSON text using the
 + * <code>put</code> and <code>toString</code> methods. A <code>get</code> method
 + * returns a value if one can be found, and throws an exception if one cannot be
 + * found. An <code>opt</code> method returns a default value instead of throwing
 + * an exception, and so is useful for obtaining optional values.
 + * <p>
 + * The generic <code>get()</code> and <code>opt()</code> methods return an
 + * object, which you can cast or query for type. There are also typed
 + * <code>get</code> and <code>opt</code> methods that do type checking and type
 + * coercion for you. The opt methods differ from the get methods in that they do
 + * not throw. Instead, they return a specified value, such as null.
 + * <p>
 + * The <code>put</code> methods add or replace values in an object. For example,
 + *
 + * <pre>
 + * myString = new JSONObject().put(&quot;JSON&quot;, &quot;Hello, World!&quot;).toString();
 + * </pre>
 + *
 + * produces the string <code>{"JSON": "Hello, World"}</code>.
 + * <p>
 + * The texts produced by the <code>toString</code> methods strictly conform to
 + * the JSON syntax rules. The constructors are more forgiving in the texts they
 + * will accept:
 + * <ul>
 + * <li>An extra <code>,</code>&nbsp;<small>(comma)</small> may appear just
 + * before the closing brace.</li>
 + * <li>Strings may be quoted with <code>'</code>&nbsp;<small>(single
 + * quote)</small>.</li>
 + * <li>Strings do not need to be quoted at all if they do not begin with a quote
 + * or single quote, and if they do not contain leading or trailing spaces, and
 + * if they do not contain any of these characters:
 + * <code>{ } [ ] / \ : , #</code> and if they do not look like numbers and if
 + * they are not the reserved words <code>true</code>, <code>false</code>, or
 + * <code>null</code>.</li>
 + * </ul>
 + *
 + * @author JSON.org
 + * @version 2016-08-15
 + */
 +public class JSONObject {
 +      /**
 +       * JSONObject.NULL is equivalent to the value that JavaScript calls null, whilst
 +       * Java's null is equivalent to the value that JavaScript calls undefined.
 +       */
 +      private static final class Null {
 +
 +              /**
 +               * There is only intended to be a single instance of the NULL object, so the
 +               * clone method returns itself.
 +               *
 +               * @return NULL.
 +               */
 +              @Override
 +              protected final Object clone() {
 +                      return this;
 +              }
 +
 +              /**
 +               * A Null object is equal to the null value and to itself.
 +               *
 +               * @param object An object to test for nullness.
 +               * @return true if the object parameter is the JSONObject.NULL object or null.
 +               */
 +              @Override
 +              public boolean equals(Object object) {
 +                      return object == null || object == this;
 +              }
 +
 +              /**
 +               * A Null object is equal to the null value and to itself.
 +               *
 +               * @return always returns 0.
 +               */
 +              @Override
 +              public int hashCode() {
 +                      return 0;
 +              }
 +
 +              /**
 +               * Get the "null" string value.
 +               *
 +               * @return The string "null".
 +               */
 +              @Override
 +              public String toString() {
 +                      return "null";
 +              }
 +      }
 +
 +      /**
 +       * The map where the JSONObject's properties are kept.
 +       */
 +      private final Map<String, Object> map;
 +
 +      /**
 +       * It is sometimes more convenient and less ambiguous to have a
 +       * <code>NULL</code> object than to use Java's <code>null</code> value.
 +       * <code>JSONObject.NULL.equals(null)</code> returns <code>true</code>.
 +       * <code>JSONObject.NULL.toString()</code> returns <code>"null"</code>.
 +       */
 +      public static final Object NULL = new Null();
 +
 +      /**
 +       * Construct an empty JSONObject.
 +       */
 +      public JSONObject() {
 +              // HashMap is used on purpose to ensure that elements are unordered by
 +              // the specification.
 +              // JSON tends to be a portable transfer format to allows the container
 +              // implementations to rearrange their items for a faster element
 +              // retrieval based on associative access.
 +              // Therefore, an implementation mustn't rely on the order of the item.
 +              this.map = new HashMap<String, Object>();
 +      }
 +
 +      /**
 +       * Construct a JSONObject from a subset of another JSONObject. An array of
 +       * strings is used to identify the keys that should be copied. Missing keys are
 +       * ignored.
 +       *
 +       * @param jo    A JSONObject.
 +       * @param names An array of strings.
 +       */
 +      public JSONObject(JSONObject jo, String[] names) {
 +              this(names.length);
 +              for (int i = 0; i < names.length; i += 1) {
 +                      try {
 +                              this.putOnce(names[i], jo.opt(names[i]));
 +                      } catch (Exception ignore) {
 +                      }
 +              }
 +      }
 +
 +      /**
 +       * Construct a JSONObject from a JSONTokener.
 +       *
 +       * @param x A JSONTokener object containing the source string.
 +       * @throws JSONException If there is a syntax error in the source string or a
 +       *                       duplicated key.
 +       */
 +      public JSONObject(JSONTokener x) throws JSONException {
 +              this();
 +              char c;
 +              String key;
 +
 +              if (x.nextClean() != '{') {
 +                      throw x.syntaxError("A JSONObject text must begin with '{'");
 +              }
 +              for (;;) {
 +                      c = x.nextClean();
 +                      switch (c) {
 +                      case 0:
 +                              throw x.syntaxError("A JSONObject text must end with '}'");
 +                      case '}':
 +                              return;
 +                      default:
 +                              x.back();
 +                              key = x.nextValue().toString();
 +                      }
 +
 +                      // The key is followed by ':'.
 +
 +                      c = x.nextClean();
 +                      if (c != ':') {
 +                              throw x.syntaxError("Expected a ':' after a key");
 +                      }
 +
 +                      // Use syntaxError(..) to include error location
 +
 +                      if (key != null) {
 +                              // Check if key exists
 +                              if (this.opt(key) != null) {
 +                                      // key already exists
 +                                      throw x.syntaxError("Duplicate key \"" + key + "\"");
 +                              }
 +                              // Only add value if non-null
 +                              Object value = x.nextValue();
 +                              if (value != null) {
 +                                      this.put(key, value);
 +                              }
 +                      }
 +
 +                      // Pairs are separated by ','.
 +
 +                      switch (x.nextClean()) {
 +                      case ';':
 +                      case ',':
 +                              if (x.nextClean() == '}') {
 +                                      return;
 +                              }
 +                              x.back();
 +                              break;
 +                      case '}':
 +                              return;
 +                      default:
 +                              throw x.syntaxError("Expected a ',' or '}'");
 +                      }
 +              }
 +      }
 +
 +      /**
 +       * Construct a JSONObject from a Map.
 +       *
 +       * @param m A map object that can be used to initialize the contents of the
 +       *          JSONObject.
 +       * @throws JSONException        If a value in the map is non-finite number.
 +       * @throws NullPointerException If a key in the map is <code>null</code>
 +       */
 +      public JSONObject(Map<?, ?> m) {
 +              if (m == null) {
 +                      this.map = new HashMap<String, Object>();
 +              } else {
 +                      this.map = new HashMap<String, Object>(m.size());
 +                      for (final Entry<?, ?> e : m.entrySet()) {
 +                              if (e.getKey() == null) {
 +                                      throw new NullPointerException("Null key.");
 +                              }
 +                              final Object value = e.getValue();
 +                              if (value != null) {
 +                                      this.map.put(String.valueOf(e.getKey()), wrap(value));
 +                              }
 +                      }
 +              }
 +      }
 +
 +      /**
 +       * Construct a JSONObject from an Object using bean getters. It reflects on all
 +       * of the public methods of the object. For each of the methods with no
 +       * parameters and a name starting with <code>"get"</code> or <code>"is"</code>
 +       * followed by an uppercase letter, the method is invoked, and a key and the
 +       * value returned from the getter method are put into the new JSONObject.
 +       * <p>
 +       * The key is formed by removing the <code>"get"</code> or <code>"is"</code>
 +       * prefix. If the second remaining character is not upper case, then the first
 +       * character is converted to lower case.
 +       * <p>
 +       * Methods that are <code>static</code>, return <code>void</code>, have
 +       * parameters, or are "bridge" methods, are ignored.
 +       * <p>
 +       * For example, if an object has a method named <code>"getName"</code>, and if
 +       * the result of calling <code>object.getName()</code> is
 +       * <code>"Larry Fine"</code>, then the JSONObject will contain
 +       * <code>"name": "Larry Fine"</code>.
 +       * <p>
 +       * The {@link JSONPropertyName} annotation can be used on a bean getter to
 +       * override key name used in the JSONObject. For example, using the object above
 +       * with the <code>getName</code> method, if we annotated it with:
 +       * 
 +       * <pre>
 +       * &#64;JSONPropertyName("FullName")
 +       * public String getName() {
 +       *      return this.name;
 +       * }
 +       * </pre>
 +       * 
 +       * The resulting JSON object would contain <code>"FullName": "Larry Fine"</code>
 +       * <p>
 +       * Similarly, the {@link JSONPropertyName} annotation can be used on non-
 +       * <code>get</code> and <code>is</code> methods. We can also override key name
 +       * used in the JSONObject as seen below even though the field would normally be
 +       * ignored:
 +       * 
 +       * <pre>
 +       * &#64;JSONPropertyName("FullName")
 +       * public String fullName() {
 +       *      return this.name;
 +       * }
 +       * </pre>
 +       * 
 +       * The resulting JSON object would contain <code>"FullName": "Larry Fine"</code>
 +       * <p>
 +       * The {@link JSONPropertyIgnore} annotation can be used to force the bean
 +       * property to not be serialized into JSON. If both {@link JSONPropertyIgnore}
 +       * and {@link JSONPropertyName} are defined on the same method, a depth
 +       * comparison is performed and the one closest to the concrete class being
 +       * serialized is used. If both annotations are at the same level, then the
 +       * {@link JSONPropertyIgnore} annotation takes precedent and the field is not
 +       * serialized. For example, the following declaration would prevent the
 +       * <code>getName</code> method from being serialized:
 +       * 
 +       * <pre>
 +       * &#64;JSONPropertyName("FullName")
 +       * &#64;JSONPropertyIgnore
 +       * public String getName() {
 +       *      return this.name;
 +       * }
 +       * </pre>
 +       * <p>
 +       * 
 +       * @param bean An object that has getter methods that should be used to make a
 +       *             JSONObject.
 +       */
 +      public JSONObject(Object bean) {
 +              this();
 +              this.populateMap(bean);
 +      }
 +
 +      /**
 +       * Construct a JSONObject from an Object, using reflection to find the public
 +       * members. The resulting JSONObject's keys will be the strings from the names
 +       * array, and the values will be the field values associated with those keys in
 +       * the object. If a key is not found or not visible, then it will not be copied
 +       * into the new JSONObject.
 +       *
 +       * @param object An object that has fields that should be used to make a
 +       *               JSONObject.
 +       * @param names  An array of strings, the names of the fields to be obtained
 +       *               from the object.
 +       */
 +      public JSONObject(Object object, String names[]) {
 +              this(names.length);
 +              Class<?> c = object.getClass();
 +              for (int i = 0; i < names.length; i += 1) {
 +                      String name = names[i];
 +                      try {
 +                              this.putOpt(name, c.getField(name).get(object));
 +                      } catch (Exception ignore) {
 +                      }
 +              }
 +      }
 +
 +      /**
 +       * Construct a JSONObject from a source JSON text string. This is the most
 +       * commonly used JSONObject constructor.
 +       *
 +       * @param source A string beginning with <code>{</code>&nbsp;<small>(left
 +       *               brace)</small> and ending with <code>}</code>
 +       *               &nbsp;<small>(right brace)</small>.
 +       * @exception JSONException If there is a syntax error in the source string or a
 +       *                          duplicated key.
 +       */
 +      public JSONObject(String source) throws JSONException {
 +              this(new JSONTokener(source));
 +      }
 +
 +      /**
 +       * Construct a JSONObject from a ResourceBundle.
 +       *
 +       * @param baseName The ResourceBundle base name.
 +       * @param locale   The Locale to load the ResourceBundle for.
 +       * @throws JSONException If any JSONExceptions are detected.
 +       */
 +      public JSONObject(String baseName, Locale locale) throws JSONException {
 +              this();
 +              ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale,
 +                              Thread.currentThread().getContextClassLoader());
 +
 +// Iterate through the keys in the bundle.
 +
 +              Enumeration<String> keys = bundle.getKeys();
 +              while (keys.hasMoreElements()) {
 +                      Object key = keys.nextElement();
 +                      if (key != null) {
 +
 +// Go through the path, ensuring that there is a nested JSONObject for each
 +// segment except the last. Add the value using the last segment's name into
 +// the deepest nested JSONObject.
 +
 +                              String[] path = ((String) key).split("\\.");
 +                              int last = path.length - 1;
 +                              JSONObject target = this;
 +                              for (int i = 0; i < last; i += 1) {
 +                                      String segment = path[i];
 +                                      JSONObject nextTarget = target.optJSONObject(segment);
 +                                      if (nextTarget == null) {
 +                                              nextTarget = new JSONObject();
 +                                              target.put(segment, nextTarget);
 +                                      }
 +                                      target = nextTarget;
 +                              }
 +                              target.put(path[last], bundle.getString((String) key));
 +                      }
 +              }
 +      }
 +
 +      /**
 +       * Constructor to specify an initial capacity of the internal map. Useful for
 +       * library internal calls where we know, or at least can best guess, how big
 +       * this JSONObject will be.
 +       * 
 +       * @param initialCapacity initial capacity of the internal map.
 +       */
 +      protected JSONObject(int initialCapacity) {
 +              this.map = new HashMap<String, Object>(initialCapacity);
 +      }
 +
 +      /**
 +       * Accumulate values under a key. It is similar to the put method except that if
 +       * there is already an object stored under the key then a JSONArray is stored
 +       * under the key to hold all of the accumulated values. If there is already a
 +       * JSONArray, then the new value is appended to it. In contrast, the put method
 +       * replaces the previous value.
 +       *
 +       * If only one value is accumulated that is not a JSONArray, then the result
 +       * will be the same as using put. But if multiple values are accumulated, then
 +       * the result will be like append.
 +       *
 +       * @param key   A key string.
 +       * @param value An object to be accumulated under the key.
 +       * @return this.
 +       * @throws JSONException        If the value is non-finite number.
 +       * @throws NullPointerException If the key is <code>null</code>.
 +       */
 +      public JSONObject accumulate(String key, Object value) throws JSONException {
 +              testValidity(value);
 +              Object object = this.opt(key);
 +              if (object == null) {
 +                      this.put(key, value instanceof JSONArray ? new JSONArray().put(value) : value);
 +              } else if (object instanceof JSONArray) {
 +                      ((JSONArray) object).put(value);
 +              } else {
 +                      this.put(key, new JSONArray().put(object).put(value));
 +              }
 +              return this;
 +      }
 +
 +      /**
 +       * Append values to the array under a key. If the key does not exist in the
 +       * JSONObject, then the key is put in the JSONObject with its value being a
 +       * JSONArray containing the value parameter. If the key was already associated
 +       * with a JSONArray, then the value parameter is appended to it.
 +       *
 +       * @param key   A key string.
 +       * @param value An object to be accumulated under the key.
 +       * @return this.
 +       * @throws JSONException        If the value is non-finite number or if the
 +       *                              current value associated with the key is not a
 +       *                              JSONArray.
 +       * @throws NullPointerException If the key is <code>null</code>.
 +       */
 +      public JSONObject append(String key, Object value) throws JSONException {
 +              testValidity(value);
 +              Object object = this.opt(key);
 +              if (object == null) {
 +                      this.put(key, new JSONArray().put(value));
 +              } else if (object instanceof JSONArray) {
 +                      this.put(key, ((JSONArray) object).put(value));
 +              } else {
 +                      throw new JSONException("JSONObject[" + key + "] is not a JSONArray.");
 +              }
 +              return this;
 +      }
 +
 +      /**
 +       * Produce a string from a double. The string "null" will be returned if the
 +       * number is not finite.
 +       *
 +       * @param d A double.
 +       * @return A String.
 +       */
 +      public static String doubleToString(double d) {
 +              if (Double.isInfinite(d) || Double.isNaN(d)) {
 +                      return "null";
 +              }
 +
 +// Shave off trailing zeros and decimal point, if possible.
 +
 +              String string = Double.toString(d);
 +              if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && string.indexOf('E') < 0) {
 +                      while (string.endsWith("0")) {
 +                              string = string.substring(0, string.length() - 1);
 +                      }
 +                      if (string.endsWith(".")) {
 +                              string = string.substring(0, string.length() - 1);
 +                      }
 +              }
 +              return string;
 +      }
 +
 +      /**
 +       * Get the value object associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return The object associated with the key.
 +       * @throws JSONException if the key is not found.
 +       */
 +      public Object get(String key) throws JSONException {
 +              if (key == null) {
 +                      throw new JSONException("Null key.");
 +              }
 +              Object object = this.opt(key);
 +              if (object == null) {
 +                      throw new JSONException("JSONObject[" + quote(key) + "] not found.");
 +              }
 +              return object;
 +      }
 +
 +      /**
 +       * Get the enum value associated with a key.
 +       * 
 +       * @param clazz The type of enum to retrieve.
 +       * @param key   A key string.
 +       * @return The enum value associated with the key
 +       * @throws JSONException if the key is not found or if the value cannot be
 +       *                       converted to an enum.
 +       */
 +      public <E extends Enum<E>> E getEnum(Class<E> clazz, String key) throws JSONException {
 +              E val = optEnum(clazz, key);
 +              if (val == null) {
 +                      // JSONException should really take a throwable argument.
 +                      // If it did, I would re-implement this with the Enum.valueOf
 +                      // method and place any thrown exception in the JSONException
 +                      throw new JSONException(
 +                                      "JSONObject[" + quote(key) + "] is not an enum of type " + quote(clazz.getSimpleName()) + ".");
 +              }
 +              return val;
 +      }
 +
 +      /**
 +       * Get the boolean value associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return The truth.
 +       * @throws JSONException if the value is not a Boolean or the String "true" or
 +       *                       "false".
 +       */
 +      public boolean getBoolean(String key) throws JSONException {
 +              Object object = this.get(key);
 +              if (object.equals(Boolean.FALSE) || (object instanceof String && ((String) object).equalsIgnoreCase("false"))) {
 +                      return false;
 +              } else if (object.equals(Boolean.TRUE)
 +                              || (object instanceof String && ((String) object).equalsIgnoreCase("true"))) {
 +                      return true;
 +              }
 +              throw new JSONException("JSONObject[" + quote(key) + "] is not a Boolean.");
 +      }
 +
 +      /**
 +       * Get the BigInteger value associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return The numeric value.
 +       * @throws JSONException if the key is not found or if the value cannot be
 +       *                       converted to BigInteger.
 +       */
 +      public BigInteger getBigInteger(String key) throws JSONException {
 +              Object object = this.get(key);
 +              try {
 +                      return new BigInteger(object.toString());
 +              } catch (Exception e) {
 +                      throw new JSONException("JSONObject[" + quote(key) + "] could not be converted to BigInteger.", e);
 +              }
 +      }
 +
 +      /**
 +       * Get the BigDecimal value associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return The numeric value.
 +       * @throws JSONException if the key is not found or if the value cannot be
 +       *                       converted to BigDecimal.
 +       */
 +      public BigDecimal getBigDecimal(String key) throws JSONException {
 +              Object object = this.get(key);
 +              if (object instanceof BigDecimal) {
 +                      return (BigDecimal) object;
 +              }
 +              try {
 +                      return new BigDecimal(object.toString());
 +              } catch (Exception e) {
 +                      throw new JSONException("JSONObject[" + quote(key) + "] could not be converted to BigDecimal.", e);
 +              }
 +      }
 +
 +      /**
 +       * Get the double value associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return The numeric value.
 +       * @throws JSONException if the key is not found or if the value is not a Number
 +       *                       object and cannot be converted to a number.
 +       */
 +      public double getDouble(String key) throws JSONException {
 +              Object object = this.get(key);
 +              try {
 +                      return object instanceof Number ? ((Number) object).doubleValue() : Double.parseDouble(object.toString());
 +              } catch (Exception e) {
 +                      throw new JSONException("JSONObject[" + quote(key) + "] is not a number.", e);
 +              }
 +      }
 +
 +      /**
 +       * Get the float value associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return The numeric value.
 +       * @throws JSONException if the key is not found or if the value is not a Number
 +       *                       object and cannot be converted to a number.
 +       */
 +      public float getFloat(String key) throws JSONException {
 +              Object object = this.get(key);
 +              try {
 +                      return object instanceof Number ? ((Number) object).floatValue() : Float.parseFloat(object.toString());
 +              } catch (Exception e) {
 +                      throw new JSONException("JSONObject[" + quote(key) + "] is not a number.", e);
 +              }
 +      }
 +
 +      /**
 +       * Get the Number value associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return The numeric value.
 +       * @throws JSONException if the key is not found or if the value is not a Number
 +       *                       object and cannot be converted to a number.
 +       */
 +      public Number getNumber(String key) throws JSONException {
 +              Object object = this.get(key);
 +              try {
 +                      if (object instanceof Number) {
 +                              return (Number) object;
 +                      }
 +                      return stringToNumber(object.toString());
 +              } catch (Exception e) {
 +                      throw new JSONException("JSONObject[" + quote(key) + "] is not a number.", e);
 +              }
 +      }
 +
 +      /**
 +       * Get the int value associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return The integer value.
 +       * @throws JSONException if the key is not found or if the value cannot be
 +       *                       converted to an integer.
 +       */
 +      public int getInt(String key) throws JSONException {
 +              Object object = this.get(key);
 +              try {
 +                      return object instanceof Number ? ((Number) object).intValue() : Integer.parseInt((String) object);
 +              } catch (Exception e) {
 +                      throw new JSONException("JSONObject[" + quote(key) + "] is not an int.", e);
 +              }
 +      }
 +
 +      /**
 +       * Get the JSONArray value associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return A JSONArray which is the value.
 +       * @throws JSONException if the key is not found or if the value is not a
 +       *                       JSONArray.
 +       */
 +      public JSONArray getJSONArray(String key) throws JSONException {
 +              Object object = this.get(key);
 +              if (object instanceof JSONArray) {
 +                      return (JSONArray) object;
 +              }
 +              throw new JSONException("JSONObject[" + quote(key) + "] is not a JSONArray.");
 +      }
 +
 +      /**
 +       * Get the JSONObject value associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return A JSONObject which is the value.
 +       * @throws JSONException if the key is not found or if the value is not a
 +       *                       JSONObject.
 +       */
 +      public JSONObject getJSONObject(String key) throws JSONException {
 +              Object object = this.get(key);
 +              if (object instanceof JSONObject) {
 +                      return (JSONObject) object;
 +              }
 +              throw new JSONException("JSONObject[" + quote(key) + "] is not a JSONObject.");
 +      }
 +
 +      /**
 +       * Get the long value associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return The long value.
 +       * @throws JSONException if the key is not found or if the value cannot be
 +       *                       converted to a long.
 +       */
 +      public long getLong(String key) throws JSONException {
 +              Object object = this.get(key);
 +              try {
 +                      return object instanceof Number ? ((Number) object).longValue() : Long.parseLong((String) object);
 +              } catch (Exception e) {
 +                      throw new JSONException("JSONObject[" + quote(key) + "] is not a long.", e);
 +              }
 +      }
 +
 +      /**
 +       * Get an array of field names from a JSONObject.
 +       *
 +       * @return An array of field names, or null if there are no names.
 +       */
 +      public static String[] getNames(JSONObject jo) {
 +              if (jo.isEmpty()) {
 +                      return null;
 +              }
 +              return jo.keySet().toArray(new String[jo.length()]);
 +      }
 +
 +      /**
 +       * Get an array of field names from an Object.
 +       *
 +       * @return An array of field names, or null if there are no names.
 +       */
 +      public static String[] getNames(Object object) {
 +              if (object == null) {
 +                      return null;
 +              }
 +              Class<?> klass = object.getClass();
 +              Field[] fields = klass.getFields();
 +              int length = fields.length;
 +              if (length == 0) {
 +                      return null;
 +              }
 +              String[] names = new String[length];
 +              for (int i = 0; i < length; i += 1) {
 +                      names[i] = fields[i].getName();
 +              }
 +              return names;
 +      }
 +
 +      /**
 +       * Get the string associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return A string which is the value.
 +       * @throws JSONException if there is no string value for the key.
 +       */
 +      public String getString(String key) throws JSONException {
 +              Object object = this.get(key);
 +              if (object instanceof String) {
 +                      return (String) object;
 +              }
 +              throw new JSONException("JSONObject[" + quote(key) + "] not a string.");
 +      }
 +
 +      /**
 +       * Determine if the JSONObject contains a specific key.
 +       *
 +       * @param key A key string.
 +       * @return true if the key exists in the JSONObject.
 +       */
 +      public boolean has(String key) {
 +              return this.map.containsKey(key);
 +      }
 +
 +      /**
 +       * Increment a property of a JSONObject. If there is no such property, create
 +       * one with a value of 1. If there is such a property, and if it is an Integer,
 +       * Long, Double, or Float, then add one to it.
 +       *
 +       * @param key A key string.
 +       * @return this.
 +       * @throws JSONException If there is already a property with this name that is
 +       *                       not an Integer, Long, Double, or Float.
 +       */
 +      public JSONObject increment(String key) throws JSONException {
 +              Object value = this.opt(key);
 +              if (value == null) {
 +                      this.put(key, 1);
 +              } else if (value instanceof BigInteger) {
 +                      this.put(key, ((BigInteger) value).add(BigInteger.ONE));
 +              } else if (value instanceof BigDecimal) {
 +                      this.put(key, ((BigDecimal) value).add(BigDecimal.ONE));
 +              } else if (value instanceof Integer) {
 +                      this.put(key, ((Integer) value).intValue() + 1);
 +              } else if (value instanceof Long) {
 +                      this.put(key, ((Long) value).longValue() + 1L);
 +              } else if (value instanceof Double) {
 +                      this.put(key, ((Double) value).doubleValue() + 1.0d);
 +              } else if (value instanceof Float) {
 +                      this.put(key, ((Float) value).floatValue() + 1.0f);
 +              } else {
 +                      throw new JSONException("Unable to increment [" + quote(key) + "].");
 +              }
 +              return this;
 +      }
 +
 +      /**
 +       * Determine if the value associated with the key is <code>null</code> or if
 +       * there is no value.
 +       *
 +       * @param key A key string.
 +       * @return true if there is no value associated with the key or if the value is
 +       *         the JSONObject.NULL object.
 +       */
 +      public boolean isNull(String key) {
 +              return JSONObject.NULL.equals(this.opt(key));
 +      }
 +
 +      /**
 +       * Get an enumeration of the keys of the JSONObject. Modifying this key Set will
 +       * also modify the JSONObject. Use with caution.
 +       *
 +       * @see Set#iterator()
 +       * 
 +       * @return An iterator of the keys.
 +       */
 +      public Iterator<String> keys() {
 +              return this.keySet().iterator();
 +      }
 +
 +      /**
 +       * Get a set of keys of the JSONObject. Modifying this key Set will also modify
 +       * the JSONObject. Use with caution.
 +       *
 +       * @see Map#keySet()
 +       *
 +       * @return A keySet.
 +       */
 +      public Set<String> keySet() {
 +              return this.map.keySet();
 +      }
 +
 +      /**
 +       * Get a set of entries of the JSONObject. These are raw values and may not
 +       * match what is returned by the JSONObject get* and opt* functions. Modifying
 +       * the returned EntrySet or the Entry objects contained therein will modify the
 +       * backing JSONObject. This does not return a clone or a read-only view.
 +       * 
 +       * Use with caution.
 +       *
 +       * @see Map#entrySet()
 +       *
 +       * @return An Entry Set
 +       */
 +      protected Set<Entry<String, Object>> entrySet() {
 +              return this.map.entrySet();
 +      }
 +
 +      /**
 +       * Get the number of keys stored in the JSONObject.
 +       *
 +       * @return The number of keys in the JSONObject.
 +       */
 +      public int length() {
 +              return this.map.size();
 +      }
 +
 +      /**
 +       * Check if JSONObject is empty.
 +       *
 +       * @return true if JSONObject is empty, otherwise false.
 +       */
 +      public boolean isEmpty() {
 +              return map.isEmpty();
 +      }
 +
 +      /**
 +       * Produce a JSONArray containing the names of the elements of this JSONObject.
 +       *
 +       * @return A JSONArray containing the key strings, or null if the JSONObject is
 +       *         empty.
 +       */
 +      public JSONArray names() {
 +              if (this.map.isEmpty()) {
 +                      return null;
 +              }
 +              return new JSONArray(this.map.keySet());
 +      }
 +
 +      /**
 +       * Produce a string from a Number.
 +       *
 +       * @param number A Number
 +       * @return A String.
 +       * @throws JSONException If n is a non-finite number.
 +       */
 +      public static String numberToString(Number number) throws JSONException {
 +              if (number == null) {
 +                      throw new JSONException("Null pointer");
 +              }
 +              testValidity(number);
 +
 +              // Shave off trailing zeros and decimal point, if possible.
 +
 +              String string = number.toString();
 +              if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && string.indexOf('E') < 0) {
 +                      while (string.endsWith("0")) {
 +                              string = string.substring(0, string.length() - 1);
 +                      }
 +                      if (string.endsWith(".")) {
 +                              string = string.substring(0, string.length() - 1);
 +                      }
 +              }
 +              return string;
 +      }
 +
 +      /**
 +       * Get an optional value associated with a key.
 +       *
 +       * @param key A key string.
 +       * @return An object which is the value, or null if there is no value.
 +       */
 +      public Object opt(String key) {
 +              return key == null ? null : this.map.get(key);
 +      }
 +
 +      /**
 +       * Get the enum value associated with a key.
 +       * 
 +       * @param clazz The type of enum to retrieve.
 +       * @param key   A key string.
 +       * @return The enum value associated with the key or null if not found
 +       */
 +      public <E extends Enum<E>> E optEnum(Class<E> clazz, String key) {
 +              return this.optEnum(clazz, key, null);
 +      }
 +
 +      /**
 +       * Get the enum value associated with a key.
 +       * 
 +       * @param clazz        The type of enum to retrieve.
 +       * @param key          A key string.
 +       * @param defaultValue The default in case the value is not found
 +       * @return The enum value associated with the key or defaultValue if the value
 +       *         is not found or cannot be assigned to <code>clazz</code>
 +       */
 +      public <E extends Enum<E>> E optEnum(Class<E> clazz, String key, E defaultValue) {
 +              try {
 +                      Object val = this.opt(key);
 +                      if (NULL.equals(val)) {
 +                              return defaultValue;
 +                      }
 +                      if (clazz.isAssignableFrom(val.getClass())) {
 +                              // we just checked it!
 +                              @SuppressWarnings("unchecked")
 +                              E myE = (E) val;
 +                              return myE;
 +                      }
 +                      return Enum.valueOf(clazz, val.toString());
 +              } catch (IllegalArgumentException e) {
 +                      return defaultValue;
 +              } catch (NullPointerException e) {
 +                      return defaultValue;
 +              }
 +      }
 +
 +      /**
 +       * Get an optional boolean associated with a key. It returns false if there is
 +       * no such key, or if the value is not Boolean.TRUE or the String "true".
 +       *
 +       * @param key A key string.
 +       * @return The truth.
 +       */
 +      public boolean optBoolean(String key) {
 +              return this.optBoolean(key, false);
 +      }
 +
 +      /**
 +       * Get an optional boolean associated with a key. It returns the defaultValue if
 +       * there is no such key, or if it is not a Boolean or the String "true" or
 +       * "false" (case insensitive).
 +       *
 +       * @param key          A key string.
 +       * @param defaultValue The default.
 +       * @return The truth.
 +       */
 +      public boolean optBoolean(String key, boolean defaultValue) {
 +              Object val = this.opt(key);
 +              if (NULL.equals(val)) {
 +                      return defaultValue;
 +              }
 +              if (val instanceof Boolean) {
 +                      return ((Boolean) val).booleanValue();
 +              }
 +              try {
 +                      // we'll use the get anyway because it does string conversion.
 +                      return this.getBoolean(key);
 +              } catch (Exception e) {
 +                      return defaultValue;
 +              }
 +      }
 +
 +      /**
 +       * Get an optional BigDecimal associated with a key, or the defaultValue if
 +       * there is no such key or if its value is not a number. If the value is a
 +       * string, an attempt will be made to evaluate it as a number.
 +       *
 +       * @param key          A key string.
 +       * @param defaultValue The default.
 +       * @return An object which is the value.
 +       */
 +      public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) {
 +              Object val = this.opt(key);
 +              if (NULL.equals(val)) {
 +                      return defaultValue;
 +              }
 +              if (val instanceof BigDecimal) {
 +                      return (BigDecimal) val;
 +              }
 +              if (val instanceof BigInteger) {
 +                      return new BigDecimal((BigInteger) val);
 +              }
 +              if (val instanceof Double || val instanceof Float) {
 +                      return new BigDecimal(((Number) val).doubleValue());
 +              }
 +              if (val instanceof Long || val instanceof Integer || val instanceof Short || val instanceof Byte) {
 +                      return new BigDecimal(((Number) val).longValue());
 +              }
 +              // don't check if it's a string in case of unchecked Number subclasses
 +              try {
 +                      return new BigDecimal(val.toString());
 +              } catch (Exception e) {
 +                      return defaultValue;
 +              }
 +      }
 +
 +      /**
 +       * Get an optional BigInteger associated with a key, or the defaultValue if
 +       * there is no such key or if its value is not a number. If the value is a
 +       * string, an attempt will be made to evaluate it as a number.
 +       *
 +       * @param key          A key string.
 +       * @param defaultValue The default.
 +       * @return An object which is the value.
 +       */
 +      public BigInteger optBigInteger(String key, BigInteger defaultValue) {
 +              Object val = this.opt(key);
 +              if (NULL.equals(val)) {
 +                      return defaultValue;
 +              }
 +              if (val instanceof BigInteger) {
 +                      return (BigInteger) val;
 +              }
 +              if (val instanceof BigDecimal) {
 +                      return ((BigDecimal) val).toBigInteger();
 +              }
 +              if (val instanceof Double || val instanceof Float) {
 +                      return new BigDecimal(((Number) val).doubleValue()).toBigInteger();
 +              }
 +              if (val instanceof Long || val instanceof Integer || val instanceof Short || val instanceof Byte) {
 +                      return BigInteger.valueOf(((Number) val).longValue());
 +              }
 +              // don't check if it's a string in case of unchecked Number subclasses
 +              try {
 +                      // the other opt functions handle implicit conversions, i.e.
 +                      // jo.put("double",1.1d);
 +                      // jo.optInt("double"); -- will return 1, not an error
 +                      // this conversion to BigDecimal then to BigInteger is to maintain
 +                      // that type cast support that may truncate the decimal.
 +                      final String valStr = val.toString();
 +                      if (isDecimalNotation(valStr)) {
 +                              return new BigDecimal(valStr).toBigInteger();
 +                      }
 +                      return new BigInteger(valStr);
 +              } catch (Exception e) {
 +                      return defaultValue;
 +              }
 +      }
 +
 +      /**
 +       * Get an optional double associated with a key, or NaN if there is no such key
 +       * or if its value is not a number. If the value is a string, an attempt will be
 +       * made to evaluate it as a number.
 +       *
 +       * @param key A string which is the key.
 +       * @return An object which is the value.
 +       */
 +      public double optDouble(String key) {
 +              return this.optDouble(key, Double.NaN);
 +      }
 +
 +      /**
 +       * Get an optional double associated with a key, or the defaultValue if there is
 +       * no such key or if its value is not a number. If the value is a string, an
 +       * attempt will be made to evaluate it as a number.
 +       *
 +       * @param key          A key string.
 +       * @param defaultValue The default.
 +       * @return An object which is the value.
 +       */
 +      public double optDouble(String key, double defaultValue) {
 +              Object val = this.opt(key);
 +              if (NULL.equals(val)) {
 +                      return defaultValue;
 +              }
 +              if (val instanceof Number) {
 +                      return ((Number) val).doubleValue();
 +              }
 +              if (val instanceof String) {
 +                      try {
 +                              return Double.parseDouble((String) val);
 +                      } catch (Exception e) {
 +                              return defaultValue;
 +                      }
 +              }
 +              return defaultValue;
 +      }
 +
 +      /**
 +       * Get the optional double value associated with an index. NaN is returned if
 +       * there is no value for the index, or if the value is not a number and cannot
 +       * be converted to a number.
 +       *
 +       * @param key A key string.
 +       * @return The value.
 +       */
 +      public float optFloat(String key) {
 +              return this.optFloat(key, Float.NaN);
 +      }
 +
 +      /**
 +       * Get the optional double value associated with an index. The defaultValue is
 +       * returned if there is no value for the index, or if the value is not a number
 +       * and cannot be converted to a number.
 +       *
 +       * @param key          A key string.
 +       * @param defaultValue The default value.
 +       * @return The value.
 +       */
 +      public float optFloat(String key, float defaultValue) {
 +              Object val = this.opt(key);
 +              if (JSONObject.NULL.equals(val)) {
 +                      return defaultValue;
 +              }
 +              if (val instanceof Number) {
 +                      return ((Number) val).floatValue();
 +              }
 +              if (val instanceof String) {
 +                      try {
 +                              return Float.parseFloat((String) val);
 +                      } catch (Exception e) {
 +                              return defaultValue;
 +                      }
 +              }
 +              return defaultValue;
 +      }
 +
 +      /**
 +       * Get an optional int value associated with a key, or zero if there is no such
 +       * key or if the value is not a number. If the value is a string, an attempt
 +       * will be made to evaluate it as a number.
 +       *
 +       * @param key A key string.
 +       * @return An object which is the value.
 +       */
 +      public int optInt(String key) {
 +              return this.optInt(key, 0);
 +      }
 +
 +      /**
 +       * Get an optional int value associated with a key, or the default if there is
 +       * no such key or if the value is not a number. If the value is a string, an
 +       * attempt will be made to evaluate it as a number.
 +       *
 +       * @param key          A key string.
 +       * @param defaultValue The default.
 +       * @return An object which is the value.
 +       */
 +      public int optInt(String key, int defaultValue) {
 +              Object val = this.opt(key);
 +              if (NULL.equals(val)) {
 +                      return defaultValue;
 +              }
 +              if (val instanceof Number) {
 +                      return ((Number) val).intValue();
 +              }
 +
 +              if (val instanceof String) {
 +                      try {
 +                              return new BigDecimal((String) val).intValue();
 +                      } catch (Exception e) {
 +                              return defaultValue;
 +                      }
 +              }
 +              return defaultValue;
 +      }
 +
 +      /**
 +       * Get an optional JSONArray associated with a key. It returns null if there is
 +       * no such key, or if its value is not a JSONArray.
 +       *
 +       * @param key A key string.
 +       * @return A JSONArray which is the value.
 +       */
 +      public JSONArray optJSONArray(String key) {
 +              Object o = this.opt(key);
 +              return o instanceof JSONArray ? (JSONArray) o : null;
 +      }
 +
 +      /**
 +       * Get an optional JSONObject associated with a key. It returns null if there is
 +       * no such key, or if its value is not a JSONObject.
 +       *
 +       * @param key A key string.
 +       * @return A JSONObject which is the value.
 +       */
 +      public JSONObject optJSONObject(String key) {
 +              Object object = this.opt(key);
 +              return object instanceof JSONObject ? (JSONObject) object : null;
 +      }
 +
 +      /**
 +       * Get an optional long value associated with a key, or zero if there is no such
 +       * key or if the value is not a number. If the value is a string, an attempt
 +       * will be made to evaluate it as a number.
 +       *
 +       * @param key A key string.
 +       * @return An object which is the value.
 +       */
 +      public long optLong(String key) {
 +              return this.optLong(key, 0);
 +      }
 +
 +      /**
 +       * Get an optional long value associated with a key, or the default if there is
 +       * no such key or if the value is not a number. If the value is a string, an
 +       * attempt will be made to evaluate it as a number.
 +       *
 +       * @param key          A key string.
 +       * @param defaultValue The default.
 +       * @return An object which is the value.
 +       */
 +      public long optLong(String key, long defaultValue) {
 +              Object val = this.opt(key);
 +              if (NULL.equals(val)) {
 +                      return defaultValue;
 +              }
 +              if (val instanceof Number) {
 +                      return ((Number) val).longValue();
 +              }
 +
 +              if (val instanceof String) {
 +                      try {
 +                              return new BigDecimal((String) val).longValue();
 +                      } catch (Exception e) {
 +                              return defaultValue;
 +                      }
 +              }
 +              return defaultValue;
 +      }
 +
 +      /**
 +       * Get an optional {@link Number} value associated with a key, or
 +       * <code>null</code> if there is no such key or if the value is not a number. If
 +       * the value is a string, an attempt will be made to evaluate it as a number
 +       * ({@link BigDecimal}). This method would be used in cases where type coercion
 +       * of the number value is unwanted.
 +       *
 +       * @param key A key string.
 +       * @return An object which is the value.
 +       */
 +      public Number optNumber(String key) {
 +              return this.optNumber(key, null);
 +      }
 +
 +      /**
 +       * Get an optional {@link Number} value associated with a key, or the default if
 +       * there is no such key or if the value is not a number. If the value is a
 +       * string, an attempt will be made to evaluate it as a number. This method would
 +       * be used in cases where type coercion of the number value is unwanted.
 +       *
 +       * @param key          A key string.
 +       * @param defaultValue The default.
 +       * @return An object which is the value.
 +       */
 +      public Number optNumber(String key, Number defaultValue) {
 +              Object val = this.opt(key);
 +              if (NULL.equals(val)) {
 +                      return defaultValue;
 +              }
 +              if (val instanceof Number) {
 +                      return (Number) val;
 +              }
 +
 +              if (val instanceof String) {
 +                      try {
 +                              return stringToNumber((String) val);
 +                      } catch (Exception e) {
 +                              return defaultValue;
 +                      }
 +              }
 +              return defaultValue;
 +      }
 +
 +      /**
 +       * Get an optional string associated with a key. It returns an empty string if
 +       * there is no such key. If the value is not a string and is not null, then it
 +       * is converted to a string.
 +       *
 +       * @param key A key string.
 +       * @return A string which is the value.
 +       */
 +      public String optString(String key) {
 +              return this.optString(key, "");
 +      }
 +
 +      /**
 +       * Get an optional string associated with a key. It returns the defaultValue if
 +       * there is no such key.
 +       *
 +       * @param key          A key string.
 +       * @param defaultValue The default.
 +       * @return A string which is the value.
 +       */
 +      public String optString(String key, String defaultValue) {
 +              Object object = this.opt(key);
 +              return NULL.equals(object) ? defaultValue : object.toString();
 +      }
 +
 +      /**
 +       * Populates the internal map of the JSONObject with the bean properties. The
 +       * bean can not be recursive.
 +       *
 +       * @see JSONObject#JSONObject(Object)
 +       *
 +       * @param bean the bean
 +       */
 +      private void populateMap(Object bean) {
 +              Class<?> klass = bean.getClass();
 +
 +              // If klass is a System class then set includeSuperClass to false.
 +
 +              boolean includeSuperClass = klass.getClassLoader() != null;
 +
 +              Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods();
 +              for (final Method method : methods) {
 +                      final int modifiers = method.getModifiers();
 +                      if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers) && method.getParameterTypes().length == 0
 +                                      && !method.isBridge() && method.getReturnType() != Void.TYPE
 +                                      && isValidMethodName(method.getName())) {
 +                              final String key = getKeyNameFromMethod(method);
 +                              if (key != null && !key.isEmpty()) {
 +                                      try {
 +                                              final Object result = method.invoke(bean);
 +                                              if (result != null) {
 +                                                      this.map.put(key, wrap(result));
 +                                                      // we don't use the result anywhere outside of wrap
 +                                                      // if it's a resource we should be sure to close it
 +                                                      // after calling toString
 +                                                      if (result instanceof Closeable) {
 +                                                              try {
 +                                                                      ((Closeable) result).close();
 +                                                              } catch (IOException ignore) {
 +                                                              }
 +                                                      }
 +                                              }
 +                                      } catch (IllegalAccessException ignore) {
 +                                      } catch (IllegalArgumentException ignore) {
 +                                      } catch (InvocationTargetException ignore) {
 +                                      }
 +                              }
 +                      }
 +              }
 +      }
 +
 +      private boolean isValidMethodName(String name) {
 +              return !"getClass".equals(name) && !"getDeclaringClass".equals(name);
 +      }
 +
 +      private String getKeyNameFromMethod(Method method) {
 +              final int ignoreDepth = -1;// getAnnotationDepth(method, JSONPropertyIgnore.class);
 +//        if (ignoreDepth > 0) {
 +//            final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class);
 +//            if (forcedNameDepth < 0 || ignoreDepth <= forcedNameDepth) {
 +//                // the hierarchy asked to ignore, and the nearest name override
 +//                // was higher or non-existent
 +//                return null;
 +//            }
 +//        }
 +//        JSONPropertyName annotation = getAnnotation(method, JSONPropertyName.class);
 +//        if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) {
 +//            return annotation.value();
 +//        }
 +              String key;
 +              final String name = method.getName();
 +              if (name.startsWith("get") && name.length() > 3) {
 +                      key = name.substring(3);
 +              } else if (name.startsWith("is") && name.length() > 2) {
 +                      key = name.substring(2);
 +              } else {
 +                      return null;
 +              }
 +              // if the first letter in the key is not uppercase, then skip.
 +              // This is to maintain backwards compatibility before PR406
 +              // (https://github.com/stleary/JSON-java/pull/406/)
 +              if (Character.isLowerCase(key.charAt(0))) {
 +                      return null;
 +              }
 +              if (key.length() == 1) {
 +                      key = key.toLowerCase(Locale.ROOT);
 +              } else if (!Character.isUpperCase(key.charAt(1))) {
 +                      key = key.substring(0, 1).toLowerCase(Locale.ROOT) + key.substring(1);
 +              }
 +              return (/** @j2sNative 1 ? key.split("$")[0] : */
 +              key);
 +      }
 +
 +//    /**
 +//     * Searches the class hierarchy to see if the method or it's super
 +//     * implementations and interfaces has the annotation.
 +//     *
 +//     * @param <A>
 +//     *            type of the annotation
 +//     *
 +//     * @param m
 +//     *            method to check
 +//     * @param annotationClass
 +//     *            annotation to look for
 +//     * @return the {@link Annotation} if the annotation exists on the current method
 +//     *         or one of it's super class definitions
 +//     */
 +//    private static <A extends Annotation> A getAnnotation(final Method m, final Class<A> annotationClass) {
 +//            return null;
 +//        // if we have invalid data the result is null
 +//        if (true || m == null || annotationClass == null) {
 +//            return null;
 +//        }
 +//
 +//        if (m.isAnnotationPresent(annotationClass)) {
 +//            return m.getAnnotation(annotationClass);
 +//        }
 +//
 +//        // if we've already reached the Object class, return null;
 +//        Class<?> c = m.getDeclaringClass();
 +//        if (c.getSuperclass() == null) {
 +//            return null;
 +//        }
 +//
 +//        // check directly implemented interfaces for the method being checked
 +//        for (Class<?> i : c.getInterfaces()) {
 +//            try {
 +//                Method im = i.getMethod(m.getName(), m.getParameterTypes());
 +//                return getAnnotation(im, annotationClass);
 +//            } catch (final SecurityException ex) {
 +//                continue;
 +//            } catch (final NoSuchMethodException ex) {
 +//                continue;
 +//            }
 +//        }
 +//
 +//        try {
 +//            return getAnnotation(
 +//                    c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()),
 +//                    annotationClass);
 +//        } catch (final SecurityException ex) {
 +//            return null;
 +//        } catch (final NoSuchMethodException ex) {
 +//            return null;
 +//        }
 +//    }
 +//
 +//    /**
 +//     * Searches the class hierarchy to see if the method or it's super
 +//     * implementations and interfaces has the annotation. Returns the depth of the
 +//     * annotation in the hierarchy.
 +//     *
 +//     * @param <A>
 +//     *            type of the annotation
 +//     *
 +//     * @param m
 +//     *            method to check
 +//     * @param annotationClass
 +//     *            annotation to look for
 +//     * @return Depth of the annotation or -1 if the annotation is not on the method.
 +//     */
 +//    private static int getAnnotationDepth(final Method m, final Class<? extends Annotation> annotationClass) {
 +//        // if we have invalid data the result is -1
 +//        if (m == null || annotationClass == null) {
 +//            return -1;
 +//        }
 +//        if (m.isAnnotationPresent(annotationClass)) {
 +//            return 1;
 +//        }
 +//
 +//        // if we've already reached the Object class, return -1;
 +//        Class<?> c = m.getDeclaringClass();
 +//        if (c.getSuperclass() == null) {
 +//            return -1;
 +//        }
 +//
 +//        // check directly implemented interfaces for the method being checked
 +//        for (Class<?> i : c.getInterfaces()) {
 +//            try {
 +//                Method im = i.getMethod(m.getName(), m.getParameterTypes());
 +//                int d = getAnnotationDepth(im, annotationClass);
 +//                if (d > 0) {
 +//                    // since the annotation was on the interface, add 1
 +//                    return d + 1;
 +//                }
 +//            } catch (final SecurityException ex) {
 +//                continue;
 +//            } catch (final NoSuchMethodException ex) {
 +//                continue;
 +//            }
 +//        }
 +//
 +//        try {
 +//            int d = getAnnotationDepth(
 +//                    c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()),
 +//                    annotationClass);
 +//            if (d > 0) {
 +//                // since the annotation was on the superclass, add 1
 +//                return d + 1;
 +//            }
 +//            return -1;
 +//        } catch (final SecurityException ex) {
 +//            return -1;
 +//        } catch (final NoSuchMethodException ex) {
 +//            return -1;
 +//        }
 +//    }
 +
 +      /**
 +       * Put a key/boolean pair in the JSONObject.
 +       *
 +       * @param key   A key string.
 +       * @param value A boolean which is the value.
 +       * @return this.
 +       * @throws JSONException        If the value is non-finite number.
 +       * @throws NullPointerException If the key is <code>null</code>.
 +       */
 +      public JSONObject put(String key, boolean value) throws JSONException {
 +              return this.put(key, value ? Boolean.TRUE : Boolean.FALSE);
 +      }
 +
 +      /**
 +       * Put a key/value pair in the JSONObject, where the value will be a JSONArray
 +       * which is produced from a Collection.
 +       *
 +       * @param key   A key string.
 +       * @param value A Collection value.
 +       * @return this.
 +       * @throws JSONException        If the value is non-finite number.
 +       * @throws NullPointerException If the key is <code>null</code>.
 +       */
 +      public JSONObject put(String key, Collection<?> value) throws JSONException {
 +              return this.put(key, new JSONArray(value));
 +      }
 +
 +      /**
 +       * Put a key/double pair in the JSONObject.
 +       *
 +       * @param key   A key string.
 +       * @param value A double which is the value.
 +       * @return this.
 +       * @throws JSONException        If the value is non-finite number.
 +       * @throws NullPointerException If the key is <code>null</code>.
 +       */
 +      public JSONObject put(String key, double value) throws JSONException {
 +              return this.put(key, Double.valueOf(value));
 +      }
 +
 +      /**
 +       * Put a key/float pair in the JSONObject.
 +       *
 +       * @param key   A key string.
 +       * @param value A float which is the value.
 +       * @return this.
 +       * @throws JSONException        If the value is non-finite number.
 +       * @throws NullPointerException If the key is <code>null</code>.
 +       */
 +      public JSONObject put(String key, float value) throws JSONException {
 +              return this.put(key, Float.valueOf(value));
 +      }
 +
 +      /**
 +       * Put a key/int pair in the JSONObject.
 +       *
 +       * @param key   A key string.
 +       * @param value An int which is the value.
 +       * @return this.
 +       * @throws JSONException        If the value is non-finite number.
 +       * @throws NullPointerException If the key is <code>null</code>.
 +       */
 +      public JSONObject put(String key, int value) throws JSONException {
 +              return this.put(key, Integer.valueOf(value));
 +      }
 +
 +      /**
 +       * Put a key/long pair in the JSONObject.
 +       *
 +       * @param key   A key string.
 +       * @param value A long which is the value.
 +       * @return this.
 +       * @throws JSONException        If the value is non-finite number.
 +       * @throws NullPointerException If the key is <code>null</code>.
 +       */
 +      public JSONObject put(String key, long value) throws JSONException {
 +              return this.put(key, Long.valueOf(value));
 +      }
 +
 +      /**
 +       * Put a key/value pair in the JSONObject, where the value will be a JSONObject
 +       * which is produced from a Map.
 +       *
 +       * @param key   A key string.
 +       * @param value A Map value.
 +       * @return this.
 +       * @throws JSONException        If the value is non-finite number.
 +       * @throws NullPointerException If the key is <code>null</code>.
 +       */
 +      public JSONObject put(String key, Map<?, ?> value) throws JSONException {
 +              return this.put(key, new JSONObject(value));
 +      }
 +
 +      /**
 +       * Put a key/value pair in the JSONObject. If the value is <code>null</code>,
 +       * then the key will be removed from the JSONObject if it is present.
 +       *
 +       * @param key   A key string.
 +       * @param value An object which is the value. It should be of one of these
 +       *              types: Boolean, Double, Integer, JSONArray, JSONObject, Long,
 +       *              String, or the JSONObject.NULL object.
 +       * @return this.
 +       * @throws JSONException        If the value is non-finite number.
 +       * @throws NullPointerException If the key is <code>null</code>.
 +       */
 +      public JSONObject put(String key, Object value) throws JSONException {
 +              if (key == null) {
 +                      throw new NullPointerException("Null key.");
 +              }
 +              if (value != null) {
 +                      testValidity(value);
 +                      this.map.put(key, value);
 +              } else {
 +                      this.remove(key);
 +              }
 +              return this;
 +      }
 +
 +      /**
 +       * Put a key/value pair in the JSONObject, but only if the key and the value are
 +       * both non-null, and only if there is not already a member with that name.
 +       *
 +       * @param key   string
 +       * @param value object
 +       * @return this.
 +       * @throws JSONException if the key is a duplicate
 +       */
 +      public JSONObject putOnce(String key, Object value) throws JSONException {
 +              if (key != null && value != null) {
 +                      if (this.opt(key) != null) {
 +                              throw new JSONException("Duplicate key \"" + key + "\"");
 +                      }
 +                      return this.put(key, value);
 +              }
 +              return this;
 +      }
 +
 +      /**
 +       * Put a key/value pair in the JSONObject, but only if the key and the value are
 +       * both non-null.
 +       *
 +       * @param key   A key string.
 +       * @param value An object which is the value. It should be of one of these
 +       *              types: Boolean, Double, Integer, JSONArray, JSONObject, Long,
 +       *              String, or the JSONObject.NULL object.
 +       * @return this.
 +       * @throws JSONException If the value is a non-finite number.
 +       */
 +      public JSONObject putOpt(String key, Object value) throws JSONException {
 +              if (key != null && value != null) {
 +                      return this.put(key, value);
 +              }
 +              return this;
 +      }
 +
 +      /**
 +       * Creates a JSONPointer using an initialization string and tries to match it to
 +       * an item within this JSONObject. For example, given a JSONObject initialized
 +       * with this document:
 +       * 
 +       * <pre>
 +       * {
 +       *     "a":{"b":"c"}
 +       * }
 +       * </pre>
 +       * 
 +       * and this JSONPointer string:
 +       * 
 +       * <pre>
 +       * "/a/b"
 +       * </pre>
 +       * 
 +       * Then this method will return the String "c". A JSONPointerException may be
 +       * thrown from code called by this method.
 +       * 
 +       * @param jsonPointer string that can be used to create a JSONPointer
 +       * @return the item matched by the JSONPointer, otherwise null
 +       */
 +      public Object query(String jsonPointer) {
 +              return query(new JSONPointer(jsonPointer));
 +      }
 +
 +      /**
 +       * Uses a user initialized JSONPointer and tries to match it to an item within
 +       * this JSONObject. For example, given a JSONObject initialized with this
 +       * document:
 +       * 
 +       * <pre>
 +       * {
 +       *     "a":{"b":"c"}
 +       * }
 +       * </pre>
 +       * 
 +       * and this JSONPointer:
 +       * 
 +       * <pre>
 +       * "/a/b"
 +       * </pre>
 +       * 
 +       * Then this method will return the String "c". A JSONPointerException may be
 +       * thrown from code called by this method.
 +       * 
 +       * @param jsonPointer string that can be used to create a JSONPointer
 +       * @return the item matched by the JSONPointer, otherwise null
 +       */
 +      public Object query(JSONPointer jsonPointer) {
 +              return jsonPointer.queryFrom(this);
 +      }
 +
 +      /**
 +       * Queries and returns a value from this object using {@code jsonPointer}, or
 +       * returns null if the query fails due to a missing key.
 +       * 
 +       * @param jsonPointer the string representation of the JSON pointer
 +       * @return the queried value or {@code null}
 +       * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax
 +       */
 +      public Object optQuery(String jsonPointer) {
 +              return optQuery(new JSONPointer(jsonPointer));
 +      }
 +
 +      /**
 +       * Queries and returns a value from this object using {@code jsonPointer}, or
 +       * returns null if the query fails due to a missing key.
 +       * 
 +       * @param jsonPointer The JSON pointer
 +       * @return the queried value or {@code null}
 +       * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax
 +       */
 +      public Object optQuery(JSONPointer jsonPointer) {
 +              try {
 +                      return jsonPointer.queryFrom(this);
 +              } catch (JSONPointerException e) {
 +                      return null;
 +              }
 +      }
 +
 +      /**
 +       * Produce a string in double quotes with backslash sequences in all the right
 +       * places. A backslash will be inserted within </, producing <\/, allowing JSON
 +       * text to be delivered in HTML. In JSON text, a string cannot contain a control
 +       * character or an unescaped quote or backslash.
 +       *
 +       * @param string A String
 +       * @return A String correctly formatted for insertion in a JSON text.
 +       */
 +      public static String quote(String string) {
 +              StringWriter sw = new StringWriter();
 +              synchronized (sw.getBuffer()) {
 +                      try {
 +                              return quote(string, sw).toString();
 +                      } catch (IOException ignored) {
 +                              // will never happen - we are writing to a string writer
 +                              return "";
 +                      }
 +              }
 +      }
 +
 +      public static Writer quote(String string, Writer w) throws IOException {
 +              if (string == null || string.isEmpty()) {
 +                      w.write("\"\"");
 +                      return w;
 +              }
 +
 +              char b;
 +              char c = 0;
 +              String hhhh;
 +              int i;
 +              int len = string.length();
 +
 +              w.write('"');
 +              for (i = 0; i < len; i += 1) {
 +                      b = c;
 +                      c = string.charAt(i);
 +                      switch (c) {
 +                      case '\\':
 +                      case '"':
 +                              w.write('\\');
 +                              w.write(c);
 +                              break;
 +                      case '/':
 +                              if (b == '<') {
 +                                      w.write('\\');
 +                              }
 +                              w.write(c);
 +                              break;
 +                      case '\b':
 +                              w.write("\\b");
 +                              break;
 +                      case '\t':
 +                              w.write("\\t");
 +                              break;
 +                      case '\n':
 +                              w.write("\\n");
 +                              break;
 +                      case '\f':
 +                              w.write("\\f");
 +                              break;
 +                      case '\r':
 +                              w.write("\\r");
 +                              break;
 +                      default:
 +                              if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100')) {
 +                                      w.write("\\u");
 +                                      hhhh = Integer.toHexString(c);
 +                                      w.write("0000", 0, 4 - hhhh.length());
 +                                      w.write(hhhh);
 +                              } else {
 +                                      w.write(c);
 +                              }
 +                      }
 +              }
 +              w.write('"');
 +              return w;
 +      }
 +
 +      /**
 +       * Remove a name and its value, if present.
 +       *
 +       * @param key The name to be removed.
 +       * @return The value that was associated with the name, or null if there was no
 +       *         value.
 +       */
 +      public Object remove(String key) {
 +              return this.map.remove(key);
 +      }
 +
 +      /**
 +       * Determine if two JSONObjects are similar. They must contain the same set of
 +       * names which must be associated with similar values.
 +       *
 +       * @param other The other JSONObject
 +       * @return true if they are equal
 +       */
 +      public boolean similar(Object other) {
 +              try {
 +                      if (!(other instanceof JSONObject)) {
 +                              return false;
 +                      }
 +                      if (!this.keySet().equals(((JSONObject) other).keySet())) {
 +                              return false;
 +                      }
 +                      for (final Entry<String, ?> entry : this.entrySet()) {
 +                              String name = entry.getKey();
 +                              Object valueThis = entry.getValue();
 +                              Object valueOther = ((JSONObject) other).get(name);
 +                              if (valueThis == valueOther) {
 +                                      continue;
 +                              }
 +                              if (valueThis == null) {
 +                                      return false;
 +                              }
 +                              if (valueThis instanceof JSONObject) {
 +                                      if (!((JSONObject) valueThis).similar(valueOther)) {
 +                                              return false;
 +                                      }
 +                              } else if (valueThis instanceof JSONArray) {
 +                                      if (!((JSONArray) valueThis).similar(valueOther)) {
 +                                              return false;
 +                                      }
 +                              } else if (!valueThis.equals(valueOther)) {
 +                                      return false;
 +                              }
 +                      }
 +                      return true;
 +              } catch (Throwable exception) {
 +                      return false;
 +              }
 +      }
 +
 +      /**
 +       * Tests if the value should be tried as a decimal. It makes no test if there
 +       * are actual digits.
 +       * 
 +       * @param val value to test
 +       * @return true if the string is "-0" or if it contains '.', 'e', or 'E', false
 +       *         otherwise.
 +       */
 +      protected static boolean isDecimalNotation(final String val) {
 +              return val.indexOf('.') > -1 || val.indexOf('e') > -1 || val.indexOf('E') > -1 || "-0".equals(val);
 +      }
 +
 +      /**
 +       * Converts a string to a number using the narrowest possible type. Possible
 +       * returns for this function are BigDecimal, Double, BigInteger, Long, and
 +       * Integer. When a Double is returned, it should always be a valid Double and
 +       * not NaN or +-infinity.
 +       * 
 +       * @param val value to convert
 +       * @return Number representation of the value.
 +       * @throws NumberFormatException thrown if the value is not a valid number. A
 +       *                               public caller should catch this and wrap it in
 +       *                               a {@link JSONException} if applicable.
 +       */
 +      protected static Number stringToNumber(final String val) throws NumberFormatException {
 +              char initial = val.charAt(0);
 +              if ((initial >= '0' && initial <= '9') || initial == '-') {
 +                      // decimal representation
 +                      if (isDecimalNotation(val)) {
 +                              // quick dirty way to see if we need a BigDecimal instead of a Double
 +                              // this only handles some cases of overflow or underflow
 +                              if (val.length() > 14) {
 +                                      return new BigDecimal(val);
 +                              }
 +                              final Double d = Double.valueOf(val);
 +                              if (d.isInfinite() || d.isNaN()) {
 +                                      // if we can't parse it as a double, go up to BigDecimal
 +                                      // this is probably due to underflow like 4.32e-678
 +                                      // or overflow like 4.65e5324. The size of the string is small
 +                                      // but can't be held in a Double.
 +                                      return new BigDecimal(val);
 +                              }
 +                              return d;
 +                      }
 +                      // integer representation.
 +                      // This will narrow any values to the smallest reasonable Object representation
 +                      // (Integer, Long, or BigInteger)
 +
 +                      // string version
 +                      // The compare string length method reduces GC,
 +                      // but leads to smaller integers being placed in larger wrappers even though not
 +                      // needed. i.e. 1,000,000,000 -> Long even though it's an Integer
 +                      // 1,000,000,000,000,000,000 -> BigInteger even though it's a Long
 +                      // if(val.length()<=9){
 +                      // return Integer.valueOf(val);
 +                      // }
 +                      // if(val.length()<=18){
 +                      // return Long.valueOf(val);
 +                      // }
 +                      // return new BigInteger(val);
 +
 +                      // BigInteger version: We use a similar bitLenth compare as
 +                      // BigInteger#intValueExact uses. Increases GC, but objects hold
 +                      // only what they need. i.e. Less runtime overhead if the value is
 +                      // long lived. Which is the better tradeoff? This is closer to what's
 +                      // in stringToValue.
 +                      BigInteger bi = new BigInteger(val);
 +                      if (bi.bitLength() <= 31) {
 +                              return Integer.valueOf(bi.intValue());
 +                      }
 +                      if (bi.bitLength() <= 63) {
 +                              return Long.valueOf(bi.longValue());
 +                      }
 +                      return bi;
 +              }
 +              throw new NumberFormatException("val [" + val + "] is not a valid number.");
 +      }
 +
 +      /**
 +       * Try to convert a string into a number, boolean, or null. If the string can't
 +       * be converted, return the string.
 +       *
 +       * @param string A String.
 +       * @return A simple JSON value.
 +       */
 +      // Changes to this method must be copied to the corresponding method in
 +      // the XML class to keep full support for Android
 +      public static Object stringToValue(String string) {
 +              if (string.equals("")) {
 +                      return string;
 +              }
 +              if (string.equalsIgnoreCase("true")) {
 +                      return Boolean.TRUE;
 +              }
 +              if (string.equalsIgnoreCase("false")) {
 +                      return Boolean.FALSE;
 +              }
 +              if (string.equalsIgnoreCase("null")) {
 +                      return JSONObject.NULL;
 +              }
 +
 +              /*
 +               * If it might be a number, try converting it. If a number cannot be produced,
 +               * then the value will just be a string.
 +               */
 +
 +              char initial = string.charAt(0);
 +              if ((initial >= '0' && initial <= '9') || initial == '-') {
 +                      try {
 +                              // if we want full Big Number support this block can be replaced with:
 +                              // return stringToNumber(string);
 +                              if (isDecimalNotation(string)) {
 +                                      Double d = Double.valueOf(string);
 +                                      if (!d.isInfinite() && !d.isNaN()) {
 +                                              return d;
 +                                      }
 +                              } else {
 +                                      Long myLong = Long.valueOf(string);
 +                                      if (string.equals(myLong.toString())) {
 +                                              if (myLong.longValue() == myLong.intValue()) {
 +                                                      return Integer.valueOf(myLong.intValue());
 +                                              }
 +                                              return myLong;
 +                                      }
 +                              }
 +                      } catch (Exception ignore) {
 +                      }
 +              }
 +              return string;
 +      }
 +
 +      /**
 +       * Throw an exception if the object is a NaN or infinite number.
 +       *
 +       * @param o The object to test.
 +       * @throws JSONException If o is a non-finite number.
 +       */
 +      public static void testValidity(Object o) throws JSONException {
 +              if (o != null) {
 +                      if (o instanceof Double) {
 +                              if (((Double) o).isInfinite() || ((Double) o).isNaN()) {
 +                                      throw new JSONException("JSON does not allow non-finite numbers.");
 +                              }
 +                      } else if (o instanceof Float) {
 +                              if (((Float) o).isInfinite() || ((Float) o).isNaN()) {
 +                                      throw new JSONException("JSON does not allow non-finite numbers.");
 +                              }
 +                      }
 +              }
 +      }
 +
 +      /**
 +       * Produce a JSONArray containing the values of the members of this JSONObject.
 +       *
 +       * @param names A JSONArray containing a list of key strings. This determines
 +       *              the sequence of the values in the result.
 +       * @return A JSONArray of values.
 +       * @throws JSONException If any of the values are non-finite numbers.
 +       */
 +      public JSONArray toJSONArray(JSONArray names) throws JSONException {
 +              if (names == null || names.isEmpty()) {
 +                      return null;
 +              }
 +              JSONArray ja = new JSONArray();
 +              for (int i = 0; i < names.length(); i += 1) {
 +                      ja.put(this.opt(names.getString(i)));
 +              }
 +              return ja;
 +      }
 +
 +      /**
 +       * Make a JSON text of this JSONObject. For compactness, no whitespace is added.
 +       * If this would not result in a syntactically correct JSON text, then null will
 +       * be returned instead.
 +       * <p>
 +       * <b> Warning: This method assumes that the data structure is acyclical. </b>
 +       * 
 +       * @return a printable, displayable, portable, transmittable representation of
 +       *         the object, beginning with <code>{</code>&nbsp;<small>(left
 +       *         brace)</small> and ending with <code>}</code>&nbsp;<small>(right
 +       *         brace)</small>.
 +       */
 +      @Override
 +      public String toString() {
 +              try {
 +                      return this.toString(0);
 +              } catch (Exception e) {
 +                      return null;
 +              }
 +      }
 +
 +      /**
 +       * Make a pretty-printed JSON text of this JSONObject.
 +       * 
 +       * <p>
 +       * If <code>indentFactor > 0</code> and the {@link JSONObject} has only one key,
 +       * then the object will be output on a single line:
 +       * 
 +       * <pre>
 +       * {@code {"key": 1}}
 +       * </pre>
 +       * 
 +       * <p>
 +       * If an object has 2 or more keys, then it will be output across multiple
 +       * lines: <code><pre>{
 +       *  "key1": 1,
 +       *  "key2": "value 2",
 +       *  "key3": 3
 +       * }</pre></code>
 +       * <p>
 +       * <b> Warning: This method assumes that the data structure is acyclical. </b>
 +       *
 +       * @param indentFactor The number of spaces to add to each level of indentation.
 +       * @return a printable, displayable, portable, transmittable representation of
 +       *         the object, beginning with <code>{</code>&nbsp;<small>(left
 +       *         brace)</small> and ending with <code>}</code>&nbsp;<small>(right
 +       *         brace)</small>.
 +       * @throws JSONException If the object contains an invalid number.
 +       */
 +      public String toString(int indentFactor) throws JSONException {
 +              StringWriter w = new StringWriter();
 +              synchronized (w.getBuffer()) {
 +                      return this.write(w, indentFactor, 0).toString();
 +              }
 +      }
 +
 +      /**
 +       * Make a JSON text of an Object value. If the object has an
 +       * value.toJSONString() method, then that method will be used to produce the
 +       * JSON text. The method is required to produce a strictly conforming text. If
 +       * the object does not contain a toJSONString method (which is the most common
 +       * case), then a text will be produced by other means. If the value is an array
 +       * or Collection, then a JSONArray will be made from it and its toJSONString
 +       * method will be called. If the value is a MAP, then a JSONObject will be made
 +       * from it and its toJSONString method will be called. Otherwise, the value's
 +       * toString method will be called, and the result will be quoted.
 +       *
 +       * <p>
 +       * Warning: This method assumes that the data structure is acyclical.
 +       *
 +       * @param value The value to be serialized.
 +       * @return a printable, displayable, transmittable representation of the object,
 +       *         beginning with <code>{</code>&nbsp;<small>(left brace)</small> and
 +       *         ending with <code>}</code>&nbsp;<small>(right brace)</small>.
 +       * @throws JSONException If the value is or contains an invalid number.
 +       */
 +      public static String valueToString(Object value) throws JSONException {
 +              // moves the implementation to JSONWriter as:
 +              // 1. It makes more sense to be part of the writer class
 +              // 2. For Android support this method is not available. By implementing it in
 +              // the Writer
 +              // Android users can use the writer with the built in Android JSONObject
 +              // implementation.
 +              return JSONWriter.valueToString(value);
 +      }
 +
 +      /**
 +       * Wrap an object, if necessary. If the object is <code>null</code>, return the
 +       * NULL object. If it is an array or collection, wrap it in a JSONArray. If it
 +       * is a map, wrap it in a JSONObject. If it is a standard property (Double,
 +       * String, et al) then it is already wrapped. Otherwise, if it comes from one of
 +       * the java packages, turn it into a string. And if it doesn't, try to wrap it
 +       * in a JSONObject. If the wrapping fails, then null is returned.
 +       *
 +       * @param object The object to wrap
 +       * @return The wrapped value
 +       */
 +      public static Object wrap(Object object) {
 +              try {
 +                      if (object == null) {
 +                              return NULL;
 +                      }
 +                      if (object instanceof JSONObject || object instanceof JSONArray || NULL.equals(object)
 +                                      || object instanceof JSONString || object instanceof Byte || object instanceof Character
 +                                      || object instanceof Short || object instanceof Integer || object instanceof Long
 +                                      || object instanceof Boolean || object instanceof Float || object instanceof Double
 +                                      || object instanceof String || object instanceof BigInteger || object instanceof BigDecimal
 +                                      || object instanceof Enum) {
 +                              return object;
 +                      }
 +
 +                      if (object instanceof Collection) {
 +                              Collection<?> coll = (Collection<?>) object;
 +                              return new JSONArray(coll);
 +                      }
 +                      if (object.getClass().isArray()) {
 +                              return new JSONArray(object);
 +                      }
 +                      if (object instanceof Map) {
 +                              Map<?, ?> map = (Map<?, ?>) object;
 +                              return new JSONObject(map);
 +                      }
 +                      Package objectPackage = object.getClass().getPackage();
 +                      String objectPackageName = objectPackage != null ? objectPackage.getName() : "";
 +                      if (objectPackageName.startsWith("java.") || objectPackageName.startsWith("javax.")
 +                                      || object.getClass().getClassLoader() == null) {
 +                              return object.toString();
 +                      }
 +                      return new JSONObject(object);
 +              } catch (Exception exception) {
 +                      return null;
 +              }
 +      }
 +
 +      /**
 +       * Write the contents of the JSONObject as JSON text to a writer. For
 +       * compactness, no whitespace is added.
 +       * <p>
 +       * <b> Warning: This method assumes that the data structure is acyclical. </b>
 +       * 
 +       * @return The writer.
 +       * @throws JSONException
 +       */
 +      public Writer write(Writer writer) throws JSONException {
 +              return this.write(writer, 0, 0);
 +      }
 +
 +      static final Writer writeValue(Writer writer, Object value, int indentFactor, int indent)
 +                      throws JSONException, IOException {
 +              if (value == null || value.equals(null)) {
 +                      writer.write("null");
 +              } else if (value instanceof JSONString) {
 +                      Object o;
 +                      try {
 +                              o = ((JSONString) value).toJSONString();
 +                      } catch (Exception e) {
 +                              throw new JSONException(e);
 +                      }
 +                      writer.write(o != null ? o.toString() : quote(value.toString()));
 +              } else if (value instanceof Number) {
 +                      // not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary
 +                      final String numberAsString = numberToString((Number) value);
 +                      try {
 +                              // Use the BigDecimal constructor for its parser to validate the format.
 +                              @SuppressWarnings("unused")
 +                              BigDecimal testNum = new BigDecimal(numberAsString);
 +                              // Close enough to a JSON number that we will use it unquoted
 +                              writer.write(numberAsString);
 +                      } catch (NumberFormatException ex) {
 +                              // The Number value is not a valid JSON number.
 +                              // Instead we will quote it as a string
 +                              quote(numberAsString, writer);
 +                      }
 +              } else if (value instanceof Boolean) {
 +                      writer.write(value.toString());
 +              } else if (value instanceof Enum<?>) {
 +                      writer.write(quote(((Enum<?>) value).name()));
 +              } else if (value instanceof JSONObject) {
 +                      ((JSONObject) value).write(writer, indentFactor, indent);
 +              } else if (value instanceof JSONArray) {
 +                      ((JSONArray) value).write(writer, indentFactor, indent);
 +              } else if (value instanceof Map) {
 +                      Map<?, ?> map = (Map<?, ?>) value;
 +                      new JSONObject(map).write(writer, indentFactor, indent);
 +              } else if (value instanceof Collection) {
 +                      Collection<?> coll = (Collection<?>) value;
 +                      new JSONArray(coll).write(writer, indentFactor, indent);
 +              } else if (value.getClass().isArray()) {
 +                      new JSONArray(value).write(writer, indentFactor, indent);
 +              } else {
 +                      quote(value.toString(), writer);
 +              }
 +              return writer;
 +      }
 +
 +      static final void indent(Writer writer, int indent) throws IOException {
 +              for (int i = 0; i < indent; i += 1) {
 +                      writer.write(' ');
 +              }
 +      }
 +
 +      /**
 +       * Write the contents of the JSONObject as JSON text to a writer.
 +       * 
 +       * <p>
 +       * If <code>indentFactor > 0</code> and the {@link JSONObject} has only one key,
 +       * then the object will be output on a single line:
 +       * 
 +       * <pre>
 +       * {@code {"key": 1}}
 +       * </pre>
 +       * 
 +       * <p>
 +       * If an object has 2 or more keys, then it will be output across multiple
 +       * lines: <code><pre>{
 +       *  "key1": 1,
 +       *  "key2": "value 2",
 +       *  "key3": 3
 +       * }</pre></code>
 +       * <p>
 +       * <b> Warning: This method assumes that the data structure is acyclical. </b>
 +       *
 +       * @param writer       Writes the serialized JSON
 +       * @param indentFactor The number of spaces to add to each level of indentation.
 +       * @param indent       The indentation of the top level.
 +       * @return The writer.
 +       * @throws JSONException
 +       */
 +      public Writer write(Writer writer, int indentFactor, int indent) throws JSONException {
 +              try {
 +                      boolean commanate = false;
 +                      final int length = this.length();
 +                      writer.write('{');
 +
 +                      if (length == 1) {
 +                              final Entry<String, ?> entry = this.entrySet().iterator().next();
 +                              final String key = entry.getKey();
 +                              writer.write(quote(key));
 +                              writer.write(':');
 +                              if (indentFactor > 0) {
 +                                      writer.write(' ');
 +                              }
 +                              try {
 +                                      writeValue(writer, entry.getValue(), indentFactor, indent);
 +                              } catch (Exception e) {
 +                                      throw new JSONException("Unable to write JSONObject value for key: " + key, e);
 +                              }
 +                      } else if (length != 0) {
 +                              final int newindent = indent + indentFactor;
 +                              for (final Entry<String, ?> entry : this.entrySet()) {
 +                                      if (commanate) {
 +                                              writer.write(',');
 +                                      }
 +                                      if (indentFactor > 0) {
 +                                              writer.write('\n');
 +                                      }
 +                                      indent(writer, newindent);
 +                                      final String key = entry.getKey();
 +                                      writer.write(quote(key));
 +                                      writer.write(':');
 +                                      if (indentFactor > 0) {
 +                                              writer.write(' ');
 +                                      }
 +                                      try {
 +                                              writeValue(writer, entry.getValue(), indentFactor, newindent);
 +                                      } catch (Exception e) {
 +                                              throw new JSONException("Unable to write JSONObject value for key: " + key, e);
 +                                      }
 +                                      commanate = true;
 +                              }
 +                              if (indentFactor > 0) {
 +                                      writer.write('\n');
 +                              }
 +                              indent(writer, indent);
 +                      }
 +                      writer.write('}');
 +                      return writer;
 +              } catch (IOException exception) {
 +                      throw new JSONException(exception);
 +              }
 +      }
 +
 +      /**
 +       * Returns a java.util.Map containing all of the entries in this object. If an
 +       * entry in the object is a JSONArray or JSONObject it will also be converted.
 +       * <p>
 +       * Warning: This method assumes that the data structure is acyclical.
 +       *
 +       * @return a java.util.Map containing the entries of this object
 +       */
 +      public Map<String, Object> toMap() {
 +              Map<String, Object> results = new HashMap<String, Object>();
 +              for (Entry<String, Object> entry : this.entrySet()) {
 +                      Object value;
 +                      if (entry.getValue() == null || NULL.equals(entry.getValue())) {
 +                              value = null;
 +                      } else if (entry.getValue() instanceof JSONObject) {
 +                              value = ((JSONObject) entry.getValue()).toMap();
 +                      } else if (entry.getValue() instanceof JSONArray) {
 +                              value = ((JSONArray) entry.getValue()).toList();
 +                      } else {
 +                              value = entry.getValue();
 +                      }
 +                      results.put(entry.getKey(), value);
 +              }
 +              return results;
 +      }
 +}
@@@ -25,16 -25,21 +25,24 @@@ import static org.testng.AssertJUnit.as
  
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.Annotation;
+ import jalview.datamodel.HiddenMarkovModel;
  import jalview.datamodel.Profile;
  import jalview.datamodel.ProfileI;
+ import jalview.datamodel.Profiles;
  import jalview.datamodel.ProfilesI;
 +import jalview.datamodel.ResidueCount;
  import jalview.datamodel.Sequence;
  import jalview.datamodel.SequenceI;
  import jalview.gui.JvOptionPane;
+ import jalview.io.DataSourceType;
+ import jalview.io.FileParse;
+ import jalview.io.HMMFile;
+ import java.io.IOException;
+ import java.net.MalformedURLException;
  
 +import java.util.Hashtable;
 +
  import org.testng.annotations.BeforeClass;
  import org.testng.annotations.Test;
  
@@@ -237,182 -254,93 +257,271 @@@ public class AAFrequencyTes
      assertEquals("T", ann.displayCharacter);
    }
  
 +  /**
 +   * Test to include rounding down of a non-zero count to 0% (JAL-3202)
 +   */
 +  @Test(groups = { "Functional" })
 +  public void testExtractProfile()
 +  {
 +    /*
 +     * 200 sequences of which 30 gapped (170 ungapped)
 +     * max count 70 for modal residue 'G'
 +     */
 +    ProfileI profile = new Profile(200, 30, 70, "G");
 +    ResidueCount counts = new ResidueCount();
 +    counts.put('G', 70);
 +    counts.put('R', 60);
 +    counts.put('L', 38);
 +    counts.put('H', 2);
 +    profile.setCounts(counts);
 +
 +    /*
 +     * [0, noOfValues, totalPercent, char1, count1, ...]
 +     * G: 70/170 = 41.2 = 41
 +     * R: 60/170 = 35.3 = 35
 +     * L: 38/170 = 22.3 = 22
 +     * H: 2/170 = 1
 +     * total (rounded) percentages = 99 
 +     */
 +    int[] extracted = AAFrequency.extractProfile(profile, true);
 +    int[] expected = new int[] { 0, 4, 99, 'G', 41, 'R', 35, 'L', 22, 'H',
 +        1 };
 +    org.testng.Assert.assertEquals(extracted, expected);
 +
 +    /*
 +     * add some counts of 1; these round down to 0% and should be discarded
 +     */
 +    counts.put('G', 68); // 68/170 = 40% exactly (percentages now total 98)
 +    counts.put('Q', 1);
 +    counts.put('K', 1);
 +    extracted = AAFrequency.extractProfile(profile, true);
 +    expected = new int[] { 0, 4, 98, 'G', 40, 'R', 35, 'L', 22, 'H', 1 };
 +    org.testng.Assert.assertEquals(extracted, expected);
 +
 +  }
 +
 +  /**
 +   * Tests for the profile calculation where gaps are included i.e. the
 +   * denominator is the total number of sequences in the column
 +   */
 +  @Test(groups = { "Functional" })
 +  public void testExtractProfile_countGaps()
 +  {
 +    /*
 +     * 200 sequences of which 30 gapped (170 ungapped)
 +     * max count 70 for modal residue 'G'
 +     */
 +    ProfileI profile = new Profile(200, 30, 70, "G");
 +    ResidueCount counts = new ResidueCount();
 +    counts.put('G', 70);
 +    counts.put('R', 60);
 +    counts.put('L', 38);
 +    counts.put('H', 2);
 +    profile.setCounts(counts);
 +  
 +    /*
 +     * [0, noOfValues, totalPercent, char1, count1, ...]
 +     * G: 70/200 = 35%
 +     * R: 60/200 = 30%
 +     * L: 38/200 = 19%
 +     * H: 2/200 = 1%
 +     * total (rounded) percentages = 85 
 +     */
 +    int[] extracted = AAFrequency.extractProfile(profile, false);
 +    int[] expected = new int[] { AlignmentAnnotation.SEQUENCE_PROFILE, 4,
 +        85, 'G', 35, 'R', 30, 'L', 19, 'H',
 +        1 };
 +    org.testng.Assert.assertEquals(extracted, expected);
 +  
 +    /*
 +     * add some counts of 1; these round down to 0% and should be discarded
 +     */
 +    counts.put('G', 68); // 68/200 = 34%
 +    counts.put('Q', 1);
 +    counts.put('K', 1);
 +    extracted = AAFrequency.extractProfile(profile, false);
 +    expected = new int[] { AlignmentAnnotation.SEQUENCE_PROFILE, 4, 84, 'G',
 +        34, 'R', 30, 'L', 19, 'H', 1 };
 +    org.testng.Assert.assertEquals(extracted, expected);
 +  
 +  }
 +
 +  @Test(groups = { "Functional" })
 +  public void testExtractCdnaProfile()
 +  {
 +    /*
 +     * 200 sequences of which 30 gapped (170 ungapped)
 +     * max count 70 for modal residue 'G'
 +     */
 +    Hashtable profile = new Hashtable();
 +
 +    /*
 +     *  cdna profile is {seqCount, ungappedCount, codonCount1, ...codonCount64}
 +     * where 1..64 positions correspond to encoded codons
 +     * see CodingUtils.encodeCodon()
 +     */
 +    int[] codonCounts = new int[66];
 +    char[] codon1 = new char[] { 'G', 'C', 'A' };
 +    char[] codon2 = new char[] { 'c', 'C', 'A' };
 +    char[] codon3 = new char[] { 't', 'g', 'A' };
 +    char[] codon4 = new char[] { 'G', 'C', 't' };
 +    int encoded1 = CodingUtils.encodeCodon(codon1);
 +    int encoded2 = CodingUtils.encodeCodon(codon2);
 +    int encoded3 = CodingUtils.encodeCodon(codon3);
 +    int encoded4 = CodingUtils.encodeCodon(codon4);
 +    codonCounts[2 + encoded1] = 30;
 +    codonCounts[2 + encoded2] = 70;
 +    codonCounts[2 + encoded3] = 9;
 +    codonCounts[2 + encoded4] = 1;
 +    codonCounts[0] = 120;
 +    codonCounts[1] = 110;
 +    profile.put(AAFrequency.PROFILE, codonCounts);
 +  
 +    /*
 +     * [0, noOfValues, totalPercent, char1, count1, ...]
 +     * codon1: 30/110 = 27.2 = 27% 
 +     * codon2: 70/110 = 63.6% = 63%
 +     * codon3: 9/110 = 8.1% = 8%
 +     * codon4: 1/110 = 0.9% = 0% should be discarded
 +     * total (rounded) percentages = 98
 +     */
 +    int[] extracted = AAFrequency.extractCdnaProfile(profile, true);
 +    int[] expected = new int[] { AlignmentAnnotation.CDNA_PROFILE, 3, 98,
 +        encoded2, 63, encoded1, 27, encoded3, 8 };
 +    org.testng.Assert.assertEquals(extracted, expected);
 +  }
 +
 +  @Test(groups = { "Functional" })
 +  public void testExtractCdnaProfile_countGaps()
 +  {
 +    /*
 +     * 200 sequences of which 30 gapped (170 ungapped)
 +     * max count 70 for modal residue 'G'
 +     */
 +    Hashtable profile = new Hashtable();
 +  
 +    /*
 +     *  cdna profile is {seqCount, ungappedCount, codonCount1, ...codonCount64}
 +     * where 1..64 positions correspond to encoded codons
 +     * see CodingUtils.encodeCodon()
 +     */
 +    int[] codonCounts = new int[66];
 +    char[] codon1 = new char[] { 'G', 'C', 'A' };
 +    char[] codon2 = new char[] { 'c', 'C', 'A' };
 +    char[] codon3 = new char[] { 't', 'g', 'A' };
 +    char[] codon4 = new char[] { 'G', 'C', 't' };
 +    int encoded1 = CodingUtils.encodeCodon(codon1);
 +    int encoded2 = CodingUtils.encodeCodon(codon2);
 +    int encoded3 = CodingUtils.encodeCodon(codon3);
 +    int encoded4 = CodingUtils.encodeCodon(codon4);
 +    codonCounts[2 + encoded1] = 30;
 +    codonCounts[2 + encoded2] = 70;
 +    codonCounts[2 + encoded3] = 9;
 +    codonCounts[2 + encoded4] = 1;
 +    codonCounts[0] = 120;
 +    codonCounts[1] = 110;
 +    profile.put(AAFrequency.PROFILE, codonCounts);
 +  
 +    /*
 +     * [0, noOfValues, totalPercent, char1, count1, ...]
 +     * codon1: 30/120 = 25% 
 +     * codon2: 70/120 = 58.3 = 58%
 +     * codon3: 9/120 = 7.5 = 7%
 +     * codon4: 1/120 = 0.8 = 0% should be discarded
 +     * total (rounded) percentages = 90
 +     */
 +    int[] extracted = AAFrequency.extractCdnaProfile(profile, false);
 +    int[] expected = new int[] { AlignmentAnnotation.CDNA_PROFILE, 3, 90,
 +        encoded2, 58, encoded1, 25, encoded3, 7 };
 +    org.testng.Assert.assertEquals(extracted, expected);
 +  }
++
+   @Test(groups = { "Functional" })
+   public void testExtractHMMProfile()
+           throws MalformedURLException, IOException
+   {
+     int[] expected = { 0, 4, 100, 'T', 71, 'C', 12, 'G', 9, 'A', 9 };
+     int[] actual = AAFrequency.extractHMMProfile(hmm, 17, false, false);
+     for (int i = 0; i < actual.length; i++)
+     {
+       if (i == 2)
+       {
+         assertEquals(actual[i], expected[i]);
+       }
+       else
+       {
+         assertEquals(actual[i], expected[i]);
+       }
+     }
+     int[] expected2 = { 0, 4, 100, 'A', 85, 'C', 0, 'G', 0, 'T', 0 };
+     int[] actual2 = AAFrequency.extractHMMProfile(hmm, 2, true, false);
+     for (int i = 0; i < actual2.length; i++)
+     {
+       if (i == 2)
+       {
+         assertEquals(actual[i], expected[i]);
+       }
+       else
+       {
+         assertEquals(actual[i], expected[i]);
+       }
+     }
+     assertNull(AAFrequency.extractHMMProfile(null, 98978867, true, false));
+   }
+   @Test(groups = { "Functional" })
+   public void testGetAnalogueCount()
+   {
+     /*
+      * 'T' in column 0 has emission probability 0.7859, scales to 7859
+      */
+     int count = AAFrequency.getAnalogueCount(hmm, 0, 'T', false, false);
+     assertEquals(7859, count);
+     /*
+      * same with 'use info height': value is multiplied by log ratio
+      * log(value / background) / log(2) = log(0.7859/0.25)/0.693
+      * = log(3.1)/0.693 = 1.145/0.693 = 1.66
+      * so value becomes 1.2987 and scales up to 12987
+      */
+     count = AAFrequency.getAnalogueCount(hmm, 0, 'T', false, true);
+     assertEquals(12987, count);
+     /*
+      * 'G' in column 20 has emission probability 0.75457, scales to 7546
+      */
+     count = AAFrequency.getAnalogueCount(hmm, 20, 'G', false, false);
+     assertEquals(7546, count);
+     /*
+      * 'G' in column 1077 has emission probability 0.0533, here
+      * ignored (set to 0) since below background of 0.25
+      */
+     count = AAFrequency.getAnalogueCount(hmm, 1077, 'G', true, false);
+     assertEquals(0, count);
+   }
+   @Test(groups = { "Functional" })
+   public void testCompleteInformation()
+   {
+     ProfileI prof1 = new Profile(1, 0, 100, "A");
+     ProfileI prof2 = new Profile(1, 0, 100, "-");
+     ProfilesI profs = new Profiles(new ProfileI[] { prof1, prof2 });
+     Annotation ann1 = new Annotation(6.5f);
+     Annotation ann2 = new Annotation(0f);
+     Annotation[] annots = new Annotation[] { ann1, ann2 };
+     SequenceI seq = new Sequence("", "AA", 0, 0);
+     seq.setHMM(hmm);
+     AlignmentAnnotation annot = new AlignmentAnnotation("", "", annots);
+     annot.setSequenceRef(seq);
+     AAFrequency.completeInformation(annot, profs, 0, 1);
+     float ic = annot.annotations[0].value;
+     assertEquals(0.91532f, ic, 0.0001f);
+     ic = annot.annotations[1].value;
+     assertEquals(0f, ic, 0.0001f);
+     int i = 0;
+   }
 -
  }
@@@ -42,10 -42,8 +42,9 @@@ import jalview.util.DBRefUtils
  import jalview.util.MapList;
  import jalview.ws.SequenceFetcher;
  import jalview.ws.SequenceFetcherFactory;
- import jalview.ws.params.InvalidArgumentException;
  
  import java.util.ArrayList;
 +import java.util.Arrays;
  import java.util.List;
  
  import org.testng.annotations.AfterClass;
@@@ -25,7 -25,7 +25,6 @@@ import static org.testng.AssertJUnit.as
  
  import jalview.gui.JvOptionPane;
  
--import javax.swing.JComboBox;
  import javax.swing.JInternalFrame;
  
  import org.testng.annotations.AfterMethod;
@@@ -27,14 -27,7 +27,15 @@@ import static org.testng.Assert.assertN
  import static org.testng.Assert.assertSame;
  import static org.testng.Assert.assertTrue;
  
 +import java.awt.Color;
 +import java.util.Iterator;
 +
 +import org.testng.annotations.AfterMethod;
 +import org.testng.annotations.BeforeClass;
 +import org.testng.annotations.BeforeMethod;
 +import org.testng.annotations.Test;
 +
+ import jalview.api.AlignViewportI;
  import jalview.api.FeatureColourI;
  import jalview.bin.Cache;
  import jalview.bin.Jalview;
@@@ -56,7 -49,16 +57,8 @@@ import jalview.schemes.JalviewColourSch
  import jalview.schemes.StrandColourScheme;
  import jalview.schemes.TurnColourScheme;
  import jalview.util.MessageManager;
+ import jalview.viewmodel.AlignmentViewport;
  
 -import java.awt.Color;
 -import java.util.Iterator;
 -
 -import org.testng.annotations.AfterMethod;
 -import org.testng.annotations.BeforeClass;
 -import org.testng.annotations.BeforeMethod;
 -import org.testng.annotations.Test;
 -
  public class AlignFrameTest
  {
    AlignFrame af;
Simple merge
Simple merge
@@@ -2,16 -2,16 +2,16 @@@ package jalview.schemes
  
  import static org.testng.Assert.assertEquals;
  
 +import java.awt.Color;
 +
 +import org.testng.annotations.Test;
 +
  import jalview.datamodel.SequenceI;
  import jalview.gui.AlignFrame;
- import jalview.gui.AlignViewport;
  import jalview.io.DataSourceType;
  import jalview.io.FileLoader;
+ import jalview.viewmodel.AlignmentViewport;
  
 -import java.awt.Color;
 -
 -import org.testng.annotations.Test;
 -
  public class PIDColourSchemeTest
  {
    static final Color white = Color.white;
       */
      AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqs,
              DataSourceType.PASTE);
-     AlignViewport viewport = af.getViewport();
+     AlignmentViewport viewport = af.getViewport();
      viewport.setIgnoreGapsConsensus(false, af.alignPanel);
 -    while (viewport.getConsensusSeq() == null)
 +    do
      {
 -      synchronized (this)
 +      try
 +      {
 +        Thread.sleep(50);
 +      } catch (InterruptedException x)
        {
 -        try
 -        {
 -          wait(50);
 -        } catch (InterruptedException e)
 -        {
 -        }
        }
 -    }
 +    } while (af.getViewport().getCalcManager().isWorking());
      af.changeColour_actionPerformed(JalviewColourScheme.PID.toString());
  
      SequenceI seq = viewport.getAlignment().getSequenceAt(0);