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

@@@ -11,6 -11,7 +11,7 @@@ action.paste = Past
  action.show_html_source = Show HTML Source
  action.print = Print...
  action.web_service = Web Service
+ action.hmmer = HMMER
  action.cancel_job = Cancel Job
  action.start_job = Start Job
  action.revert = Revert
@@@ -59,6 -60,8 +60,8 @@@ action.boxes = Boxe
  action.text = Text
  action.by_pairwise_id = By Pairwise Identity
  action.by_id = By Id
+ action.by_evalue = By E-Value
+ action.by_bit_score = By Bit Score
  action.by_length = By Length
  action.by_group = By Group
  action.unmark_as_reference = Unmark as Reference 
@@@ -98,6 -101,7 +101,7 @@@ action.edit_group = Edit Grou
  action.border_colour = Border colour
  action.edit_new_group = Edit New Group
  action.hide_sequences = Hide Sequences
+ action.add_background_frequencies = Add Background Frequencies
  action.sequences = Sequences
  action.ids = IDS
  action.ids_sequences = IDS and sequences
@@@ -131,6 -135,8 +135,8 @@@ action.select_highlighted_columns = Sel
  tooltip.select_highlighted_columns = Press B to mark highlighted columns, Ctrl-(or Cmd)-B to toggle, and Alt-B to mark all but highlighted columns 
  action.deselect_all = Deselect all
  action.invert_selection = Invert selection
+ action.filter_by_evalue = Filter by E-Value
+ action.filter_by_score = Filter by Score
  action.using_jmol = Using Jmol
  action.undo_changes_to_feature_settings = Undo all unapplied changes to feature settings
  action.undo_changes_to_feature_settings_and_close_the_dialog = Undo all pending changes and close the feature settings dialog
@@@ -200,6 -206,9 +206,9 @@@ label.colourScheme_turnpropensity = Tur
  label.colourScheme_buriedindex = Buried Index
  label.colourScheme_purine/pyrimidine = Purine/Pyrimidine
  label.colourScheme_nucleotide = Nucleotide
+ label.colourScheme_hmmer-uniprot = HMMER profile v global background
+ label.colourScheme_hmmer-alignment = HMMER profile v alignment background
+ label.colourScheme_hmm_match_score = HMM Match Score
  label.colourScheme_t-coffeescores = T-Coffee Scores
  label.colourScheme_rnahelices = By RNA Helices
  label.colourScheme_sequenceid = Sequence ID Colour
@@@ -363,8 -372,7 +372,8 @@@ label.open_saved_vamsas_session = Open 
  label.groovy_console = Groovy Console...
  label.lineart = Lineart
  label.dont_ask_me_again = Don't ask me again
 -label.select_eps_character_rendering_style = Select EPS character rendering style
 +label.select_character_rendering_style = {0} character rendering style
 +label.select_character_style_title = {0} Rendering options
  label.invert_selection = Invert Selection
  label.optimise_order = Optimise Order
  label.seq_sort_by_score = Sequence sort by Score
@@@ -413,7 -421,7 +422,7 @@@ label.input_alignment_from_url = Input 
  label.input_alignment = Input Alignment
  label.couldnt_import_as_vamsas_session = Couldn't import {0} as a new vamsas session.
  label.vamsas_document_import_failed = Vamsas Document Import Failed
 -label.couldnt_locate = Could not locate {0}
 +label.couldnt_locate = Couldn''t locate {0}
  label.url_not_found = URL not found
  label.new_sequence_url_link = New sequence URL link
  label.cannot_edit_annotations_in_wrapped_view = Cannot edit annotations in wrapped view
@@@ -599,7 -607,7 +608,7 @@@ label.check_for_questionnaires = Check 
  label.check_for_latest_version = Check for latest version
  label.url_linkfrom_sequence_id = URL link from Sequence ID
  label.use_proxy_server = Use a proxy server
 -label.eps_rendering_style = EPS rendering style
 +label.rendering_style = {0} rendering style
  label.append_start_end = Append /start-end (/15-380)
  label.full_sequence_id = Full Sequence Id
  label.smooth_font = Smooth Font
@@@ -680,7 -688,7 +689,7 @@@ label.sequence_details_for = Sequence D
  label.sequence_name = Sequence Name
  label.sequence_description = Sequence Description
  label.edit_sequence_name_description = Edit Sequence Name/Description
 -label.spaces_converted_to_backslashes = Spaces have been converted to _
 +label.spaces_converted_to_underscores = Spaces have been converted to _
  label.no_spaces_allowed_sequence_name = No spaces allowed in Sequence Name
  label.select_outline_colour = Select Outline Colour
  label.web_browser_not_found_unix = Unixers\: Couldn't find default web browser.\nAdd the full path to your browser in Preferences."
@@@ -810,8 -818,8 +819,8 @@@ label.fetch_retrieve_from_all_sources 
  label.feature_settings_click_drag = Drag up or down to change render order.<br/>Double click to select columns containing feature.
  label.transparency_tip = Adjust transparency to 'see through' feature colours.
  label.opt_and_params_further_details = see further details by right-clicking
- label.opt_and_params_show_brief_desc_image_link = <html>Click to show brief description<br><img src="{0}"/> Right click for further information.</html> 
- label.opt_and_params_show_brief_desc = <html>Click to show brief description<br></html>
+ label.opt_and_params_show_brief_desc_image_link = Click to show brief description<br><img src="{0}"/> Right click for further information. 
+ label.opt_and_params_show_brief_desc = Click to show brief description<br>
  label.adjusts_width_generated_eps_png = <html>Adjusts the width of the generated EPS or PNG file to ensure even the longest sequence ID or annotation label is displayed</html>
  label.manually_specify_width_left_column = <html>Manually specify the width of the left hand column where sequence IDs and annotation labels will be rendered in exported alignment figures. This setting will be ignored if 'Automatically set ID width' is set</html>
  label.job_created_when_checked = <html>When checked, a job is created for every sequence in the current selection.</html>
@@@ -885,6 -893,8 +894,6 @@@ label.save_feature_colours = Save Featu
  label.select_startup_file = Select startup file
  label.select_default_browser = Select default web browser
  label.save_tree_as_newick = Save tree as newick file
 -label.create_eps_from_tree = Create EPS file from tree
 -label.create_png_from_tree = Create PNG image from tree
  label.save_colour_scheme = Save colour scheme
  label.edit_params_for = Edit parameters for {0}
  label.choose_filename_for_param_file = Choose a filename for this parameter file
@@@ -940,13 -950,14 +949,12 @@@ error.call_setprogressbar_before_regist
  label.cancelled_params = Cancelled {0}
  error.implementation_error_cannot_show_view_alignment_frame = Implementation error: cannot show a view from another alignment in an AlignFrame.
  error.implementation_error_dont_know_about_threshold_setting = Implementation error: don't know about threshold setting for current AnnotationColourGradient.
 -error.eps_generation_not_implemented = EPS Generation not yet implemented
 -error.png_generation_not_implemented = PNG Generation not yet implemented
  error.try_join_vamsas_session_another = Trying to join a vamsas session when another is already connected
  error.invalid_vamsas_session_id = Invalid vamsas session id
  label.groovy_support_failed = Jalview Groovy Support Failed
  label.couldnt_create_groovy_shell = Couldn't create the groovy Shell. Check the error log for the details of what went wrong.
  error.unsupported_version_calcIdparam = Unsupported Version for calcIdparam {0}
  error.implementation_error_cant_reorder_tree = Implementation Error: Can't reorder this tree. Not DefaultMutableTreeNode.
- error.invalid_value_for_option = Invalid value {0} for option {1}
  error.implementation_error_cannot_import_vamsas_doc = Implementation Error - cannot import existing vamsas document into an existing session, Yet!
  label.vamsas_doc_couldnt_be_opened_as_new_session = VAMSAS Document could not be opened as a new session - please choose another
  error.implementation_error_vamsas_operation_not_init = Impementation error! Vamsas Operations when client not initialised and connected
@@@ -1011,7 -1022,7 +1019,7 @@@ label.pca_recalculating = Recalculatin
  label.pca_calculating = Calculating PCA
  label.select_foreground_colour = Choose foreground colour
  label.select_colour_for_text = Select Colour for Text
 -label.adjunst_foreground_text_colour_threshold = Adjust Foreground Text Colour Threshold
 +label.adjust_foreground_text_colour_threshold = Adjust Foreground Text Colour Threshold
  label.select_subtree_colour = Select Sub-Tree Colour
  label.create_new_sequence_features = Create New Sequence Feature(s)
  label.amend_delete_features = Amend/Delete Features for {0}
@@@ -1054,6 -1065,7 +1062,7 @@@ exception.ranml_couldnt_process_data = 
  exception.ranml_invalid_file = Invalid RNAML file ({0})
  exception.ranml_problem_parsing_data = Problem parsing data as RNAML ({0})
  exception.pfam_no_sequences_found = No sequences found (PFAM input)
+ exception.hmmer_no_valid_sequences_found = No valid sequences found
  exception.stockholm_invalid_format = This file is not in valid STOCKHOLM format: First line does not contain '# STOCKHOLM'
  exception.couldnt_parse_sequence_line = Could not parse sequence line: {0}
  exception.unknown_annotation_detected = Unknown annotation detected: {0} {1}
@@@ -1083,6 -1095,7 +1092,6 @@@ error.implementation_error_cannot_find_
  exception.jobsubmission_invalid_params_set = Invalid parameter set. Check Jalview implementation
  exception.notvaliddata_group_contains_less_than_min_seqs = Group contains less than {0} sequences.
  exception.outofmemory_loading_pdb_file = Out of memory loading PDB File
 -exception.eps_coudnt_write_output_file = Could not write to the output file: {0}
  exception.eps_method_not_supported = Method not currently supported by EpsGraphics2D version {0}
  exception.eps_unable_to_get_inverse_matrix = Unable to get inverse of matrix: {0}
  warn.job_cannot_be_cancelled_close_window = This job cannot be cancelled.\nJust close the window.
@@@ -1107,7 -1120,8 +1116,7 @@@ status.searching_for_sequences_from = S
  status.finished_searching_for_sequences_from = Finished searching for sequences from {0}
  label.eps_file = EPS file
  label.png_image = PNG image
 -status.saving_file = Saving {0}
 -status.export_complete = {0} Export completed.
 +status.export_complete = {0} Export completed
  status.fetching_pdb = Fetching PDB {0}
  status.refreshing_news = Refreshing news
  status.importing_vamsas_session_from = Importing VAMSAS session from {0}
@@@ -1125,6 -1139,9 +1134,9 @@@ status.loading_cached_pdb_entries = Loa
  status.searching_for_pdb_structures = Searching for PDB Structures
  status.opening_file_for = opening file for
  status.colouring_chimera = Colouring Chimera
+ status.running_hmmbuild = Building Hidden Markov Model
+ status.running_hmmalign = Creating alignment with Hidden Markov Model
+ status.running_search = Searching for matching sequences
  label.font_doesnt_have_letters_defined = Font doesn't have letters defined\nso cannot be used\nwith alignment data
  label.font_too_small = Font size is too small
  label.error_loading_file_params = Error loading file {0}
@@@ -1231,6 -1248,7 +1243,6 @@@ exception.fts_server_unreachable = Jalv
  label.nw_mapping = Needleman & Wunsch Alignment
  label.sifts_mapping = SIFTs Mapping
  label.mapping_method = Sequence \u27f7 Structure mapping method
 -status.waiting_for_user_to_select_output_file = Waiting for user to select {0} file
  status.cancelled_image_export_operation = Cancelled {0} export operation
  info.error_creating_file = Error creating {0} file
  exception.outofmemory_loading_mmcif_file = Out of memory loading mmCIF File
@@@ -1313,7 -1331,7 +1325,7 @@@ label.delete_condition = Delete this co
  label.score = Score
  label.colour_by_label = Colour by label
  label.variable_colour = Variable colour...
 -label.select_colour = Select colour
 +label.select_colour_for = Select colour for {0}
  option.enable_disable_autosearch = When ticked, search is performed automatically
  option.autosearch = Autosearch
  label.retrieve_ids = Retrieve IDs
@@@ -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
@@@ -1413,3 -1497,4 +1498,4 @@@ label.include_linked_features = Includ
  label.include_linked_tooltip = Include visible {0} features<br>converted to local sequence coordinates
  label.features_not_shown = {0} feature(s) not shown
  label.no_features_to_sort_by = No features to sort by
@@@ -328,8 -328,7 +328,8 @@@ label.open_saved_vamsas_session = Abri
  label.groovy_console = Consola Groovy 
  label.lineart = Lineart
  label.dont_ask_me_again = No volver a preguntar
 -label.select_eps_character_rendering_style = Seleccionar el carácter EPS como estilo de visualización 
 +label.select_character_rendering_style = Estilo de visualización para carácter {0} 
 +label.select_character_style_title = Opciones de visualización {0}
  label.invert_selection = Invertir selección
  label.optimise_order = Optimizar orden
  label.seq_sort_by_score = Ordenar las secuencias por puntuación
@@@ -551,7 -550,7 +551,7 @@@ label.check_for_questionnaires = Compro
  label.check_for_latest_version = Comprobar la Ãºltima versión
  label.url_linkfrom_sequence_id = URL del enlace del ID de la secuencia
  label.use_proxy_server = Utilizar un servidor proxy
 -label.eps_rendering_style = Estilo de visualización EPS
 +label.rendering_style = Estilo de visualización {0}
  label.append_start_end = Añadir /inicio-fin (/15-380)
  label.full_sequence_id = ID de la secuencia completo
  label.smooth_font = Fuente alargada
@@@ -627,7 -626,7 +627,7 @@@ label.sequence_details_for = Detalles d
  label.sequence_name = Nombre de la secuencia
  label.sequence_description = Descripción de la secuencia
  label.edit_sequence_name_description = Editar el nombre/descripción de la secuencia
 -label.spaces_converted_to_backslashes = Los espacios se han convertido en _
 +label.spaces_converted_to_underscores = Los espacios se han convertido en _
  label.no_spaces_allowed_sequence_name = No se permiten espacios en el nombre de la secuencia
  label.select_outline_colour = Seleccionar el color del límite
  label.web_browser_not_found_unix = Unixers\: No es posible encontrar el navegador web por defecto.\nA\u00F1ada la ruta completa de su navegador en la pesta\u00F1a de Preferencias.
@@@ -804,6 -803,8 +804,6 @@@ label.save_feature_colours = Guardar es
  label.select_startup_file = Seleccionar fichero de arranque
  label.select_default_browser = Seleccionar navegador web por defecto
  label.save_tree_as_newick = Guardar Ã¡rbol como fichero newick
 -label.create_eps_from_tree = Crear un fichero EPS a partir de un Ã¡rbol
 -label.create_png_from_tree = Crear una imagen PNG a partir de un Ã¡rbol
  label.save_colour_scheme = Guardar esquema cromático
  label.edit_params_for = Editar los parámetros de {0}
  label.choose_filename_for_param_file = Escoja un nombre de fichero para este fichero de parámetros
@@@ -859,13 -860,14 +859,12 @@@ error.call_setprogressbar_before_regist
  label.cancelled_params = {0} cancelado
  error.implementation_error_cannot_show_view_alignment_frame = Error de implementación: no es posible mostrar una vista de otro alineamiento en un AlignFrame.
  error.implementation_error_dont_know_about_threshold_setting = Error de implementación: no se conoce la configuración del umbral para el AnnotationColourGradient actual.
 -error.eps_generation_not_implemented = La generación de EPS no se ha implementado todavía
 -error.png_generation_not_implemented = La generación de PNG no se ha implementado todavía
  error.try_join_vamsas_session_another = Tratando de establecer una sesión VAMSAS cuando ya había otra conectada
  error.invalid_vamsas_session_id = Identificador de sesión VAMSAS no válido
  label.groovy_support_failed = El soporte Groovy de Jalview ha fallado
  label.couldnt_create_groovy_shell = No es posible crear el shell de Groovy. Compruebe el fichero de log para conocer los detalles.
  error.unsupported_version_calcIdparam = Versión no soportada de {0}
  error.implementation_error_cant_reorder_tree = Error de implementación: no es posible reordenar este Ã¡rbol. No DefaultMutableTreeNode.
- error.invalid_value_for_option = Valor no válido de {0} para la opción {1}
  error.implementation_error_cannot_import_vamsas_doc = Error de implementación - todavía no es posible importar el documento VAMSAS existente en una sesión existente.
  label.vamsas_doc_couldnt_be_opened_as_new_session = El documento VAMSAS no ha podido abrirse como una nueva sesión. Por favor, escoja otra.
  error.implementation_error_vamsas_operation_not_init = Â¡Error de implementación! Operaciones VAMSAS cuando el cliente no estaba inicializado ni conectado
@@@ -930,7 -932,7 +929,7 @@@ label.pca_recalculating = Recalculando 
  label.pca_calculating = Calculando ACP
  label.select_foreground_colour = Escoger color del primer plano
  label.select_colour_for_text = Seleccione el color del texto
 -label.adjunst_foreground_text_colour_threshold = Ajustar el umbral del color del texto en primer plano
 +label.adjust_foreground_text_colour_threshold = Ajustar el umbral del color del texto en primer plano
  label.select_subtree_colour = Seleccioanr el color del sub-árbol
  label.create_new_sequence_features = Crear nueva(s) característica(s) de secuencia
  label.amend_delete_features = Arrelgar/Borrar características de {0}
@@@ -1002,6 -1004,7 +1001,6 @@@ error.implementation_error_cannot_find_
  exception.jobsubmission_invalid_params_set = Conjunto de parámetros no válido. Comprueba la implementación de Jalview
  exception.notvaliddata_group_contains_less_than_min_seqs = El grupo contiene menos de {0} secuencias.
  exception.outofmemory_loading_pdb_file = Sin memoria al cargar el fichero PDB
 -exception.eps_coudnt_write_output_file = No es posible escribir el fichero de salida: {0}
  exception.eps_method_not_supported = Método actualmente no suportado por la versión {0} de EpsGraphics2D
  exception.eps_unable_to_get_inverse_matrix = Imposible obtener la inversa de la matrix: {0}
  warn.job_cannot_be_cancelled_close_window = Este trabajo no se puede cancelar.\nSimplemente, cierre la ventana.
@@@ -1023,7 -1026,8 +1022,7 @@@ status.searching_for_sequences_from = B
  status.finished_searching_for_sequences_from = Finalizada la búsqueda de secuencias en {0}
  label.eps_file = Fichero EPS
  label.png_image = Imagen PNG
 -status.saving_file = Guardando {0}
 -status.export_complete = Exportación completada.
 +status.export_complete = Exportación completada
  status.fetching_pdb = Recuperando PDB {0}
  status.refreshing_news = Refrescando noticias
  status.importing_vamsas_session_from = Importando sesión VAMSAS de {0}
@@@ -1309,7 -1313,7 +1308,7 @@@ label.join_conditions = Combinar condic
  label.score = Puntuación
  label.colour_by_label = Colorear por texto
  label.variable_colour = Color variable...
 -label.select_colour = Seleccionar color
 +label.select_colour_for = Seleccionar color para {0}
  option.enable_disable_autosearch = Marcar para buscar automáticamente
  option.autosearch = Auto búsqueda
  label.retrieve_ids = Recuperar IDs
@@@ -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
@@@ -24,6 -24,7 +24,7 @@@ import jalview.datamodel.AlignedCodonFr
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.Annotation;
+ import jalview.datamodel.HiddenMarkovModel;
  import jalview.datamodel.Profile;
  import jalview.datamodel.ProfileI;
  import jalview.datamodel.Profiles;
@@@ -32,6 -33,7 +33,7 @@@ import jalview.datamodel.ResidueCount
  import jalview.datamodel.ResidueCount.SymbolCounts;
  import jalview.datamodel.SequenceI;
  import jalview.ext.android.SparseIntArray;
+ import jalview.schemes.ResidueProperties;
  import jalview.util.Comparison;
  import jalview.util.Format;
  import jalview.util.MappingUtils;
@@@ -48,25 -50,12 +50,12 @@@ import java.util.List
   * This class is used extensively in calculating alignment colourschemes that
   * depend on the amount of conservation in each alignment column.
   * 
   */
  public class AAFrequency
  {
-   public static final String PROFILE = "P";
-   /*
-    * Quick look-up of String value of char 'A' to 'Z'
-    */
-   private static final String[] CHARS = new String['Z' - 'A' + 1];
+   private static final double LOG2 = Math.log(2);
  
-   static
-   {
-     for (char c = 'A'; c <= 'Z'; c++)
-     {
-       CHARS[c - 'A'] = String.valueOf(c);
-     }
-   }
+   public static final String PROFILE = "P";
  
    public static final ProfilesI calculate(List<SequenceI> list, int start,
            int end)
    }
  
    /**
+    * Returns the full set of profiles for a hidden Markov model. The underlying
+    * data is the raw probabilities of a residue being emitted at each node,
+    * however the profiles returned by this function contain the percentage
+    * chance of a residue emission.
+    * 
+    * @param hmm
+    * @param width
+    *          The width of the Profile array (Profiles) to be returned.
+    * @param start
+    *          The alignment column on which the first profile is based.
+    * @param end
+    *          The alignment column on which the last profile is based.
+    * @param removeBelowBackground
+    *          if true, symbols with a match emission probability less than
+    *          background frequency are ignored
+    * @return
+    */
+   public static ProfilesI calculateHMMProfiles(final HiddenMarkovModel hmm,
+           int width, int start, int end, boolean removeBelowBackground,
+           boolean infoLetterHeight)
+   {
+     ProfileI[] result = new ProfileI[width];
+     char[] symbols = hmm.getSymbols().toCharArray();
+     int symbolCount = symbols.length;
+     for (int column = start; column < end; column++)
+     {
+       ResidueCount counts = new ResidueCount();
+       for (char symbol : symbols)
+       {
+         int value = getAnalogueCount(hmm, column, symbol,
+                 removeBelowBackground, infoLetterHeight);
+         counts.put(symbol, value);
+       }
+       int maxCount = counts.getModalCount();
+       String maxResidue = counts.getResiduesForCount(maxCount);
+       int gapCount = counts.getGapCount();
+       ProfileI profile = new Profile(symbolCount, gapCount, maxCount,
+               maxResidue);
+       profile.setCounts(counts);
+       result[column] = profile;
+     }
+     return new Profiles(result);
+   }
+   /**
     * Make an estimate of the profile size we are going to compute i.e. how many
     * different characters may be present in it. Overestimating has a cost of
     * using more memory than necessary. Underestimating has a cost of needing to
    }
  
    /**
-    * Derive the gap count annotation row.
+    * Derive the information annotations to be added to the alignment for
+    * display. This does not recompute the raw data, but may be called on a
+    * change in display options, such as 'ignore below background frequency',
+    * which may in turn result in a change in the derived values.
+    * 
+    * @param information
+    *          the annotation row to add annotations to
+    * @param profiles
+    *          the source information data
+    * @param startCol
+    *          start column (inclusive)
+    * @param endCol
+    *          end column (exclusive)
+    * @param ignoreGaps
+    *          if true, normalise residue percentages
+    * @param showSequenceLogo
+    *          if true include all information symbols, else just show modal
+    *          residue
+    */
+   public static float completeInformation(AlignmentAnnotation information,
+           ProfilesI profiles, int startCol, int endCol)
+   {
+     // long now = System.currentTimeMillis();
+     if (information == null || information.annotations == null)
+     {
+       /*
+        * called with a bad alignment annotation row 
+        * wait for it to be initialised properly
+        */
+       return 0;
+     }
+     float max = 0f;
+     SequenceI hmmSeq = information.sequenceRef;
+     int seqLength = hmmSeq.getLength();
+     if (information.annotations.length < seqLength)
+     {
+       return 0;
+     }
+     HiddenMarkovModel hmm = hmmSeq.getHMM();
+     for (int column = startCol; column < endCol; column++)
+     {
+       if (column >= seqLength)
+       {
+         // hmm consensus sequence is shorter than the alignment
+         break;
+       }
+       
+       float value = hmm.getInformationContent(column);
+       boolean isNaN = Float.isNaN(value);
+       if (!isNaN)
+       {
+         max = Math.max(max, value);
+       }
+       String description = isNaN ? null
+               : String.format("%.4f bits", value);
+       information.annotations[column] = new Annotation(
+               Character.toString(
+                       Character.toUpperCase(hmmSeq.getCharAt(column))),
+               description, ' ', value);
+     }
+     information.graphMax = max;
+     return max;
+   }
+   /**
+    * Derive the occupancy count annotation
     * 
-    * @param gaprow
+    * @param occupancy
     *          the annotation row to add annotations to
     * @param profiles
     *          the source consensus data
     * @param endCol
     *          end column (exclusive)
     */
-   public static void completeGapAnnot(AlignmentAnnotation gaprow,
+   public static void completeGapAnnot(AlignmentAnnotation occupancy,
            ProfilesI profiles, int startCol, int endCol, long nseq)
    {
-     if (gaprow == null || gaprow.annotations == null
-             || gaprow.annotations.length < endCol)
+     if (occupancy == null || occupancy.annotations == null
+             || occupancy.annotations.length < endCol)
      {
        /*
         * called with a bad alignment annotation row 
        return;
      }
      // always set ranges again
-     gaprow.graphMax = nseq;
-     gaprow.graphMin = 0;
+     occupancy.graphMax = nseq;
+     occupancy.graphMin = 0;
      double scale = 0.8 / nseq;
      for (int i = startCol; i < endCol; i++)
      {
           * happens if sequences calculated over were 
           * shorter than alignment width
           */
-         gaprow.annotations[i] = null;
+         occupancy.annotations[i] = null;
          return;
        }
  
  
        String description = "" + gapped;
  
-       gaprow.annotations[i] = new Annotation("", description, '\0', gapped,
+       occupancy.annotations[i] = new Annotation("", description, '\0',
+               gapped,
                jalview.util.ColorUtils.bleachColour(Color.DARK_GRAY,
                        (float) scale * gapped));
      }
     * contains
     * 
     * <pre>
 -   *    [profileType, numberOfValues, nonGapCount, charValue1, percentage1, charValue2, percentage2, ...]
 +   *    [profileType, numberOfValues, totalPercent, charValue1, percentage1, charValue2, percentage2, ...]
     * in descending order of percentage value
     * </pre>
     * 
     */
    public static int[] extractProfile(ProfileI profile, boolean ignoreGaps)
    {
 -    int[] rtnval = new int[64];
      ResidueCount counts = profile.getCounts();
      if (counts == null)
      {
      char[] symbols = symbolCounts.symbols;
      int[] values = symbolCounts.values;
      QuickSort.sort(values, symbols);
 -    int nextArrayPos = 2;
      int totalPercentage = 0;
      final int divisor = ignoreGaps ? profile.getNonGapped()
              : profile.getHeight();
      /*
       * traverse the arrays in reverse order (highest counts first)
       */
 +    int[] result = new int[3 + 2 * symbols.length];
 +    int nextArrayPos = 3;
 +    int nonZeroCount = 0;
 +
      for (int i = symbols.length - 1; i >= 0; i--)
      {
        int theChar = symbols[i];
        int charCount = values[i];
 -
 -      rtnval[nextArrayPos++] = theChar;
        final int percentage = (charCount * 100) / divisor;
 -      rtnval[nextArrayPos++] = percentage;
 +      if (percentage == 0)
 +      {
 +        /*
 +         * this count (and any remaining) round down to 0% - discard
 +         */
 +        break;
 +      }
 +      nonZeroCount++;
 +      result[nextArrayPos++] = theChar;
 +      result[nextArrayPos++] = percentage;
        totalPercentage += percentage;
      }
 -    rtnval[0] = symbols.length;
 -    rtnval[1] = totalPercentage;
 -    int[] result = new int[rtnval.length + 1];
 +
 +    /*
 +     * truncate array if any zero values were discarded
 +     */
 +    if (nonZeroCount < symbols.length)
 +    {
 +      int[] tmp = new int[3 + 2 * nonZeroCount];
 +      System.arraycopy(result, 0, tmp, 0, tmp.length);
 +      result = tmp;
 +    }
 +
 +    /*
 +     * fill in 'header' values
 +     */
      result[0] = AlignmentAnnotation.SEQUENCE_PROFILE;
 -    System.arraycopy(rtnval, 0, result, 1, rtnval.length);
 +    result[1] = nonZeroCount;
 +    result[2] = totalPercentage;
  
      return result;
    }
  
    /**
     * Extract a sorted extract of cDNA codon profile data. The returned array
     * contains
     * 
     * <pre>
 -   *    [profileType, numberOfValues, totalCount, charValue1, percentage1, charValue2, percentage2, ...]
 +   *    [profileType, numberOfValues, totalPercentage, charValue1, percentage1, charValue2, percentage2, ...]
     * in descending order of percentage value, where the character values encode codon triplets
     * </pre>
     * 
     * @param hashtable
     * @return
     */
 -  public static int[] extractCdnaProfile(Hashtable hashtable,
 +  public static int[] extractCdnaProfile(
 +          Hashtable<String, Object> hashtable,
            boolean ignoreGaps)
    {
      // this holds #seqs, #ungapped, and then codon count, indexed by encoded
        {
          break; // nothing else of interest here
        }
 +      final int percentage = codonCount * 100 / divisor;
 +      if (percentage == 0)
 +      {
 +        /*
 +         * this (and any remaining) values rounded down to 0 - discard
 +         */
 +        break;
 +      }
        distinctValuesCount++;
        result[j++] = codons[i];
 -      final int percentage = codonCount * 100 / divisor;
        result[j++] = percentage;
        totalPercentage += percentage;
      }
     *          the consensus data stores to be populated (one per column)
     */
    public static void calculateCdna(AlignmentI alignment,
 -          Hashtable[] hconsensus)
 +          Hashtable<String, Object>[] hconsensus)
    {
      final char gapCharacter = alignment.getGapCharacter();
      List<AlignedCodonFrame> mappings = alignment.getCodonFrames();
      for (int col = 0; col < cols; col++)
      {
        // todo would prefer a Java bean for consensus data
 -      Hashtable<String, int[]> columnHash = new Hashtable<>();
 +      Hashtable<String, Object> columnHash = new Hashtable<>();
        // #seqs, #ungapped seqs, counts indexed by (codon encoded + 1)
        int[] codonCounts = new int[66];
        codonCounts[0] = alignment.getSequences().size();
     */
    public static void completeCdnaConsensus(
            AlignmentAnnotation consensusAnnotation,
 -          Hashtable[] consensusData, boolean showProfileLogo, int nseqs)
 +          Hashtable<String, Object>[] consensusData, boolean showProfileLogo,
 +          int nseqs)
    {
      if (consensusAnnotation == null
              || consensusAnnotation.annotations == null
      consensusAnnotation.scaleColLabel = true;
      for (int col = 0; col < consensusData.length; col++)
      {
 -      Hashtable hci = consensusData[col];
 +      Hashtable<String, Object> hci = consensusData[col];
        if (hci == null)
        {
          // gapped protein column?
      }
      return scale;
    }
+   /**
+    * Returns the sorted HMM profile for the given column of the alignment. The
+    * returned array contains
+    * 
+    * <pre>
+    *    [profileType=0, numberOfValues, 100, charValue1, percentage1, charValue2, percentage2, ...]
+    * in descending order of percentage value
+    * </pre>
+    * 
+    * @param hmm
+    * @param column
+    * @param removeBelowBackground
+    *          if true, ignores residues with probability less than their
+    *          background frequency
+    * @param infoHeight
+    *          if true, uses the log ratio 'information' measure to scale the
+    *          value
+    * @return
+    */
+   public static int[] extractHMMProfile(HiddenMarkovModel hmm, int column,
+           boolean removeBelowBackground, boolean infoHeight)
+   {
+     if (hmm == null)
+     {
+       return null;
+     }
+     String alphabet = hmm.getSymbols();
+     int size = alphabet.length();
+     char symbols[] = new char[size];
+     int values[] = new int[size];
+     int totalCount = 0;
+     for (int i = 0; i < size; i++)
+     {
+       char symbol = alphabet.charAt(i);
+       symbols[i] = symbol;
+       int value = getAnalogueCount(hmm, column, symbol,
+               removeBelowBackground, infoHeight);
+       values[i] = value;
+       totalCount += value;
+     }
+     /*
+      * sort symbols by increasing emission probability
+      */
+     QuickSort.sort(values, symbols);
+     int[] profile = new int[3 + size * 2];
+     profile[0] = AlignmentAnnotation.SEQUENCE_PROFILE;
+     profile[1] = size;
+     profile[2] = 100;
+     /*
+      * order symbol/count profile by decreasing emission probability
+      */
+     if (totalCount != 0)
+     {
+       int arrayPos = 3;
+       for (int k = size - 1; k >= 0; k--)
+       {
+         Float percentage;
+         int value = values[k];
+         if (removeBelowBackground)
+         {
+           percentage = ((float) value) / totalCount * 100f;
+         }
+         else
+         {
+           percentage = value / 100f;
+         }
+         int intPercent = Math.round(percentage);
+         profile[arrayPos] = symbols[k];
+         profile[arrayPos + 1] = intPercent;
+         arrayPos += 2;
+       }
+     }
+     return profile;
+   }
+   /**
+    * Converts the emission probability of a residue at a column in the alignment
+    * to a 'count', suitable for rendering as an annotation value
+    * 
+    * @param hmm
+    * @param column
+    * @param symbol
+    * @param removeBelowBackground
+    *          if true, returns 0 for any symbol with a match emission
+    *          probability less than the background frequency
+    * @infoHeight if true, uses the log ratio 'information content' to scale the
+    *             value
+    * @return
+    */
+   static int getAnalogueCount(HiddenMarkovModel hmm, int column,
+           char symbol, boolean removeBelowBackground, boolean infoHeight)
+   {
+     double value = hmm.getMatchEmissionProbability(column, symbol);
+     double freq = ResidueProperties.backgroundFrequencies
+             .get(hmm.getAlphabetType()).get(symbol);
+     if (value < freq && removeBelowBackground)
+     {
+       return 0;
+     }
+     if (infoHeight)
+     {
+       value = value * (Math.log(value / freq) / LOG2);
+     }
+     value = value * 10000d;
+     return Math.round((float) value);
+   }
  }
@@@ -86,6 -86,10 +86,10 @@@ public class AlignmentSorte
  
    private static boolean sortLengthAscending;
  
+   private static boolean sortEValueAscending;
+   private static boolean sortBitScoreAscending;
    /**
     * Sorts sequences in the alignment by Percentage Identity with the given
     * reference sequence, sorting the highest identity to the top
      }
  
      // NOTE: DO NOT USE align.setSequenceAt() here - it will NOT work
 -    List<SequenceI> asq;
 -    synchronized (asq = align.getSequences())
 +    List<SequenceI> asq = align.getSequences();
 +    synchronized (asq)
      {
        for (int i = 0; i < len; i++)
        {
    public static void setOrder(AlignmentI align, SequenceI[] seqs)
    {
      // NOTE: DO NOT USE align.setSequenceAt() here - it will NOT work
 -    List<SequenceI> algn;
 -    synchronized (algn = align.getSequences())
 +    List<SequenceI> algn = align.getSequences();
 +    synchronized (algn)
      {
        List<SequenceI> tmp = new ArrayList<>();
  
    }
  
    /**
+    * Sorts by sequence evalue. Currently moves all sequences without an evalue to
+    * the top of the alignment.
+    * 
+    * @param align
+    *                The alignment object to sort
+    */
+   public static void sortByEValue(AlignmentI align)
+   {
+     int nSeq = align.getHeight();
+     double[] evalue = new double[nSeq];
+     SequenceI[] seqs = new SequenceI[nSeq];
+     for (int i = 0; i < nSeq; i++)
+     {
+       seqs[i] = align.getSequenceAt(i);
+       AlignmentAnnotation[] ann = seqs[i].getAnnotation("Search Scores");
+       if (ann != null)
+       {
+         evalue[i] = ann[0].getEValue();
+       }
+       else
+       {
+         evalue[i] = -1;
+       }
+     }
+     QuickSort.sort(evalue, seqs);
+     if (sortEValueAscending)
+     {
+       setReverseOrder(align, seqs);
+     }
+     else
+     {
+       setOrder(align, seqs);
+     }
+     sortEValueAscending = !sortEValueAscending;
+   }
+   /**
+    * Sorts by sequence bit score. Currently moves all sequences without a bit
+    * score to the top of the alignment
+    * 
+    * @param align
+    *                The alignment object to sort
+    */
+   public static void sortByBitScore(AlignmentI align)
+   {
+     int nSeq = align.getHeight();
+     double[] score = new double[nSeq];
+     SequenceI[] seqs = new SequenceI[nSeq];
+     for (int i = 0; i < nSeq; i++)
+     {
+       seqs[i] = align.getSequenceAt(i);
+       AlignmentAnnotation[] ann = seqs[i].getAnnotation("Search Scores");
+       if (ann != null)
+       {
+         score[i] = ann[0].getEValue();
+       }
+       else
+       {
+         score[i] = -1;
+       }
+     }
+     QuickSort.sort(score, seqs);
+     if (sortBitScoreAscending)
+     {
+       setReverseOrder(align, seqs);
+     }
+     else
+     {
+       setOrder(align, seqs);
+     }
+     sortBitScoreAscending = !sortBitScoreAscending;
+   }
+   /**
     * Sorts the alignment by size of group. <br>
     * Maintains the order of sequences in each group by order in given alignment
     * object.
@@@ -1459,28 -1459,31 +1459,31 @@@ public class AlignmentUtil
        final List<AlignmentAnnotation> result = new ArrayList<>();
        for (AlignmentAnnotation dsann : datasetAnnotations)
        {
-         /*
-          * Find matching annotations on the alignment. If none is found, then
-          * add this annotation to the list of 'addable' annotations for this
-          * sequence.
-          */
-         final Iterable<AlignmentAnnotation> matchedAlignmentAnnotations = al
-                 .findAnnotations(seq, dsann.getCalcId(), dsann.label);
-         if (!matchedAlignmentAnnotations.iterator().hasNext())
+         if (dsann.annotations != null) // ignore non-positional annotation
          {
-           result.add(dsann);
-           if (labelForCalcId != null)
+           /*
+            * Find matching annotations on the alignment. If none is found, then
+            * add this annotation to the list of 'addable' annotations for this
+            * sequence.
+            */
+           final Iterable<AlignmentAnnotation> matchedAlignmentAnnotations = al
+                   .findAnnotations(seq, dsann.getCalcId(), dsann.label);
+           if (!matchedAlignmentAnnotations.iterator().hasNext())
            {
-             labelForCalcId.put(dsann.getCalcId(), dsann.label);
+             result.add(dsann);
+             if (labelForCalcId != null)
+             {
+               labelForCalcId.put(dsann.getCalcId(), dsann.label);
+             }
            }
          }
-       }
-       /*
-        * Save any addable annotations for this sequence
-        */
-       if (!result.isEmpty())
-       {
-         candidates.put(seq, result);
+         /*
+          * Save any addable annotations for this sequence
+          */
+         if (!result.isEmpty())
+         {
+           candidates.put(seq, result);
+         }
        }
      }
    }
        return false;
      }
      String name = seq2.getName();
 -    final DBRefEntry[] xrefs = seq1.getDBRefs();
 +    final List<DBRefEntry> xrefs = seq1.getDBRefs();
      if (xrefs != null)
      {
 -      for (DBRefEntry xref : xrefs)
 +      for (int ix = 0, nx = xrefs.size(); ix < nx; ix++)
        {
 +        DBRefEntry xref = xrefs.get(ix);
          String xrefName = xref.getSource() + "|" + xref.getAccessionId();
          // case-insensitive test, consistent with DBRefEntry.equalRef()
          if (xrefName.equalsIgnoreCase(name))
            // need to
            // synthesize an xref.
  
 -          for (DBRefEntry primRef : dnaDss.getPrimaryDBRefs())
 +          List<DBRefEntry> primrefs = dnaDss.getPrimaryDBRefs();
 +          for (int ip = 0, np = primrefs.size(); ip < np; ip++)
            {
 +                DBRefEntry primRef = primrefs.get(ip);
              /*
               * create a cross-reference from CDS to the source sequence's
               * primary reference and vice versa
  
              dnaSeq.addDBRef(new DBRefEntry(source, version, cdsSeq
                      .getName(), new Mapping(cdsSeqDss, dnaToCdsMap)));
 -
              // problem here is that the cross-reference is synthesized -
              // cdsSeq.getName() may be like 'CDS|dnaaccession' or
              // 'CDS|emblcdsacc'
                      .getInverse()));
              proteinProduct.addDBRef(proteinToCdsRef);
            }
 -
            /*
             * transfer any features on dna that overlap the CDS
             */
      List<DBRefEntry> direct = new ArrayList<>();
      HashSet<String> directSources = new HashSet<>();
  
 -    if (contig.getDBRefs() != null)
 +    List<DBRefEntry> refs = contig.getDBRefs();
 +    if (refs != null)
      {
 -      for (DBRefEntry dbr : contig.getDBRefs())
 +      for (int ib = 0, nb = refs.size(); ib < nb; ib++)
        {
 -        if (dbr.hasMap() && dbr.getMap().getMap().isTripletMap())
 +        DBRefEntry dbr = refs.get(ib);
 +        MapList map;
 +        if (dbr.hasMap() && (map = dbr.getMap().getMap()).isTripletMap())
          {
 -          MapList map = dbr.getMap().getMap();
            // check if map is the CDS mapping
            if (mapping.getMap().equals(map))
            {
          }
        }
      }
 -    DBRefEntry[] onSource = DBRefUtils.selectRefs(
 +    List<DBRefEntry> onSource = DBRefUtils.selectRefs(
              proteinProduct.getDBRefs(),
              directSources.toArray(new String[0]));
      List<DBRefEntry> propagated = new ArrayList<>();
  
      // and generate appropriate mappings
 -    for (DBRefEntry cdsref : direct)
 +    for (int ic = 0, nc = direct.size(); ic < nc; ic++)
      {
 +      DBRefEntry cdsref = direct.get(ic);
 +      Mapping m = cdsref.getMap();
        // clone maplist and mapping
        MapList cdsposmap = new MapList(
                Arrays.asList(new int[][]
                { new int[] { cdsSeq.getStart(), cdsSeq.getEnd() } }),
 -              cdsref.getMap().getMap().getToRanges(), 3, 1);
 -      Mapping cdsmap = new Mapping(cdsref.getMap().getTo(),
 -              cdsref.getMap().getMap());
 +              m.getMap().getToRanges(), 3, 1);
 +      Mapping cdsmap = new Mapping(m.getTo(), m.getMap());
  
        // create dbref
        DBRefEntry newref = new DBRefEntry(cdsref.getSource(),
        int phase = 0;
        try
        {
 -        phase = Integer.parseInt(sf.getPhase());
 +      String s = sf.getPhase();
 +      if (s != null) 
 +      {
 +              phase = Integer.parseInt(s);
 +      }
        } catch (NumberFormatException e)
        {
 -        // ignore
 +        // leave as zero
        }
        /*
         * phase > 0 on first codon means 5' incomplete - skip to the start
      SequenceIdMatcher matcher = new SequenceIdMatcher(seqs);
      if (xrefs != null)
      {
 -      for (SequenceI xref : xrefs)
 +      // BH 2019.01.25 recoded to remove iterators
 +      
 +      for (int ix = 0, nx = xrefs.length; ix < nx; ix++)
        {
 -        DBRefEntry[] dbrefs = xref.getDBRefs();
 +      SequenceI xref = xrefs[ix];
 +        List<DBRefEntry> dbrefs = xref.getDBRefs();
          if (dbrefs != null)
          {
 -          for (DBRefEntry dbref : dbrefs)
 +          for (int ir = 0, nir = dbrefs.size(); ir < nir; ir++)
            {
 -            if (dbref.getMap() == null || dbref.getMap().getTo() == null
 -                    || dbref.getMap().getTo().isProtein() != isProtein)
 +            DBRefEntry dbref = dbrefs.get(ir);
 +            Mapping map = dbref.getMap();
 +            SequenceI mto;
 +            if (map == null || (mto = map.getTo()) == null
 +                    || mto.isProtein() != isProtein)
              {
                continue;
              }
 -            SequenceI mappedTo = dbref.getMap().getTo();
 +            SequenceI mappedTo = mto;
              SequenceI match = matcher.findIdMatch(mappedTo);
              if (match == null)
              {
@@@ -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;
@@@ -253,30 -253,29 +252,30 @@@ public class Dn
      for (int gd = 0; gd < selection.length; gd++)
      {
        SequenceI dna = selection[gd];
 -      DBRefEntry[] dnarefs = DBRefUtils.selectRefs(dna.getDBRefs(),
 +      List<DBRefEntry> dnarefs = DBRefUtils.selectRefs(dna.getDBRefs(),
                jalview.datamodel.DBRefSource.DNACODINGDBS);
        if (dnarefs != null)
        {
          // intersect with pep
          List<DBRefEntry> mappedrefs = new ArrayList<>();
 -        DBRefEntry[] refs = dna.getDBRefs();
 -        for (int d = 0; d < refs.length; d++)
 +        List<DBRefEntry> refs = dna.getDBRefs();
 +        for (int d = 0, nd = refs.size(); d < nd; d++)
          {
 -          if (refs[d].getMap() != null && refs[d].getMap().getMap() != null
 -                  && refs[d].getMap().getMap().getFromRatio() == 3
 -                  && refs[d].getMap().getMap().getToRatio() == 1)
 +          DBRefEntry ref = refs.get(d);
 +          if (ref.getMap() != null && ref.getMap().getMap() != null
 +                  && ref.getMap().getMap().getFromRatio() == 3
 +                  && ref.getMap().getMap().getToRatio() == 1)
            {
 -            mappedrefs.add(refs[d]); // add translated protein maps
 +            mappedrefs.add(ref); // add translated protein maps
            }
          }
 -        dnarefs = mappedrefs.toArray(new DBRefEntry[mappedrefs.size()]);
 -        for (int d = 0; d < dnarefs.length; d++)
 +        dnarefs = mappedrefs;//.toArray(new DBRefEntry[mappedrefs.size()]);
 +        for (int d = 0, nd = dnarefs.size(); d < nd; d++)
          {
 -          Mapping mp = dnarefs[d].getMap();
 +          Mapping mp = dnarefs.get(d).getMap();
            if (mp != null)
            {
 -            for (int vc = 0; vc < viscontigs.length; vc += 2)
 +            for (int vc = 0, nv = viscontigs.length; vc < nv; vc += 2)
              {
                int[] mpr = mp.locateMappedRange(viscontigs[vc],
                        viscontigs[vc + 1]);
    private static void transferCodedFeatures(SequenceI dna, SequenceI pep,
            MapList map)
    {
 -    DBRefEntry[] dnarefs = DBRefUtils.selectRefs(dna.getDBRefs(),
 -            DBRefSource.DNACODINGDBS);
 -    if (dnarefs != null)
 -    {
 -      // intersect with pep
 -      for (int d = 0; d < dnarefs.length; d++)
 -      {
 -        Mapping mp = dnarefs[d].getMap();
 -        if (mp != null)
 -        {
 -        }
 -      }
 -    }
 +      //  BH 2019.01.25 nop?
 +//    List<DBRefEntry> dnarefs = DBRefUtils.selectRefs(dna.getDBRefs(),
 +//            DBRefSource.DNACODINGDBS);
 +//    if (dnarefs != null)
 +//    {
 +//      // intersect with pep
 +//      for (int d = 0, nd = dnarefs.size(); d < nd; d++)
 +//      {
 +//        Mapping mp = dnarefs.get(d).getMap();
 +//        if (mp != null)
 +//        {
 +//        }
 +//      }
 +//    }
      for (SequenceFeature sf : dna.getFeatures().getAllFeatures())
      {
          if (FeatureProperties.isCodingFeature(null, sf.getType()))
@@@ -23,7 -23,6 +23,7 @@@ package jalview.api
  import jalview.analysis.Conservation;
  import jalview.analysis.TreeModel;
  import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentExportData;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentView;
  import jalview.datamodel.ColumnSelection;
@@@ -55,7 -54,7 +55,7 @@@ public interface AlignViewportI extend
     * 
     * @return
     */
-   public ViewportRanges getRanges();
+   ViewportRanges getRanges();
  
    /**
     * calculate the height for visible annotation, revalidating bounds where
@@@ -63,7 -62,7 +63,7 @@@
     * 
     * @return total height of annotation
     */
-   public int calcPanelHeight();
+   int calcPanelHeight();
  
    /**
     * Answers true if the viewport has at least one column selected
  
    boolean isNormaliseSequenceLogo();
  
+   boolean isShowInformationHistogram();
+   boolean isShowHMMSequenceLogo();
+   boolean isNormaliseHMMSequenceLogo();
    ColourSchemeI getGlobalColourScheme();
  
    /**
     * 
     * @return
     */
 -  Hashtable[] getComplementConsensusHash();
 +  Hashtable<String, Object>[] getComplementConsensusHash();
  
 -  Hashtable[] getRnaStructureConsensusHash();
 +  Hashtable<String, Object>[] getRnaStructureConsensusHash();
  
    boolean isIgnoreGapsConsensus();
  
+   boolean isIgnoreBelowBackground();
    boolean isCalculationInProgress(AlignmentAnnotation alignmentAnnotation);
  
    AlignmentAnnotation getAlignmentQualityAnnot();
     * 
     * @param hconsensus
     */
 -  void setComplementConsensusHash(Hashtable[] hconsensus);
 +  void setComplementConsensusHash(Hashtable<String, Object>[] hconsensus);
  
    /**
     * 
     * 
     * @param hStrucConsensus
     */
 -  void setRnaStructureConsensusHash(Hashtable[] hStrucConsensus);
 +  void setRnaStructureConsensusHash(
 +          Hashtable<String, Object>[] hStrucConsensus);
  
    /**
     * Sets the colour scheme for the background alignment (as distinct from
    @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
@@@ -236,6 -236,7 +236,7 @@@ public class AlignFrame extends Embmenu
              alignPanel);
      viewport.updateConservation(alignPanel);
      viewport.updateConsensus(alignPanel);
+     viewport.initInformationWorker(alignPanel);
  
      displayNonconservedMenuItem.setState(viewport.getShowUnconserved());
      followMouseOverFlag.setState(viewport.isFollowHighlight());
          viewport.featureSettings.refreshTable();
        }
        alignPanel.paintAlignment(true, true);
 -      statusBar.setText(MessageManager
 +      setStatus(MessageManager
                .getString("label.successfully_added_features_alignment"));
      }
      return featuresFile;
  
      case KeyEvent.VK_F2:
        viewport.cursorMode = !viewport.cursorMode;
 -      statusBar.setText(MessageManager
 +      setStatus(MessageManager
                .formatMessage("label.keyboard_editing_mode", new String[]
                { (viewport.cursorMode ? "on" : "off") }));
        if (viewport.cursorMode)
      {
        sortGroupMenuItem_actionPerformed();
      }
+     else if (source == sortEValueMenuItem)
+     {
+       sortEValueMenuItem_actionPerformed();
+     }
+     else if (source == sortBitScoreMenuItem)
+     {
+       sortBitScoreMenuItem_actionPerformed();
+     }
      else if (source == removeRedundancyMenuItem)
      {
        removeRedundancyMenuItem_actionPerformed();
              seqs, 0, viewport.getAlignment().getWidth(),
              viewport.getAlignment()));
  
 -    viewport.getRanges().setEndSeq(viewport.getAlignment().getHeight());
 +    viewport.getRanges().setEndSeq(viewport.getAlignment().getHeight() - 1); // BH
 +                                                                             // 2019.04.18
      viewport.getAlignment().getWidth();
      viewport.firePropertyChange("alignment", null,
              viewport.getAlignment().getSequences());
                  column, al);
        }
  
 -      statusBar.setText(MessageManager
 +      setStatus(MessageManager
                .formatMessage("label.removed_columns", new String[]
                { Integer.valueOf(trimRegion.getSize()).toString() }));
        addHistoryItem(trimRegion);
  
      addHistoryItem(removeGapCols);
  
 -    statusBar.setText(MessageManager
 +    setStatus(MessageManager
              .formatMessage("label.removed_empty_columns", new String[]
              { Integer.valueOf(removeGapCols.getSize()).toString() }));
  
  
    }
  
+   public void sortEValueMenuItem_actionPerformed()
+   {
+     SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
+     AlignmentSorter.sortByEValue(viewport.getAlignment());
+     addHistoryItem(new OrderCommand("Group Sort", oldOrder,
+             viewport.getAlignment()));
+     alignPanel.paintAlignment(true, false);
+   }
+   public void sortBitScoreMenuItem_actionPerformed()
+   {
+     SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
+     AlignmentSorter.sortByBitScore(viewport.getAlignment());
+     addHistoryItem(new OrderCommand("Group Sort", oldOrder,
+             viewport.getAlignment()));
+     alignPanel.paintAlignment(true, false);
+   }
    public void removeRedundancyMenuItem_actionPerformed()
    {
      new RedundancyPanel(alignPanel);
  
    MenuItem sortGroupMenuItem = new MenuItem();
  
+   MenuItem sortEValueMenuItem = new MenuItem();
+   MenuItem sortBitScoreMenuItem = new MenuItem();
    MenuItem removeRedundancyMenuItem = new MenuItem();
  
    MenuItem pairwiseAlignmentMenuItem = new MenuItem();
       */
      statusBar.setBackground(Color.white);
      statusBar.setFont(new java.awt.Font("Verdana", 0, 11));
 -    statusBar.setText(MessageManager.getString("label.status_bar"));
 +    setStatus(MessageManager.getString("label.status_bar"));
      this.add(statusBar, BorderLayout.SOUTH);
    }
  
      }
      else
      {
 -      new MCview.AppletPDBViewer(pdb, seqs, chains, alignPanel, protocol);
 +      new mc_view.AppletPDBViewer(pdb, seqs, chains, alignPanel, protocol);
      }
  
    }
@@@ -1,6 -1,6 +1,6 @@@
  /*
-  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
   * Copyright (C) $$Year-Rel$$ The Jalview Authors
+  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
   * 
   * This file is part of Jalview.
   * 
@@@ -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);
      }
      setFont(font, true);
  
 -    MAC = new jalview.util.Platform().isAMac();
 -
      if (applet != null)
      {
        setShowJVSuffix(
                          colour));
          if (residueShading != null)
          {
-           residueShading.setConsensus(hconsensus);
+           residueShading.setConsensus(consensusProfiles);
          }
        }
  
        }
      }
      initAutoAnnotation();
    }
  
-   java.awt.Frame nullFrame;
-   protected FeatureSettings featureSettings = null;
-   private float heightScale = 1, widthScale = 1;
    /**
     * {@inheritDoc}
     */
  
    public void resetSeqLimits(int height)
    {
 -    ranges.setEndSeq(height / getCharHeight());
 +    ranges.setEndSeq(height / getCharHeight() - 1); // BH 2019.04.18
    }
  
    boolean centreColumnLabels;
              .getStructureSelectionManager(applet);
    }
  
-   @Override
-   public boolean isNormaliseSequenceLogo()
-   {
-     return normaliseSequenceLogo;
-   }
-   public void setNormaliseSequenceLogo(boolean state)
-   {
-     normaliseSequenceLogo = state;
-   }
    /**
     * 
     * @return true if alignment characters should be displayed
@@@ -46,7 -46,6 +46,7 @@@ import java.awt.event.ComponentEvent
  import java.beans.PropertyChangeEvent;
  import java.util.List;
  
 +@SuppressWarnings("serial")
  public class AlignmentPanel extends Panel
          implements AdjustmentListener, AlignmentViewPanel, ViewportListenerI
  {
    public boolean scrollTo(int ostart, int end, int seqIndex,
            boolean scrollToNearest, boolean redrawOverview)
    {
 -    int startv, endv, starts, ends, width;
 +    int startv, endv, starts, ends;//, width;
  
      int start = -1;
      if (av.hasHiddenColumns())
      // this is called after loading new annotation onto alignment
      if (alignFrame.getSize().height == 0)
      {
-       System.out.println(
-               "adjustAnnotationHeight frame size zero NEEDS FIXING");
+       // panel not laid out yet?
+       return;
      }
      fontChanged();
      validateAnnotationDimensions(true);
@@@ -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;
  
@@@ -99,14 -99,14 +98,14 @@@ public class AnnotationPanel extends Pa
  
    public static int GRAPH_HEIGHT = 40;
  
 -  boolean MAC = false;
 +//  boolean MAC = false;
  
    public final AnnotationRenderer renderer;
  
    public AnnotationPanel(AlignmentPanel ap)
    {
      new jalview.util.Platform();
 -    MAC = Platform.isAMac();
 +//    MAC = Platform.isAMac();
      this.ap = ap;
      av = ap.av;
      setLayout(null);
@@@ -44,8 -44,6 +44,8 @@@ import java.awt.event.MouseListener
  import java.awt.event.MouseMotionListener;
  import java.beans.PropertyChangeEvent;
  
 +import javax.swing.SwingUtilities;
 +
  public class OverviewPanel extends Panel implements Runnable,
          MouseMotionListener, MouseListener, ViewportListenerI
  {
@@@ -71,7 -69,7 +71,7 @@@
  
      od = new OverviewDimensionsShowHidden(av.getRanges(),
              (av.isShowAnnotation()
-                     && av.getSequenceConsensusHash() != null));
+                                               && av.getSequenceConsensusHash() != null));
  
      oviewCanvas = new OverviewCanvas(od, av);
      setLayout(new BorderLayout());
      if ((evt.getModifiersEx()
              & InputEvent.BUTTON3_DOWN_MASK) == InputEvent.BUTTON3_DOWN_MASK)
      {
 -      if (!Platform.isAMac())
 +      if (!Platform.isMac()) // BH was excluding JavaScript
        {
          showPopupMenu(evt);
        }
    @Override
    public void mouseDragged(MouseEvent evt)
    {
 -    if ((evt.getModifiersEx()
 -            & InputEvent.BUTTON3_DOWN_MASK) == InputEvent.BUTTON3_DOWN_MASK)
 +    if (Platform.isWinRightButton(evt)) 
      {
 -      if (!Platform.isAMac())
 -      {
 -        showPopupMenu(evt);
 -      }
 +      showPopupMenu(evt);
 +      return;
      }
 -    else
 -    {
 -      if (draggingBox)
 -      {
 -        // set the mouse position as a fixed point in the box
 -        // and drag relative to that position
 -        od.adjustViewportFromMouse(evt.getX(), evt.getY(),
 -                av.getAlignment().getHiddenSequences(),
 -                av.getAlignment().getHiddenColumns());
 -      }
 -      else
 -      {
 -        od.updateViewportFromMouse(evt.getX(), evt.getY(),
 -                av.getAlignment().getHiddenSequences(),
 -                av.getAlignment().getHiddenColumns());
 -      }
 -      ap.paintAlignment(false, false);
 +
 +    if (SwingUtilities.isRightMouseButton(evt))
 +    { 
 +      return;
      }
 +    
 +        if (draggingBox)
 +        {
 +          // set the mouse position as a fixed point in the box
 +          // and drag relative to that position
 +          od.adjustViewportFromMouse(evt.getX(), evt.getY(),
 +                  av.getAlignment().getHiddenSequences(),
 +                  av.getAlignment().getHiddenColumns());
 +        }
 +        else
 +        {
 +          od.updateViewportFromMouse(evt.getX(), evt.getY(),
 +                  av.getAlignment().getHiddenSequences(),
 +                  av.getAlignment().getHiddenColumns());
 +        }
 +        ap.paintAlignment(false, false);
    }
  
    /**
   */
  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
  {
      g.drawString(getTitle(), 10, 10);
    }
  
 -  public static void main(String[] args)
 -  {
 -    Frame f = new Frame("TitledPanel Tester");
 -
 -    TitledPanel p = new TitledPanel("Title of Panel");
 -    p.add(new Label("Label 1"));
 -    p.add(new Label("Label 2"));
 -    p.add(new Label("Label 3"));
 -    f.add(p);
 -
 -    f.addWindowListener(new WindowAdapter()
 -    {
 -      public void windowClosing(WindowEvent e)
 -      {
 -        System.exit(0);
 -      }
 -    });
 -    f.setBounds(300, 300, 300, 300);
 -    f.setVisible(true);
 -  }
 -
    public String getTitle()
    {
      return title;
@@@ -60,9 -60,6 +60,9 @@@ import java.security.Policy
  import java.util.HashMap;
  import java.util.Map;
  import java.util.Vector;
 +import java.util.logging.ConsoleHandler;
 +import java.util.logging.Level;
 +import java.util.logging.Logger;
  
  import javax.swing.LookAndFeel;
  import javax.swing.UIManager;
@@@ -89,13 -86,9 +89,13 @@@ import groovy.util.GroovyScriptEngine
   */
  public class Jalview
  {
 -  /*
 -   * singleton instance of this class
 -   */
 +  static
 +  {
 +    Platform.getURLCommandArguments();
 +  }
 +
 +  // singleton instance of this class
 +
    private static Jalview instance;
  
    private Desktop desktop;
  
    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()
++        {
++        }
++      });
 +    }
    }
  
    /**
    class FeatureFetcher
    {
      /*
-      * TODO: generalise to track all jalview events to orchestrate batch processing
-      * events.
+      * TODO: generalise to track all jalview events to orchestrate batch
+      * processing events.
       */
  
      private int queued = 0;
     */
    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
     */
    void doMain(String[] args)
    {
 -    System.setSecurityManager(null);
 +
 +    if (!Platform.isJS())
 +    {
 +      System.setSecurityManager(null);
 +    }
 +
      System.out
              .println("Java version: "
                      + System.getProperty("java.version"));
      ArgsParser aparser = new ArgsParser(args);
      boolean headless = false;
  
 -    if (aparser.contains("help") || aparser.contains("h"))
 -    {
 -      showUsage();
 -      System.exit(0);
 -    }
 -    if (aparser.contains("nodisplay") || aparser.contains("nogui")
 -            || aparser.contains("headless"))
 -    {
 -      System.setProperty("java.awt.headless", "true");
 -      headless = true;
 -    }
      String usrPropsFile = aparser.getValue("props");
      Cache.loadProperties(usrPropsFile); // must do this before
      if (usrPropsFile != null)
                "CMD [-props " + usrPropsFile + "] executed successfully!");
      }
  
 -    // anything else!
 -
 -    final String jabawsUrl = aparser.getValue("jabaws");
 -    if (jabawsUrl != null)
 +    if (!Platform.isJS())
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
      {
 -      try
 +      if (aparser.contains("help") || aparser.contains("h"))
 +      {
 +        showUsage();
 +        System.exit(0);
 +      }
 +      if (aparser.contains("nodisplay") || aparser.contains("nogui")
 +              || aparser.contains("headless"))
        {
 -        Jws2Discoverer.getDiscoverer().setPreferredUrl(jabawsUrl);
 -        System.out.println(
 -                "CMD [-jabaws " + jabawsUrl + "] executed successfully!");
 -      } catch (MalformedURLException e)
 +        System.setProperty("java.awt.headless", "true");
 +        headless = true;
 +      }
 +      // anything else!
 +
 +      final String jabawsUrl = aparser.getValue("jabaws");
 +      if (jabawsUrl != null)
        {
 -        System.err.println(
 -                "Invalid jabaws parameter: " + jabawsUrl + " ignored");
 +        try
 +        {
 +          Jws2Discoverer.getDiscoverer().setPreferredUrl(jabawsUrl);
 +          System.out.println(
 +                  "CMD [-jabaws " + jabawsUrl + "] executed successfully!");
 +        } catch (MalformedURLException e)
 +        {
 +          System.err.println(
 +                  "Invalid jabaws parameter: " + jabawsUrl + " ignored");
 +        }
        }
 -    }
  
 +    }
      String defs = aparser.getValue("setprop");
      while (defs != null)
      {
        else
        {
          System.out.println("Executing setprop argument: " + defs);
 -        // DISABLED FOR SECURITY REASONS
 -        // TODO: add a property to allow properties to be overriden by cli args
 -        // Cache.setProperty(defs.substring(0,p), defs.substring(p+1));
 +        if (Platform.isJS())
 +        {
 +          Cache.setProperty(defs.substring(0,p), defs.substring(p+1));
 +        }
        }
        defs = aparser.getValue("setprop");
      }
        System.err.println("Unexpected Look and Feel Exception");
        ex.printStackTrace();
      }
 -    if (Platform.isAMac())
 +    if (Platform.isAMacAndNotJS())
      {
  
        LookAndFeel lookAndFeel = ch.randelshofer.quaqua.QuaquaManager
      }
  
      /*
 -     * configure 'full' SO model 
 +     * configure 'full' SO model if preferences say to, else use the default (full SO)
 +     * - as JS currently doesn't have OBO parsing, it must use 'Lite' version
       */
 -    if (Cache.getDefault("USE_FULL_SO", true))
 +    boolean soDefault = !Platform.isJS();
 +    if (Cache.getDefault("USE_FULL_SO", soDefault))
      {
        SequenceOntologyFactory.setInstance(new SequenceOntology());
      }
        try
        {
          JalviewTaskbar.setTaskbar(this);
 -      } catch (Exception e)
 -      {
 -        System.out.println("Cannot set Taskbar");
 -        // e.printStackTrace();
        } catch (Throwable t)
        {
 -        System.out.println("Cannot set Taskbar");
 -        // t.printStackTrace();
 +        System.out.println("Error setting Taskbar: " + t.getMessage());
        }
  
        desktop.setVisible(true);
 -      desktop.startServiceDiscovery();
 -      if (!aparser.contains("nousagestats"))
 -      {
 -        startUsageStats(desktop);
 -      }
 -      else
 -      {
 -        System.err.println("CMD [-nousagestats] executed successfully!");
 -      }
  
 -      if (!aparser.contains("noquestionnaire"))
 +      if (!Platform.isJS())
 +      /**
 +       * Java only
 +       * 
 +       * @j2sIgnore
 +       */
        {
 -        String url = aparser.getValue("questionnaire");
 -        if (url != null)
 +        desktop.startServiceDiscovery();
 +        if (!aparser.contains("nousagestats"))
          {
 -          // Start the desktop questionnaire prompter with the specified
 -          // questionnaire
 -          Cache.log.debug("Starting questionnaire url at " + url);
 -          desktop.checkForQuestionnaire(url);
 -          System.out.println(
 -                  "CMD questionnaire[-" + url + "] executed successfully!");
 +          startUsageStats(desktop);
          }
          else
          {
 -          if (Cache.getProperty("NOQUESTIONNAIRES") == null)
 +          System.err.println("CMD [-nousagestats] executed successfully!");
 +        }
 +
 +        if (!aparser.contains("noquestionnaire"))
 +        {
 +          String url = aparser.getValue("questionnaire");
 +          if (url != null)
            {
              // Start the desktop questionnaire prompter with the specified
              // questionnaire
 -            // String defurl =
 -            // "http://anaplog.compbio.dundee.ac.uk/cgi-bin/questionnaire.pl";
 -            // //
 -            String defurl = "http://www.jalview.org/cgi-bin/questionnaire.pl";
 -            Cache.log.debug(
 -                    "Starting questionnaire with default url: " + defurl);
 -            desktop.checkForQuestionnaire(defurl);
 +            Cache.log.debug("Starting questionnaire url at " + url);
 +            desktop.checkForQuestionnaire(url);
 +            System.out.println("CMD questionnaire[-" + url
 +                    + "] executed successfully!");
 +          }
 +          else
 +          {
 +            if (Cache.getProperty("NOQUESTIONNAIRES") == null)
 +            {
 +              // Start the desktop questionnaire prompter with the specified
 +              // questionnaire
 +              // String defurl =
 +              // "http://anaplog.compbio.dundee.ac.uk/cgi-bin/questionnaire.pl";
 +              // //
 +              String defurl = "http://www.jalview.org/cgi-bin/questionnaire.pl";
 +              Cache.log.debug(
 +                      "Starting questionnaire with default url: " + defurl);
 +              desktop.checkForQuestionnaire(defurl);
 +            }
            }
          }
 -      }
 -      else
 -      {
 -        System.err.println("CMD [-noquestionnaire] executed successfully!");
 -      }
 +        else
 +        {
 +          System.err
 +                  .println("CMD [-noquestionnaire] executed successfully!");
 +        }
  
 -      if (!aparser.contains("nonews"))
 -      {
 -        desktop.checkForNews();
 -      }
 +        if (!aparser.contains("nonews"))
 +        {
 +          desktop.checkForNews();
 +        }
  
 -      BioJsHTMLOutput.updateBioJS();
 +        BioJsHTMLOutput.updateBioJS();
 +      }
      }
  
      // Move any new getdown-launcher-new.jar into place over old
        }
        System.out.println("CMD [-open " + file + "] executed successfully!");
  
 -      if (!file.startsWith("http://"))
 +      if (!Platform.isJS())
 +        /**
 +         * ignore in JavaScript -- can't just file existence - could load it?
 +         * 
 +         * @j2sIgnore
 +         */
        {
 -        if (!(new File(file)).exists())
 +        if (!file.startsWith("http://") && !file.startsWith("https://"))
 +        // BH 2019 added https check for Java
          {
 -          System.out.println("Can't find " + file);
 -          if (headless)
 +          if (!(new File(file)).exists())
            {
 -            System.exit(1);
 +            System.out.println("Can't find " + file);
 +            if (headless)
 +            {
 +              System.exit(1);
 +            }
            }
          }
        }
  
 -      protocol = AppletFormatAdapter.checkProtocol(file);
 +        protocol = AppletFormatAdapter.checkProtocol(file);
  
        try
        {
              continue;
            }
  
 -          if (af.saveAlignment(file, format))
 +          af.saveAlignment(file, format);
 +          if (af.isSaveAlignmentSuccessful())
            {
              System.out.println("Written alignment in " + format
                      + " format to " + file);
      // And the user
      // ////////////////////
  
 -    if (!headless && file == null
 -            && jalview.bin.Cache.getDefault("SHOW_STARTUP_FILE", true))
 +    if (!Platform.isJS() && !headless && file == null
 +            && Cache.getDefault("SHOW_STARTUP_FILE", true))
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
      {
 -      file = jalview.bin.Cache.getDefault("STARTUP_FILE",
 -              jalview.bin.Cache.getDefault("www.jalview.org",
 +      file = Cache.getDefault("STARTUP_FILE",
 +              Cache.getDefault("www.jalview.org",
                        "http://www.jalview.org")
                        + "/examples/exampleFile_2_7.jar");
        if (file.equals(
          // hardwire upgrade of the startup file
          file.replace("_2_3.jar", "_2_7.jar");
          // and remove the stale setting
 -        jalview.bin.Cache.removeProperty("STARTUP_FILE");
 +        Cache.removeProperty("STARTUP_FILE");
        }
  
        protocol = DataSourceType.FILE;
@@@ -195,7 -195,6 +195,7 @@@ public class Alignment implements Align
    {
      synchronized (sequences)
      {
 +    
        if (i > -1 && i < sequences.size())
        {
          return sequences.get(i);
    }
  
    /**
+    * Inserts a sequence at a point in the alignment.
+    * 
+    * @param i
+    *          the index of the position the sequence is to be inserted in.
+    */
+   @Override
+   public void insertSequenceAt(int i, SequenceI snew)
+   {
+     synchronized (sequences)
+     {
+       if (sequences.size() > i)
+       {
+         sequences.add(i, snew);
+         return;
+       }
+       else
+       {
+         sequences.add(snew);
+         hiddenSequences.adjustHeightSequenceAdded();
+       }
+       return;
+     }
+   }
+   /**
     * DOCUMENT ME!
     * 
     * @return DOCUMENT ME!
      int i = 0;
      SequenceI sq = null;
      String sqname = null;
 +    int nseq = sequences.size();
      if (startAfter != null)
      {
        // try to find the sequence in the alignment
        boolean matched = false;
 -      while (i < sequences.size())
 +      while (i < nseq)
        {
          if (getSequenceAt(i++) == startAfter)
          {
          i = 0;
        }
      }
 -    while (i < sequences.size())
 +    while (i < nseq)
      {
        sq = getSequenceAt(i);
        sqname = sq.getName();
      int maxLength = -1;
  
      SequenceI current;
 -    for (int i = 0; i < sequences.size(); i++)
 +    int nseq = sequences.size();
 +    for (int i = 0; i < nseq; i++)
      {
        current = getSequenceAt(i);
        for (int j = current.getLength(); j > maxLength; j--)
      maxLength++;
  
      int cLength;
 -    for (int i = 0; i < sequences.size(); i++)
 +    for (int i = 0; i < nseq; i++)
      {
        current = getSequenceAt(i);
        cLength = current.getLength();
            String calcId, boolean autoCalc, SequenceI seqRef,
            SequenceGroup groupRef)
    {
-     if (annotations != null)
+     AlignmentAnnotation annot = annotations == null ? null
+             : AlignmentAnnotation.findFirstAnnotation(
+               Arrays.asList(getAlignmentAnnotation()), name, calcId,
+               autoCalc, seqRef, groupRef);
+     if (annot == null)
      {
-       for (AlignmentAnnotation annot : getAlignmentAnnotation())
+       annot = new AlignmentAnnotation(name, name, new Annotation[1], 0f, 0f,
+               AlignmentAnnotation.BAR_GRAPH);
+       annot.hasText = false;
+       if (calcId != null)
        {
-         if (annot.autoCalculated == autoCalc && (name.equals(annot.label))
-                 && (calcId == null || annot.getCalcId().equals(calcId))
-                 && annot.sequenceRef == seqRef
-                 && annot.groupRef == groupRef)
-         {
-           return annot;
-         }
+         annot.setCalcId(calcId);
        }
+       annot.autoCalculated = autoCalc;
+       if (seqRef != null)
+       {
+         annot.setSequenceRef(seqRef);
+       }
+       annot.groupRef = groupRef;
+       addAnnotation(annot);
      }
-     AlignmentAnnotation annot = new AlignmentAnnotation(name, name,
-             new Annotation[1], 0f, 0f, AlignmentAnnotation.BAR_GRAPH);
-     annot.hasText = false;
-     if (calcId != null)
+     return annot;
+   }
+   @Override
+   public AlignmentAnnotation updateFromOrCopyAnnotation(
+           AlignmentAnnotation ala)
+   {
+     AlignmentAnnotation annot = AlignmentAnnotation.findFirstAnnotation(
+             Arrays.asList(getAlignmentAnnotation()), ala.label, ala.calcId,
+             ala.autoCalculated, ala.sequenceRef, ala.groupRef);
+     if (annot == null)
      {
-       annot.setCalcId(new String(calcId));
+       annot = new AlignmentAnnotation(ala);
+       addAnnotation(annot);
      }
-     annot.autoCalculated = autoCalc;
-     if (seqRef != null)
+     else
      {
-       annot.setSequenceRef(seqRef);
+       annot.updateAlignmentAnnotationFrom(ala);
      }
-     annot.groupRef = groupRef;
-     addAnnotation(annot);
+     validateAnnotation(annot);
      return annot;
    }
  
      }
    }
  
+   @Override
+   public List<SequenceI> getHmmSequences()
+   {
+     List<SequenceI> result = new ArrayList<>();
+     for (int i = 0; i < sequences.size(); i++)
+     {
+       SequenceI seq = sequences.get(i);
+       if (seq.hasHMMProfile())
+       {
+         result.add(seq);
+       }
+     }
+     return result;
+   }
  }
  package jalview.datamodel;
  
  import jalview.analysis.AlignSeq;
 -import jalview.api.DBRefEntryI;
  import jalview.datamodel.features.SequenceFeatures;
  import jalview.datamodel.features.SequenceFeaturesI;
  import jalview.util.Comparison;
  import jalview.util.DBRefUtils;
  import jalview.util.MapList;
  import jalview.util.StringUtils;
+ import jalview.workers.InformationThread;
  
  import java.util.ArrayList;
  import java.util.Arrays;
@@@ -46,52 -48,29 +47,56 @@@ import fr.orsay.lri.varna.models.rna.RN
   */
  public class Sequence extends ASequence implements SequenceI
  {
 +
 +  /**
 +   * A subclass that gives us access to modCount, which tracks whether there
 +   * have been any changes. We use this to update
 +   * 
 +   * @author hansonr
 +   *
 +   * @param <T>
 +   */
 +  @SuppressWarnings("serial")
 +  public class DBModList<T> extends ArrayList<DBRefEntry>
 +  {
 +
 +    protected int getModCount()
 +    {
 +      return modCount;
 +    }
 +
 +  }
 +
    SequenceI datasetSequence;
  
 -  String name;
 +  private String name;
  
    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
     *
     * TODO: change to List<>
     */
 -  Vector<AlignmentAnnotation> annotation;
 +  private Vector<AlignmentAnnotation> annotation;
  
    private SequenceFeaturesI sequenceFeatureStore;
  
      {
        setDatasetSequence(seq.getDatasetSequence());
      }
 -    
 +
      /*
       * only copy DBRefs and seqfeatures if we really are a dataset sequence
       */
      if (datasetSequence == null)
      {
 -      if (seq.getDBRefs() != null)
 +      List<DBRefEntry> dbr = seq.getDBRefs();
 +      if (dbr != null)
        {
 -        DBRefEntry[] dbr = seq.getDBRefs();
 -        for (int i = 0; i < dbr.length; i++)
 +        for (int i = 0, n = dbr.size(); i < n; i++)
          {
 -          addDBRef(new DBRefEntry(dbr[i]));
 +          addDBRef(new DBRefEntry(dbr.get(i)));
          }
        }
  
          this.addPDBId(new PDBEntry(pdb));
        }
      }
+     if (seq.getHMM() != null)
+     {
+       this.hmm = new HiddenMarkovModel(seq.getHMM(), this);
+     }
    }
  
    @Override
    {
      if (sf.getType() == null)
      {
 -      System.err.println("SequenceFeature type may not be null: "
 -              + sf.toString());
 +      System.err.println(
 +              "SequenceFeature type may not be null: " + sf.toString());
        return false;
      }
  
    public char[] getSequence()
    {
      // return sequence;
 -    return sequence == null ? null : Arrays.copyOf(sequence,
 -            sequence.length);
 +    return sequence == null ? null
 +            : Arrays.copyOf(sequence, sequence.length);
    }
  
    /*
    @Override
    public GeneLociI getGeneLoci()
    {
 -    DBRefEntry[] refs = getDBRefs();
 +    List<DBRefEntry> refs = getDBRefs();
      if (refs != null)
      {
        for (final DBRefEntry ref : refs)
      {
        return findPosition(column + 1, cursor);
      }
 -    
 +
      // TODO recode this more naturally i.e. count residues only
      // as they are found, not 'in anticipation'
  
    @Override
    public ContiguousI findPositions(int fromColumn, int toColumn)
    {
-     if (toColumn < fromColumn || fromColumn < 1)
+     fromColumn = Math.max(fromColumn, 1);
+     if (toColumn < fromColumn)
      {
        return null;
      }
    {
      ArrayList<int[]> map = new ArrayList<>();
      int lastj = -1, j = 0;
 -    int pos = start;
 +    // int pos = start;
      int seqlen = sequence.length;
      while ((j < seqlen))
      {
    {
      BitSet map = new BitSet();
      int lastj = -1, j = 0;
 -    int pos = start;
 +    // int pos = start;
      int seqlen = sequence.length;
      while ((j < seqlen))
      {
      vamsasId = id;
    }
  
 +  @Deprecated
    @Override
 -  public void setDBRefs(DBRefEntry[] dbref)
 +  public void setDBRefs(DBModList<DBRefEntry> newDBrefs)
    {
      if (dbrefs == null && datasetSequence != null
              && this != datasetSequence)
      {
 -      datasetSequence.setDBRefs(dbref);
 +      datasetSequence.setDBRefs(newDBrefs);
        return;
      }
 -    dbrefs = dbref;
 -    if (dbrefs != null)
 -    {
 -      DBRefUtils.ensurePrimaries(this);
 -    }
 +    dbrefs = newDBrefs;
 +    refModCount = 0;
    }
  
    @Override
 -  public DBRefEntry[] getDBRefs()
 +  public DBModList<DBRefEntry> getDBRefs()
    {
      if (dbrefs == null && datasetSequence != null
              && this != datasetSequence)
  
      if (dbrefs == null)
      {
 -      dbrefs = new DBRefEntry[0];
 +      dbrefs = new DBModList<>();
      }
  
 -    for (DBRefEntryI dbr : dbrefs)
 +    for (int ib = 0, nb = dbrefs.size(); ib < nb; ib++)
      {
 -      if (dbr.updateFrom(entry))
 +      if (dbrefs.get(ib).updateFrom(entry))
        {
          /*
           * found a dbref that either matched, or could be
        }
      }
  
 -    /*
 -     * extend the array to make room for one more
 -     */
 -    // TODO use an ArrayList instead
 -    int j = dbrefs.length;
 -    DBRefEntry[] temp = new DBRefEntry[j + 1];
 -    System.arraycopy(dbrefs, 0, temp, 0, j);
 -    temp[temp.length - 1] = entry;
 -
 -    dbrefs = temp;
 +    // /// BH OUCH!
 +    // /*
 +    // * extend the array to make room for one more
 +    // */
 +    // // TODO use an ArrayList instead
 +    // int j = dbrefs.length;
 +    // List<DBRefEntry> temp = new DBRefEntry[j + 1];
 +    // System.arraycopy(dbrefs, 0, temp, 0, j);
 +    // temp[temp.length - 1] = entry;
 +    //
 +    // dbrefs = temp;
  
 -    DBRefUtils.ensurePrimaries(this);
 +    dbrefs.add(entry);
    }
  
    @Override
  
    private int _seqhash = 0;
  
 +  private List<DBRefEntry> primaryRefs;
 +
    /**
     * Answers false if the sequence is more than 85% nucleotide (ACGTU), else
     * true
        _isNa = Comparison.isNucleotide(this);
      }
      return !_isNa;
 -  };
 +  }
  
    /*
     * (non-Javadoc)
        // TODO: could merge DBRefs
        return datasetSequence.updatePDBIds();
      }
 -    if (dbrefs == null || dbrefs.length == 0)
 +    if (dbrefs == null || dbrefs.size() == 0)
      {
        return false;
      }
      boolean added = false;
 -    for (DBRefEntry dbr : dbrefs)
 +    for (int ib = 0, nb = dbrefs.size(); ib < nb; ib++)
      {
 +      DBRefEntry dbr = dbrefs.get(ib);
        if (DBRefSource.PDB.equals(dbr.getSource()))
        {
          /*
        List<SequenceFeature> sfs = entry.getSequenceFeatures();
        for (SequenceFeature feature : sfs)
        {
 -       SequenceFeature sf[] = (mp != null) ? mp.locateFeature(feature)
 -                : new SequenceFeature[] { new SequenceFeature(feature) };
 +        SequenceFeature sf[] = (mp != null) ? mp.locateFeature(feature)
 +                : new SequenceFeature[]
 +                { new SequenceFeature(feature) };
          if (sf != null)
          {
            for (int sfi = 0; sfi < sf.length; sfi++)
        }
      }
      // transfer database references
 -    DBRefEntry[] entryRefs = entry.getDBRefs();
 +    List<DBRefEntry> entryRefs = entry.getDBRefs();
      if (entryRefs != null)
      {
 -      for (int r = 0; r < entryRefs.length; r++)
 +      for (int r = 0, n = entryRefs.size(); r < n; r++)
        {
 -        DBRefEntry newref = new DBRefEntry(entryRefs[r]);
 +        DBRefEntry newref = new DBRefEntry(entryRefs.get(r));
          if (newref.getMap() != null && mp != null)
          {
            // remap ref using our local mapping
      {
        for (AlignmentAnnotation ann : annotation)
        {
-         if (ann.calcId != null && ann.calcId.equals(calcId)
+         String id = ann.getCalcId();
+         if (id != null && id.equals(calcId)
                  && ann.label != null && ann.label.equals(label))
          {
            result.add(ann);
      return null;
    }
  
 +  private List<DBRefEntry> tmpList;
 +
    @Override
    public List<DBRefEntry> getPrimaryDBRefs()
    {
      {
        return datasetSequence.getPrimaryDBRefs();
      }
 -    if (dbrefs == null || dbrefs.length == 0)
 +    if (dbrefs == null || dbrefs.size() == 0)
      {
        return Collections.emptyList();
      }
      synchronized (dbrefs)
      {
 -      List<DBRefEntry> primaries = new ArrayList<>();
 -      DBRefEntry[] tmp = new DBRefEntry[1];
 -      for (DBRefEntry ref : dbrefs)
 +      if (refModCount == dbrefs.getModCount() && primaryRefs != null)
 +      {
 +        return primaryRefs; // no changes
 +      }
 +      refModCount = dbrefs.getModCount();
 +      List<DBRefEntry> primaries = (primaryRefs == null
 +              ? (primaryRefs = new ArrayList<>())
 +              : primaryRefs);
 +      primaries.clear();
 +      if (tmpList == null)
        {
 +        tmpList = new ArrayList<>();
 +        tmpList.add(null); // for replacement
 +      }
 +      for (int i = 0, n = dbrefs.size(); i < n; i++)
 +      {
 +        DBRefEntry ref = dbrefs.get(i);
          if (!ref.isPrimaryCandidate())
          {
            continue;
            }
          }
          // whilst it looks like it is a primary ref, we also sanity check type
 -        if (DBRefUtils.getCanonicalName(DBRefSource.PDB)
 -                .equals(DBRefUtils.getCanonicalName(ref.getSource())))
 +        if (DBRefSource.PDB_CANONICAL_NAME
 +                .equals(ref.getCanonicalSourceName()))
          {
            // PDB dbrefs imply there should be a PDBEntry associated
            // TODO: tighten PDB dbrefs
            // handle on the PDBEntry, and a real mapping between sequence and
            // extracted sequence from PDB file
            PDBEntry pdbentry = getPDBEntry(ref.getAccessionId());
 -          if (pdbentry != null && pdbentry.getFile() != null)
 +          if (pdbentry == null || pdbentry.getFile() == null)
            {
 -            primaries.add(ref);
 +            continue;
            }
 -          continue;
          }
 -        // check standard protein or dna sources
 -        tmp[0] = ref;
 -        DBRefEntry[] res = DBRefUtils.selectDbRefs(!isProtein(), tmp);
 -        if (res != null && res[0] == tmp[0])
 +        else
          {
 -          primaries.add(ref);
 -          continue;
 +          // check standard protein or dna sources
 +          tmpList.set(0, ref);
 +          List<DBRefEntry> res = DBRefUtils.selectDbRefs(!isProtein(),
 +                  tmpList);
 +          if (res == null || res.get(0) != tmpList.get(0))
 +          {
 +            continue;
 +          }
          }
 +        primaries.add(ref);
        }
 +
 +      // version must be not null, as otherwise it will not be a candidate,
 +      // above
 +      DBRefUtils.ensurePrimaries(this, primaries);
        return primaries;
      }
    }
  
+   @Override
+   public HiddenMarkovModel getHMM()
+   {
+     return hmm;
+   }
+   @Override
+   public void setHMM(HiddenMarkovModel hmm)
+   {
+     this.hmm = hmm;
+   }
+   @Override
+   public boolean hasHMMAnnotation()
+   {
+     if (this.annotation == null) {
+       return false;
+     }
+     for (AlignmentAnnotation ann : annotation)
+     {
+       if (InformationThread.HMM_CALC_ID.equals(ann.getCalcId()))
+       {
+         return true;
+       }
+     }
+     return false;
+   }
    /**
     * {@inheritDoc}
     */
      // otherwise, sequence was completely hidden
      return 0;
    }
+   @Override
+   public boolean hasHMMProfile()
+   {
+     return hmm != null;
+   }
  }
@@@ -25,6 -25,8 +25,8 @@@ import jalview.analysis.Conservation
  import jalview.renderer.ResidueShader;
  import jalview.renderer.ResidueShaderI;
  import jalview.schemes.ColourSchemeI;
+ import jalview.util.MessageManager;
+ import jalview.workers.InformationThread;
  
  import java.awt.Color;
  import java.beans.PropertyChangeListener;
@@@ -43,11 -45,10 +45,10 @@@ import java.util.Map
  public class SequenceGroup implements AnnotatedCollectionI
  {
    // TODO ideally this event notification functionality should be separated into
-   // a
-   // subclass of ViewportProperties similarly to ViewportRanges. Done here as
-   // quick fix for JAL-2665
+   // a subclass of ViewportProperties similarly to ViewportRanges.
+   // Done here as a quick fix for JAL-2665
    public static final String SEQ_GROUP_CHANGED = "Sequence group changed";
+   
    protected PropertyChangeSupport changeSupport = new PropertyChangeSupport(
            this);
  
    String groupName;
  
    String description;
+   
    Conservation conserve;
  
+   Conservation conservationData;
+   ProfilesI consensusProfiles;
+   ProfilesI hmmProfiles;
    boolean displayBoxes = true;
  
    boolean displayText = true;
  
    boolean colourText = false;
  
-   /**
-    * True if the group is defined as a group on the alignment, false if it is
-    * just a selection.
+   /*
+    * true if the group is defined as a group on the alignment, false if it is
+    * just a selection
     */
    boolean isDefined = false;
  
-   /**
+   /*
     * after Olivier's non-conserved only character display
     */
    boolean showNonconserved = false;
  
-   /**
-    * group members
+   /*
+    * sequences in the group
     */
    private List<SequenceI> sequences;
  
-   /**
+   /*
     * representative sequence for this group (if any)
     */
    private SequenceI seqrep = null;
  
    int width = -1;
  
-   /**
-    * Colourscheme applied to group if any
+   /*
+    * colour scheme applied to group if any
     */
    public ResidueShaderI cs;
  
  
    public Color textColour2 = Color.white;
  
-   /**
-    * consensus calculation property
+   /*
+    * properties for consensus annotation
     */
    private boolean ignoreGapsInConsensus = true;
  
-   /**
-    * consensus calculation property
-    */
    private boolean showSequenceLogo = false;
  
    private boolean normaliseSequenceLogo;
  
    /*
+    * properties for HMM information annotation
+    */
+   private boolean hmmIgnoreBelowBackground = true;
+   private boolean hmmUseInfoLetterHeight;
+   private boolean hmmShowSequenceLogo;
+   private boolean hmmNormaliseSequenceLogo;
+   private boolean hmmShowHistogram;
+   /*
     * visibility of rows or represented rows covered by group
     */
    private boolean hidereps = false;
    /*
     * visibility of columns intersecting this group
     */
-   private boolean hidecols = false;
+   private boolean hidecols;
  
    AlignmentAnnotation consensus = null;
  
    AlignmentAnnotation conservation = null;
  
+   private AlignmentAnnotation hmmInformation;
+   
    private boolean showConsensusHistogram;
+   
    private AnnotatedCollectionI context;
  
    /**
-    * Creates a new SequenceGroup object.
+    * Constructor, assigning a generated default name of "JGroup:" with object
+    * hashcode appended
     */
    public SequenceGroup()
    {
     * copy constructor
     * 
     * @param seqsel
+    * @param keepsequences
+    *          if false do not add sequences from seqsel to new instance
     */
    public SequenceGroup(SequenceGroup seqsel)
    {
      this();
      if (seqsel != null)
      {
        sequences = new ArrayList<>();
        showSequenceLogo = seqsel.showSequenceLogo;
        normaliseSequenceLogo = seqsel.normaliseSequenceLogo;
        showConsensusHistogram = seqsel.showConsensusHistogram;
+       hmmShowSequenceLogo = seqsel.hmmShowSequenceLogo;
+       hmmNormaliseSequenceLogo = seqsel.hmmNormaliseSequenceLogo;
+       hmmShowHistogram = seqsel.hmmShowHistogram;
        idColour = seqsel.idColour;
        outlineColour = seqsel.outlineColour;
        seqrep = seqsel.seqrep;
        thresholdTextColour = seqsel.thresholdTextColour;
        width = seqsel.width;
        ignoreGapsInConsensus = seqsel.ignoreGapsInConsensus;
+       hmmIgnoreBelowBackground = seqsel.hmmIgnoreBelowBackground;
+       hmmUseInfoLetterHeight = seqsel.hmmUseInfoLetterHeight;
        if (seqsel.conserve != null)
        {
+         // todo avoid doing this if we don't actually want derived calculations
+         // !
          recalcConservation(); // safer than
          // aaFrequency = (Vector) seqsel.aaFrequency.clone(); // ??
        }
      for (int i = 0, ipos = 0; i < inorder.length; i++)
      {
        SequenceI seq = inorder[i];
 -
 -      seqs[ipos] = seq.getSubSequence(startRes, endRes + 1);
 -      if (seqs[ipos] != null)
 +      SequenceI seqipos = seqs[ipos] = seq.getSubSequence(startRes, endRes + 1);
 +      if (seqipos != null)
        {
 -        seqs[ipos].setDescription(seq.getDescription());
 -        seqs[ipos].setDBRefs(seq.getDBRefs());
 -        seqs[ipos].setSequenceFeatures(seq.getSequenceFeatures());
 +        seqipos.setDescription(seq.getDescription());
 +        seqipos.setDBRefs(seq.getDBRefs());
 +        seqipos.setSequenceFeatures(seq.getSequenceFeatures());
          if (seq.getDatasetSequence() != null)
          {
 -          seqs[ipos].setDatasetSequence(seq.getDatasetSequence());
 +          seqipos.setDatasetSequence(seq.getDatasetSequence());
          }
  
          if (seq.getAnnotation() != null)
              if (alann != null)
              {
                boolean found = false;
 -              for (int pos = 0; pos < alann.length; pos++)
 +              for (int pos = 0, np = alann.length; pos < np; pos++)
                {
                  if (alann[pos] == tocopy)
                  {
              newannot.restrict(startRes, endRes);
              newannot.setSequenceRef(seqs[ipos]);
              newannot.adjustForAlignment();
 -            seqs[ipos].addAlignmentAnnotation(newannot);
 +            seqipos.addAlignmentAnnotation(newannot);
            }
          }
          ipos++;
    }
  
    /**
-    * calculate residue conservation for group - but only if necessary. returns
-    * true if the calculation resulted in a visible change to group
+    * Recalculates column consensus, conservation, and HMM annotation for the
+    * group (as applicable). Returns true if the calculation resulted in a
+    * visible change to group.
     * 
     * @param defer
     *          when set, colourschemes for this group are not refreshed after
     */
    public boolean recalcConservation(boolean defer)
    {
-     if (cs == null && consensus == null && conservation == null)
+     if (cs == null && consensus == null && conservation == null
+             && hmmInformation == null)
      {
        return false;
      }
      {
        ProfilesI cnsns = AAFrequency.calculate(sequences, startRes,
                endRes + 1, showSequenceLogo);
+       if (hmmInformation != null)
+       {
+         HiddenMarkovModel hmm = hmmInformation.sequenceRef.getHMM();
+         ProfilesI info = AAFrequency.calculateHMMProfiles(hmm,
+                 (endRes + 1) - startRes, startRes, endRes + 1,
+                 hmmIgnoreBelowBackground, hmmUseInfoLetterHeight);
+         _updateInformationRow(info);
+         upd = true;
+       }
        if (consensus != null)
        {
          _updateConsensusRow(cnsns, sequences.size());
    }
  
    /**
+    * Recalculates the information content on the HMM annotation
+    * 
+    * @param cnsns
+    */
+   private void _updateInformationRow(ProfilesI cnsns)
+   {
+     if (hmmInformation == null)
+     {
+       createInformationAnnotation();
+     }
+     hmmInformation.description = MessageManager
+             .getString("label.information_description");
+     setHmmProfiles(cnsns);
+     // preserve width if already set
+     int aWidth = (hmmInformation.annotations != null)
+             ? (endRes < hmmInformation.annotations.length
+                     ? hmmInformation.annotations.length : endRes + 1)
+             : endRes + 1;
+     hmmInformation.annotations = null;
+     hmmInformation.annotations = new Annotation[aWidth]; // should be alignment
+                                                       // width
+     hmmInformation.setCalcId(InformationThread.HMM_CALC_ID);
+     AAFrequency.completeInformation(hmmInformation, cnsns, startRes,
+             endRes + 1);
+   }
+   /**
     * @param s
     *          sequence to either add or remove from group
     * @param recalc
    }
  
    /**
+    * Creates the Hidden Markov Model annotation for this group
+    */
+   void createInformationAnnotation()
+   {
+     hmmInformation = new AlignmentAnnotation("", "", new Annotation[1], 0f,
+             6.25f, AlignmentAnnotation.BAR_GRAPH);
+     hmmInformation.hasText = true;
+     hmmInformation.autoCalculated = false;
+     hmmInformation.groupRef = this;
+     hmmInformation.label = getName();
+     hmmInformation.description = MessageManager
+             .getString("label.information_description");
+     hmmInformation.setCalcId(InformationThread.HMM_CALC_ID);
+   }
+   /**
     * set this alignmentAnnotation object as the one used to render consensus
     * annotation
     * 
      return ignoreGapsInConsensus;
    }
  
+   public void setIgnoreBelowBackground(boolean state)
+   {
+     hmmIgnoreBelowBackground = state;
+   }
+   public boolean isIgnoreBelowBackground()
+   {
+     return hmmIgnoreBelowBackground;
+   }
+   public void setInfoLetterHeight(boolean state)
+   {
+     hmmUseInfoLetterHeight = state;
+   }
+   public boolean isUseInfoLetterHeight()
+   {
+     return hmmUseInfoLetterHeight;
+   }
    /**
     * @param showSequenceLogo
     *          indicates if a sequence logo is shown for consensus annotation
    {
      return (startRes <= apos && endRes >= apos) && sequences.contains(seq);
    }
+   public boolean isShowInformationHistogram()
+   {
+     return hmmShowHistogram;
+   }
+   public void setShowInformationHistogram(boolean state)
+   {
+     if (hmmShowHistogram != state && hmmInformation != null)
+     {
+       this.hmmShowHistogram = state;
+       // recalcConservation(); TODO don't know what to do here next
+     }
+     this.hmmShowHistogram = state;
+   }
+   public boolean isShowHMMSequenceLogo()
+   {
+     return hmmShowSequenceLogo;
+   }
+   public void setShowHMMSequenceLogo(boolean state)
+   {
+     hmmShowSequenceLogo = state;
+   }
+   public boolean isNormaliseHMMSequenceLogo()
+   {
+     return hmmNormaliseSequenceLogo;
+   }
+   public void setNormaliseHMMSequenceLogo(boolean state)
+   {
+     hmmNormaliseSequenceLogo = state;
+   }
+   public ProfilesI getConsensusData()
+   {
+     return consensusProfiles;
+   }
+   public ProfilesI getHmmProfiles()
+   {
+     return hmmProfiles;
+   }
+   public void setHmmProfiles(ProfilesI hmmProfiles)
+   {
+     this.hmmProfiles = hmmProfiles;
+   }
+   @Override
+   public List<SequenceI> getHmmSequences()
+   {
+     List<SequenceI> result = new ArrayList<>();
+     for (int i = 0; i < sequences.size(); i++)
+     {
+       SequenceI seq = sequences.get(i);
+       if (seq.hasHMMProfile())
+       {
+         result.add(seq);
+       }
+     }
+     return result;
+   }
  }
   */
  package jalview.datamodel;
  
 +import jalview.datamodel.Sequence.DBModList;
  import jalview.datamodel.features.SequenceFeaturesI;
  import jalview.util.MapList;
 +import jalview.ws.params.InvalidArgumentException;
  
  import java.util.BitSet;
  import java.util.Iterator;
@@@ -48,6 -46,10 +48,10 @@@ public interface SequenceI extends ASeq
     */
    public void setName(String name);
  
+   public HiddenMarkovModel getHMM();
+   public void setHMM(HiddenMarkovModel hmm);
    /**
     * Get the display name
     */
     * from 1), or null if no residues are included in the range
     * 
     * @param fromColum
-    *          - first column base 1
+    *          - first column base 1. (0 and negative positions are rounded up)
     * @param toColumn
     *          - last column, base 1
-    * @return
+    * @return null if fromColum>toColumn
     */
    public ContiguousI findPositions(int fromColum, int toColumn);
  
    /**
     * set the array of Database references for the sequence.
     * 
 +   * BH 2019.02.04 changes param to DBModlist 
 +   * 
     * @param dbs
     * @deprecated - use is discouraged since side-effects may occur if DBRefEntry
     *             set are not normalised.
 +   * @throws InvalidArgumentException if the is not one created by Sequence itself
     */
    @Deprecated
 -  public void setDBRefs(DBRefEntry[] dbs);
 +  public void setDBRefs(DBModList<DBRefEntry> dbs);
  
 -  public DBRefEntry[] getDBRefs();
 +  public DBModList<DBRefEntry> getDBRefs();
  
    /**
     * add the given entry to the list of DBRefs for this sequence, or replace a
    public List<DBRefEntry> getPrimaryDBRefs();
  
    /**
+    * Answers true if the sequence has annotation for Hidden Markov Model
+    * information content, else false
+    */
+   boolean hasHMMAnnotation();
+   /**
     * Returns a (possibly empty) list of sequence features that overlap the given
     * alignment column range, optionally restricted to one or more specified
     * feature types. If the range is all gaps, then features which enclose it are
     * @param c1
     * @param c2
     */
-   public int replace(char c1, char c2);
+   int replace(char c1, char c2);
  
    /**
     * Answers the GeneLociI, or null if not known
     *          the iterator to use
     * @return a String corresponding to the sequence
     */
-   public String getSequenceStringFromIterator(Iterator<int[]> it);
+   String getSequenceStringFromIterator(Iterator<int[]> it);
  
    /**
     * Locate the first position in this sequence which is not contained in an
     *          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,17 -27,18 +27,15 @@@ 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;
  import java.util.ArrayList;
  import java.util.Iterator;
  import java.util.List;
 +import java.util.Map;
  
 -import org.json.simple.JSONArray;
 -import org.json.simple.JSONObject;
 -import org.json.simple.parser.JSONParser;
  import org.json.simple.parser.ParseException;
  
  /**
@@@ -91,14 -92,16 +89,14 @@@ class EnsemblFeatures extends EnsemblRe
    public AlignmentI getSequenceRecords(String query) throws IOException
    {
      // TODO: use a vararg String... for getSequenceRecords instead?
 +        
      List<String> queries = new ArrayList<>();
      queries.add(query);
 -    BufferedReader fp = getSequenceReader(queries);
 -    if (fp == null)
 -    {
 -      return null;
 -    }
 -
 -    SequenceI seq = parseFeaturesJson(fp);
 +    SequenceI seq = parseFeaturesJson(queries);
 +    if (seq == null)
 +      return null;
      return new Alignment(new SequenceI[] { seq });
 +
    }
  
    /**
     * @param br
     * @return
     */
 -  private SequenceI parseFeaturesJson(BufferedReader br)
 +  @SuppressWarnings("unchecked")
 +  private SequenceI parseFeaturesJson(List<String> queries)
    {
      SequenceI seq = new Sequence("Dummy", "");
 -
 -    JSONParser jp = new JSONParser();
      try
      {
 -      JSONArray responses = (JSONArray) jp.parse(br);
 -      Iterator rvals = responses.iterator();
 -      while (rvals.hasNext())
 +      Iterator<Object> rvals = (Iterator<Object>) getJSON(null, queries, -1, MODE_ITERATOR, null);
 +      if (rvals == null)
        {
 +        return null;
 +      }
 +      while (rvals.hasNext())
 +      {         
          try
          {
 -          JSONObject obj = (JSONObject) rvals.next();
 +          Map<String, Object> obj = (Map<String, Object>) rvals.next();
            String type = obj.get("feature_type").toString();
            int start = Integer.parseInt(obj.get("start").toString());
            int end = Integer.parseInt(obj.get("end").toString());
            String strand = obj.get("strand").toString();
            Object phase = obj.get("phase");
            String alleles = JSONUtils
 -                  .arrayToList((JSONArray) obj.get("alleles"));
 +                  .arrayToStringList((List<Object>) obj.get("alleles"));
            String clinSig = JSONUtils
 -                  .arrayToList(
 -                          (JSONArray) obj.get("clinical_significance"));
 +                  .arrayToStringList(
 +                          (List<Object>) obj.get("clinical_significance"));
  
            /*
             * convert 'variation' to 'sequence_variant', and 'cds' to 'CDS'
            {
              type = SequenceOntologyI.CDS;
            }
 -          
 +
            String desc = getFirstNotNull(obj, "alleles", "external_name",
                    JSON_ID);
            SequenceFeature sf = new SequenceFeature(type, desc, start, end,
            sf.setValue("clinical_significance", clinSig);
  
            seq.addSequenceFeature(sf);
 +          
          } catch (Throwable t)
          {
            // ignore - keep trying other features
        }
      } catch (ParseException | IOException e)
      {
 +      e.printStackTrace();
        // ignore
      }
  
     * @param keys
     * @return
     */
 -  protected String getFirstNotNull(JSONObject obj, String... keys)
 +  @SuppressWarnings("unchecked")
 +  protected String getFirstNotNull(Map<String, Object> obj, String... keys)
    {
 -    String desc = null;
 -
      for (String key : keys)
      {
        Object val = obj.get(key);
        if (val != null)
        {
 -        String s = val instanceof JSONArray
 -                ? JSONUtils.arrayToList((JSONArray) val)
 +        String s = val instanceof List<?>
 +                ? JSONUtils.arrayToStringList((List<Object>) val)
                  : val.toString();
          if (!s.isEmpty())
          {
          }
        }
      }
 -    return desc;
 +    return null;
    }
  
    /**
     * @param obj
     * @param key
     */
 -  protected void setFeatureAttribute(SequenceFeature sf, JSONObject obj,
 +  protected void setFeatureAttribute(SequenceFeature sf, Map<String, Object> obj,
            String key)
    {
      Object object = obj.get(key);
@@@ -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;
@@@ -34,6 -33,8 +32,6 @@@ import java.util.List
  import java.util.Map;
  import java.util.Set;
  
 -import org.json.simple.JSONArray;
 -import org.json.simple.parser.JSONParser;
  import org.json.simple.parser.ParseException;
  
  public class EnsemblInfo extends EnsemblRestClient
       * for convenience, pre-fill ensembl.org as the domain for "ENSEMBL"
       */
      divisions.put(DBRefSource.ENSEMBL.toUpperCase(), ensemblDomain);
 -
 -    BufferedReader br = null;
      try
      {
 -      URL url = getDivisionsUrl(ensemblGenomesDomain);
 -      if (url != null)
 -      {
 -        br = getHttpResponse(url, null);
 -      }
 -      parseResponse(br, ensemblGenomesDomain);
 -    } catch (IOException e)
 -    {
 -      // ignore
 -    } finally
 -    {
 -      if (br != null)
 -      {
 -        try
 -        {
 -          br.close();
 -        } catch (IOException e)
 -        {
 -          // ignore
 -        }
 -      }
 -    }
 -  }
 -
 -  /**
 -   * Parses the JSON response to /info/divisions, and add each to the lookup map
 -   * 
 -   * @param br
 -   * @param domain
 -   */
 -  void parseResponse(BufferedReader br, String domain)
 -  {
 -    JSONParser jp = new JSONParser();
 -
 -    try
 -    {
 -      JSONArray parsed = (JSONArray) jp.parse(br);
 -
 -      Iterator rvals = parsed.iterator();
 +      @SuppressWarnings("unchecked")
 +        Iterator<Object> rvals = (Iterator<Object>) getJSON(getDivisionsUrl(ensemblGenomesDomain), null, -1, MODE_ITERATOR, null);
 +      if (rvals == null)
 +        return;
        while (rvals.hasNext())
        {
          String division = rvals.next().toString();
 -        divisions.put(division.toUpperCase(), domain);
 +        divisions.put(division.toUpperCase(), ensemblGenomesDomain);
        }
      } catch (IOException | ParseException | NumberFormatException e)
      {
@@@ -23,17 -23,18 +23,15 @@@ 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;
  import java.util.ArrayList;
  import java.util.Iterator;
  import java.util.List;
 +import java.util.Map;
  
 -import org.json.simple.JSONArray;
 -import org.json.simple.JSONObject;
 -import org.json.simple.parser.JSONParser;
  import org.json.simple.parser.ParseException;
  
  /**
@@@ -98,19 -99,66 +96,19 @@@ class EnsemblXref extends EnsemblRestCl
     *          an Ensembl stable identifier
     * @return
     */
 +  @SuppressWarnings("unchecked")
    public List<DBRefEntry> getCrossReferences(String identifier)
    {
      List<DBRefEntry> result = new ArrayList<>();
      List<String> ids = new ArrayList<>();
      ids.add(identifier);
  
 -    BufferedReader br = null;
      try
      {
 -      URL url = getUrl(identifier);
 -      if (url != null)
 -      {
 -        br = getHttpResponse(url, ids);
 -        if (br != null)
 -        {
 -          result = parseResponse(br);
 -        }
 -      }
 -    } catch (IOException e)
 -    {
 -      // ignore
 -    } finally
 -    {
 -      if (br != null)
 -      {
 -        try
 -        {
 -          br.close();
 -        } catch (IOException e)
 -        {
 -          // ignore
 -        }
 -      }
 -    }
 -
 -    return result;
 -  }
 -
 -  /**
 -   * Parses "primary_id" and "dbname" values from the JSON response and
 -   * constructs a DBRefEntry. Returns a list of the DBRefEntry created. Note we
 -   * don't parse "synonyms" as they appear to be either redirected or obsolete
 -   * in Uniprot.
 -   * 
 -   * @param br
 -   * @return
 -   * @throws IOException
 -   */
 -  protected List<DBRefEntry> parseResponse(BufferedReader br)
 -          throws IOException
 -  {
 -    JSONParser jp = new JSONParser();
 -    List<DBRefEntry> result = new ArrayList<>();
 -    try
 -    {
 -      JSONArray responses = (JSONArray) jp.parse(br);
 -      Iterator rvals = responses.iterator();
 +      Iterator<Object> rvals = (Iterator<Object>) getJSON(getUrl(identifier), ids, -1, MODE_ITERATOR, null);
        while (rvals.hasNext())
        {
 -        JSONObject val = (JSONObject) rvals.next();
 +        Map<String, Object> val = (Map<String, Object>) rvals.next();
          String db = val.get("dbname").toString();
          String id = val.get("primary_id").toString();
          if (db != null && id != null
            result.add(dbref);
          }
        }
 -    } catch (ParseException e)
 +    } catch (ParseException | IOException e)
      {
        // ignore
      }
      return result;
    }
  
 +//  /**
 +//   * Parses "primary_id" and "dbname" values from the JSON response and
 +//   * constructs a DBRefEntry. Returns a list of the DBRefEntry created. Note we
 +//   * don't parse "synonyms" as they appear to be either redirected or obsolete
 +//   * in Uniprot.
 +//   * 
 +//   * @param br
 +//   * @return
 +//   * @throws IOException
 +//   */
 +//  @SuppressWarnings("unchecked")
 +//protected List<DBRefEntry> parseResponse(BufferedReader br)
 +//          throws IOException
 +//  {
 +//    return result;
 +//  }
 +//
    private String xrefVersion = "ENSEMBL:0";
  
    /**
   */
  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;
@@@ -79,7 -27,7 +77,7 @@@ import jalview.analysis.Dna
  import jalview.analysis.GeneticCodeI;
  import jalview.analysis.ParseProperties;
  import jalview.analysis.SequenceIdMatcher;
 -import jalview.api.AlignExportSettingI;
 +import jalview.api.AlignExportSettingsI;
  import jalview.api.AlignViewControllerGuiI;
  import jalview.api.AlignViewControllerI;
  import jalview.api.AlignViewportI;
@@@ -99,7 -47,6 +97,7 @@@ import jalview.commands.RemoveGapColCom
  import jalview.commands.RemoveGapsCommand;
  import jalview.commands.SlideSequencesCommand;
  import jalview.commands.TrimRegionCommand;
 +import jalview.datamodel.AlignExportSettingsAdapter;
  import jalview.datamodel.AlignedCodonFrame;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentAnnotation;
@@@ -109,6 -56,7 +107,6 @@@ import jalview.datamodel.AlignmentOrder
  import jalview.datamodel.AlignmentView;
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.HiddenColumns;
 -import jalview.datamodel.HiddenSequences;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SeqCigar;
  import jalview.datamodel.Sequence;
@@@ -116,6 -64,13 +114,13 @@@ import jalview.datamodel.SequenceGroup
  import jalview.datamodel.SequenceI;
  import jalview.gui.ColourMenuHelper.ColourChangeListener;
  import jalview.gui.ViewSelectionMenu.ViewSetProvider;
+ import jalview.hmmer.HMMAlign;
+ import jalview.hmmer.HMMBuild;
+ import jalview.hmmer.HMMERParamStore;
+ import jalview.hmmer.HMMERPreset;
+ import jalview.hmmer.HMMSearch;
+ import jalview.hmmer.HmmerCommand;
+ import jalview.hmmer.JackHMMER;
  import jalview.io.AlignmentProperties;
  import jalview.io.AnnotationFile;
  import jalview.io.BackupFiles;
@@@ -138,22 -93,73 +143,32 @@@ import jalview.io.ScoreMatrixFile
  import jalview.io.TCoffeeScoreFile;
  import jalview.io.vcf.VCFLoader;
  import jalview.jbgui.GAlignFrame;
 +import jalview.project.Jalview2XML;
  import jalview.schemes.ColourSchemeI;
  import jalview.schemes.ColourSchemes;
  import jalview.schemes.ResidueColourScheme;
  import jalview.schemes.TCoffeeColourScheme;
 +import jalview.util.ImageMaker.TYPE;
  import jalview.util.MessageManager;
 +import jalview.util.Platform;
  import jalview.viewmodel.AlignmentViewport;
  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!
   * @author $author$
   * @version $Revision$
   */
 +@SuppressWarnings("serial")
  public class AlignFrame extends GAlignFrame implements DropTargetListener,
          IProgressIndicator, AlignViewControllerGuiI, ColourChangeListener
  {
    public static final int DEFAULT_WIDTH = 700;
  
    public static final int DEFAULT_HEIGHT = 500;
     */
    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.
     */
    void init()
    {
 +//      setBackground(Color.white); // BH 2019
 +                
      if (!Jalview.isHeadlessMode())
      {
        progressBar = new ProgressBar(this.statusPanel, this.statusBar);
      if (Desktop.desktop != null)
      {
        this.setDropTarget(new java.awt.dnd.DropTarget(this, this));
 -      addServiceListeners();
 +      if (!Platform.isJS())
 +      {
 +        addServiceListeners();
 +      }
        setGUINucleotide();
      }
  
    }
  
    /**
 +   * JavaScript will have this, maybe others. More dependable than a file name
 +   * and maintains a reference to the actual bytes loaded.
 +   * 
 +   * @param file
 +   */
 +  public void setFileObject(File file)
 +  {
 +    this.fileObject = file;
 +  }
 +
 +  /**
     * Add a KeyListener with handlers for various KeyPressed and KeyReleased
     * events
     */
          case KeyEvent.VK_BACK_SPACE:
            if (!viewport.cursorMode)
            {
 -            cut_actionPerformed(null);
 +            cut_actionPerformed();
            }
            else
            {
  
          case KeyEvent.VK_F2:
            viewport.cursorMode = !viewport.cursorMode;
 -          statusBar.setText(MessageManager
 +          setStatus(MessageManager
                    .formatMessage("label.keyboard_editing_mode", new String[]
                    { (viewport.cursorMode ? "on" : "off") }));
            if (viewport.cursorMode)
        ap.av.updateConservation(ap);
        ap.av.updateConsensus(ap);
        ap.av.updateStrucConsensus(ap);
+       ap.av.initInformationWorker(ap);
      }
    }
  
          Desktop.instance.removeJalviewPropertyChangeListener("services",
                  thisListener);
          closeMenuItem_actionPerformed(true);
 -      };
 +      }
      });
      // Finally, build the menu once to get current service state
      new Thread(new Runnable()
      showConsensusHistogram.setSelected(av.isShowConsensusHistogram());
      showSequenceLogo.setSelected(av.isShowSequenceLogo());
      normaliseSequenceLogo.setSelected(av.isNormaliseSequenceLogo());
+     showInformationHistogram.setSelected(av.isShowInformationHistogram());
+     showHMMSequenceLogo.setSelected(av.isShowHMMSequenceLogo());
+     normaliseHMMSequenceLogo.setSelected(av.isNormaliseHMMSequenceLogo());
  
      ColourMenuHelper.setColourSelected(colourMenu,
              av.getGlobalColourScheme());
    }
  
    @Override
 -  public void fetchSequence_actionPerformed(ActionEvent e)
 +  public void fetchSequence_actionPerformed()
    {
 -    new jalview.gui.SequenceFetcher(this);
 +    new SequenceFetcher(this);
    }
  
    @Override
    }
  
    @Override
+   public void hmmBuild_actionPerformed(boolean withDefaults)
+   {
+     if (!alignmentIsSufficient(1))
+     {
+       return;
+     }
+     /*
+      * get default parameters, and optionally show a dialog
+      * to allow them to be modified
+      */
+     ParamDatastoreI store = HMMERParamStore.forBuild(viewport);
+     List<ArgumentI> args = store.getServiceParameters();
+     if (!withDefaults)
+     {
+       WsParamSetI set = new HMMERPreset();
+       WsJobParameters params = new WsJobParameters(store, set, args);
+       if (params.showRunDialog())
+       {
+         args = params.getJobParams();
+       }
+       else
+       {
+         return; // user cancelled
+       }
+     }
+     new Thread(new HMMBuild(this, args)).start();
+   }
+   @Override
+   public void hmmAlign_actionPerformed(boolean withDefaults)
+   {
+     if (!(checkForHMM() && alignmentIsSufficient(2)))
+     {
+       return;
+     }
+     /*
+      * get default parameters, and optionally show a dialog
+      * to allow them to be modified
+      */
+     ParamDatastoreI store = HMMERParamStore.forAlign(viewport);
+     List<ArgumentI> args = store.getServiceParameters();
+     if (!withDefaults)
+     {
+       WsParamSetI set = new HMMERPreset();
+       WsJobParameters params = new WsJobParameters(store, set, args);
+       if (params.showRunDialog())
+       {
+         args = params.getJobParams();
+       }
+       else
+       {
+         return; // user cancelled
+       }
+     }
+     new Thread(new HMMAlign(this, args)).start();
+   }
+   @Override
+   public void hmmSearch_actionPerformed(boolean withDefaults)
+   {
+     if (!checkForHMM())
+     {
+       return;
+     }
+     /*
+      * get default parameters, and (if requested) show 
+      * dialog to allow modification
+      */
+     ParamDatastoreI store = HMMERParamStore.forSearch(viewport);
+     List<ArgumentI> args = store.getServiceParameters();
+     if (!withDefaults)
+     {
+       WsParamSetI set = new HMMERPreset();
+       WsJobParameters params = new WsJobParameters(store, set, args);
+       if (params.showRunDialog())
+       {
+         args = params.getJobParams();
+       }
+       else
+       {
+         return; // user cancelled
+       }
+     }
+     new Thread(new HMMSearch(this, args)).start();
+     alignPanel.repaint();
+   }
+   
+   @Override
+   public void jackhmmer_actionPerformed(boolean withDefaults)
+   {
+     
+     /*
+      * get default parameters, and (if requested) show 
+      * dialog to allow modification
+      */
+     
+     ParamDatastoreI store = HMMERParamStore.forJackhmmer(viewport);
+     List<ArgumentI> args = store.getServiceParameters();
+     if (!withDefaults)
+     {
+       WsParamSetI set = new HMMERPreset();
+       WsJobParameters params = new WsJobParameters(store, set, args);
+       if (params.showRunDialog())
+       {
+         args = params.getJobParams();
+       }
+       else
+       {
+         return; // user cancelled
+       }
+     }
+     new Thread(new JackHMMER(this, args)).start();
+     alignPanel.repaint();
+     
+   }
+   /**
+    * Checks if the alignment has at least one hidden Markov model, if not shows
+    * a dialog advising to run hmmbuild or load an HMM profile
+    * 
+    * @return
+    */
+   private boolean checkForHMM()
+   {
+     if (viewport.getAlignment().getHmmSequences().isEmpty())
+     {
+       JOptionPane.showMessageDialog(this,
+               MessageManager.getString("warn.no_hmm"));
+       return false;
+     }
+     return true;
+   }
+   
+   @Override
+   protected void filterByEValue_actionPerformed()
+   {
+     viewport.filterByEvalue(inputDouble("Enter E-Value Cutoff"));
+   }
+   
+   @Override
+   protected void filterByScore_actionPerformed()
+   {
+     viewport.filterByScore(inputDouble("Enter Bit Score Threshold"));
+   }
+   
+   private double inputDouble(String message)
+   {
+     String str = null;
+     Double d = null;
+     while(d == null || d <= 0)
+     {
+       str = JOptionPane.showInputDialog(this.alignPanel, message);
+       try
+       {
+         d = Double.valueOf(str);
+       }
+       catch (NumberFormatException e)
+       {
+       }
+     }
+     return d;
+   }
+   /**
+    * Checks if the alignment contains the required number of sequences.
+    * 
+    * @param required
+    * @return
+    */
+   public boolean alignmentIsSufficient(int required)
+   {
+       if (getViewport().getSequenceSelection().length < required)
+       {
+         JOptionPane.showMessageDialog(this,
+                 MessageManager.getString("label.not_enough_sequences"));
+         return false;
+       }
+       return true;
+   }
+   /**
+    * Opens a file browser and adds the selected file, if in Fasta, Stockholm or
+    * Pfam format, to the list held under preference key "HMMSEARCH_DBS" (as a
+    * comma-separated list)
+    */
+   @Override
+   public void addDatabase_actionPerformed() throws IOException
+   {
+     if (Cache.getProperty(Preferences.HMMSEARCH_DBS) == null)
+     {
+       Cache.setProperty(Preferences.HMMSEARCH_DBS, "");
+     }
+     String path = openFileChooser(false);
+     if (path != null && new File(path).exists())
+     {
+       IdentifyFile identifier = new IdentifyFile();
+       FileFormatI format = identifier.identify(path, DataSourceType.FILE);
+       if (format == FileFormat.Fasta || format == FileFormat.Stockholm
+               || format == FileFormat.Pfam)
+       {
+         String currentDbPaths = Cache
+                 .getProperty(Preferences.HMMSEARCH_DBS);
+         currentDbPaths += Preferences.COMMA + path;
+         Cache.setProperty(Preferences.HMMSEARCH_DBS, currentDbPaths);
+       }
+       else
+       {
+         JOptionPane.showMessageDialog(this,
+                 MessageManager.getString("warn.invalid_format"));
+       }
+     }
+   }
+   /**
+    * Opens a file chooser, optionally restricted to selecting folders
+    * (directories) only. Answers the path to the selected file or folder, or
+    * null if none is chosen.
+    * 
+    * @param
+    * @return
+    */
+   protected String openFileChooser(boolean forFolder)
+   {
+     // TODO duplicates GPreferences method - relocate to JalviewFileChooser?
+     String choice = null;
+     JFileChooser chooser = new JFileChooser();
+     if (forFolder)
+     {
+       chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+     }
+     chooser.setDialogTitle(
+             MessageManager.getString("label.open_local_file"));
+     chooser.setToolTipText(MessageManager.getString("action.open"));
+     int value = chooser.showOpenDialog(this);
+     if (value == JFileChooser.APPROVE_OPTION)
+     {
+       choice = chooser.getSelectedFile().getPath();
+     }
+     return choice;
+   }
+   @Override
    public void reload_actionPerformed(ActionEvent e)
    {
      if (fileName != null)
          Rectangle bounds = this.getBounds();
  
          FileLoader loader = new FileLoader();
 -        DataSourceType protocol = fileName.startsWith("http:")
 -                ? DataSourceType.URL
 -                : DataSourceType.FILE;
 -        AlignFrame newframe = loader.LoadFileWaitTillLoaded(fileName,
 -                protocol, currentFileFormat);
 +
 +        AlignFrame newframe = null;
 +
 +        if (fileObject == null)
 +        {
 +
 +          DataSourceType protocol = (fileName.startsWith("http:")
 +                  ? DataSourceType.URL
 +                  : DataSourceType.FILE);
 +          newframe = loader.LoadFileWaitTillLoaded(fileName, protocol,
 +                  currentFileFormat);
 +        }
 +        else
 +        {
 +          newframe = loader.LoadFileWaitTillLoaded(fileObject,
 +                  DataSourceType.FILE, currentFileFormat);
 +        }
  
          newframe.setBounds(bounds);
          if (featureSettings != null && featureSettings.isShowing())
      if (fileName == null || (currentFileFormat == null)
              || fileName.startsWith("http"))
      {
 -      saveAs_actionPerformed(null);
 +      saveAs_actionPerformed();
      }
      else
      {
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Saves the alignment to a file with a name chosen by the user, if necessary
 +   * warning if a file would be overwritten
     */
    @Override
 -  public void saveAs_actionPerformed(ActionEvent e)
 +  public void saveAs_actionPerformed()
    {
      String format = currentFileFormat == null ? null
              : currentFileFormat.getName();
  
      int value = chooser.showSaveDialog(this);
  
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    if (value != JalviewFileChooser.APPROVE_OPTION)
      {
 +      return;
 +    }
 +    currentFileFormat = chooser.getSelectedFormat();
 +    // todo is this (2005) test now obsolete - value is never null?
 +    while (currentFileFormat == null)
 +    {
 +      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 +              MessageManager
 +                      .getString("label.select_file_format_before_saving"),
 +              MessageManager.getString("label.file_format_not_specified"),
 +              JvOptionPane.WARNING_MESSAGE);
        currentFileFormat = chooser.getSelectedFormat();
 -      while (currentFileFormat == null)
 +      value = chooser.showSaveDialog(this);
 +      if (value != JalviewFileChooser.APPROVE_OPTION)
        {
 -        JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 -                MessageManager.getString(
 -                        "label.select_file_format_before_saving"),
 -                MessageManager.getString("label.file_format_not_specified"),
 -                JvOptionPane.WARNING_MESSAGE);
 -        currentFileFormat = chooser.getSelectedFormat();
 -        value = chooser.showSaveDialog(this);
 -        if (value != JalviewFileChooser.APPROVE_OPTION)
 -        {
 -          return;
 -        }
 +        return;
        }
 +    }
  
 -      fileName = chooser.getSelectedFile().getPath();
 +    fileName = chooser.getSelectedFile().getPath();
  
 -      Cache.setProperty("DEFAULT_FILE_FORMAT", currentFileFormat.getName());
 +    Cache.setProperty("DEFAULT_FILE_FORMAT", currentFileFormat.getName());
 +    Cache.setProperty("LAST_DIRECTORY", fileName);
 +    saveAlignment(fileName, currentFileFormat);
 +  }
 +
 +  boolean lastSaveSuccessful = false;
 +
 +  FileFormatI lastFormatSaved;
 +
 +  String lastFilenameSaved;
 +
 +  /**
 +   * Raise a dialog or status message for the last call to saveAlignment.
 +   *
 +   * @return true if last call to saveAlignment(file, format) was successful.
 +   */
 +  public boolean isSaveAlignmentSuccessful()
 +  {
 +
 +    if (!lastSaveSuccessful)
 +    {
 +      JvOptionPane.showInternalMessageDialog(this, MessageManager
 +              .formatMessage("label.couldnt_save_file", new Object[]
 +              { lastFilenameSaved }),
 +              MessageManager.getString("label.error_saving_file"),
 +              JvOptionPane.WARNING_MESSAGE);
 +    }
 +    else
 +    {
 +
 +      setStatus(MessageManager.formatMessage(
 +              "label.successfully_saved_to_file_in_format", new Object[]
 +              { lastFilenameSaved, lastFormatSaved }));
  
 -      Cache.setProperty("LAST_DIRECTORY", fileName);
 -      saveAlignment(fileName, currentFileFormat);
      }
 +    return lastSaveSuccessful;
    }
  
 -  public boolean saveAlignment(String file, FileFormatI format)
 +  /**
 +   * Saves the alignment to the specified file path, in the specified format,
 +   * which may be an alignment format, or Jalview project format. If the
 +   * alignment has hidden regions, or the format is one capable of including
 +   * non-sequence data (features, annotations, groups), then the user may be
 +   * prompted to specify what to include in the output.
 +   * 
 +   * @param file
 +   * @param format
 +   */
 +  public void saveAlignment(String file, FileFormatI format)
    {
 -    boolean success = true;
 +    lastSaveSuccessful = true;
 +    lastFilenameSaved = file;
 +    lastFormatSaved = format;
  
      if (FileFormat.Jalview.equals(format))
      {
        String shortName = title;
 -
 -      if (shortName.indexOf(java.io.File.separatorChar) > -1)
 +      if (shortName.indexOf(File.separatorChar) > -1)
        {
          shortName = shortName.substring(
 -                shortName.lastIndexOf(java.io.File.separatorChar) + 1);
 +                shortName.lastIndexOf(File.separatorChar) + 1);
        }
 -
 -      success = new jalview.project.Jalview2XML().saveAlignment(this, file,
 -              shortName);
 -
 +      lastSaveSuccessful = new Jalview2XML().saveAlignment(this, file, shortName);
 +      
        statusBar.setText(MessageManager.formatMessage(
                "label.successfully_saved_to_file_in_format", new Object[]
                { fileName, format }));
 -
 +      
 +      return;
      }
 -    else
 +
 +    AlignExportSettingsI options = new AlignExportSettingsAdapter(false);
 +    Runnable cancelAction = new Runnable()
      {
 -      AlignmentExportData exportData = getAlignmentForExport(format,
 -              viewport, null);
 -      if (exportData.getSettings().isCancelled())
 -      {
 -        return false;
 -      }
 -      FormatAdapter f = new FormatAdapter(alignPanel,
 -              exportData.getSettings());
 -      String output = f.formatSequences(format, exportData.getAlignment(), // class
 -                                                                           // cast
 -                                                                           // exceptions
 -                                                                           // will
 -              // occur in the distant future
 -              exportData.getOmitHidden(), exportData.getStartEndPostions(),
 -              f.getCacheSuffixDefault(format),
 -              viewport.getAlignment().getHiddenColumns());
 -
 -      if (output == null)
 +      @Override
 +      public void run()
        {
 -        success = false;
 +        lastSaveSuccessful = false;
        }
 -      else
 +    };
 +    Runnable outputAction = new Runnable()
 +    {
 +      @Override
 +      public void run()
        {
 -        // create backupfiles object and get new temp filename destination
 -        BackupFiles backupfiles = new BackupFiles(file);
 -
 -        try
 +        // todo defer this to inside formatSequences (or later)
 +        AlignmentExportData exportData = viewport
 +                .getAlignExportData(options);
 +        String output = new FormatAdapter(alignPanel, options)
 +                .formatSequences(format, exportData.getAlignment(),
 +                        exportData.getOmitHidden(),
 +                        exportData.getStartEndPostions(),
 +                        viewport.getAlignment().getHiddenColumns());
 +        if (output == null)
 +        {
 +          lastSaveSuccessful = false;
 +        }
 +        else
          {
 -          PrintWriter out = new PrintWriter(
 -                  new FileWriter(backupfiles.getTempFilePath()));
 +          // create backupfiles object and get new temp filename destination
 +          boolean doBackup = BackupFiles.getEnabled();
 +          BackupFiles backupfiles = doBackup ? new BackupFiles(file) : null;
 +          try
 +          {
 +            String tempFilePath = doBackup ? backupfiles.getTempFilePath() : file;
 +                      PrintWriter out = new PrintWriter(
 +                    new FileWriter(tempFilePath));
  
 -          out.print(output);
 -          out.close();
 -          this.setTitle(file);
 -          statusBar.setText(MessageManager.formatMessage(
 +            out.print(output);
 +            out.close();
 +            AlignFrame.this.setTitle(file);
 +            statusBar.setText(MessageManager.formatMessage(
                    "label.successfully_saved_to_file_in_format", new Object[]
                    { fileName, format.getName() }));
 -        } catch (Exception ex)
 -        {
 -          success = false;
 -          ex.printStackTrace();
 -        }
 -
 -        backupfiles.setWriteSuccess(success);
 -        // do the backup file roll and rename the temp file to actual file
 -        success = backupfiles.rollBackupsAndRenameTempFile();
 +            lastSaveSuccessful = true;
 +          } catch (Exception ex)
 +          {
 +            lastSaveSuccessful = false;
 +            ex.printStackTrace();
 +          }
  
 +          if (doBackup)
 +          {
 +            backupfiles.setWriteSuccess(lastSaveSuccessful);
 +            // do the backup file roll and rename the temp file to actual file
 +            lastSaveSuccessful = backupfiles.rollBackupsAndRenameTempFile();
 +          }
 +        }
        }
 -    }
 -
 -    if (!success)
 -    {
 -      JvOptionPane.showInternalMessageDialog(this, MessageManager
 -              .formatMessage("label.couldnt_save_file", new Object[]
 -              { fileName }),
 -              MessageManager.getString("label.error_saving_file"),
 -              JvOptionPane.WARNING_MESSAGE);
 -    }
 +    };
  
 -    return success;
 -  }
 -
 -  private void warningMessage(String warning, String title)
 -  {
 -    if (new jalview.util.Platform().isHeadless())
 +    /*
 +     * show dialog with export options if applicable; else just do it
 +     */
 +    if (AlignExportOptions.isNeeded(viewport, format))
      {
 -      System.err.println("Warning: " + title + "\nWarning: " + warning);
 -
 +      AlignExportOptions choices = new AlignExportOptions(
 +              alignPanel.getAlignViewport(), format, options);
 +      choices.setResponseAction(0, outputAction);
 +      choices.setResponseAction(1, cancelAction);
 +      choices.showDialog();
      }
      else
      {
 -      JvOptionPane.showInternalMessageDialog(this, warning, title,
 -              JvOptionPane.WARNING_MESSAGE);
 +      outputAction.run();
      }
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Outputs the alignment to textbox in the requested format, if necessary
 +   * first prompting the user for whether to include hidden regions or
 +   * non-sequence data
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param fileFormatName
     */
    @Override
 -  protected void outputText_actionPerformed(ActionEvent e)
 +  protected void outputText_actionPerformed(String fileFormatName)
    {
      FileFormatI fileFormat = FileFormats.getInstance()
 -            .forName(e.getActionCommand());
 -    AlignmentExportData exportData = getAlignmentForExport(fileFormat,
 -            viewport, null);
 -    if (exportData.getSettings().isCancelled())
 -    {
 -      return;
 -    }
 -    CutAndPasteTransfer cap = new CutAndPasteTransfer();
 -    cap.setForInput(null);
 -    try
 -    {
 -      FileFormatI format = fileFormat;
 -      cap.setText(new FormatAdapter(alignPanel, exportData.getSettings())
 -              .formatSequences(format, exportData.getAlignment(),
 -                      exportData.getOmitHidden(),
 -                      exportData.getStartEndPostions(),
 -                      viewport.getAlignment().getHiddenColumns()));
 -      Desktop.addInternalFrame(cap, MessageManager
 -              .formatMessage("label.alignment_output_command", new Object[]
 -              { e.getActionCommand() }), 600, 500);
 -    } catch (OutOfMemoryError oom)
 -    {
 -      new OOMWarning("Outputting alignment as " + e.getActionCommand(),
 -              oom);
 -      cap.dispose();
 -    }
 -
 -  }
 -
 -  public static AlignmentExportData getAlignmentForExport(
 -          FileFormatI format, AlignViewportI viewport,
 -          AlignExportSettingI exportSettings)
 -  {
 -    AlignmentI alignmentToExport = null;
 -    AlignExportSettingI settings = exportSettings;
 -    String[] omitHidden = null;
 -
 -    HiddenSequences hiddenSeqs = viewport.getAlignment()
 -            .getHiddenSequences();
 -
 -    alignmentToExport = viewport.getAlignment();
 -
 -    boolean hasHiddenSeqs = hiddenSeqs.getSize() > 0;
 -    if (settings == null)
 +            .forName(fileFormatName);
 +    AlignExportSettingsI options = new AlignExportSettingsAdapter(false);
 +    Runnable outputAction = new Runnable()
      {
 -      settings = new AlignExportSettings(hasHiddenSeqs,
 -              viewport.hasHiddenColumns(), format);
 -    }
 -    // settings.isExportAnnotations();
 -
 -    if (viewport.hasHiddenColumns() && !settings.isExportHiddenColumns())
 -    {
 -      omitHidden = viewport.getViewAsString(false,
 -              settings.isExportHiddenSequences());
 -    }
 +      @Override
 +      public void run()
 +      {
 +        // todo defer this to inside formatSequences (or later)
 +        AlignmentExportData exportData = viewport
 +                .getAlignExportData(options);
 +        CutAndPasteTransfer cap = new CutAndPasteTransfer();
 +        cap.setForInput(null);
 +        try
 +        {
 +          FileFormatI format = fileFormat;
 +          cap.setText(new FormatAdapter(alignPanel, options)
 +                  .formatSequences(format, exportData.getAlignment(),
 +                          exportData.getOmitHidden(),
 +                          exportData.getStartEndPostions(),
 +                          viewport.getAlignment().getHiddenColumns()));
 +          Desktop.addInternalFrame(cap, MessageManager.formatMessage(
 +                  "label.alignment_output_command", new Object[]
 +                  { fileFormat.getName() }), 600, 500);
 +        } catch (OutOfMemoryError oom)
 +        {
 +          new OOMWarning("Outputting alignment as " + fileFormat.getName(),
 +                  oom);
 +          cap.dispose();
 +        }
 +      }
 +    };
  
 -    int[] alignmentStartEnd = new int[2];
 -    if (hasHiddenSeqs && settings.isExportHiddenSequences())
 +    /*
 +     * show dialog with export options if applicable; else just do it
 +     */
 +    if (AlignExportOptions.isNeeded(viewport, fileFormat))
      {
 -      alignmentToExport = hiddenSeqs.getFullAlignment();
 +      AlignExportOptions choices = new AlignExportOptions(
 +              alignPanel.getAlignViewport(), fileFormat, options);
 +      choices.setResponseAction(0, outputAction);
 +      choices.showDialog();
      }
      else
      {
 -      alignmentToExport = viewport.getAlignment();
 +      outputAction.run();
      }
 -    alignmentStartEnd = viewport.getAlignment().getHiddenColumns()
 -            .getVisibleStartAndEndIndex(alignmentToExport.getWidth());
 -    AlignmentExportData ed = new AlignmentExportData(alignmentToExport,
 -            omitHidden, alignmentStartEnd, settings);
 -    return ed;
    }
  
    /**
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Creates a PNG image of the alignment and writes it to the given file. If
 +   * the file is null, the user is prompted to choose a file.
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param f
     */
    @Override
    public void createPNG(File f)
    {
 -    alignPanel.makePNG(f);
 +    alignPanel.makeAlignmentImage(TYPE.PNG, f);
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Creates an EPS image of the alignment and writes it to the given file. If
 +   * the file is null, the user is prompted to choose a file.
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param f
     */
    @Override
    public void createEPS(File f)
    {
 -    alignPanel.makeEPS(f);
 +    alignPanel.makeAlignmentImage(TYPE.EPS, f);
    }
  
 +  /**
 +   * Creates an SVG image of the alignment and writes it to the given file. If
 +   * the file is null, the user is prompted to choose a file.
 +   * 
 +   * @param f
 +   */
    @Override
    public void createSVG(File f)
    {
 -    alignPanel.makeSVG(f);
 +    alignPanel.makeAlignmentImage(TYPE.SVG, f);
    }
  
    @Override
  
    @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);
    }
  
    /**
     *          DOCUMENT ME!
     */
    @Override
 -  protected void copy_actionPerformed(ActionEvent e)
 +  protected void copy_actionPerformed()
    {
      if (viewport.getSelectionGroup() == null)
      {
  
      Desktop.jalviewClipboard = new Object[] { seqs,
          viewport.getAlignment().getDataset(), hiddenColumns };
 -    statusBar.setText(MessageManager.formatMessage(
 +    setStatus(MessageManager.formatMessage(
              "label.copied_sequences_to_clipboard", new Object[]
              { Integer.valueOf(seqs.length).toString() }));
    }
     * 
     * @param e
     *          DOCUMENT ME!
+    * @throws InterruptedException
+    * @throws IOException
     */
    @Override
    protected void pasteNew_actionPerformed(ActionEvent e)
+           throws IOException, InterruptedException
    {
      paste(true);
    }
     * 
     * @param e
     *          DOCUMENT ME!
+    * @throws InterruptedException
+    * @throws IOException
     */
    @Override
    protected void pasteThis_actionPerformed(ActionEvent e)
+           throws IOException, InterruptedException
    {
      paste(false);
    }
     * 
     * @param newAlignment
     *          true to paste to a new alignment, otherwise add to this.
+    * @throws InterruptedException
+    * @throws IOException
     */
-   void paste(boolean newAlignment)
+   void paste(boolean newAlignment) throws IOException, InterruptedException
    {
      boolean externalPaste = true;
      try
                  && Desktop.jalviewClipboard[1] != alignment.getDataset();
          // importDs==true instructs us to copy over new dataset sequences from
          // an existing alignment
 -        Vector newDs = (importDs) ? new Vector() : null; // used to create
 +        Vector<SequenceI> newDs = (importDs) ? new Vector<>() : null; // used to
 +                                                                      // create
          // minimum dataset set
  
          for (int i = 0; i < sequences.length; i++)
        {
  
          // propagate alignment changed.
 -        viewport.getRanges().setEndSeq(alignment.getHeight());
 +        viewport.getRanges().setEndSeq(alignment.getHeight() - 1);
          if (annotationAdded)
          {
            // Duplicate sequence annotation in all views.
        System.out.println("Exception whilst pasting: " + ex);
        // could be anything being pasted in here
      }
    }
  
    @Override
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Action Cut (delete and copy) the selected region
     */
    @Override
 -  protected void cut_actionPerformed(ActionEvent e)
 +  protected void cut_actionPerformed()
    {
 -    copy_actionPerformed(null);
 -    delete_actionPerformed(null);
 +    copy_actionPerformed();
 +    delete_actionPerformed();
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Performs menu option to Delete the currently selected region
     */
    @Override
 -  protected void delete_actionPerformed(ActionEvent evt)
 +  protected void delete_actionPerformed()
    {
  
      SequenceGroup sg = viewport.getSelectionGroup();
        return;
      }
  
 +    Runnable okAction = new Runnable() 
 +    {
 +              @Override
 +              public void run() 
 +              {
 +                  SequenceI[] cut = sg.getSequences()
 +                          .toArray(new SequenceI[sg.getSize()]);
 +
 +                  addHistoryItem(new EditCommand(
 +                          MessageManager.getString("label.cut_sequences"), Action.CUT,
 +                          cut, sg.getStartRes(), sg.getEndRes() - sg.getStartRes() + 1,
 +                          viewport.getAlignment()));
 +
 +                  viewport.setSelectionGroup(null);
 +                  viewport.sendSelection();
 +                  viewport.getAlignment().deleteGroup(sg);
 +
 +                  viewport.firePropertyChange("alignment", null,
 +                          viewport.getAlignment().getSequences());
 +                  if (viewport.getAlignment().getHeight() < 1)
 +                  {
 +                    try
 +                    {
 +                      AlignFrame.this.setClosed(true);
 +                    } catch (Exception ex)
 +                    {
 +                    }
 +                  }
 +              }};
 +
      /*
 -     * If the cut affects all sequences, warn, remove highlighted columns
 +     * If the cut affects all sequences, prompt for confirmation
       */
 -    if (sg.getSize() == viewport.getAlignment().getHeight())
 -    {
 -      boolean isEntireAlignWidth = (((sg.getEndRes() - sg.getStartRes())
 -              + 1) == viewport.getAlignment().getWidth()) ? true : false;
 -      if (isEntireAlignWidth)
 -      {
 -        int confirm = JvOptionPane.showConfirmDialog(this,
 -                MessageManager.getString("warn.delete_all"), // $NON-NLS-1$
 -                MessageManager.getString("label.delete_all"), // $NON-NLS-1$
 -                JvOptionPane.OK_CANCEL_OPTION);
 -
 -        if (confirm == JvOptionPane.CANCEL_OPTION
 -                || confirm == JvOptionPane.CLOSED_OPTION)
 -        {
 -          return;
 -        }
 -      }
 -      viewport.getColumnSelection().removeElements(sg.getStartRes(),
 -              sg.getEndRes() + 1);
 -    }
 -    SequenceI[] cut = sg.getSequences()
 -            .toArray(new SequenceI[sg.getSize()]);
 -
 -    addHistoryItem(new EditCommand(
 -            MessageManager.getString("label.cut_sequences"), Action.CUT,
 -            cut, sg.getStartRes(), sg.getEndRes() - sg.getStartRes() + 1,
 -            viewport.getAlignment()));
 -
 -    viewport.setSelectionGroup(null);
 -    viewport.sendSelection();
 -    viewport.getAlignment().deleteGroup(sg);
 -
 -    viewport.firePropertyChange("alignment", null,
 -            viewport.getAlignment().getSequences());
 -    if (viewport.getAlignment().getHeight() < 1)
 -    {
 -      try
 -      {
 -        this.setClosed(true);
 -      } catch (Exception ex)
 -      {
 -      }
 -    }
 +    boolean wholeHeight = sg.getSize() == viewport.getAlignment().getHeight();
 +    boolean wholeWidth = (((sg.getEndRes() - sg.getStartRes())
 +            + 1) == viewport.getAlignment().getWidth()) ? true : false;
 +      if (wholeHeight && wholeWidth)
 +      {
 +          JvOptionPane dialog = JvOptionPane.newOptionDialog(Desktop.desktop);
 +              dialog.setResponseHandler(0, okAction); // 0 = OK_OPTION
 +          Object[] options = new Object[] { MessageManager.getString("action.ok"),
 +                  MessageManager.getString("action.cancel") };
 +              dialog.showDialog(MessageManager.getString("warn.delete_all"),
 +                  MessageManager.getString("label.delete_all"),
 +                  JvOptionPane.DEFAULT_OPTION, JvOptionPane.PLAIN_MESSAGE, null,
 +                  options, options[0]);
 +      } else 
 +      {
 +              okAction.run();
 +      }
    }
  
    /**
                  column, viewport.getAlignment());
        }
  
 -      statusBar.setText(MessageManager
 +      setStatus(MessageManager
                .formatMessage("label.removed_columns", new String[]
                { Integer.valueOf(trimRegion.getSize()).toString() }));
  
  
      addHistoryItem(removeGapCols);
  
 -    statusBar.setText(MessageManager
 +    setStatus(MessageManager
              .formatMessage("label.removed_empty_columns", new Object[]
              { Integer.valueOf(removeGapCols.getSize()).toString() }));
  
     * @param toggleSeqs
     * @param toggleCols
     */
 -  private void toggleHiddenRegions(boolean toggleSeqs, boolean toggleCols)
 +  protected void toggleHiddenRegions(boolean toggleSeqs, boolean toggleCols)
    {
  
      boolean hide = false;
    @Override
    public void alignmentProperties()
    {
 -    JEditorPane editPane = new JEditorPane("text/html", "");
 -    editPane.setEditable(false);
 +    JComponent pane;
      StringBuffer contents = new AlignmentProperties(viewport.getAlignment())
 +
              .formatAsHtml();
 -    editPane.setText(
 -            MessageManager.formatMessage("label.html_content", new Object[]
 -            { contents.toString() }));
 +    String content = MessageManager.formatMessage("label.html_content",
 +            new Object[]
 +            { contents.toString() });
 +    contents = null;
 +
 +    if (Platform.isJS())
 +    {
 +      JLabel textLabel = new JLabel();
 +      textLabel.setText(content);
 +      textLabel.setBackground(Color.WHITE);
 +      
 +      pane = new JPanel(new BorderLayout());
 +      ((JPanel) pane).setOpaque(true);
 +      pane.setBackground(Color.WHITE);
 +      ((JPanel) pane).add(textLabel, BorderLayout.NORTH);
 +    }
 +    else
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
 +    {
 +      JEditorPane editPane = new JEditorPane("text/html", "");
 +      editPane.setEditable(false);
 +      editPane.setText(content);
 +      pane = editPane;
 +    }
 +
      JInternalFrame frame = new JInternalFrame();
 -    frame.getContentPane().add(new JScrollPane(editPane));
 +
 +    frame.getContentPane().add(new JScrollPane(pane));
  
      Desktop.addInternalFrame(frame, MessageManager
              .formatMessage("label.alignment_properties", new Object[]
                {
                  overview.dispose();
                  alignPanel.setOverviewPanel(null);
 -              };
 +              }
              });
      if (getKeyListeners().length > 0)
      {
      alignPanel.paintAlignment(true, false);
    }
  
+   @Override
+   public void sortEValueMenuItem_actionPerformed(ActionEvent e)
+   {
+     SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
+     AlignmentSorter.sortByEValue(viewport.getAlignment());
+     addHistoryItem(new OrderCommand("Group Sort", oldOrder,
+             viewport.getAlignment()));
+     alignPanel.paintAlignment(true, false);
+   }
+   @Override
+   public void sortBitScoreMenuItem_actionPerformed(ActionEvent e)
+   {
+     SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
+     AlignmentSorter.sortByBitScore(viewport.getAlignment());
+     addHistoryItem(new OrderCommand("Group Sort", oldOrder,
+             viewport.getAlignment()));
+     alignPanel.paintAlignment(true, false);
+   }
+   
    /**
     * DOCUMENT ME!
     * 
      }
  
      if (viewport.getAlignment().getAlignmentAnnotation()
-             .hashCode() != _annotationScoreVectorHash)
+             .hashCode() == _annotationScoreVectorHash)
+     {
+       return;
+     }
+     sortByAnnotScore.removeAll();
+     Set<String> scoreSorts = new HashSet<>();
+     for (SequenceI sqa : viewport.getAlignment().getSequences())
      {
-       sortByAnnotScore.removeAll();
-       // almost certainly a quicker way to do this - but we keep it simple
-       Hashtable<String, String> scoreSorts = new Hashtable<>();
-       AlignmentAnnotation aann[];
-       for (SequenceI sqa : viewport.getAlignment().getSequences())
+       AlignmentAnnotation[] anns = sqa.getAnnotation();
+       for (int i = 0; anns != null && i < anns.length; i++)
        {
-         aann = sqa.getAnnotation();
-         for (int i = 0; aann != null && i < aann.length; i++)
+         AlignmentAnnotation aa = anns[i];
+         if (aa != null && aa.hasScore() && aa.sequenceRef != null)
          {
-           if (aann[i].hasScore() && aann[i].sequenceRef != null)
-           {
-             scoreSorts.put(aann[i].label, aann[i].label);
-           }
+           scoreSorts.add(aa.label);
          }
        }
-       Enumeration<String> labels = scoreSorts.keys();
-       while (labels.hasMoreElements())
-       {
-         addSortByAnnotScoreMenuItem(sortByAnnotScore,
-                 labels.nextElement());
-       }
-       sortByAnnotScore.setVisible(scoreSorts.size() > 0);
-       scoreSorts.clear();
-       _annotationScoreVectorHash = viewport.getAlignment()
-               .getAlignmentAnnotation().hashCode();
      }
+     for (String label : scoreSorts)
+     {
+       addSortByAnnotScoreMenuItem(sortByAnnotScore, label);
+     }
+     sortByAnnotScore.setVisible(!scoreSorts.isEmpty());
+     _annotationScoreVectorHash = viewport.getAlignment()
+             .getAlignmentAnnotation().hashCode();
    }
  
    /**
      chooser.setToolTipText(
              MessageManager.getString("label.load_tree_file"));
  
 -    int value = chooser.showOpenDialog(null);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    chooser.setResponseHandler(0,new Runnable()
      {
 -      String filePath = chooser.getSelectedFile().getPath();
 -      Cache.setProperty("LAST_DIRECTORY", filePath);
 -      NewickFile fin = null;
 -      try
 -      {
 -        fin = new NewickFile(filePath, DataSourceType.FILE);
 -        viewport.setCurrentTree(showNewickTree(fin, filePath).getTree());
 -      } catch (Exception ex)
 -      {
 -        JvOptionPane.showMessageDialog(Desktop.desktop, ex.getMessage(),
 -                MessageManager.getString("label.problem_reading_tree_file"),
 -                JvOptionPane.WARNING_MESSAGE);
 -        ex.printStackTrace();
 -      }
 -      if (fin != null && fin.hasWarningMessage())
 +      @Override
 +      public void run()
        {
 -        JvOptionPane.showMessageDialog(Desktop.desktop,
 -                fin.getWarningMessage(),
 -                MessageManager
 -                        .getString("label.possible_problem_with_tree_file"),
 -                JvOptionPane.WARNING_MESSAGE);
 +        String filePath = chooser.getSelectedFile().getPath();
 +        Cache.setProperty("LAST_DIRECTORY", filePath);
 +        NewickFile fin = null;
 +        try
 +        {
 +          fin = new NewickFile(new FileParse(chooser.getSelectedFile(),
 +                  DataSourceType.FILE));
 +          viewport.setCurrentTree(showNewickTree(fin, filePath).getTree());
 +        } catch (Exception ex)
 +        {
 +          JvOptionPane.showMessageDialog(Desktop.desktop, ex.getMessage(),
 +                  MessageManager
 +                          .getString("label.problem_reading_tree_file"),
 +                  JvOptionPane.WARNING_MESSAGE);
 +          ex.printStackTrace();
 +        }
 +        if (fin != null && fin.hasWarningMessage())
 +        {
 +          JvOptionPane.showMessageDialog(Desktop.desktop,
 +                  fin.getWarningMessage(),
 +                  MessageManager.getString(
 +                          "label.possible_problem_with_tree_file"),
 +                  JvOptionPane.WARNING_MESSAGE);
 +        }
        }
 -    }
 +    });
 +    chooser.showOpenDialog(this);
    }
  
    public TreePanel showNewickTree(NewickFile nf, String treeTitle)
              // No MSAWS used any more:
              // Vector msaws = null; // (Vector)
              // Discoverer.services.get("MsaWS");
 -            Vector secstrpr = (Vector) Discoverer.services
 +            Vector<ServiceHandle> secstrpr = Discoverer.services
                      .get("SecStrPred");
              if (secstrpr != null)
              {
                // Add any secondary structure prediction services
                for (int i = 0, j = secstrpr.size(); i < j; i++)
                {
 -                final ext.vamsas.ServiceHandle sh = (ext.vamsas.ServiceHandle) secstrpr
 +                final ext.vamsas.ServiceHandle sh = secstrpr
                          .get(i);
                  jalview.ws.WSMenuEntryProviderI impl = jalview.ws.jws1.Discoverer
                          .getServiceClient(sh);
                      if (jws2servs.hasServices())
                      {
                        jws2servs.attachWSMenuEntry(webService, me);
-                       for (Jws2Instance sv : jws2servs.getServices())
+                       for (ServiceWithParameters sv : jws2servs.getServices())
                        {
-                         if (sv.description.toLowerCase().contains("jpred"))
+                         if (sv.getName().toLowerCase().contains("jpred"))
                          {
                            for (JMenuItem jmi : legacyItems)
                            {
                    }
                  }
                  build_urlServiceMenu(me.webService);
+                 // TODO Mateusz - follow pattern for adding web service
+                 // JMenuItems for slivka-based services
+                 SlivkaWSDiscoverer slivkaDiscoverer = SlivkaWSDiscoverer.getInstance();
+                 if (slivkaDiscoverer.hasServices())
+                 {
+                 slivkaDiscoverer.attachWSMenuEntry(webService, me);
+                 } else {
+                   if (slivkaDiscoverer.isRunning())
+                   {
+                     {
+                       JMenuItem tm = new JMenuItem(
+                               "Still discovering Slivka Services");
+                       tm.setEnabled(false);
+                       webService.add(tm);
+                     }
+                   }
+                 }
+               
                  build_fetchdbmenu(webService);
                  for (JMenu item : wsmenu)
                  {
     * 
     * @param webService
     */
 -  private void build_urlServiceMenu(JMenu webService)
 +  protected void build_urlServiceMenu(JMenu webService)
    {
      // TODO: remove this code when 2.7 is released
      // DEBUG - alignmentView
     * Try to load a features file onto the alignment.
     * 
     * @param file
 -   *          contents or path to retrieve file
 +   *          contents or path to retrieve file or a File object
     * @param sourceType
     *          access mode of file (see jalview.io.AlignFile)
     * @return true if features file was parsed correctly.
     */
 -  public boolean parseFeaturesFile(String file, DataSourceType sourceType)
 +  public boolean parseFeaturesFile(Object file, DataSourceType sourceType)
    {
 +    // BH 2018
      return avc.parseFeaturesFile(file, sourceType,
              Cache.getDefault("RELAXEDSEQIDMATCHING", false));
  
      // Java's Transferable for native dnd
      evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
      Transferable t = evt.getTransferable();
 +
      final AlignFrame thisaf = this;
 -    final List<String> files = new ArrayList<>();
 +    final List<Object> files = new ArrayList<>();
      List<DataSourceType> protocols = new ArrayList<>();
  
      try
               * Object[] { String,SequenceI}
               */
              ArrayList<Object[]> filesmatched = new ArrayList<>();
 -            ArrayList<String> filesnotmatched = new ArrayList<>();
 +            ArrayList<Object> filesnotmatched = new ArrayList<>();
              for (int i = 0; i < files.size(); i++)
              {
 -              String file = files.get(i).toString();
 +              // BH 2018
 +              Object file = files.get(i);
 +              String fileName = file.toString();
                String pdbfn = "";
 -              DataSourceType protocol = FormatAdapter.checkProtocol(file);
 +              DataSourceType protocol = (file instanceof File
 +                      ? DataSourceType.FILE
 +                      : FormatAdapter.checkProtocol(fileName));
                if (protocol == DataSourceType.FILE)
                {
 -                File fl = new File(file);
 +                File fl;
 +                if (file instanceof File) {
 +                  fl = (File) file;
 +                  Platform.cacheFileData(fl);
 +                } else {
 +                  fl = new File(fileName);
 +                }
                  pdbfn = fl.getName();
                }
                else if (protocol == DataSourceType.URL)
                {
 -                URL url = new URL(file);
 +                URL url = new URL(fileName);
                  pdbfn = url.getFile();
                }
                if (pdbfn.length() > 0)
                  }
                  if (mtch != null)
                  {
 -                  FileFormatI type = null;
 +                  FileFormatI type;
                    try
                    {
                      type = new IdentifyFile().identify(file, protocol);
                    for (SequenceI toassoc : (SequenceI[]) fm[2])
                    {
                      PDBEntry pe = new AssociatePdbFileWithSeq()
 -                            .associatePdbWithSeq((String) fm[0],
 +                            .associatePdbWithSeq(fm[0].toString(),
                                      (DataSourceType) fm[1], toassoc, false,
                                      Desktop.instance);
                      if (pe != null)
                      {
                        System.err.println("Associated file : "
 -                              + ((String) fm[0]) + " with "
 +                              + (fm[0].toString()) + " with "
                                + toassoc.getDisplayId(true));
                        assocfiles++;
                      }
                   */
                  for (Object[] o : filesmatched)
                  {
 -                  filesnotmatched.add((String) o[0]);
 +                  filesnotmatched.add(o[0]);
                  }
                }
              }
                {
                  return;
                }
 -              for (String fn : filesnotmatched)
 +              for (Object fn : filesnotmatched)
                {
                  loadJalviewDataFile(fn, null, null, null);
                }
     * 
     * @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)
                changeColour(
                        new TCoffeeColourScheme(viewport.getAlignment()));
                isAnnotation = true;
 -              statusBar.setText(MessageManager.getString(
 +              setStatus(MessageManager.getString(
                        "label.successfully_pasted_tcoffee_scores_to_alignment"));
              }
              else
                      new FileParse(file, sourceType));
              sm.parse();
              // todo: i18n this message
 -            statusBar.setText(MessageManager.formatMessage(
 +            setStatus(MessageManager.formatMessage(
                      "label.successfully_loaded_matrix",
                      sm.getMatrixName()));
            }
        }
        if (isAnnotation)
        {
          alignPanel.adjustAnnotationHeight();
          viewport.updateSequenceIdColours();
          buildSortByAnnotationScoresMenu();
      if (e.isPopupTrigger())
      {
        String msg = MessageManager.getString("label.enter_view_name");
 -      String reply = JvOptionPane.showInternalInputDialog(this, msg, msg,
 -              JvOptionPane.QUESTION_MESSAGE);
 +      String ttl = tabbedPane.getTitleAt(tabbedPane.getSelectedIndex());
 +      String reply = JvOptionPane.showInputDialog(msg, ttl);
  
        if (reply != null)
        {
          trimrs.setSelected(trimrs.isSelected());
          Cache.setProperty(DBRefFetcher.TRIM_RETRIEVED_SEQUENCES,
                  Boolean.valueOf(trimrs.isSelected()).toString());
 -      };
 +      }
      });
      rfetch.add(trimrs);
      JMenuItem fetchr = new JMenuItem(
  
      });
      rfetch.add(fetchr);
 -    final AlignFrame me = this;
      new Thread(new Runnable()
      {
        @Override
        public void run()
        {
          final jalview.ws.SequenceFetcher sf = jalview.gui.SequenceFetcher
 -                .getSequenceFetcherSingleton(me);
 +                .getSequenceFetcherSingleton();
          javax.swing.SwingUtilities.invokeLater(new Runnable()
          {
            @Override
            public void run()
            {
 -            String[] dbclasses = sf.getOrderedSupportedSources();
 -            // sf.getDbInstances(jalview.ws.dbsources.DasSequenceSource.class);
 -            // jalview.util.QuickSort.sort(otherdb, otherdb);
 +            String[] dbclasses = sf.getNonAlignmentSources();
              List<DbSourceProxy> otherdb;
              JMenu dfetch = new JMenu();
              JMenu ifetch = new JMenu();
                {
                  continue;
                }
 -              // List<DbSourceProxy> dbs=otherdb;
 -              // otherdb=new ArrayList<DbSourceProxy>();
 -              // for (DbSourceProxy db:dbs)
 -              // {
 -              // if (!db.isA(DBRefSource.ALIGNMENTDB)
 -              // }
                if (mname == null)
                {
                  mname = "From " + dbclass;
      }
    }
  
+   /**
+    * Sets the status of the HMMER menu
+    */
+   public void updateHMMERStatus()
+   {
+     hmmerMenu.setEnabled(HmmerCommand.isHmmerAvailable());
+   }
    @Override
    protected void loadVcf_actionPerformed()
    {
      chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(MessageManager.getString("label.load_vcf_file"));
      chooser.setToolTipText(MessageManager.getString("label.load_vcf_file"));
 -
 -    int value = chooser.showOpenDialog(null);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    final AlignFrame us = this;
 +    chooser.setResponseHandler(0, new Runnable()
      {
 -      String choice = chooser.getSelectedFile().getPath();
 -      Cache.setProperty("LAST_DIRECTORY", choice);
 -      SequenceI[] seqs = viewport.getAlignment().getSequencesArray();
 -      new VCFLoader(choice).loadVCF(seqs, this);
 -    }
 +      @Override
 +      public void run()
 +      {
 +        String choice = chooser.getSelectedFile().getPath();
 +        Cache.setProperty("LAST_DIRECTORY", choice);
 +        SequenceI[] seqs = viewport.getAlignment().getSequencesArray();
 +        new VCFLoader(choice).loadVCF(seqs, us);
 +      }
 +    });
 +    chooser.showOpenDialog(null);
  
    }
  
@@@ -39,6 -39,7 +39,7 @@@ import jalview.datamodel.SearchResults
  import jalview.datamodel.SearchResultsI;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
+ import jalview.datamodel.features.FeatureMatcherSetI;
  import jalview.renderer.ResidueShader;
  import jalview.schemes.ColourSchemeI;
  import jalview.schemes.ColourSchemeProperty;
@@@ -256,14 -257,13 +257,13 @@@ public class AlignViewport extends Alig
  
      setFont(new Font(fontName, style, Integer.parseInt(fontSize)), true);
  
-     alignment
-             .setGapCharacter(Cache.getDefault("GAP_SYMBOL", "-").charAt(0));
+               alignment.setGapCharacter(Cache.getDefault("GAP_SYMBOL", "-").charAt(0));
  
      // We must set conservation and consensus before setting colour,
      // as Blosum and Clustal require this to be done
-     if (hconsensus == null && !isDataset)
+               if (hconsensus == null && !isDataset)
      {
-       if (!alignment.isNucleotide())
+                       if (!alignment.isNucleotide())
        {
          showConservation = Cache.getDefault("SHOW_CONSERVATION", true);
          showQuality = Cache.getDefault("SHOW_QUALITY", true);
        showSequenceLogo = Cache.getDefault("SHOW_CONSENSUS_LOGO", false);
        normaliseSequenceLogo = Cache.getDefault("NORMALISE_CONSENSUS_LOGO",
                false);
+       // for now, use consensus options for Information till it gets its own
+       setShowHMMSequenceLogo(showSequenceLogo);
+       setNormaliseHMMSequenceLogo(normaliseSequenceLogo);
+       setShowInformationHistogram(showConsensusHistogram);
        showGroupConsensus = Cache.getDefault("SHOW_GROUP_CONSENSUS", false);
        showConsensus = Cache.getDefault("SHOW_IDENTITY", true);
  
        showOccupancy = Cache.getDefault(Preferences.SHOW_OCCUPANCY, true);
      }
      initAutoAnnotation();
-     String colourProperty = alignment.isNucleotide()
+     // initInformation();
+               String colourProperty = alignment.isNucleotide()
              ? Preferences.DEFAULT_COLOUR_NUC
              : Preferences.DEFAULT_COLOUR_PROT;
      String schemeName = Cache.getProperty(colourProperty);
  
      if (residueShading != null)
      {
-       residueShading.setConsensus(hconsensus);
+                       residueShading.setConsensus(hconsensus);
      }
      setColourAppliesToAllGroups(true);
    }
+   
    boolean validCharWidth;
  
    /**
      /*
       * replace mappings on our alignment
       */
-     if (alignment != null && align != null)
+               if (alignment != null && align != null)
      {
        alignment.setCodonFrames(align.getCodonFrames());
      }
    }
  
    /**
-    * returns the visible column regions of the alignment
+    * Returns an iterator over the visible column regions of the alignment
     * 
     * @param selectedRegionOnly
     *          true to just return the contigs intersecting with the selected
      {
        end = alignment.getWidth();
      }
-     return (alignment.getHiddenColumns().getVisContigsIterator(start, end,
-             false));
+     return (alignment.getHiddenColumns().getVisContigsIterator(start,
+             end, false));
    }
  
    /**
    }
  
    public boolean followSelection = true;
+   
    /**
     * @return true if view selection should always follow the selections
     *         broadcast by other selection sources
      return StructureSelectionManager
              .getStructureSelectionManager(Desktop.instance);
    }
+   
    @Override
    public boolean isNormaliseSequenceLogo()
    {
      return normaliseSequenceLogo;
    }
  
-   public void setNormaliseSequenceLogo(boolean state)
+   @Override
+ public void setNormaliseSequenceLogo(boolean state)
    {
      normaliseSequenceLogo = state;
    }
  
    /**
     * 
     * @return true if alignment characters should be displayed
    {
      return validCharWidth;
    }
+   
    private Hashtable<String, AutoCalcSetting> calcIdParams = new Hashtable<>();
  
    public AutoCalcSetting getCalcIdSettingsFor(String calcId)
      {
        if (AlignmentUtils.isMappable(toAdd, getAlignment()))
        {
 -        if (openLinkedAlignment(toAdd, title))
 -        {
 -          return;
 -        }
 +        openLinkedAlignment(toAdd, title);
 +        return;
        }
      }
 +    addDataToAlignment(toAdd);
 +  }
  
 -    /*
 -     * No mappings, or offer declined - add sequences to this alignment
 -     */
 +  /**
 +   * adds sequences to this alignment
 +   * 
 +   * @param toAdd
 +   */
 +  void addDataToAlignment(AlignmentI toAdd)
 +  {
      // TODO: JAL-407 regardless of above - identical sequences (based on ID and
      // provenance) should share the same dataset sequence
  
        }
      }
  
 -    ranges.setEndSeq(getAlignment().getHeight());
 +    ranges.setEndSeq(getAlignment().getHeight() - 1); // BH 2019.04.18
      firePropertyChange("alignment", null, getAlignment().getSequences());
    }
  
     * @param al
     * @param title
     */
 -  protected boolean openLinkedAlignment(AlignmentI al, String title)
 +  protected void openLinkedAlignment(AlignmentI al, String title)
    {
      String[] options = new String[] { MessageManager.getString("action.no"),
          MessageManager.getString("label.split_window"),
          MessageManager.getString("label.new_window"), };
      final String question = JvSwingUtils.wrapTooltip(true,
              MessageManager.getString("label.open_split_window?"));
 -    int response = JvOptionPane.showOptionDialog(Desktop.desktop, question,
 +    final AlignViewport us = this;
 +    
 +    /*
 +     * options No, Split Window, New Window correspond to
 +     * dialog responses 0, 1, 2 (even though JOptionPane shows them
 +     * in reverse order)
 +     */
 +    JvOptionPane dialog = JvOptionPane.newOptionDialog(Desktop.desktop)
 +            .setResponseHandler(0, new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                  addDataToAlignment(al);
 +              }
 +            }).setResponseHandler(1, new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                us.openLinkedAlignmentAs(al, title, true);
 +              }
 +            }).setResponseHandler(2, new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                us.openLinkedAlignmentAs(al, title, false);
 +              }
 +            });
 +      dialog.showDialog(question,
              MessageManager.getString("label.open_split_window"),
              JvOptionPane.DEFAULT_OPTION, JvOptionPane.PLAIN_MESSAGE, null,
              options, options[0]);
 +  }
  
 -    if (response != 1 && response != 2)
 +  protected void openLinkedAlignmentAs(AlignmentI al, String title,
 +          boolean newWindowOrSplitPane)
      {
 -      return false;
 -    }
 -    final boolean openSplitPane = (response == 1);
 -    final boolean openInNewWindow = (response == 2);
 -
      /*
       * Identify protein and dna alignments. Make a copy of this one if opening
       * in a new split pane.
       */
 -    AlignmentI thisAlignment = openSplitPane ? new Alignment(getAlignment())
 +    AlignmentI thisAlignment = newWindowOrSplitPane
 +            ? new Alignment(getAlignment())
              : getAlignment();
      AlignmentI protein = al.isNucleotide() ? thisAlignment : al;
      final AlignmentI cdna = al.isNucleotide() ? al : thisAlignment;
      // alignFrame.setFileName(file, format);
      // }
  
 -    if (openInNewWindow)
 +    if (!newWindowOrSplitPane)
      {
        Desktop.addInternalFrame(newAlignFrame, title,
                AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
      {
      }
  
 -    if (openSplitPane)
 +    if (newWindowOrSplitPane)
      {
        al.alignAs(thisAlignment);
        protein = openSplitFrame(newAlignFrame, thisAlignment);
      }
 -
 -    return true;
    }
  
    /**
      {
        FeatureColourI preferredColour = featureSettings
                .getFeatureColour(type);
+       FeatureMatcherSetI preferredFilters = featureSettings
+               .getFeatureFilters(type);
        FeatureColourI origColour = fr.getFeatureStyle(type);
        if (!mergeOnly || (!origRenderOrder.contains(type)
                || origColour == null
          {
            fr.setColour(type, preferredColour);
          }
+         if (preferredFilters != null
+                 && (!mergeOnly || fr.getFeatureFilter(type) != null))
+         {
+           fr.setFeatureFilter(type, preferredFilters);
+         }
          if (featureSettings.isFeatureDisplayed(type))
          {
            displayed.setVisible(type);
@@@ -34,6 -34,7 +34,7 @@@ import jalview.io.FormatAdapter
  import jalview.util.Comparison;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
+ import jalview.workers.InformationThread;
  
  import java.awt.Color;
  import java.awt.Cursor;
@@@ -135,7 -136,6 +136,7 @@@ public class AnnotationLabels extends J
     */
    public AnnotationLabels(AlignmentPanel ap)
    {
 +        
      this.ap = ap;
      av = ap.av;
      ToolTipManager.sharedInstance().registerComponent(this);
      AlignmentAnnotation[] aa = ap.av.getAlignment()
              .getAlignmentAnnotation();
  
 -    boolean fullRepaint = false;
 -    if (evt.getActionCommand().equals(ADDNEW))
 +    String action = evt.getActionCommand();
 +    if (ADDNEW.equals(action))
      {
 +      /*
 +       * non-returning dialog
 +       */
        AlignmentAnnotation newAnnotation = new AlignmentAnnotation(null,
                null, new Annotation[ap.av.getAlignment().getWidth()]);
 -
 -      if (!editLabelDescription(newAnnotation))
 -      {
 -        return;
 -      }
 -
 -      ap.av.getAlignment().addAnnotation(newAnnotation);
 -      ap.av.getAlignment().setAnnotationIndex(newAnnotation, 0);
 -      fullRepaint = true;
 +      editLabelDescription(newAnnotation, true);
      }
 -    else if (evt.getActionCommand().equals(EDITNAME))
 +    else if (EDITNAME.equals(action))
      {
 -      String name = aa[selectedRow].label;
 -      editLabelDescription(aa[selectedRow]);
 -      if (!name.equalsIgnoreCase(aa[selectedRow].label))
 -      {
 -        fullRepaint = true;
 -      }
 +      /*
 +       * non-returning dialog
 +       */
 +      editLabelDescription(aa[selectedRow], false);
      }
 -    else if (evt.getActionCommand().equals(HIDE))
 +    else if (HIDE.equals(action))
      {
        aa[selectedRow].visible = false;
      }
 -    else if (evt.getActionCommand().equals(DELETE))
 +    else if (DELETE.equals(action))
      {
        ap.av.getAlignment().deleteAnnotation(aa[selectedRow]);
        ap.av.getCalcManager().removeWorkerForAnnotation(aa[selectedRow]);
 -      fullRepaint = true;
      }
 -    else if (evt.getActionCommand().equals(SHOWALL))
 +    else if (SHOWALL.equals(action))
      {
        for (int i = 0; i < aa.length; i++)
        {
            aa[i].visible = true;
          }
        }
 -      fullRepaint = true;
      }
 -    else if (evt.getActionCommand().equals(OUTPUT_TEXT))
 +    else if (OUTPUT_TEXT.equals(action))
      {
        new AnnotationExporter(ap).exportAnnotation(aa[selectedRow]);
      }
 -    else if (evt.getActionCommand().equals(COPYCONS_SEQ))
 +    else if (COPYCONS_SEQ.equals(action))
      {
        SequenceI cons = null;
        if (aa[selectedRow].groupRef != null)
        {
          copy_annotseqtoclipboard(cons);
        }
 -
      }
 -    else if (evt.getActionCommand().equals(TOGGLE_LABELSCALE))
 +    else if (TOGGLE_LABELSCALE.equals(action))
      {
        aa[selectedRow].scaleColLabel = !aa[selectedRow].scaleColLabel;
      }
  
 -    ap.refresh(fullRepaint);
 -
 +    ap.refresh(true);
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Shows a dialog where the annotation name and description may be edited. If
 +   * parameter addNew is true, then on confirmation, a new AlignmentAnnotation
 +   * is added, else an existing annotation is updated.
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param annotation
 +   * @param addNew
     */
 -  boolean editLabelDescription(AlignmentAnnotation annotation)
 +  void editLabelDescription(AlignmentAnnotation annotation, boolean addNew)
    {
 -    // TODO i18n
 +    String name = MessageManager.getString("label.annotation_name");
 +    String description = MessageManager
 +            .getString("label.annotation_description");
 +    String title = MessageManager
 +            .getString("label.edit_annotation_name_description");
      EditNameDialog dialog = new EditNameDialog(annotation.label,
 -            annotation.description, "       Annotation Name ",
 -            "Annotation Description ", "Edit Annotation Name/Description",
 -            ap.alignFrame);
 -
 -    if (!dialog.accept)
 -    {
 -      return false;
 -    }
 -
 -    annotation.label = dialog.getName();
 -
 -    String text = dialog.getDescription();
 -    if (text != null && text.length() == 0)
 -    {
 -      text = null;
 -    }
 -    annotation.description = text;
 -
 -    return true;
 +            annotation.description, name, description);
 +
 +    dialog.showDialog(ap.alignFrame, title,
 +            new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                annotation.label = dialog.getName();
 +                String text = dialog.getDescription();
 +                if (text != null && text.length() == 0)
 +                {
 +                  text = null;
 +                }
 +                annotation.description = text;
 +                if (addNew)
 +                {
 +                  ap.av.getAlignment().addAnnotation(annotation);
 +                  ap.av.getAlignment().setAnnotationIndex(annotation, 0);
 +                }
 +                ap.refresh(true);
 +              }
 +            });
    }
  
    @Override
        pop.show(this, evt.getX(), evt.getY());
        return;
      }
+     final AlignmentAnnotation ann = aa[selectedRow];
+     final boolean isSequenceAnnotation = ann.sequenceRef != null;
      item = new JMenuItem(EDITNAME);
      item.addActionListener(this);
      pop.add(item);
      if (selectedRow < aa.length)
      {
        final String label = aa[selectedRow].label;
-       if (!aa[selectedRow].autoCalculated)
+       if (!(aa[selectedRow].autoCalculated)
+               && !(InformationThread.HMM_CALC_ID.equals(ann.getCalcId())))
        {
          if (aa[selectedRow].graph == AlignmentAnnotation.NO_GRAPH)
          {
            pop.addSeparator();
            // av and sequencegroup need to implement same interface for
            item = new JCheckBoxMenuItem(TOGGLE_LABELSCALE,
-                   aa[selectedRow].scaleColLabel);
+                         aa[selectedRow].scaleColLabel);
            item.addActionListener(this);
            pop.add(item);
          }
          consclipbrd.addActionListener(this);
          pop.add(consclipbrd);
        }
+       else if (InformationThread.HMM_CALC_ID.equals(ann.getCalcId()))
+       {
+         addHmmerMenu(pop, ann);
+       }
      }
      pop.show(this, evt.getX(), evt.getY());
    }
  
    /**
+    * Adds context menu options for (alignment or group) Hmmer annotation
+    * 
+    * @param pop
+    * @param ann
+    */
+   protected void addHmmerMenu(JPopupMenu pop, final AlignmentAnnotation ann)
+   {
+     final boolean isGroupAnnotation = ann.groupRef != null;
+     pop.addSeparator();
+     final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
+             MessageManager.getString(
+                     "label.ignore_below_background_frequency"),
+             isGroupAnnotation
+                     ? ann.groupRef
+                             .isIgnoreBelowBackground()
+                     : ap.av.isIgnoreBelowBackground());
+     cbmi.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         if (isGroupAnnotation)
+         {
+           if (!ann.groupRef.isUseInfoLetterHeight())
+           {
+             ann.groupRef.setIgnoreBelowBackground(cbmi.getState());
+             // todo and recompute group annotation
+           }
+         }
+         else if (!ap.av.isInfoLetterHeight())
+         {
+           ap.av.setIgnoreBelowBackground(cbmi.getState(), ap);
+           // todo and recompute annotation
+         }
+         ap.alignmentChanged(); // todo not like this
+       }
+     });
+     pop.add(cbmi);
+     final JCheckBoxMenuItem letterHeight = new JCheckBoxMenuItem(
+             MessageManager.getString("label.use_info_for_height"),
+             isGroupAnnotation ? ann.groupRef.isUseInfoLetterHeight()
+                     : ap.av.isInfoLetterHeight());
+     letterHeight.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         if (isGroupAnnotation)
+         {
+           ann.groupRef.setInfoLetterHeight((letterHeight.getState()));
+           ann.groupRef.setIgnoreBelowBackground(true);
+           // todo and recompute group annotation
+         }
+         else
+         {
+           ap.av.setInfoLetterHeight(letterHeight.getState(), ap);
+           ap.av.setIgnoreBelowBackground(true, ap);
+           // todo and recompute annotation
+         }
+         ap.alignmentChanged();
+       }
+     });
+     pop.add(letterHeight);
+     if (isGroupAnnotation)
+     {
+       final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
+               MessageManager.getString("label.show_group_histogram"),
+               ann.groupRef.isShowInformationHistogram());
+       chist.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           ann.groupRef.setShowInformationHistogram(chist.getState());
+           ap.repaint();
+         }
+       });
+       pop.add(chist);
+       final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
+               MessageManager.getString("label.show_group_logo"),
+               ann.groupRef.isShowHMMSequenceLogo());
+       cprofl.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           ann.groupRef.setShowHMMSequenceLogo(cprofl.getState());
+           ap.repaint();
+         }
+       });
+       pop.add(cprofl);
+       final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
+               MessageManager.getString("label.normalise_group_logo"),
+               ann.groupRef.isNormaliseHMMSequenceLogo());
+       cproflnorm.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           ann.groupRef
+                   .setNormaliseHMMSequenceLogo(cproflnorm.getState());
+           // automatically enable logo display if we're clicked
+           ann.groupRef.setShowHMMSequenceLogo(true);
+           ap.repaint();
+         }
+       });
+       pop.add(cproflnorm);
+     }
+     else
+     {
+       final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
+               MessageManager.getString("label.show_histogram"),
+               av.isShowInformationHistogram());
+       chist.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           av.setShowInformationHistogram(chist.getState());
+           ap.repaint();
+         }
+       });
+       pop.add(chist);
+       final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
+               MessageManager.getString("label.show_logo"),
+               av.isShowHMMSequenceLogo());
+       cprof.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           av.setShowHMMSequenceLogo(cprof.getState());
+           ap.repaint();
+         }
+       });
+       pop.add(cprof);
+       final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
+               MessageManager.getString("label.normalise_logo"),
+               av.isNormaliseHMMSequenceLogo());
+       cprofnorm.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           av.setShowHMMSequenceLogo(true);
+           av.setNormaliseHMMSequenceLogo(cprofnorm.getState());
+           ap.repaint();
+         }
+       });
+       pop.add(cprofnorm);
+     }
+   }
+   /**
     * A helper method that adds menu options for calculation and visualisation of
     * group and/or alignment consensus annotation to a popup menu. This is
     * designed to be reusable for either unwrapped mode (popup menu is shown on
    protected void showOrHideAdjuster(MouseEvent evt)
    {
      boolean was = resizePanel;
 -    resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
 +    resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT
 +            && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
  
      if (resizePanel != was)
      {
 -      setCursor(Cursor.getPredefinedCursor(
 -              resizePanel ? Cursor.S_RESIZE_CURSOR
 +      setCursor(Cursor
 +              .getPredefinedCursor(resizePanel ? Cursor.S_RESIZE_CURSOR
                        : Cursor.DEFAULT_CURSOR));
        repaint();
      }
              PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
              ap.av.sendSelection();
            }
          }
        }
        return;
    @Override
    public void paintComponent(Graphics g)
    {
      int width = getWidth();
      if (width == 0)
      {
      }
  
      drawComponent(g2, true, width);
    }
  
    /**
@@@ -23,12 -23,12 +23,12 @@@ package jalview.gui
  import jalview.analysis.TreeBuilder;
  import jalview.analysis.scoremodels.ScoreModels;
  import jalview.analysis.scoremodels.SimilarityParams;
+ import jalview.api.AlignViewportI;
  import jalview.api.analysis.ScoreModelI;
  import jalview.api.analysis.SimilarityParamsI;
  import jalview.bin.Cache;
  import jalview.datamodel.SequenceGroup;
  import jalview.util.MessageManager;
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Component;
@@@ -156,15 -156,12 +156,15 @@@ public class CalculationChooser extend
      pca = new JRadioButton(
              MessageManager.getString("label.principal_component_analysis"));
      pca.setOpaque(false);
 +    
      neighbourJoining = new JRadioButton(
              MessageManager.getString("label.tree_calc_nj"));
      neighbourJoining.setSelected(true);
 +    neighbourJoining.setOpaque(false);
 +
      averageDistance = new JRadioButton(
              MessageManager.getString("label.tree_calc_av"));
 -    neighbourJoining.setOpaque(false);
 +    averageDistance.setOpaque(false);
  
      JPanel calcChoicePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
      calcChoicePanel.setOpaque(false);
        };
      });
  
 +    validateCalcTypes();
      frame.setLayer(JLayeredPane.PALETTE_LAYER);
    }
  
       * gui validation shouldn't allow insufficient sequences here, but leave
       * this check in in case this method gets exposed programmatically in future
       */
-     AlignViewport viewport = af.getViewport();
+     AlignViewportI viewport = af.getViewport();
      SequenceGroup sg = viewport.getSelectionGroup();
      if (sg != null && sg.getSize() < MIN_TREE_SELECTION)
      {
     */
    protected void openPcaPanel(String modelName, SimilarityParamsI params)
    {
-     AlignViewport viewport = af.getViewport();
+     AlignViewportI viewport = af.getViewport();
  
      /*
       * gui validation shouldn't allow insufficient sequences here, but leave
   */
  package jalview.gui;
  
 -import jalview.api.AlignViewportI;
 -import jalview.api.AlignmentViewPanel;
 -import jalview.bin.Cache;
 -import jalview.bin.Jalview;
 -import jalview.io.BackupFiles;
 -import jalview.io.DataSourceType;
 -import jalview.io.FileFormat;
 -import jalview.io.FileFormatException;
 -import jalview.io.FileFormatI;
 -import jalview.io.FileFormats;
 -import jalview.io.FileLoader;
 -import jalview.io.FormatAdapter;
 -import jalview.io.IdentifyFile;
 -import jalview.io.JalviewFileChooser;
 -import jalview.io.JalviewFileView;
 -import jalview.jbgui.GSplitFrame;
 -import jalview.jbgui.GStructureViewer;
 -import jalview.project.Jalview2XML;
 -import jalview.structure.StructureSelectionManager;
 -import jalview.urls.IdOrgSettings;
 -import jalview.util.ImageMaker;
 -import jalview.util.MessageManager;
 -import jalview.util.Platform;
 -import jalview.util.UrlConstants;
 -import jalview.viewmodel.AlignmentViewport;
 -import jalview.ws.params.ParamManager;
 -import jalview.ws.utils.UrlDownloadClient;
 -
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Dimension;
@@@ -58,6 -86,7 +58,6 @@@ import java.util.HashMap
  import java.util.Hashtable;
  import java.util.List;
  import java.util.ListIterator;
 -import java.util.StringTokenizer;
  import java.util.Vector;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
@@@ -82,7 -111,6 +82,7 @@@ import javax.swing.JMenuItem
  import javax.swing.JPanel;
  import javax.swing.JPopupMenu;
  import javax.swing.JProgressBar;
 +import javax.swing.JTextField;
  import javax.swing.KeyStroke;
  import javax.swing.SwingUtilities;
  import javax.swing.event.HyperlinkEvent;
@@@ -92,37 -120,6 +92,37 @@@ import javax.swing.event.InternalFrameE
  
  import org.stackoverflowusers.file.WindowsShortcut;
  
 +import jalview.api.AlignViewportI;
 +import jalview.api.AlignmentViewPanel;
 +import jalview.bin.Cache;
 +import jalview.bin.Jalview;
 +import jalview.gui.ImageExporter.ImageWriterI;
 +import jalview.io.BackupFiles;
 +import jalview.io.DataSourceType;
 +import jalview.io.FileFormat;
 +import jalview.io.FileFormatException;
 +import jalview.io.FileFormatI;
 +import jalview.io.FileFormats;
 +import jalview.io.FileLoader;
 +import jalview.io.FormatAdapter;
 +import jalview.io.IdentifyFile;
 +import jalview.io.JalviewFileChooser;
 +import jalview.io.JalviewFileView;
 +import jalview.jbgui.GSplitFrame;
 +import jalview.jbgui.GStructureViewer;
 +import jalview.project.Jalview2XML;
 +import jalview.structure.StructureSelectionManager;
 +import jalview.urls.IdOrgSettings;
 +import jalview.util.BrowserLauncher;
 +import jalview.util.ImageMaker.TYPE;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
 +import jalview.util.ShortcutKeyMaskExWrapper;
 +import jalview.util.UrlConstants;
 +import jalview.viewmodel.AlignmentViewport;
 +import jalview.ws.params.ParamManager;
 +import jalview.ws.utils.UrlDownloadClient;
 +
  /**
   * Jalview Desktop
   * 
@@@ -134,15 -131,6 +134,15 @@@ public class Desktop extends jalview.jb
          implements DropTargetListener, ClipboardOwner, IProgressIndicator,
          jalview.api.StructureSelectionManagerProvider
  {
 +  private static final String CITATION = "<br><br>Development managed by The Barton Group, University of Dundee, Scotland, UK.<br>"
 +          + "<br><br>For help, see the FAQ at <a href=\"http://www.jalview.org/faq\">www.jalview.org/faq</a> and/or join the jalview-discuss@jalview.org mailing list"
 +          + "<br><br>If  you use Jalview, please cite:"
 +          + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
 +          + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
 +          + "<br>Bioinformatics doi: 10.1093/bioinformatics/btp033";
 +
 +  private static final String DEFAULT_AUTHORS = "The Jalview Authors (See AUTHORS file for current list)";
 +
    private static int DEFAULT_MIN_WIDTH = 300;
  
    private static int DEFAULT_MIN_HEIGHT = 250;
  
    public static MyDesktopPane desktop;
  
 +  public static MyDesktopPane getDesktop()
 +  {
 +    // BH 2018 could use currentThread() here as a reference to a
 +    // Hashtable<Thread, MyDesktopPane> in JavaScript
 +    return desktop;
 +  }
 +
    static int openFrameCount = 0;
  
    static final int xOffset = 30;
     */
    public Desktop()
    {
 +    super();
      /**
       * A note to implementors. It is ESSENTIAL that any activities that might
       * block are spawned off as threads rather than waited for during this
      instance = this;
  
      doConfigureStructurePrefs();
 -    setTitle("Jalview " + jalview.bin.Cache.getProperty("VERSION"));
 +    setTitle("Jalview " + Cache.getProperty("VERSION"));
      /*
      if (!Platform.isAMac())
      {
      try
      {
        APQHandlers.setAPQHandlers(this);
 -    } catch (Exception e)
 -    {
 -      System.out.println("Cannot set APQHandlers");
 -      // e.printStackTrace();
      } catch (Throwable t)
      {
 -      System.out.println("Cannot set APQHandlers");
 +      System.out.println("Error setting APQHandlers: " + t.toString());
        // t.printStackTrace();
      }
  
 -
      addWindowListener(new WindowAdapter()
      {
  
        }
      });
  
 -    boolean selmemusage = jalview.bin.Cache.getDefault("SHOW_MEMUSAGE",
 +    boolean selmemusage = Cache.getDefault("SHOW_MEMUSAGE",
              false);
  
 -    boolean showjconsole = jalview.bin.Cache.getDefault("SHOW_JAVA_CONSOLE",
 +    boolean showjconsole = Cache.getDefault("SHOW_JAVA_CONSOLE",
              false);
      desktop = new MyDesktopPane(selmemusage);
 +
      showMemusage.setSelected(selmemusage);
      desktop.setBackground(Color.white);
  
      // JScrollPane sp = new JScrollPane();
      // sp.getViewport().setView(desktop);
      // getContentPane().add(sp, BorderLayout.CENTER);
 +
 +    // BH 2018 - just an experiment to try unclipped JInternalFrames.
 +    if (Platform.isJS())
 +    {
 +      getRootPane().putClientProperty("swingjs.overflow.hidden", "false");
 +    }
 +
      getContentPane().add(desktop, BorderLayout.CENTER);
      desktop.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
  
      // This line prevents Windows Look&Feel resizing all new windows to maximum
      // if previous window was maximised
      desktop.setDesktopManager(new MyDesktopManager(
 -            (Platform.isWindows() ? new DefaultDesktopManager()
 -                    : Platform.isAMac()
 +            (Platform.isWindowsAndNotJS() ? new DefaultDesktopManager()
 +                    : Platform.isAMacAndNotJS()
                              ? new AquaInternalFrameManager(
                                      desktop.getDesktopManager())
                              : desktop.getDesktopManager())));
      else
      {
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 -      setBounds((screenSize.width - 900) / 2, (screenSize.height - 650) / 2,
 -              900, 650);
 +      int xPos = Math.max(5, (screenSize.width - 900) / 2);
 +      int yPos = Math.max(5, (screenSize.height - 650) / 2);
 +      setBounds(xPos, yPos, 900, 650);
      }
 -    jconsole = new Console(this, showjconsole);
 -    // add essential build information
 -    jconsole.setHeader(jalview.bin.Cache.getVersionDetailsForConsole());
  
 -    showConsole(showjconsole);
 +    if (!Platform.isJS())
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
 +    {
 +      jconsole = new Console(this, showjconsole);
 +      jconsole.setHeader(Cache.getVersionDetailsForConsole());
 +      showConsole(showjconsole);
 +
 +      showNews.setVisible(false);
  
 -    showNews.setVisible(false);
 +      experimentalFeatures.setSelected(showExperimental());
  
 -    experimentalFeatures.setSelected(showExperimental());
 +      getIdentifiersOrgData();
  
 -    getIdentifiersOrgData();
 +      checkURLLinks();
  
 -    checkURLLinks();
 +      // Spawn a thread that shows the splashscreen
 +
 +      SwingUtilities.invokeLater(new Runnable()
 +      {
 +        @Override
 +        public void run()
 +        {
 +          new SplashScreen(true);
 +        }
 +      });
 +
 +      // Thread off a new instance of the file chooser - this reduces the time
 +      // it
 +      // takes to open it later on.
 +      new Thread(new Runnable()
 +      {
 +        @Override
 +        public void run()
 +        {
 +          Cache.log.debug("Filechooser init thread started.");
 +          String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
 +          JalviewFileChooser.forRead(Cache.getProperty("LAST_DIRECTORY"),
 +                  fileFormat);
 +          Cache.log.debug("Filechooser init thread finished.");
 +        }
 +      }).start();
 +      // Add the service change listener
 +      changeSupport.addJalviewPropertyChangeListener("services",
 +              new PropertyChangeListener()
 +              {
 +
 +                @Override
 +                public void propertyChange(PropertyChangeEvent evt)
 +                {
 +                  Cache.log.debug("Firing service changed event for "
 +                          + evt.getNewValue());
 +                  JalviewServicesChanged(evt);
 +                }
 +              });
 +    }
 +
 +    this.setDropTarget(new java.awt.dnd.DropTarget(desktop, this));
  
      this.addWindowListener(new WindowAdapter()
      {
      });
      desktop.addMouseListener(ma);
  
 -    this.setDropTarget(new java.awt.dnd.DropTarget(desktop, this));
 -    // Spawn a thread that shows the splashscreen
 -    SwingUtilities.invokeLater(new Runnable()
 -    {
 -      @Override
 -      public void run()
 -      {
 -        new SplashScreen();
 -      }
 -    });
 -
 -    // Thread off a new instance of the file chooser - this reduces the time it
 -    // takes to open it later on.
 -    new Thread(new Runnable()
 -    {
 -      @Override
 -      public void run()
 -      {
 -        Cache.log.debug("Filechooser init thread started.");
 -        String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
 -        JalviewFileChooser.forRead(Cache.getProperty("LAST_DIRECTORY"),
 -                fileFormat);
 -        Cache.log.debug("Filechooser init thread finished.");
 -      }
 -    }).start();
 -    // Add the service change listener
 -    changeSupport.addJalviewPropertyChangeListener("services",
 -            new PropertyChangeListener()
 -            {
 -
 -              @Override
 -              public void propertyChange(PropertyChangeEvent evt)
 -              {
 -                Cache.log.debug("Firing service changed event for "
 -                        + evt.getNewValue());
 -                JalviewServicesChanged(evt);
 -              }
 -
 -            });
    }
  
    /**
      // configure services
      StructureSelectionManager ssm = StructureSelectionManager
              .getStructureSelectionManager(this);
 -    if (jalview.bin.Cache.getDefault(Preferences.ADD_SS_ANN, true))
 +    if (Cache.getDefault(Preferences.ADD_SS_ANN, true))
      {
 -      ssm.setAddTempFacAnnot(jalview.bin.Cache
 +      ssm.setAddTempFacAnnot(Cache
                .getDefault(Preferences.ADD_TEMPFACT_ANN, true));
 -      ssm.setProcessSecondaryStructure(jalview.bin.Cache
 +      ssm.setProcessSecondaryStructure(Cache
                .getDefault(Preferences.STRUCT_FROM_PDB, true));
        ssm.setSecStructServices(
 -              jalview.bin.Cache.getDefault(Preferences.USE_RNAVIEW, true));
 +              Cache.getDefault(Preferences.USE_RNAVIEW, true));
      }
      else
      {
        public void run()
        {
          Cache.log.debug("Starting news thread.");
 -
          jvnews = new BlogReader(me);
          showNews.setVisible(true);
          Cache.log.debug("Completed news thread.");
        public void run()
        {
          Cache.log.debug("Downloading data from identifiers.org");
 -        UrlDownloadClient client = new UrlDownloadClient();
          try
          {
 -          client.download(IdOrgSettings.getUrl(),
 +          UrlDownloadClient.download(IdOrgSettings.getUrl(),
                    IdOrgSettings.getDownloadLocation());
          } catch (IOException e)
          {
          }
        }
      }).start();
 -    ;
 +    
    }
  
    @Override
  
    void showNews(boolean visible)
    {
 +    Cache.log.debug((visible ? "Showing" : "Hiding") + " news.");
 +    showNews.setSelected(visible);
 +    if (visible && !jvnews.isVisible())
      {
 -      Cache.log.debug((visible ? "Showing" : "Hiding") + " news.");
 -      showNews.setSelected(visible);
 -      if (visible && !jvnews.isVisible())
 +      new Thread(new Runnable()
        {
 -        new Thread(new Runnable()
 +        @Override
 +        public void run()
          {
 -          @Override
 -          public void run()
 -          {
 -            long now = System.currentTimeMillis();
 -            Desktop.instance.setProgressBar(
 -                    MessageManager.getString("status.refreshing_news"),
 -                    now);
 -            jvnews.refreshNews();
 -            Desktop.instance.setProgressBar(null, now);
 -            jvnews.showNews();
 -          }
 -        }).start();
 -      }
 +          long now = System.currentTimeMillis();
 +          Desktop.instance.setProgressBar(
 +                  MessageManager.getString("status.refreshing_news"), now);
 +          jvnews.refreshNews();
 +          Desktop.instance.setProgressBar(null, now);
 +          jvnews.showNews();
 +        }
 +      }).start();
      }
    }
  
    {
      // TODO: lock aspect ratio for scaling desktop Bug #0058199
      Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 -    String x = jalview.bin.Cache.getProperty(windowName + "SCREEN_X");
 -    String y = jalview.bin.Cache.getProperty(windowName + "SCREEN_Y");
 -    String width = jalview.bin.Cache
 +    String x = Cache.getProperty(windowName + "SCREEN_X");
 +    String y = Cache.getProperty(windowName + "SCREEN_Y");
 +    String width = Cache
              .getProperty(windowName + "SCREEN_WIDTH");
 -    String height = jalview.bin.Cache
 +    String height = Cache
              .getProperty(windowName + "SCREEN_HEIGHT");
      if ((x != null) && (y != null) && (width != null) && (height != null))
      {
        int ix = Integer.parseInt(x), iy = Integer.parseInt(y),
                iw = Integer.parseInt(width), ih = Integer.parseInt(height);
 -      if (jalview.bin.Cache.getProperty("SCREENGEOMETRY_WIDTH") != null)
 +      if (Cache.getProperty("SCREENGEOMETRY_WIDTH") != null)
        {
          // attempt #1 - try to cope with change in screen geometry - this
          // version doesn't preserve original jv aspect ratio.
          // take ratio of current screen size vs original screen size.
          double sw = ((1f * screenSize.width) / (1f * Integer.parseInt(
 -                jalview.bin.Cache.getProperty("SCREENGEOMETRY_WIDTH"))));
 +                Cache.getProperty("SCREENGEOMETRY_WIDTH"))));
          double sh = ((1f * screenSize.height) / (1f * Integer.parseInt(
 -                jalview.bin.Cache.getProperty("SCREENGEOMETRY_HEIGHT"))));
 +                Cache.getProperty("SCREENGEOMETRY_HEIGHT"))));
          // rescale the bounds depending upon the current screen geometry.
          ix = (int) (ix * sw);
          iw = (int) (iw * sw);
          ih = (int) (ih * sh);
          while (ix >= screenSize.width)
          {
 -          jalview.bin.Cache.log.debug(
 +          Cache.log.debug(
                    "Window geometry location recall error: shifting horizontal to within screenbounds.");
            ix -= screenSize.width;
          }
          while (iy >= screenSize.height)
          {
 -          jalview.bin.Cache.log.debug(
 +          Cache.log.debug(
                    "Window geometry location recall error: shifting vertical to within screenbounds.");
            iy -= screenSize.height;
          }
 -        jalview.bin.Cache.log.debug(
 +        Cache.log.debug(
                  "Got last known dimensions for " + windowName + ": x:" + ix
                          + " y:" + iy + " width:" + iw + " height:" + ih);
        }
      frame.setResizable(resizable);
      frame.setMaximizable(resizable);
      frame.setIconifiable(resizable);
 -    frame.setOpaque(false);
 +    frame.setOpaque(Platform.isJS());
  
      if (frame.getX() < 1 && frame.getY() < 1)
      {
            menuItem.removeActionListener(menuItem.getActionListeners()[0]);
          }
          windowMenu.remove(menuItem);
 -      };
 +      }
      });
  
      menuItem.addActionListener(new ActionListener()
            frame.setIcon(false);
          } catch (java.beans.PropertyVetoException ex)
          {
 -
 +          // System.err.println(ex.toString());
          }
        }
      });
    }
  
    /**
 -   * Add key bindings to a JInternalFrame so that Ctrl-W and Cmd-W will close
 -   * the window
 +   * Add key bindings to a JInternalFrame so that Ctrl-W and Cmd-W will close the
 +   * window
     * 
     * @param frame
     */
      KeyStroke ctrlWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
              InputEvent.CTRL_DOWN_MASK);
      KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
 -            jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx());
 +            ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx());
  
      InputMap inputMap = frame
              .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
      // Java's Transferable for native dnd
      evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
      Transferable t = evt.getTransferable();
 -    List<String> files = new ArrayList<>();
 +    List<Object> files = new ArrayList<>();
      List<DataSourceType> protocols = new ArrayList<>();
  
      try
        {
          for (int i = 0; i < files.size(); i++)
          {
 -          String file = files.get(i).toString();
 +          // BH 2018 File or String
 +          Object file = files.get(i);
 +          String fileName = file.toString();
            DataSourceType protocol = (protocols == null)
                    ? DataSourceType.FILE
                    : protocols.get(i);
            FileFormatI format = null;
  
 -          if (file.endsWith(".jar"))
 +          if (fileName.endsWith(".jar"))
            {
              format = FileFormat.Jalview;
  
            {
              format = new IdentifyFile().identify(file, protocol);
            }
 -
 -          new FileLoader().LoadFile(file, protocol, format);
 +          if (file instanceof File)
 +          {
 +            Platform.cacheFileData((File) file);
 +          }
 +          new FileLoader().LoadFile(null, file, protocol, format);
  
          }
        } catch (Exception ex)
              MessageManager.getString("label.open_local_file"));
      chooser.setToolTipText(MessageManager.getString("action.open"));
  
 -    int value = chooser.showOpenDialog(this);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    chooser.setResponseHandler(0, new Runnable()
      {
 -      String choice = chooser.getSelectedFile().getPath();
 -      Cache.setProperty("LAST_DIRECTORY",
 -              chooser.getSelectedFile().getParent());
 +      @Override
 +      public void run()
 +      {
 +        File selectedFile = chooser.getSelectedFile();
 +        Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
  
 -      FileFormatI format = chooser.getSelectedFormat();
 +        FileFormatI format = chooser.getSelectedFormat();
  
 -      /*
 -       * Call IdentifyFile to verify the file contains what its extension implies.
 -       * Skip this step for dynamically added file formats, because
 -       * IdentifyFile does not know how to recognise them.
 -       */
 -      if (FileFormats.getInstance().isIdentifiable(format))
 -      {
 -        try
 -        {
 -          format = new IdentifyFile().identify(choice, DataSourceType.FILE);
 -        } catch (FileFormatException e)
 +        /*
 +         * Call IdentifyFile to verify the file contains what its extension implies.
 +         * Skip this step for dynamically added file formats, because
 +         * IdentifyFile does not know how to recognise them.
 +         */
 +        if (FileFormats.getInstance().isIdentifiable(format))
          {
 -          // format = null; //??
 +          try
 +          {
 +            format = new IdentifyFile().identify(selectedFile,
 +                    DataSourceType.FILE);
 +          } catch (FileFormatException e)
 +          {
 +            // format = null; //??
 +          }
          }
 -      }
  
 -      if (viewport != null)
 -      {
 -        new FileLoader().LoadFile(viewport, choice, DataSourceType.FILE,
 -                format);
 +        new FileLoader().LoadFile(viewport, selectedFile,
 +                DataSourceType.FILE, format);
        }
 -      else
 -      {
 -        new FileLoader().LoadFile(choice, DataSourceType.FILE, format);
 -      }
 -    }
 +    });
 +    chooser.showOpenDialog(this);
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Shows a dialog for input of a URL at which to retrieve alignment data
     * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * @param viewport
     */
    @Override
    public void inputURLMenuItem_actionPerformed(AlignViewport viewport)
      // for viewing
      JLabel label = new JLabel(
              MessageManager.getString("label.input_file_url"));
  
      JPanel panel = new JPanel(new GridLayout(2, 1));
      panel.add(label);
 -    panel.add(history);
 -    history.setPreferredSize(new Dimension(400, 20));
 -    history.setEditable(true);
 -    history.addItem("http://www.");
 -
 -    String historyItems = jalview.bin.Cache.getProperty("RECENT_URL");
  
 -    StringTokenizer st;
 -
 -    if (historyItems != null)
 -    {
 -      st = new StringTokenizer(historyItems, "\t");
 -
 -      while (st.hasMoreTokens())
 -      {
 -        history.addItem(st.nextElement());
 -      }
 -    }
 -
 -    int reply = JvOptionPane.showInternalConfirmDialog(desktop, panel,
 -            MessageManager.getString("label.input_alignment_from_url"),
 -            JvOptionPane.OK_CANCEL_OPTION);
 -
 -    if (reply != JvOptionPane.OK_OPTION)
 +    /*
 +     * the URL to fetch is
 +     * Java: an editable combobox with history
 +     * JS: (pending JAL-3038) a plain text field
 +     */
 +    JComponent history;
 +    String urlBase = "http://www.";
 +    if (Platform.isJS())
      {
 -      return;
 +      history = new JTextField(urlBase, 35);
      }
 -
 -    String url = history.getSelectedItem().toString();
 -
 -    if (url.toLowerCase().endsWith(".jar"))
 +    else
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
      {
 -      if (viewport != null)
 -      {
 -        new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
 -                FileFormat.Jalview);
 -      }
 -      else
 +      JComboBox<String> asCombo = new JComboBox<>();
 +      asCombo.setPreferredSize(new Dimension(400, 20));
 +      asCombo.setEditable(true);
 +      asCombo.addItem(urlBase);
 +      String historyItems = Cache.getProperty("RECENT_URL");
 +      if (historyItems != null)
        {
 -        new FileLoader().LoadFile(url, DataSourceType.URL,
 -                FileFormat.Jalview);
 +        for (String token : historyItems.split("\\t"))
 +        {
 +          asCombo.addItem(token);
 +        }
        }
 +      history = asCombo;
      }
 -    else
 +    panel.add(history);
 +
 +    Object[] options = new Object[] { MessageManager.getString("action.ok"),
 +        MessageManager.getString("action.cancel") };
 +    Runnable action = new Runnable()
      {
 -      FileFormatI format = null;
 -      try
 -      {
 -        format = new IdentifyFile().identify(url, DataSourceType.URL);
 -      } catch (FileFormatException e)
 +      @Override
 +      public void run()
        {
 -        // TODO revise error handling, distinguish between
 -        // URL not found and response not valid
 -      }
 +        @SuppressWarnings("unchecked")
 +        String url = (history instanceof JTextField
 +                ? ((JTextField) history).getText()
 +                : ((JComboBox<String>) history).getSelectedItem()
 +                        .toString());
  
 -      if (format == null)
 -      {
 -        JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 -                MessageManager.formatMessage("label.couldnt_locate",
 -                        new Object[]
 -                        { url }),
 -                MessageManager.getString("label.url_not_found"),
 -                JvOptionPane.WARNING_MESSAGE);
 +        if (url.toLowerCase().endsWith(".jar"))
 +        {
 +          if (viewport != null)
 +          {
 +            new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
 +                    FileFormat.Jalview);
 +          }
 +          else
 +          {
 +            new FileLoader().LoadFile(url, DataSourceType.URL,
 +                    FileFormat.Jalview);
 +          }
 +        }
 +        else
 +        {
 +          FileFormatI format = null;
 +          try
 +          {
 +            format = new IdentifyFile().identify(url, DataSourceType.URL);
 +          } catch (FileFormatException e)
 +          {
 +            // TODO revise error handling, distinguish between
 +            // URL not found and response not valid
 +          }
  
 -        return;
 -      }
 +          if (format == null)
 +          {
 +            String msg = MessageManager
 +                    .formatMessage("label.couldnt_locate", url);
 +            JvOptionPane.showInternalMessageDialog(Desktop.desktop, msg,
 +                    MessageManager.getString("label.url_not_found"),
 +                    JvOptionPane.WARNING_MESSAGE);
  
 -      if (viewport != null)
 -      {
 -        new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
 -                format);
 -      }
 -      else
 -      {
 -        new FileLoader().LoadFile(url, DataSourceType.URL, format);
 +            return;
 +          }
 +
 +          if (viewport != null)
 +          {
 +            new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
 +                    format);
 +          }
 +          else
 +          {
 +            new FileLoader().LoadFile(url, DataSourceType.URL, format);
 +          }
 +        }
        }
 -    }
 +    };
 +    String dialogOption = MessageManager
 +            .getString("label.input_alignment_from_url");
 +    JvOptionPane.newOptionDialog(desktop).setResponseHandler(0, action)
 +            .showInternalDialog(panel, dialogOption,
 +                    JvOptionPane.YES_NO_CANCEL_OPTION,
 +                    JvOptionPane.PLAIN_MESSAGE, null, options,
 +                    MessageManager.getString("action.ok"));
    }
  
    /**
    public void quit()
    {
      Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
 -    jalview.bin.Cache.setProperty("SCREENGEOMETRY_WIDTH",
 +    Cache.setProperty("SCREENGEOMETRY_WIDTH",
              screen.width + "");
 -    jalview.bin.Cache.setProperty("SCREENGEOMETRY_HEIGHT",
 +    Cache.setProperty("SCREENGEOMETRY_HEIGHT",
              screen.height + "");
      storeLastKnownDimensions("", new Rectangle(getBounds().x, getBounds().y,
              getWidth(), getHeight()));
  
    private void storeLastKnownDimensions(String string, Rectangle jc)
    {
 -    jalview.bin.Cache.log.debug("Storing last known dimensions for "
 +    Cache.log.debug("Storing last known dimensions for "
              + string + ": x:" + jc.x + " y:" + jc.y + " width:" + jc.width
              + " height:" + jc.height);
  
 -    jalview.bin.Cache.setProperty(string + "SCREEN_X", jc.x + "");
 -    jalview.bin.Cache.setProperty(string + "SCREEN_Y", jc.y + "");
 -    jalview.bin.Cache.setProperty(string + "SCREEN_WIDTH", jc.width + "");
 -    jalview.bin.Cache.setProperty(string + "SCREEN_HEIGHT", jc.height + "");
 +    Cache.setProperty(string + "SCREEN_X", jc.x + "");
 +    Cache.setProperty(string + "SCREEN_Y", jc.y + "");
 +    Cache.setProperty(string + "SCREEN_WIDTH", jc.width + "");
 +    Cache.setProperty(string + "SCREEN_HEIGHT", jc.height + "");
    }
  
    /**
    @Override
    public void aboutMenuItem_actionPerformed(ActionEvent e)
    {
 -    // StringBuffer message = getAboutMessage(false);
 -    // JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 -    //
 -    // message.toString(), "About Jalview", JvOptionPane.INFORMATION_MESSAGE);
      new Thread(new Runnable()
      {
        @Override
        public void run()
        {
 -        new SplashScreen(true);
 +        new SplashScreen(false);
        }
      }).start();
    }
  
 -  public StringBuffer getAboutMessage(boolean shortv)
 +  /**
 +   * Returns the html text for the About screen, including any available version
 +   * number, build details, author details and citation reference, but without
 +   * the enclosing {@code html} tags
 +   * 
 +   * @return
 +   */
 +  public String getAboutMessage()
    {
 -    StringBuffer message = new StringBuffer();
 -    message.append("<html>");
 -    if (shortv)
 -    {
 -      message.append("<h1><strong>Version: "
 -              + jalview.bin.Cache.getProperty("VERSION")
 -              + "</strong></h1>");
 -      message.append("<strong>Built: <em>"
 -              + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown")
 -              + "</em> from " + jalview.bin.Cache.getBuildDetailsForSplash()
 -              + "</strong>");
 -
 -    }
 -    else
 -    {
 -
 -      message.append("<strong>Version "
 -              + jalview.bin.Cache.getProperty("VERSION")
 -              + "; last updated: "
 -              + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown"));
 -    }
 +    StringBuilder message = new StringBuilder(1024);
 +    message.append("<h1><strong>Version: ")
 +            .append(Cache.getProperty("VERSION")).append("</strong></h1>")
 +            .append("<strong>Built: <em>")
 +            .append(Cache.getDefault("BUILD_DATE", "unknown"))
 +            .append("</em> from ").append(Cache.getBuildDetailsForSplash())
 +            .append("</strong>");
  
 -    if (jalview.bin.Cache.getDefault("LATEST_VERSION", "Checking")
 -            .equals("Checking"))
 +    String latestVersion = Cache.getDefault("LATEST_VERSION", "Checking");
 +    if (latestVersion.equals("Checking"))
      {
        // JBP removed this message for 2.11: May be reinstated in future version
        // message.append("<br>...Checking latest version...</br>");
      }
 -    else if (!jalview.bin.Cache.getDefault("LATEST_VERSION", "Checking")
 -            .equals(jalview.bin.Cache.getProperty("VERSION")))
 +    else if (!latestVersion.equals(Cache.getProperty("VERSION")))
      {
        boolean red = false;
 -      if (jalview.bin.Cache.getProperty("VERSION").toLowerCase()
 +      if (Cache.getProperty("VERSION").toLowerCase()
                .indexOf("automated build") == -1)
        {
          red = true;
          message.append("<div style=\"color: #FF0000;font-style: bold;\">");
        }
  
 -      message.append("<br>!! Version "
 -              + jalview.bin.Cache.getDefault("LATEST_VERSION",
 -                      "..Checking..")
 -              + " is available for download from "
 -              + jalview.bin.Cache.getDefault("www.jalview.org",
 -                      "http://www.jalview.org")
 -              + " !!");
 +      message.append("<br>!! Version ")
 +              .append(Cache.getDefault("LATEST_VERSION", "..Checking.."))
 +              .append(" is available for download from ")
 +              .append(Cache.getDefault("www.jalview.org",
 +                      "http://www.jalview.org"))
 +              .append(" !!");
        if (red)
        {
          message.append("</div>");
        }
      }
 -    message.append("<br>Authors:  " + jalview.bin.Cache.getDefault(
 -            "AUTHORFNAMES",
 -            "The Jalview Authors (See AUTHORS file for current list)")
 -            + "<br><br>Development managed by The Barton Group, University of Dundee, Scotland, UK.<br>"
 -            + "<br><br>For help, see the FAQ at <a href=\"http://www.jalview.org/faq\">www.jalview.org/faq</a> and/or join the jalview-discuss@jalview.org mailing list"
 -            + "<br><br>If  you use Jalview, please cite:"
 -            + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
 -            + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
 -            + "<br>Bioinformatics doi: 10.1093/bioinformatics/btp033"
 -            + "</html>");
 -    return message;
 +    message.append("<br>Authors:  ");
 +    message.append(Cache.getDefault("AUTHORFNAMES", DEFAULT_AUTHORS));
 +    message.append(CITATION);
 +
 +    return message.toString();
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Action on requesting Help documentation
     */
    @Override
 -  public void documentationMenuItem_actionPerformed(ActionEvent e)
 +  public void documentationMenuItem_actionPerformed()
    {
      try
      {
 -      Help.showHelpWindow();
 +      if (Platform.isJS())
 +      {
 +        BrowserLauncher.openURL("http://www.jalview.org/help.html");
 +      }
 +      else
 +      /**
 +       * Java only
 +       * 
 +       * @j2sIgnore
 +       */
 +      {
 +        Help.showHelpWindow();
 +      }
      } catch (Exception ex)
      {
 +      System.err.println("Error opening help: " + ex.getMessage());
      }
    }
  
    protected void garbageCollect_actionPerformed(ActionEvent e)
    {
      // We simply collect the garbage
 -    jalview.bin.Cache.log.debug("Collecting garbage...");
 +    Cache.log.debug("Collecting garbage...");
      System.gc();
 -    jalview.bin.Cache.log.debug("Finished garbage collection.");
 +    Cache.log.debug("Finished garbage collection.");
    }
  
    /*
     */
    void showConsole(boolean selected)
    {
 -    showConsole.setSelected(selected);
      // TODO: decide if we should update properties file
 -    Cache.setProperty("SHOW_JAVA_CONSOLE",
 -            Boolean.valueOf(selected).toString());
 -    jconsole.setVisible(selected);
 +    if (jconsole != null) // BH 2018
 +    {
 +      showConsole.setSelected(selected);
 +      Cache.setProperty("SHOW_JAVA_CONSOLE",
 +              Boolean.valueOf(selected).toString());
 +      jconsole.setVisible(selected);
 +    }
    }
  
    void reorderAssociatedWindows(boolean minimize, boolean close)
        return;
      }
  
-     AlignmentViewport source = null, target = null;
+     AlignViewportI source = null;
+     AlignViewportI target = null;
      if (frames[0] instanceof AlignFrame)
      {
        source = ((AlignFrame) frames[0]).getCurrentView();
    }
  
    /**
 -   * Shows a file chooser dialog and writes out the current session as a Jalview
 -   * project file
 +   * Prompts the user to choose a file and then saves the Jalview state as a
 +   * Jalview project file
     */
    @Override
    public void saveState_actionPerformed()
            setProgressBar(MessageManager.formatMessage(
                    "label.saving_jalview_project", new Object[]
                    { chosenFile.getName() }), chosenFile.hashCode());
 -          jalview.bin.Cache.setProperty("LAST_DIRECTORY",
 +          Cache.setProperty("LAST_DIRECTORY",
                    chosenFile.getParent());
            // TODO catch and handle errors for savestate
            // TODO prevent user from messing with the Desktop whilst we're saving
            try
            {
 -            BackupFiles backupfiles = new BackupFiles(chosenFile);
 +              boolean doBackup = BackupFiles.getEnabled();
 +            BackupFiles backupfiles = doBackup ? new BackupFiles(chosenFile) : null;
  
 -            new Jalview2XML().saveState(backupfiles.getTempFile());
 +            new Jalview2XML().saveState(doBackup ? backupfiles.getTempFile() : chosenFile);
  
 -            backupfiles.setWriteSuccess(true);
 -            backupfiles.rollBackupsAndRenameTempFile();
 +            if (doBackup)
 +            {
 +              backupfiles.setWriteSuccess(true);
 +              backupfiles.rollBackupsAndRenameTempFile();
 +            }
            } catch (OutOfMemoryError oom)
            {
              new OOMWarning("Whilst saving current state to "
            setProgressBar(null, chosenFile.hashCode());
          }
        }).start();
 -    }
 +      }
    }
  
    @Override
                                              // allowBackupFiles
      chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(MessageManager.getString("label.restore_state"));
 -
 -    int value = chooser.showOpenDialog(this);
 -
 -    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    chooser.setResponseHandler(0, new Runnable()
      {
 -      final File selectedFile = chooser.getSelectedFile();
 -      setProjectFile(selectedFile);
 -      final String choice = selectedFile.getAbsolutePath();
 -      Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
 -      new Thread(new Runnable()
 +      @Override
 +      public void run()
        {
 -        @Override
 -        public void run()
 +        File selectedFile = chooser.getSelectedFile();
 +        setProjectFile(selectedFile);
 +        String choice = selectedFile.getAbsolutePath();
 +        Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
 +        new Thread(new Runnable()
          {
 -          setProgressBar(MessageManager.formatMessage(
 -                  "label.loading_jalview_project", new Object[]
 -                  { choice }), choice.hashCode());
 -          try
 -          {
 -            new Jalview2XML().loadJalviewAlign(choice);
 -          } catch (OutOfMemoryError oom)
 -          {
 -            new OOMWarning("Whilst loading project from " + choice, oom);
 -          } catch (Exception ex)
 +          @Override
 +          public void run()
            {
 -            Cache.log.error(
 -                    "Problems whilst loading project from " + choice, ex);
 -            JvOptionPane.showMessageDialog(Desktop.desktop,
 -                    MessageManager.formatMessage(
 -                            "label.error_whilst_loading_project_from",
 -                            new Object[]
 -                            { choice }),
 -                    MessageManager.getString("label.couldnt_load_project"),
 -                    JvOptionPane.WARNING_MESSAGE);
 +              try 
 +            {
 +              new Jalview2XML().loadJalviewAlign(selectedFile);
 +            } catch (OutOfMemoryError oom)
 +              {
 +                new OOMWarning("Whilst loading project from " + choice, oom);
 +              } catch (Exception ex)
 +              {
 +                Cache.log.error(
 +                        "Problems whilst loading project from " + choice, ex);
 +                JvOptionPane.showMessageDialog(Desktop.desktop,
 +                        MessageManager.formatMessage(
 +                                "label.error_whilst_loading_project_from",
 +                              new Object[]
 +                                  { choice }),
 +                        MessageManager.getString("label.couldnt_load_project"),
 +                        JvOptionPane.WARNING_MESSAGE);
 +              }
            }
 -          setProgressBar(null, choice.hashCode());
 -        }
 -      }).start();
 -    }
 +        }).start();
 +      }
 +    });
 +    
 +    chooser.showOpenDialog(this);
    }
  
    @Override
  
    ArrayList<JPanel> fileLoadingPanels = new ArrayList<>();
  
 -  public void startLoading(final String fileName)
 +  public void startLoading(final Object fileName)
    {
      if (fileLoadingCount == 0)
      {
  
    /**
     * Gather expanded views (separate AlignFrame's) with the same sequence set
 -   * identifier back in to this frame as additional views, and close the
 -   * expanded views. Note the expanded frames may themselves have multiple
 -   * views. We take the lot.
 +   * identifier back in to this frame as additional views, and close the expanded
 +   * views. Note the expanded frames may themselves have multiple views. We take
 +   * the lot.
     * 
     * @param source
     */
          }
        }
      }
 +
      // refresh the feature setting UI for the source frame if it exists
      if (source.featureSettings != null
              && source.featureSettings.isOpen())
      {
        source.showFeatureSettingsUI();
      }
 -
    }
  
    public JInternalFrame[] getAllFrames()
  
    /**
     * Proxy class for JDesktopPane which optionally displays the current memory
 -   * usage and highlights the desktop area with a red bar if free memory runs
 -   * low.
 +   * usage and highlights the desktop area with a red bar if free memory runs low.
     * 
     * @author AMW
     */
 -  public class MyDesktopPane extends JDesktopPane implements Runnable
 +  public class MyDesktopPane extends JDesktopPane
 +          implements Runnable
    {
 -
      private static final float ONE_MB = 1048576f;
  
      boolean showMemoryUsage = false;
        openGroovyConsole();
      } catch (Exception ex)
      {
 -      jalview.bin.Cache.log.error("Groovy Shell Creation failed.", ex);
 +      Cache.log.error("Groovy Shell Creation failed.", ex);
        JvOptionPane.showInternalMessageDialog(Desktop.desktop,
  
                MessageManager.getString("label.couldnt_create_groovy_shell"),
    }
  
    /**
 -   * Bind Ctrl/Cmd-Q to Quit - for reset as Groovy Console takes over this
 -   * binding when opened
 +   * Bind Ctrl/Cmd-Q to Quit - for reset as Groovy Console takes over this binding
 +   * when opened
     */
    protected void addQuitHandler()
    {
    @Override
    public void setProgressBar(String message, long id)
    {
 +    // Platform.timeCheck("Desktop " + message, Platform.TIME_MARK);
 +
      if (progressBars == null)
      {
        progressBars = new Hashtable<>();
    }
  
    /**
 -   * This will return the first AlignFrame holding the given viewport instance.
 -   * It will break if there are more than one AlignFrames viewing a particular
 -   * av.
 +   * This will return the first AlignFrame holding the given viewport instance. It
 +   * will break if there are more than one AlignFrames viewing a particular av.
     * 
     * @param viewport
     * @return alignFrame for viewport
      }
      Thread t3 = null;
      {
-       // TODO: do rest service discovery
+       // start slivka discovery
+       t3 = new Thread(jalview.ws.slivkaws.SlivkaWSDiscoverer.getInstance());
+       t3.start();
      }
      if (blocking)
      {
            } catch (InterruptedException x)
            {
            }
 -          ;
          }
          if (instance == null)
          {
      block.release();
    }
  
 +  /**
 +   * Outputs an image of the desktop to file in EPS format, after prompting the
 +   * user for choice of Text or Lineart character rendering (unless a preference
 +   * has been set). The file name is generated as
 +   * 
 +   * <pre>
 +   * Jalview_snapshot_nnnnn.eps where nnnnn is the current timestamp in milliseconds
 +   * </pre>
 +   */
    @Override
    protected void snapShotWindow_actionPerformed(ActionEvent e)
    {
 +    // currently the menu option to do this is not shown
      invalidate();
 -    File of;
 -    ImageMaker im = new jalview.util.ImageMaker(
 -            this, ImageMaker.TYPE.EPS, "View of Desktop", getWidth(),
 -            getHeight(), of = new File("Jalview_snapshot"
 -                    + System.currentTimeMillis() + ".eps"),
 -            "View of desktop", null, 0, false);
 -    try
 -    {
 -      paintAll(im.getGraphics());
 -      im.writeImage();
 -    } catch (Exception q)
 +
 +    int width = getWidth();
 +    int height = getHeight();
 +    File of = new File(
 +            "Jalview_snapshot_" + System.currentTimeMillis() + ".eps");
 +    ImageWriterI writer = new ImageWriterI()
      {
 -      Cache.log.error("Couldn't write snapshot to " + of.getAbsolutePath(),
 -              q);
 -      return;
 -    }
 -    Cache.log.info("Successfully written snapshot to file "
 -            + of.getAbsolutePath());
 +      @Override
 +      public void exportImage(Graphics g) throws Exception
 +      {
 +        paintAll(g);
 +        Cache.log.info("Successfully written snapshot to file "
 +                + of.getAbsolutePath());
 +      }
 +    };
 +    String title = "View of desktop";
 +    ImageExporter exporter = new ImageExporter(writer, null, TYPE.EPS,
 +            title);
 +    exporter.doExport(of, this, width, height, title);
    }
  
    /**
     * Explode the views in the given SplitFrame into separate SplitFrame windows.
 -   * This respects (remembers) any previous 'exploded geometry' i.e. the size
 -   * and location last time the view was expanded (if any). However it does not
 +   * This respects (remembers) any previous 'exploded geometry' i.e. the size and
 +   * location last time the view was expanded (if any). However it does not
     * remember the split pane divider location - this is set to match the
     * 'exploding' frame.
     * 
     *          - the payload from the drop event
     * @throws Exception
     */
 -  public static void transferFromDropTarget(List<String> files,
 +  public static void transferFromDropTarget(List<Object> files,
            List<DataSourceType> protocols, DropTargetDropEvent evt,
            Transferable t) throws Exception
    {
  
 +    // BH 2018 changed List<String> to List<Object> to allow for File from SwingJS
 +
 +    // DataFlavor[] flavors = t.getTransferDataFlavors();
 +    // for (int i = 0; i < flavors.length; i++) {
 +    // if (flavors[i].isFlavorJavaFileListType()) {
 +    // evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
 +    // List<File> list = (List<File>) t.getTransferData(flavors[i]);
 +    // for (int j = 0; j < list.size(); j++) {
 +    // File file = (File) list.get(j);
 +    // byte[] data = getDroppedFileBytes(file);
 +    // fileName.setText(file.getName() + " - " + data.length + " " +
 +    // evt.getLocation());
 +    // JTextArea target = (JTextArea) ((DropTarget) evt.getSource()).getComponent();
 +    // target.setText(new String(data));
 +    // }
 +    // dtde.dropComplete(true);
 +    // return;
 +    // }
 +    //
 +
      DataFlavor uriListFlavor = new DataFlavor(
              "text/uri-list;class=java.lang.String"), urlFlavour = null;
      try
          }
          else
          {
 -          if (Platform.isAMac())
 +          if (Platform.isAMacAndNotJS())
            {
              System.err.println(
                      "Please ignore plist error - occurs due to problem with java 8 on OSX");
            }
 -          ;
          }
        } catch (Throwable ex)
        {
        for (Object file : (List) t
                .getTransferData(DataFlavor.javaFileListFlavor))
        {
 -        files.add(((File) file).toString());
 +        files.add(file);
          protocols.add(DataSourceType.FILE);
        }
      }
          }
        }
      }
 -    if (Platform.isWindows())
 -
 +    if (Platform.isWindowsAndNotJS())
      {
        Cache.log.debug("Scanning dropped content for Windows Link Files");
  
        // resolve any .lnk files in the file drop
        for (int f = 0; f < files.size(); f++)
        {
 -        String source = files.get(f).toLowerCase();
 +        String source = files.get(f).toString().toLowerCase();
          if (protocols.get(f).equals(DataSourceType.FILE)
                  && (source.endsWith(".lnk") || source.endsWith(".url")
                          || source.endsWith(".site")))
          {
            try
            {
 -            File lf = new File(files.get(f));
 +            Object obj = files.get(f);
 +            File lf = (obj instanceof File ? (File) obj
 +                    : new File((String) obj));
              // process link file to get a URL
              Cache.log.debug("Found potential link file: " + lf);
              WindowsShortcut wscfile = new WindowsShortcut(lf);
    }
  
    /**
 -   * Answers a (possibly empty) list of any structure viewer frames (currently
 -   * for either Jmol or Chimera) which are currently open. This may optionally
 -   * be restricted to viewers of a specified class, or viewers linked to a
 -   * specified alignment panel.
 +   * Answers a (possibly empty) list of any structure viewer frames (currently for
 +   * either Jmol or Chimera) which are currently open. This may optionally be
 +   * restricted to viewers of a specified class, or viewers linked to a specified
 +   * alignment panel.
     * 
     * @param apanel
     *          if not null, only return viewers linked to this panel
   */
  package jalview.gui;
  
- import java.awt.BorderLayout;
+ import jalview.util.MessageManager;
  import java.awt.Color;
  import java.awt.Component;
+ import java.awt.Container;
  import java.awt.Font;
- import java.awt.GridLayout;
- import java.awt.Rectangle;
  import java.awt.event.ActionListener;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
@@@ -37,17 -37,13 +37,13 @@@ import javax.swing.BorderFactory
  import javax.swing.JButton;
  import javax.swing.JComboBox;
  import javax.swing.JComponent;
- import javax.swing.JLabel;
  import javax.swing.JMenu;
  import javax.swing.JMenuItem;
- import javax.swing.JPanel;
  import javax.swing.JScrollBar;
  import javax.swing.SwingConstants;
  import javax.swing.border.Border;
  import javax.swing.border.TitledBorder;
  
- import jalview.util.MessageManager;
  /**
   * useful functions for building Swing GUIs
   * 
@@@ -96,14 -92,9 +92,14 @@@ public final class JvSwingUtil
      }
  
      return (enclose ? "<html>" : "")
 -            + "<style> p.ttip {width: 350; text-align: left; word-wrap: break-word;}</style><p class=\"ttip\">"
 -            + ttext + "</p>" + ((enclose ? "</html>" : ""));
 -
 +     // BH 2018
 +            + "<style> div.ttip {width:350px;white-space:pre-wrap;padding:2px;overflow-wrap:break-word;}</style><div class=\"ttip\">"
 +//            + "<style> p.ttip {width:350px;margin:-14px 0px -14px 0px;padding:2px;overflow-wrap:break-word;}"
 +//            + "</style><p class=\"ttip\">"
 +            + ttext
 +            + " </div>"
 +//            + "</p>"
 +            + ((enclose ? "</html>" : ""));
    }
  
    public static JButton makeButton(String label, String tooltip,
    }
  
    /**
+    * A convenience method that that adds a component with label to a container,
+    * sets a tooltip on both component and label, and optionally specifies layout
+    * constraints for the added component (but not the label)
     * 
-    * @param panel
+    * @param container
     * @param tooltip
     * @param label
-    * @param valBox
-    * @return the GUI element created that was added to the layout so it's
-    *         attributes can be changed.
+    * @param comp
+    * @param constraints
     */
-   public static JPanel addtoLayout(JPanel panel, String tooltip,
-           JComponent label, JComponent valBox)
-   {
-     JPanel laypanel = new JPanel(new GridLayout(1, 2));
-     JPanel labPanel = new JPanel(new BorderLayout());
-     JPanel valPanel = new JPanel();
-     labPanel.setBounds(new Rectangle(7, 7, 158, 23));
-     valPanel.setBounds(new Rectangle(172, 7, 270, 23));
-     labPanel.add(label, BorderLayout.WEST);
-     valPanel.add(valBox);
-     laypanel.add(labPanel);
-     laypanel.add(valPanel);
-     valPanel.setToolTipText(tooltip);
-     labPanel.setToolTipText(tooltip);
-     valBox.setToolTipText(tooltip);
-     panel.add(laypanel);
-     panel.validate();
-     return laypanel;
-   }
-   public static void mgAddtoLayout(JPanel cpanel, String tooltip,
-           JLabel jLabel, JComponent name)
+   public static void addtoLayout(Container container, String tooltip,
+           JComponent label, JComponent comp, String constraints)
    {
-     mgAddtoLayout(cpanel, tooltip, jLabel, name, null);
-   }
-   public static void mgAddtoLayout(JPanel cpanel, String tooltip,
-           JLabel jLabel, JComponent name, String params)
-   {
-     cpanel.add(jLabel);
-     if (params == null)
-     {
-       cpanel.add(name);
-     }
-     else
-     {
-       cpanel.add(name, params);
-     }
-     name.setToolTipText(tooltip);
-     jLabel.setToolTipText(tooltip);
+     container.add(label);
+     container.add(comp, constraints);
+     comp.setToolTipText(tooltip); // this doesn't seem to show?
+     label.setToolTipText(tooltip);
    }
  
    /**
  
    /**
     * Adds a titled border to the component in the default font and position (top
-    * left), optionally witht italic text
+    * left), optionally with italic text
     * 
     * @param comp
     * @param title
@@@ -20,6 -20,7 +20,7 @@@
   */
  package jalview.gui;
  
+ import jalview.api.AlignViewportI;
  import jalview.bin.Cache;
  import jalview.renderer.OverviewRenderer;
  import jalview.util.MessageManager;
@@@ -55,25 -56,24 +56,25 @@@ import javax.swing.SwingUtilities
   * @author $author$
   * @version $Revision$
   */
 +@SuppressWarnings("serial")
  public class OverviewPanel extends JPanel
          implements Runnable, ViewportListenerI
  {
 -  private OverviewDimensions od;
 +  protected OverviewDimensions od;
  
    private OverviewCanvas oviewCanvas;
  
-   protected AlignViewport av;
+   private AlignViewportI av;
  
    private AlignmentPanel ap;
  
 -  private JCheckBoxMenuItem displayToggle;
 +  protected JCheckBoxMenuItem displayToggle;
  
 -  private boolean showHidden = true;
 +  protected boolean showHidden = true;
  
 -  private boolean draggingBox = false;
 +  protected boolean draggingBox = false;
  
 -  private ProgressPanel progressPanel;
 +  protected ProgressPanel progressPanel;
  
    /**
     * Creates a new OverviewPanel object.
        @Override
        public void mousePressed(MouseEvent evt)
        {
 -        if (SwingUtilities.isRightMouseButton(evt))
 -        {
 -          if (!Platform.isAMac())
 -          {
 -            showPopupMenu(evt);
 -          }
 +        
 +      if (Platform.isWinRightButton(evt)) {
 +              showPopupMenu(evt);
 +              return;
 +      }
 +        if (SwingUtilities.isRightMouseButton(evt)) {
 +              return;
          }
 -        else
 -        {
            // don't do anything if the mouse press is in the overview's box
            // (wait to see if it's a drag instead)
            // otherwise update the viewport
                      av.getAlignment().getHiddenSequences(),
                      av.getAlignment().getHiddenColumns());
            }
 -        }
        }
  
        @Override
        }
  
      });
 +
 +    /*
 +     * Javascript does not call componentResized on initial display,
 +     * so do the update here
 +     */
 +    if (Platform.isJS())
 +    {
 +      updateOverviewImage();
 +    }
    }
  
    /*
     * Displays the popup menu and acts on user input
     */
 -  private void showPopupMenu(MouseEvent e)
 +  protected void showPopupMenu(MouseEvent e)
    {
      JPopupMenu popup = new JPopupMenu();
      ActionListener menuListener = new ActionListener()
    /*
     * Toggle overview display between showing hidden columns and hiding hidden columns
     */
 -  private void toggleHiddenColumns()
 +  protected void toggleHiddenColumns()
    {
      if (showHidden)
      {
@@@ -20,7 -20,6 +20,7 @@@
   */
  package jalview.gui;
  
 +import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
@@@ -40,14 -39,11 +40,14 @@@ import java.util.Vector
  
  import javax.swing.ButtonGroup;
  import javax.swing.JCheckBoxMenuItem;
 -import javax.swing.JColorChooser;
 +import javax.swing.JInternalFrame;
 +import javax.swing.JLabel;
  import javax.swing.JMenu;
  import javax.swing.JMenuItem;
 +import javax.swing.JPanel;
  import javax.swing.JPopupMenu;
  import javax.swing.JRadioButtonMenuItem;
 +import javax.swing.JScrollPane;
  
  import jalview.analysis.AAFrequency;
  import jalview.analysis.AlignmentAnnotationUtils;
@@@ -64,11 -60,12 +64,13 @@@ import jalview.datamodel.DBRefEntry
  import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.MappedFeatures;
  import jalview.datamodel.PDBEntry;
+ import jalview.datamodel.ResidueCount;
  import jalview.datamodel.SequenceFeature;
  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;
@@@ -82,11 -79,13 +84,14 @@@ import jalview.util.Comparison
  import jalview.util.GroupUrlLink;
  import jalview.util.GroupUrlLink.UrlStringTooLongException;
  import jalview.util.MessageManager;
 +import jalview.util.Platform;
  import jalview.util.StringUtils;
  import jalview.util.UrlLink;
  import jalview.viewmodel.seqfeatures.FeatureRendererModel;
  
+ import java.io.IOException;
+ import java.net.MalformedURLException;
  /**
   * The popup menu that is displayed on right-click on a sequence id, or in the
   * sequence alignment.
@@@ -545,6 -544,36 +550,36 @@@ public class PopupMenu extends JPopupMe
          }
        }
  
+       if (seq.hasHMMProfile())
+       {
+         menuItem = new JMenuItem(MessageManager
+                 .getString("action.add_background_frequencies"));
+         menuItem.addActionListener(new ActionListener()
+         {
+           @Override
+           public void actionPerformed(ActionEvent e)
+           {
+             try
+             {
+               ResidueCount counts = CountReader.getBackgroundFrequencies(ap,
+                       seq);
+               if (counts != null)
+               {
+                 seq.getHMM().setBackgroundFrequencies(counts);
+                 ap.alignFrame.buildColourMenu();
+               }
+             } catch (MalformedURLException e1)
+             {
+               e1.printStackTrace();
+             } catch (IOException e1)
+             {
+               e1.printStackTrace();
+             }
+           }
+         });
+         add(menuItem);
+       }
        menuItem = new JMenuItem(
                MessageManager.getString("action.hide_sequences"));
        menuItem.addActionListener(new ActionListener()
          buildGroupURLMenu(sg, groupLinks);
        }
        // Add a 'show all structures' for the current selection
-       Hashtable<String, PDBEntry> pdbe = new Hashtable<>(), reppdb = new Hashtable<>();
+       Hashtable<String, PDBEntry> pdbe = new Hashtable<>();
+       Hashtable<String, PDBEntry> reppdb = new Hashtable<>();
  
        SequenceI sqass = null;
        for (SequenceI sq : alignPanel.av.getSequenceSelection())
    protected void showFeatureDetails(SequenceFeature sf, String seqName,
            MappedFeatures mf)
    {
 -    CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
 -    // it appears Java's CSS does not support border-collapse :-(
 -    cap.addStylesheetRule("table { border-collapse: collapse;}");
 -    cap.addStylesheetRule("table, td, th {border: 1px solid black;}");
 -    cap.setText(sf.getDetailsReport(seqName, mf));
 -
 -    Desktop.addInternalFrame(cap,
 +    JInternalFrame details;
 +    if (Platform.isJS())
 +    {
 +      details = new JInternalFrame();
 +      JPanel panel = new JPanel(new BorderLayout());
 +      panel.setOpaque(true);
 +      panel.setBackground(Color.white);
 +      // TODO JAL-3026 set style of table correctly for feature details
 +      JLabel reprt = new JLabel(MessageManager
 +              .formatMessage("label.html_content", new Object[]
 +              { sf.getDetailsReport(seqName, mf) }));
 +      reprt.setBackground(Color.WHITE);
 +      reprt.setOpaque(true);
 +      panel.add(reprt, BorderLayout.CENTER);
 +      details.setContentPane(panel);
 +      details.pack();
 +    }
 +    else
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
 +    {
 +      CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
 +      // it appears Java's CSS does not support border-collapse :-(
 +      cap.addStylesheetRule("table { border-collapse: collapse;}");
 +      cap.addStylesheetRule("table, td, th {border: 1px solid black;}");
 +      cap.setText(sf.getDetailsReport(seqName, mf));
 +      details = cap;
 +    }
 +    Desktop.addInternalFrame(details,
              MessageManager.getString("label.feature_details"), 500, 500);
    }
  
        {
          sqi = sqi.getDatasetSequence();
        }
 -      DBRefEntry[] dbr = sqi.getDBRefs();
 -      if (dbr != null && dbr.length > 0)
 +      List<DBRefEntry> dbr = sqi.getDBRefs();
 +      int nd;
 +      if (dbr != null && (nd = dbr.size()) > 0)
        {
 -        for (int d = 0; d < dbr.length; d++)
 +        for (int d = 0; d < nd; d++)
          {
 -          String src = dbr[d].getSource(); // jalview.util.DBRefUtils.getCanonicalName(dbr[d].getSource()).toUpperCase();
 +          DBRefEntry e = dbr.get(d);
 +          String src = e.getSource(); // jalview.util.DBRefUtils.getCanonicalName(dbr[d].getSource()).toUpperCase();
            Object[] sarray = commonDbrefs.get(src);
            if (sarray == null)
            {
  
            if (((String[]) sarray[1])[sq] == null)
            {
 -            if (!dbr[d].hasMap() || (dbr[d].getMap()
 +            if (!e.hasMap() || (e.getMap()
                      .locateMappedRange(start, end) != null))
              {
 -              ((String[]) sarray[1])[sq] = dbr[d].getAccessionId();
 +              ((String[]) sarray[1])[sq] = e.getAccessionId();
                ((int[]) sarray[0])[0]++;
              }
            }
          Cache.log.error("Exception for GroupURLLink '" + link + "'", foo);
          continue;
        }
 -      ;
        if (!urlLink.isValid())
        {
          Cache.log.error(urlLink.getInvalidMessage());
        @Override
        public void actionPerformed(ActionEvent actionEvent)
        {
 -        editSequence_actionPerformed(actionEvent);
 +        editSequence_actionPerformed();
        }
      });
      makeReferenceSeq.setText(
  
    public void createSequenceDetailsReport(SequenceI[] sequences)
    {
 -    CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
      StringBuilder contents = new StringBuilder(128);
 +    contents.append("<html><body>");
      for (SequenceI seq : sequences)
      {
        contents.append("<p><h2>" + MessageManager.formatMessage(
                contents, seq, true, true, ap.getSeqPanel().seqCanvas.fr);
        contents.append("</p>");
      }
 -    cap.setText("<html>" + contents.toString() + "</html>");
 +    contents.append("</body></html>");
 +    String report = contents.toString();
 +
 +    JInternalFrame frame;
 +    if (Platform.isJS())
 +    {
 +      JLabel textLabel = new JLabel();
 +      textLabel.setText(report);
 +      textLabel.setBackground(Color.WHITE);
 +      JPanel pane = new JPanel(new BorderLayout());
 +      pane.setOpaque(true);
 +      pane.setBackground(Color.WHITE);
 +      pane.add(textLabel, BorderLayout.NORTH);
 +      frame = new JInternalFrame();
 +      frame.getContentPane().add(new JScrollPane(pane));
 +    }
 +    else
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
 +    {
 +      CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
 +      cap.setText(report);
 +      frame = cap;
 +    }
  
 -    Desktop.addInternalFrame(cap,
 +    Desktop.addInternalFrame(frame,
              MessageManager.formatMessage("label.sequence_details_for",
                      (sequences.length == 1 ? new Object[]
                      { sequences[0].getDisplayId(true) }
                              { MessageManager
                                      .getString("label.selection") })),
              500, 400);
 -
    }
  
    protected void showNonconserved_actionPerformed()
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Shows a dialog where group name and description may be edited
     */
    protected void groupName_actionPerformed()
    {
      SequenceGroup sg = getGroup();
      EditNameDialog dialog = new EditNameDialog(sg.getName(),
              sg.getDescription(),
 -            "       " + MessageManager.getString("label.group_name") + " ",
 -            MessageManager.getString("label.group_description") + " ",
 +            MessageManager.getString("label.group_name"),
 +            MessageManager.getString("label.group_description"));
 +    dialog.showDialog(ap.alignFrame,
              MessageManager.getString("label.edit_group_name_description"),
 -            ap.alignFrame);
 -
 -    if (!dialog.accept)
 -    {
 -      return;
 -    }
 -
 -    sg.setName(dialog.getName());
 -    sg.setDescription(dialog.getDescription());
 -    refresh();
 +            new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                sg.setName(dialog.getName());
 +                sg.setDescription(dialog.getDescription());
 +                refresh();
 +              }
 +            });
    }
  
    /**
    {
      EditNameDialog dialog = new EditNameDialog(sequence.getName(),
              sequence.getDescription(),
 -            "       " + MessageManager.getString("label.sequence_name")
 -                    + " ",
 -            MessageManager.getString("label.sequence_description") + " ",
 +            MessageManager.getString("label.sequence_name"),
 +            MessageManager.getString("label.sequence_description"));
 +    dialog.showDialog(ap.alignFrame,
              MessageManager.getString(
                      "label.edit_sequence_name_description"),
 -            ap.alignFrame);
 -
 -    if (!dialog.accept)
 -    {
 -      return;
 -    }
 -
 -    String name = dialog.getName();
 -    if (name != null)
 -    {
 -      if (name.indexOf(" ") > -1)
 -      {
 -        JvOptionPane.showMessageDialog(ap,
 -                MessageManager
 -                        .getString("label.spaces_converted_to_backslashes"),
 -                MessageManager
 -                        .getString("label.no_spaces_allowed_sequence_name"),
 -                JvOptionPane.WARNING_MESSAGE);
 -        name = name.replace(' ', '_');
 -      }
 -
 -      sequence.setName(name);
 -      ap.paintAlignment(false, false);
 -    }
 -
 -    sequence.setDescription(dialog.getDescription());
 -
 -    ap.av.firePropertyChange("alignment", null,
 -            ap.av.getAlignment().getSequences());
 -
 +            new Runnable()
 +            {
 +              @Override
 +              public void run()
 +              {
 +                if (dialog.getName() != null)
 +                {
 +                  if (dialog.getName().indexOf(" ") > -1)
 +                  {
 +                    JvOptionPane.showMessageDialog(ap,
 +                            MessageManager.getString(
 +                                    "label.spaces_converted_to_underscores"),
 +                            MessageManager.getString(
 +                                    "label.no_spaces_allowed_sequence_name"),
 +                            JvOptionPane.WARNING_MESSAGE);
 +                  }
 +                  sequence.setName(dialog.getName().replace(' ', '_'));
 +                  ap.paintAlignment(false, false);
 +                }
 +                sequence.setDescription(dialog.getDescription());
 +                ap.av.firePropertyChange("alignment", null,
 +                        ap.av.getAlignment().getSequences());
 +              }
 +            });
    }
  
    /**
    }
  
    /**
 -   * DOCUMENT ME!
 -   * 
 -   * @param e
 -   *          DOCUMENT ME!
 +   * Offers a colour chooser and sets the selected colour as the group outline
     */
    protected void outline_actionPerformed()
    {
 -    SequenceGroup sg = getGroup();
 -    Color col = JColorChooser.showDialog(this,
 -            MessageManager.getString("label.select_outline_colour"),
 -            Color.BLUE);
 -
 -    if (col != null)
 +    String title = MessageManager
 +            .getString("label.select_outline_colour");
 +    ColourChooserListener listener = new ColourChooserListener()
      {
 -      sg.setOutlineColour(col);
 -    }
 -
 -    refresh();
 +      @Override
 +      public void colourSelected(Color c)
 +      {
 +        getGroup().setOutlineColour(c);
 +        refresh();
 +      }
 +    };
 +    JalviewColourChooser.showColourChooser(Desktop.getDesktop(),
 +            title, Color.BLUE, listener);
    }
  
    /**
  
    public void copy_actionPerformed()
    {
 -    ap.alignFrame.copy_actionPerformed(null);
 +    ap.alignFrame.copy_actionPerformed();
    }
  
    public void cut_actionPerformed()
    {
 -    ap.alignFrame.cut_actionPerformed(null);
 +    ap.alignFrame.cut_actionPerformed();
    }
  
    void changeCase(ActionEvent e)
       */
      if (!seqs.isEmpty())
      {
 -      if (ap.getSeqPanel().seqCanvas.getFeatureRenderer()
 -              .amendFeatures(seqs, features, true, ap))
 -      {
 -        ap.alignFrame.setShowSeqFeatures(true);
 -        ap.av.setSearchResults(null); // clear highlighting
 -        ap.repaint(); // draw new/amended features
 -      }
 +      new FeatureEditor(ap, seqs, features, true).showDialog();
      }
    }
  
      }
    }
  
 -  public void editSequence_actionPerformed(ActionEvent actionEvent)
 +  /**
 +   * Shows a dialog where sequence characters may be edited. Any changes are
 +   * applied, and added as an available 'Undo' item in the edit commands
 +   * history.
 +   */
 +  public void editSequence_actionPerformed()
    {
      SequenceGroup sg = ap.av.getSelectionGroup();
  
        }
  
        EditNameDialog dialog = new EditNameDialog(
 -              seq.getSequenceAsString(sg.getStartRes(),
 -                      sg.getEndRes() + 1),
 -              null, MessageManager.getString("label.edit_sequence"), null,
 +              seq.getSequenceAsString(sg.getStartRes(), sg.getEndRes() + 1),
 +              null, MessageManager.getString("label.edit_sequence"), null);
 +      dialog.showDialog(ap.alignFrame,
                MessageManager.getString("label.edit_sequence"),
 -              ap.alignFrame);
 -
 -      if (dialog.accept)
 -      {
 -        EditCommand editCommand = new EditCommand(
 -                MessageManager.getString("label.edit_sequences"),
 -                Action.REPLACE,
 -                dialog.getName().replace(' ', ap.av.getGapCharacter()),
 -                sg.getSequencesAsArray(ap.av.getHiddenRepSequences()),
 -                sg.getStartRes(), sg.getEndRes() + 1, ap.av.getAlignment());
 -
 -        ap.alignFrame.addHistoryItem(editCommand);
 -
 -        ap.av.firePropertyChange("alignment", null,
 -                ap.av.getAlignment().getSequences());
 -      }
 +              new Runnable()
 +              {
 +                @Override
 +                public void run()
 +                {
 +                  EditCommand editCommand = new EditCommand(
 +                          MessageManager.getString("label.edit_sequences"),
 +                          Action.REPLACE,
 +                          dialog.getName().replace(' ',
 +                                  ap.av.getGapCharacter()),
 +                          sg.getSequencesAsArray(
 +                                  ap.av.getHiddenRepSequences()),
 +                          sg.getStartRes(), sg.getEndRes() + 1,
 +                          ap.av.getAlignment());
 +                  ap.alignFrame.addHistoryItem(editCommand);
 +                  ap.av.firePropertyChange("alignment", null,
 +                          ap.av.getAlignment().getSequences());
 +                }
 +              });
      }
    }
  
@@@ -24,6 -24,7 +24,7 @@@ import jalview.analysis.AnnotationSorte
  import jalview.bin.Cache;
  import jalview.gui.Help.HelpId;
  import jalview.gui.StructureViewer.ViewerType;
+ import jalview.hmmer.HmmerCommand;
  import jalview.io.BackupFiles;
  import jalview.io.BackupFilesPresetEntry;
  import jalview.io.FileFormatI;
@@@ -38,6 -39,7 +39,7 @@@ import jalview.urls.UrlLinkTableModel
  import jalview.urls.api.UrlProviderFactoryI;
  import jalview.urls.api.UrlProviderI;
  import jalview.urls.desktop.DesktopUrlProviderFactory;
+ import jalview.util.FileUtils;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
  import jalview.util.UrlConstants;
@@@ -50,16 -52,19 +52,19 @@@ import java.awt.Dimension
  import java.awt.Font;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
+ import java.awt.event.FocusAdapter;
+ import java.awt.event.FocusEvent;
  import java.awt.event.MouseEvent;
  import java.io.File;
  import java.util.ArrayList;
  import java.util.List;
  
  import javax.help.HelpSetException;
 -import javax.swing.JColorChooser;
 +import javax.swing.JComboBox;
  import javax.swing.JFileChooser;
  import javax.swing.JInternalFrame;
  import javax.swing.JPanel;
+ import javax.swing.JTextField;
  import javax.swing.ListSelectionModel;
  import javax.swing.RowFilter;
  import javax.swing.RowSorter;
@@@ -83,6 -88,15 +88,15 @@@ import ext.edu.ucsf.rbvi.strucviz2.Stru
   */
  public class Preferences extends GPreferences
  {
+   // suggested list delimiter character
+   public static final String COMMA = ",";
+   public static final String HMMSEARCH_SEQCOUNT = "HMMSEARCH_SEQCOUNT";
+   public static final String HMMINFO_GLOBAL_BACKGROUND = "HMMINFO_GLOBAL_BACKGROUND";
+   public static final String HMMALIGN_TRIM_TERMINI = "HMMALIGN_TRIM_TERMINI";
    public static final String ENABLE_SPLIT_FRAME = "ENABLE_SPLIT_FRAME";
  
    public static final String SCALE_PROTEIN_TO_CDNA = "SCALE_PROTEIN_TO_CDNA";
    public static final String STRUCTURE_DISPLAY = "STRUCTURE_DISPLAY";
  
    public static final String CHIMERA_PATH = "CHIMERA_PATH";
+   
+   public static final String HMMER_PATH = "HMMER_PATH";
+   public static final String CYGWIN_PATH = "CYGWIN_PATH";
+   public static final String HMMSEARCH_DBS = "HMMSEARCH_DBS";
  
    public static final String SORT_ANNOTATIONS = "SORT_ANNOTATIONS";
  
      super();
      frame = new JInternalFrame();
      frame.setContentPane(this);
 -    wsPrefs = new WsPreferences();
 -    wsTab.add(wsPrefs, BorderLayout.CENTER);
 +    if (!Platform.isJS())
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
 +    {
 +      wsPrefs = new WsPreferences();
 +      wsTab.add(wsPrefs, BorderLayout.CENTER);
 +    }
      int width = 500, height = 450;
 -    new jalview.util.Platform();
 -    if (Platform.isAMac())
 +    if (Platform.isAMacAndNotJS())
      {
        width = 570;
        height = 480;
      frame.setMinimumSize(new Dimension(width, height));
  
      /*
+      * Set HMMER tab defaults
+      */
+     hmmrTrimTermini.setSelected(Cache.getDefault(HMMALIGN_TRIM_TERMINI, false));
+     if (Cache.getDefault(HMMINFO_GLOBAL_BACKGROUND, false))
+     {
+       hmmerBackgroundUniprot.setSelected(true);
+     }
+     else
+     {
+       hmmerBackgroundAlignment.setSelected(true);
+     }
+     hmmerSequenceCount
+             .setText(Cache.getProperty(HMMSEARCH_SEQCOUNT));
+     hmmerPath.setText(Cache.getProperty(HMMER_PATH));
+     hmmerPath.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         validateHmmerPath();
+       }
+     });
+     hmmerPath.addFocusListener(new FocusAdapter()
+     {
+       @Override
+       public void focusLost(FocusEvent e)
+       {
+         validateHmmerPath();
+       }
+     });
+     if (cygwinPath != null)
+     {
+       String path = Cache.getProperty(CYGWIN_PATH);
+       if (path == null)
+       {
+         path = FileUtils.getPathTo("bash");
+       }
+       cygwinPath.setText(path);
+       cygwinPath.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           validateCygwinPath();
+         }
+       });
+       cygwinPath.addFocusListener(new FocusAdapter()
+       {
+         @Override
+         public void focusLost(FocusEvent e)
+         {
+           validateCygwinPath();
+         }
+       });
+     }
+     /*
       * Set Visual tab defaults
       */
      seqLimit.setSelected(Cache.getDefault("SHOW_JVSUFFIX", true));
              Cache.getDefault("SHOW_CONSENSUS_HISTOGRAM", true));
      showConsensLogo
              .setSelected(Cache.getDefault("SHOW_CONSENSUS_LOGO", false));
+     showInformationHistogram.setSelected(
+             Cache.getDefault("SHOW_INFORMATION_HISTOGRAM", true));
+     showHMMLogo.setSelected(Cache.getDefault("SHOW_HMM_LOGO", false));
      showNpTooltip
              .setSelected(Cache.getDefault("SHOW_NPFEATS_TOOLTIP", true));
      showDbRefTooltip
              new RowSorter.SortKey(m.getNameColumn(), SortOrder.ASCENDING));
  
      sorter.setSortKeys(sortKeys);
 -    sorter.sort();
 +    // BH 2018 setSortKeys will do the sort
 +    // sorter.sort();
  
      // set up filtering
      ActionListener onReset;
      /*
       * Set Output tab defaults
       */
 -    epsRendering.addItem(promptEachTimeOpt);
 -    epsRendering.addItem(lineArtOpt);
 -    epsRendering.addItem(textOpt);
 -    String defaultEPS = Cache.getDefault("EPS_RENDERING",
 -            "Prompt each time");
 -    if (defaultEPS.equalsIgnoreCase("Text"))
 -    {
 -      epsRendering.setSelectedItem(textOpt);
 -    }
 -    else if (defaultEPS.equalsIgnoreCase("Lineart"))
 -    {
 -      epsRendering.setSelectedItem(lineArtOpt);
 -    }
 -    else
 -    {
 -      epsRendering.setSelectedItem(promptEachTimeOpt);
 -    }
 +    setupOutputCombo(epsRendering, "EPS_RENDERING");
 +    setupOutputCombo(htmlRendering, "HTML_RENDERING");
 +    setupOutputCombo(svgRendering, "SVG_RENDERING");
      autoIdWidth.setSelected(Cache.getDefault("FIGURE_AUTOIDWIDTH", false));
      userIdWidth.setEnabled(!autoIdWidth.isSelected());
      userIdWidthlabel.setEnabled(!autoIdWidth.isSelected());
    }
  
    /**
 +   * A helper method that sets the items and initial selection in a character
 +   * rendering option list (Prompt each time/Lineart/Text)
 +   * 
 +   * @param comboBox
 +   * @param propertyKey
 +   */
 +  protected void setupOutputCombo(JComboBox<Object> comboBox,
 +          String propertyKey)
 +  {
 +    comboBox.addItem(promptEachTimeOpt);
 +    comboBox.addItem(lineArtOpt);
 +    comboBox.addItem(textOpt);
 +    
 +    /*
 +     * JalviewJS doesn't support Lineart so force it to Text
 +     */
 +    String defaultOption = Platform.isJS() ? "Text"
 +            : Cache.getDefault(propertyKey, "Prompt each time");
 +    if (defaultOption.equalsIgnoreCase("Text"))
 +    {
 +      comboBox.setSelectedItem(textOpt);
 +    }
 +    else if (defaultOption.equalsIgnoreCase("Lineart"))
 +    {
 +      comboBox.setSelectedItem(lineArtOpt);
 +    }
 +    else
 +    {
 +      comboBox.setSelectedItem(promptEachTimeOpt);
 +    }
 +  }
 +
 +  /**
     * Save user selections on the Preferences tabs to the Cache and write out to
     * file.
     * 
              Boolean.toString(showConsensHistogram.isSelected()));
      Cache.applicationProperties.setProperty("SHOW_CONSENSUS_LOGO",
              Boolean.toString(showConsensLogo.isSelected()));
+     Cache.applicationProperties.setProperty("SHOW_INFORMATION_HISTOGRAM",
+             Boolean.toString(showConsensHistogram.isSelected()));
+     Cache.applicationProperties.setProperty("SHOW_HMM_LOGO",
+             Boolean.toString(showHMMLogo.isSelected()));
      Cache.applicationProperties.setProperty("ANTI_ALIAS",
              Boolean.toString(smoothFont.isSelected()));
      Cache.applicationProperties.setProperty(SCALE_PROTEIN_TO_CDNA,
              maxColour.getBackground());
  
      /*
+      * Save HMMER settings
+      */
+     Cache.applicationProperties.setProperty(HMMALIGN_TRIM_TERMINI,
+             Boolean.toString(hmmrTrimTermini.isSelected()));
+     Cache.applicationProperties.setProperty(HMMINFO_GLOBAL_BACKGROUND,
+             Boolean.toString(hmmerBackgroundUniprot.isSelected()));
+     Cache.applicationProperties.setProperty(HMMSEARCH_SEQCOUNT,
+             hmmerSequenceCount.getText());
+     Cache.setOrRemove(HMMER_PATH, hmmerPath.getText());
+     if (cygwinPath != null)
+     {
+       Cache.setOrRemove(CYGWIN_PATH, cygwinPath.getText());
+     }
+     AlignFrame[] frames = Desktop.getAlignFrames();
+     if (frames != null && frames.length > 0)
+     {
+       for (AlignFrame f : frames)
+       {
+         f.updateHMMERStatus();
+       }
+     }
+     
+     hmmrTrimTermini.setSelected(Cache.getDefault(HMMALIGN_TRIM_TERMINI, false));
+     if (Cache.getDefault(HMMINFO_GLOBAL_BACKGROUND, false))
+     {
+       hmmerBackgroundUniprot.setSelected(true);
+     }
+     else
+     {
+       hmmerBackgroundAlignment.setSelected(true);
+     }
+     hmmerSequenceCount
+             .setText(Cache.getProperty(HMMSEARCH_SEQCOUNT));
+     hmmerPath.setText(Cache.getProperty(HMMER_PATH));
+     /*
       * Save Overview settings
       */
      Cache.setColourProperty(GAP_COLOUR, gapColour.getBackground());
       */
      Cache.applicationProperties.setProperty("EPS_RENDERING",
              ((OptionsParam) epsRendering.getSelectedItem()).getCode());
 +    Cache.applicationProperties.setProperty("HTML_RENDERING",
 +            ((OptionsParam) htmlRendering.getSelectedItem()).getCode());
 +    Cache.applicationProperties.setProperty("SVG_RENDERING",
 +            ((OptionsParam) svgRendering.getSelectedItem()).getCode());
  
      /*
       * Save Connections settings
      Cache.applicationProperties.setProperty("PAD_GAPS",
              Boolean.toString(padGaps.isSelected()));
  
 -    wsPrefs.updateAndRefreshWsMenuConfig(false);
 +    if (!Platform.isJS())
 +    {
 +      wsPrefs.updateAndRefreshWsMenuConfig(false);
 +    }
  
      /*
       * Save Backups settings
    @Override
    public void startupFileTextfield_mouseClicked()
    {
 +    // TODO: JAL-3048 not needed for Jalview-JS
      String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
      JalviewFileChooser chooser = JalviewFileChooser
              .forRead(Cache.getProperty("LAST_DIRECTORY"), fileFormat);
    {
      try
      {
 -      wsPrefs.updateWsMenuConfig(true);
 -      wsPrefs.refreshWs_actionPerformed(e);
 +      if (!Platform.isJS())
 +      {
 +        wsPrefs.updateWsMenuConfig(true);
 +        wsPrefs.refreshWs_actionPerformed(e);
 +      }
        frame.setClosed(true);
      } catch (Exception ex)
      {
              && (identity.isSelected() || showGroupConsensus.isSelected()));
      showConsensLogo.setEnabled(annotations.isSelected()
              && (identity.isSelected() || showGroupConsensus.isSelected()));
+     showInformationHistogram.setEnabled(annotations.isSelected());
+     showHMMLogo.setEnabled(annotations.isSelected());
    }
  
    @Override
    @Override
    public void defaultBrowser_mouseClicked(MouseEvent e)
    {
 -    JFileChooser chooser = new JFileChooser(".");
 -    chooser.setDialogTitle(
 -            MessageManager.getString("label.select_default_browser"));
 +    // TODO: JAL-3048 not needed for j2s
 +    if (!Platform.isJS()) // BH 2019
 +    /**
 +     * Java only
 +     * 
 +     * @j2sIgnore
 +     */
 +    {
 +      JFileChooser chooser = new JFileChooser(".");
 +      chooser.setDialogTitle(
 +              MessageManager.getString("label.select_default_browser"));
  
 -    int value = chooser.showOpenDialog(this);
 +      int value = chooser.showOpenDialog(this);
  
 -    if (value == JFileChooser.APPROVE_OPTION)
 -    {
 -      defaultBrowser.setText(chooser.getSelectedFile().getAbsolutePath());
 +      if (value == JFileChooser.APPROVE_OPTION)
 +      {
 +        defaultBrowser.setText(chooser.getSelectedFile().getAbsolutePath());
 +      }
      }
 -
    }
  
    /*
    @Override
    public void minColour_actionPerformed(JPanel panel)
    {
 -    Color col = JColorChooser.showDialog(this,
 +    JalviewColourChooser.showColourChooser(this,
              MessageManager.getString("label.select_colour_minimum_value"),
 -            minColour.getBackground());
 -    if (col != null)
 -    {
 -      panel.setBackground(col);
 -    }
 -    panel.repaint();
 +            panel);
    }
  
    @Override
    public void maxColour_actionPerformed(JPanel panel)
    {
 -    Color col = JColorChooser.showDialog(this,
 +    JalviewColourChooser.showColourChooser(this,
              MessageManager.getString("label.select_colour_maximum_value"),
 -            maxColour.getBackground());
 -    if (col != null)
 -    {
 -      panel.setBackground(col);
 -    }
 -    panel.repaint();
 +            panel);
    }
  
    @Override
    {
      if (!useLegacyGap.isSelected())
      {
 -      Color col = JColorChooser.showDialog(this,
 +      JalviewColourChooser.showColourChooser(this,
                MessageManager.getString("label.select_gap_colour"),
 -              gapColour.getBackground());
 -      if (col != null)
 -      {
 -        gap.setBackground(col);
 -      }
 -      gap.repaint();
 +              gap);
      }
    }
  
    @Override
    public void hiddenColour_actionPerformed(JPanel hidden)
    {
 -    Color col = JColorChooser.showDialog(this,
 +    JalviewColourChooser.showColourChooser(this,
              MessageManager.getString("label.select_hidden_colour"),
 -            hiddenColour.getBackground());
 -    if (col != null)
 -    {
 -      hidden.setBackground(col);
 -    }
 -    hidden.repaint();
 +            hidden);
    }
  
    @Override
        }
      } catch (NumberFormatException x)
      {
 +      userIdWidth.setText("");
        JvOptionPane.showInternalMessageDialog(Desktop.desktop,
                MessageManager
                        .getString("warn.user_defined_width_requirements"),
                MessageManager.getString("label.invalid_id_column_width"),
                JvOptionPane.WARNING_MESSAGE);
 -      userIdWidth.setText("");
      }
    }
  
      }
      return true;
    }
+   
+   /**
+    * Returns true if the given text field contains a path to a folder that
+    * contains an executable with the given name, else false (after showing a
+    * warning dialog). The executable name will be tried with .exe appended if not
+    * found.
+    * 
+    * @param textField
+    * @param executable
+    */
+   protected boolean validateExecutablePath(JTextField textField, String executable)
+   {
+     String folder = textField.getText().trim();
+     if (FileUtils.getExecutable(executable, folder) != null)
+     {
+       return true;
+     }
+     if (folder.length() > 0)
+     {
+       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
+               MessageManager.formatMessage("label.executable_not_found",
+                       executable),
+               MessageManager.getString("label.invalid_folder"),
+               JvOptionPane.ERROR_MESSAGE);
+     }
+     return false;
+   }
+   /**
+    * Checks if a file can be executed
+    * 
+    * @param path
+    *          the path to the file
+    * @return
+    */
+   public boolean canExecute(String path)
+   {
+     File file = new File(path);
+     if (!file.canExecute())
+     {
+       file = new File(path + ".exe");
+       {
+         if (!file.canExecute())
+         {
+           return false;
+         }
+       }
+     }
+     return true;
+   }
  
    /**
     * If Chimera is selected, check it can be found on default or user-specified
      }
    }
  
+   @Override
+   protected void validateHmmerPath()
+   {
+     validateExecutablePath(hmmerPath, HmmerCommand.HMMBUILD);
+   }
+   @Override
+   protected void validateCygwinPath()
+   {
+     validateExecutablePath(cygwinPath, "run");
+   }
    public class OptionsParam
    {
      private String name;
@@@ -147,9 -147,9 +147,9 @@@ public class RestServiceEditorPane exte
  
    }
  
 -  Vector<String> _iparam = new Vector<String>();
 +  Vector<String> _iparam = new Vector<>();
  
 -  Vector<String> _rparam = new Vector<String>();
 +  Vector<String> _rparam = new Vector<>();
  
    /**
     * generate an editable URL service string and parameter list using the
  
    private boolean updateServiceFromGui()
    {
 -    Map<String, InputType> inputTypes = new HashMap<String, InputType>();
 +    Map<String, InputType> inputTypes = new HashMap<>();
      StringBuffer warnings = new StringBuffer();
      for (String its : _iparam)
      {
  
    }
  
 +  /**
 +   * @j2sIgnore
 +   * @param args
 +   */
    public static void main(String[] args)
    {
      if (args.length == 0)
        {
          boolean visible = true;
  
 +        @Override
          public void run()
          {
            boolean nulserv = true;
              final Thread runner = Thread.currentThread();
              JFrame df = new JFrame();
              df.getContentPane().setLayout(new BorderLayout());
-             df.getContentPane().add((nulserv = !nulserv)
-                     ? new RestServiceEditorPane(jalview.ws.rest.RestClient
-                             .makeShmmrRestClient().getRestDescription())
-                     : new RestServiceEditorPane(), BorderLayout.CENTER);
+             df.getContentPane().add(
+                     (nulserv = !nulserv) ? new RestServiceEditorPane(
+                             jalview.ws.rest.clientdefs.ShmrRestClient
+                                     .makeShmmrRestClient()
+                                     .getRestDescription())
+                             : new RestServiceEditorPane(),
+                     BorderLayout.CENTER);
              df.setBounds(100, 100, 600, 400);
              df.addComponentListener(new ComponentListener()
              {
                } catch (Exception x)
                {
                }
 -              ;
              }
              visible = true;
            }
@@@ -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;
@@@ -118,7 -118,7 +117,7 @@@ public class SliderPanel extends GSlide
        @Override
        public void stateChanged(ChangeEvent evt)
        {
 -        valueField.setText(slider.getValue() + "");
 +        valueField.setText(String.valueOf(slider.getValue()));
          valueChanged(slider.getValue());
        }
      });
      });
  
      slider.setValue(value);
 -    valueField.setText(value + "");
 +    valueField.setText(String.valueOf(value));
    }
  
    /**
      if (!conservationSlider.isVisible())
      {
        Desktop.addInternalFrame(conservationSlider,
 -              conservationSlider.getTitle(), 420, 90, false);
 +              conservationSlider.getTitle(), true, FRAME_WIDTH,
 +              FRAME_HEIGHT, false, true);
        conservationSlider.addInternalFrameListener(new InternalFrameAdapter()
        {
          @Override
  
      if (!PIDSlider.isVisible())
      {
 -      Desktop.addInternalFrame(PIDSlider, PIDSlider.getTitle(), 420, 90,
 -              false);
 +      Desktop.addInternalFrame(PIDSlider, PIDSlider.getTitle(), true,
 +              FRAME_WIDTH, FRAME_HEIGHT, false, true);
        PIDSlider.setLayer(JLayeredPane.PALETTE_LAYER);
        PIDSlider.addInternalFrameListener(new InternalFrameAdapter()
        {
      }
      return title;
    }
 -
 -  @Override
 -  protected void allGroupsCheck_actionPerformed(ActionEvent e)
 -  {
 -    if (allGroupsCheck.isSelected())
 -    {
 -      valueChanged(slider.getValue());
 -    }
 -  }
  }
   */
  package jalview.gui;
  
+ import jalview.api.AlignViewportI;
+ import jalview.api.AlignViewControllerGuiI;
+ import jalview.api.FeatureSettingsControllerI;
+ import jalview.api.SplitContainerI;
+ import jalview.controller.FeatureSettingsControllerGuiI;
+ import jalview.datamodel.AlignmentI;
+ import jalview.jbgui.GAlignFrame;
+ import jalview.jbgui.GSplitFrame;
+ import jalview.structure.StructureSelectionManager;
+ import jalview.util.MessageManager;
+ import jalview.util.Platform;
+ import jalview.viewmodel.AlignmentViewport;
  import java.awt.BorderLayout;
  import java.awt.Component;
  import java.awt.Dimension;
@@@ -49,18 -62,6 +62,6 @@@ import javax.swing.event.ChangeListener
  import javax.swing.event.InternalFrameAdapter;
  import javax.swing.event.InternalFrameEvent;
  
- import jalview.api.AlignViewControllerGuiI;
- import jalview.api.FeatureSettingsControllerI;
- import jalview.api.SplitContainerI;
- import jalview.controller.FeatureSettingsControllerGuiI;
- import jalview.datamodel.AlignmentI;
- import jalview.jbgui.GAlignFrame;
- import jalview.jbgui.GSplitFrame;
- import jalview.structure.StructureSelectionManager;
- import jalview.util.MessageManager;
- import jalview.util.Platform;
- import jalview.viewmodel.AlignmentViewport;
  /**
   * An internal frame on the desktop that hosts a horizontally split view of
   * linked DNA and Protein alignments. Additional views can be created in linked
@@@ -117,9 -118,9 +118,9 @@@ public class SplitFrame extends GSplitF
       * estimate width and height of SplitFrame; this.getInsets() doesn't seem to
       * give the full additional size (a few pixels short)
       */
 -    int widthFudge = Platform.isAMac() ? MAC_INSETS_WIDTH
 +    int widthFudge = Platform.isAMacAndNotJS() ? MAC_INSETS_WIDTH
              : WINDOWS_INSETS_WIDTH;
 -    int heightFudge = Platform.isAMac() ? MAC_INSETS_HEIGHT
 +    int heightFudge = Platform.isAMacAndNotJS() ? MAC_INSETS_HEIGHT
              : WINDOWS_INSETS_HEIGHT;
      int width = ((AlignFrame) getTopFrame()).getWidth() + widthFudge;
      int height = ((AlignFrame) getTopFrame()).getHeight()
      topFrame.alignPanel.adjustAnnotationHeight();
      bottomFrame.alignPanel.adjustAnnotationHeight();
  
-     final AlignViewport topViewport = topFrame.viewport;
-     final AlignViewport bottomViewport = bottomFrame.viewport;
+     final AlignViewportI topViewport = topFrame.viewport;
+     final AlignViewportI bottomViewport = bottomFrame.viewport;
      final AlignmentI topAlignment = topViewport.getAlignment();
      final AlignmentI bottomAlignment = bottomViewport.getAlignment();
      boolean topAnnotations = topViewport.isShowAnnotation();
      /*
       * estimate ratio of (topFrameContent / bottomFrameContent)
       */
 -    int insets = Platform.isAMac() ? MAC_INSETS_HEIGHT
 +    int insets = Platform.isAMacAndNotJS() ? MAC_INSETS_HEIGHT
              : WINDOWS_INSETS_HEIGHT;
      // allow 3 'rows' for scale, scrollbar, status bar
      int topHeight = insets + (3 + topCount) * topCharHeight
  
      if (showInternalFrame)
      {
 -      if (Platform.isAMac())
 +      if (Platform.isAMacAndNotJS())
        {
          Desktop.addInternalFrame(featureSettingsUI,
                  MessageManager.getString(
    {
      return featureSettingsUI != null && !featureSettingsUI.isClosed();
    }
--}
++}
@@@ -23,10 -23,7 +23,7 @@@ package jalview.gui
  import jalview.gui.OptsAndParamsPage.OptionBox;
  import jalview.gui.OptsAndParamsPage.ParamBox;
  import jalview.util.MessageManager;
- import jalview.ws.jws2.JabaParamStore;
- import jalview.ws.jws2.JabaPreset;
- import jalview.ws.jws2.Jws2Discoverer;
- import jalview.ws.jws2.jabaws2.Jws2Instance;
+ import jalview.ws.api.UIinfo;
  import jalview.ws.params.ArgumentI;
  import jalview.ws.params.OptionI;
  import jalview.ws.params.ParamDatastoreI;
@@@ -48,33 -45,21 +45,21 @@@ import java.awt.event.HierarchyBoundsLi
  import java.awt.event.HierarchyEvent;
  import java.awt.event.ItemEvent;
  import java.awt.event.ItemListener;
- import java.awt.event.WindowEvent;
- import java.awt.event.WindowListener;
- import java.net.URL;
  import java.util.Hashtable;
- import java.util.Iterator;
  import java.util.List;
  import java.util.Vector;
  
  import javax.swing.JButton;
  import javax.swing.JComboBox;
  import javax.swing.JDialog;
  import javax.swing.JLabel;
  import javax.swing.JPanel;
  import javax.swing.JScrollPane;
- import javax.swing.JSplitPane;
  import javax.swing.JTextArea;
  import javax.swing.border.TitledBorder;
  import javax.swing.event.DocumentEvent;
  import javax.swing.event.DocumentListener;
  
- import compbio.metadata.Argument;
- import compbio.metadata.Option;
- import compbio.metadata.Parameter;
- import compbio.metadata.Preset;
- import compbio.metadata.PresetManager;
- import compbio.metadata.RunnerConfig;
  import net.miginfocom.swing.MigLayout;
  
  /**
  public class WsJobParameters extends JPanel implements ItemListener,
          ActionListener, DocumentListener, OptsParametersContainerI
  {
-   URL linkImageURL = getClass().getResource("/images/link.gif");
+   private static final int PREFERRED_WIDTH = 540;
  
-   private static final String SVC_DEF = "Defaults"; // this is the null
-                                                     // parameter set as shown to
-                                                     // user
+   private static final int DEFAULT_HEIGHT = 640;
+   // the default parameter set shown to the user
+   private static final String SVC_DEF = "Defaults";
+   private int maxOptWidth = 200;
+   // URL linkImageURL = getClass().getResource("/images/link.gif");
+   // TODO ABSRACT FROM JABAWS CLASSES
  
    /**
     * manager for options and parameters.
     */
-   OptsAndParamsPage opanp = new OptsAndParamsPage(this);
+   OptsAndParamsPage opanp;
  
-   /**
+   /*
     * panel containing job options
     */
-   JPanel jobOptions = new JPanel();
+   JPanel optionsPanel = new JPanel();
  
-   /**
+   /*
     * panel containing job parameters
     */
-   JPanel paramList = new JPanel();
-   JPanel SetNamePanel = new JPanel();
-   JPanel setDetails = new JPanel();
-   JSplitPane settingsPanel = new JSplitPane();
-   JPanel jobPanel = new JPanel();
+   JPanel paramsPanel = new JPanel();
  
-   JScrollPane jobOptionsPane = new JScrollPane();
+   JPanel setNamePanel = new JPanel();
  
    JButton createpref = new JButton();
  
  
    JButton updatepref = new JButton();
  
-   JButton startjob = new JButton();
-   JButton canceljob = new JButton();
-   JComboBox setName = new JComboBox();
+   JComboBox<String> setName = new JComboBox<>();
  
    JTextArea setDescr = new JTextArea();
  
    JScrollPane paramPane = new JScrollPane();
  
-   // ScrollablePanel optsAndparams = new ScrollablePanel();
-   JPanel optsAndparams = new JPanel();
-   RunnerConfig serviceOptions;
    ParamDatastoreI paramStore;
  
-   private int MAX_OPTWIDTH = 200;
+   // set true when 'Start Job' is clicked
+   boolean startJob = false;
  
-   WsJobParameters(Jws2Instance service)
-   {
-     this(service, null);
-   }
+   JDialog frame = null;
  
-   public WsJobParameters(Jws2Instance service, WsParamSetI preset)
-   {
-     this(null, service, preset, null);
-   }
+   UIinfo service;
  
-   /**
-    * 
-    * @param desktop
-    *          - if null, create new JFrame outside of desktop
-    * @param service
-    * @param preset
+   /*
+    * list of service presets in the gui
+    */
+   Hashtable<String, String> servicePresets = null;
+   /*
+    * set if dialog is being set - so handlers will avoid spurious events
     */
-   public WsJobParameters(JFrame parent, Jws2Instance service,
-           WsParamSetI preset, List<Argument> jobArgset)
+   boolean settingDialog = false;
+   private Hashtable<Object, Object> modifiedElements = new Hashtable<>();
+   String lastParmSet = null;
+   public WsJobParameters(ParamDatastoreI store, WsParamSetI preset,
+           List<ArgumentI> args)
    {
-     this(parent, null, service, preset, jobArgset);
+     super();
+     // parameters dialog in 'compact' format (help as tooltips)
+     opanp = new OptsAndParamsPage(this, true);
+     jbInit();
+     this.paramStore = store;
+     this.service = null;
+     init(preset, args);
+     validate();
    }
  
    /**
+    * Constructor given a set of parameters and presets, a service to be invoked,
+    * and a list of (Jabaws client) arguments
     * 
-    * @param parent
     * @param paramStorei
     * @param service
     * @param preset
     * @param jobArgset
     */
-   public WsJobParameters(JFrame parent, ParamDatastoreI paramStorei,
-           Jws2Instance service, WsParamSetI preset,
-           List<Argument> jobArgset)
+   public WsJobParameters(ParamDatastoreI paramStorei, UIinfo service,
+           WsParamSetI preset, List<ArgumentI> jobArgset)
    {
      super();
+     // parameters dialog in 'expanded' format (help text boxes)
+     opanp = new OptsAndParamsPage(this, false);
      jbInit();
      this.paramStore = paramStorei;
-     if (paramStore == null)
+     if (paramStore == null && service != null)
      {
        paramStore = service.getParamStore();
      }
      this.service = service;
-     // argSetModified(false);
-     // populate parameter table
-     initForService(service, preset, jobArgset);
-     // display in new JFrame attached to parent.
+     initForService(preset, jobArgset);
      validate();
    }
  
-   int response = -1;
-   JDialog frame = null;
    /**
-    * shows a modal dialog containing the parameters.
+    * Shows a modal dialog containing the parameters and Start or Cancel options.
+    * Answers true if the job is started, false if cancelled.
     * 
     * @return
     */
    {
  
      frame = new JDialog(Desktop.instance, true);
+     if (service != null)
+     {
+       frame.setTitle(MessageManager.formatMessage("label.edit_params_for",
+               new String[]
+       { service.getActionText() }));
+     }
  
-     frame.setTitle(MessageManager.formatMessage("label.edit_params_for",
-             new String[]
-             { service.getActionText() }));
      Rectangle deskr = Desktop.instance.getBounds();
      Dimension pref = this.getPreferredSize();
      frame.setBounds(
      });
      frame.setVisible(true);
  
-     if (response > 0)
-     {
-       return true;
-     }
-     return false;
+     return startJob;
    }
  
    private void jbInit()
                @Override
                public void actionPerformed(ActionEvent e)
                {
-                 update_actionPerformed(e);
+                 update_actionPerformed();
                }
              });
      deletepref = JvSwingUtils.makeButton(
                @Override
                public void actionPerformed(ActionEvent e)
                {
-                 delete_actionPerformed(e);
+                 delete_actionPerformed();
                }
              });
      createpref = JvSwingUtils.makeButton(
                @Override
                public void actionPerformed(ActionEvent e)
                {
-                 create_actionPerformed(e);
+                 create_actionPerformed();
                }
              });
      revertpref = JvSwingUtils.makeButton(
                @Override
                public void actionPerformed(ActionEvent e)
                {
-                 revert_actionPerformed(e);
+                 revert_actionPerformed();
                }
              });
-     startjob = JvSwingUtils.makeButton(
+     JButton startjob = JvSwingUtils.makeButton(
              MessageManager.getString("action.start_job"),
              MessageManager.getString("label.start_job_current_settings"),
              new ActionListener()
                @Override
                public void actionPerformed(ActionEvent e)
                {
-                 startjob_actionPerformed(e);
+                 startjob_actionPerformed();
                }
              });
-     canceljob = JvSwingUtils.makeButton(
+     JButton canceljob = JvSwingUtils.makeButton(
              MessageManager.getString("action.cancel_job"),
              MessageManager.getString("label.cancel_job_close_dialog"),
              new ActionListener()
                @Override
                public void actionPerformed(ActionEvent e)
                {
-                 canceljob_actionPerformed(e);
+                 canceljob_actionPerformed();
                }
              });
  
+     JPanel setDetails = new JPanel();
      setDetails.setBorder(
              new TitledBorder(MessageManager.getString("label.details")));
      setDetails.setLayout(new BorderLayout());
      setName.getEditor().addActionListener(this);
      JPanel setNameInfo = new JPanel(new FlowLayout(FlowLayout.LEFT));
      GridBagLayout gbl = new GridBagLayout();
-     SetNamePanel.setLayout(gbl);
+     setNamePanel.setLayout(gbl);
  
      JLabel setNameLabel = new JLabel(
              MessageManager.getString("label.current_parameter_set_name"));
      revertpref.setVisible(false);
      createpref.setVisible(false);
      JPanel setsavebuts = new JPanel();
-     setsavebuts.setLayout(new FlowLayout(FlowLayout.LEFT)); // GridLayout(1,2));
-     ((FlowLayout) setsavebuts.getLayout()).setHgap(10);
-     ((FlowLayout) setsavebuts.getLayout()).setVgap(0);
+     setsavebuts.setLayout(new FlowLayout(FlowLayout.LEFT, 10, 0)); // GridLayout(1,2));
      JPanel spacer = new JPanel();
      spacer.setPreferredSize(new Dimension(2, 30));
      setsavebuts.add(spacer);
      // setsavebuts.setSize(new Dimension(150, 30));
      JPanel buttonArea = new JPanel(new GridLayout(1, 1));
      buttonArea.add(setsavebuts);
-     SetNamePanel.add(setNameInfo);
+     setNamePanel.add(setNameInfo);
      GridBagConstraints gbc = new GridBagConstraints();
      gbc.gridheight = 2;
      gbl.setConstraints(setNameInfo, gbc);
-     SetNamePanel.add(buttonArea);
+     setNamePanel.add(buttonArea);
      gbc = new GridBagConstraints();
      gbc.gridx = 0;
      gbc.gridy = 2;
  
      // paramPane.setPreferredSize(new Dimension(360, 400));
      // paramPane.setPreferredSize(null);
-     jobOptions.setBorder(
+     optionsPanel.setBorder(
              new TitledBorder(MessageManager.getString("label.options")));
-     jobOptions.setOpaque(true);
-     paramList.setBorder(
+     optionsPanel.setOpaque(true);
+     paramsPanel.setBorder(
              new TitledBorder(MessageManager.getString("label.parameters")));
-     paramList.setOpaque(true);
-     JPanel bjo = new JPanel(new BorderLayout()),
-             bjp = new JPanel(new BorderLayout());
-     bjo.add(jobOptions, BorderLayout.CENTER);
-     bjp.add(paramList, BorderLayout.CENTER);
-     bjp.setOpaque(true);
-     bjo.setOpaque(true);
+     paramsPanel.setOpaque(true);
      // optsAndparams.setScrollableWidth(ScrollableSizeHint.FIT);
      // optsAndparams.setScrollableHeight(ScrollableSizeHint.NONE);
      // optsAndparams.setLayout(new BorderLayout());
+     JPanel optsAndparams = new JPanel();
      optsAndparams.setLayout(new BorderLayout());
-     optsAndparams.add(jobOptions, BorderLayout.NORTH);
-     optsAndparams.add(paramList, BorderLayout.CENTER);
+     optsAndparams.add(optionsPanel, BorderLayout.NORTH);
+     optsAndparams.add(paramsPanel, BorderLayout.CENTER);
      JPanel jp = new JPanel(new BorderLayout());
      jp.add(optsAndparams, BorderLayout.CENTER);
      paramPane.getViewport().setView(jp);
      paramPane.setBorder(null);
      setLayout(new BorderLayout());
+     JPanel jobPanel = new JPanel();
      jobPanel.setPreferredSize(null);
      jobPanel.setLayout(new BorderLayout());
      jobPanel.add(setDetails, BorderLayout.NORTH);
      jobPanel.add(paramPane, BorderLayout.CENTER);
      // jobPanel.setOrientation(JSplitPane.VERTICAL_SPLIT);
  
-     add(SetNamePanel, BorderLayout.NORTH);
+     add(setNamePanel, BorderLayout.NORTH);
      add(jobPanel, BorderLayout.CENTER);
  
      JPanel dialogpanel = new JPanel();
      dialogpanel.add(canceljob);
      // JAL-1580: setMaximumSize() doesn't work, so just size for the worst case:
      // check for null is for JUnit usage
-     final int windowHeight = Desktop.instance == null ? 540
+     final int windowHeight = Desktop.instance == null ? DEFAULT_HEIGHT
              : Desktop.instance.getHeight();
-     setPreferredSize(new Dimension(540, windowHeight));
+     // setPreferredSize(new Dimension(PREFERRED_WIDTH, windowHeight));
      add(dialogpanel, BorderLayout.SOUTH);
      validate();
    }
  
-   protected void revert_actionPerformed(ActionEvent e)
+   protected void revert_actionPerformed()
    {
      reInitDialog(lastParmSet);
      updateWebServiceMenus();
    }
  
-   protected void update_actionPerformed(ActionEvent e)
+   protected void update_actionPerformed()
    {
      if (isUserPreset)
      {
      paramStore.deletePreset(lastParmSet2);
    }
  
-   protected void delete_actionPerformed(ActionEvent e)
+   protected void delete_actionPerformed()
    {
      if (isUserPreset)
      {
      updateWebServiceMenus();
    }
  
-   protected void create_actionPerformed(ActionEvent e)
+   protected void create_actionPerformed()
    {
      String curname = ((String) setName.getSelectedItem()).trim();
      if (curname.length() > 0)
      }
    }
  
-   protected void canceljob_actionPerformed(ActionEvent e)
+   protected void canceljob_actionPerformed()
    {
-     response = 0;
+     startJob = false;
      if (frame != null)
      {
        frame.setVisible(false);
      }
    }
  
-   protected void startjob_actionPerformed(ActionEvent e)
+   protected void startjob_actionPerformed()
    {
-     response = 1;
+     startJob = true;
      if (frame != null)
      {
        frame.setVisible(false);
      }
    }
  
-   Jws2Instance service;
+   void initForService(WsParamSetI paramSet, List<ArgumentI> jobArgset)
+   {
+     settingDialog = true;
  
-   /**
-    * list of service presets in the gui
-    */
-   Hashtable servicePresets = null;
+     init(paramSet, jobArgset);
  
-   /**
-    * set if dialog is being set - so handlers will avoid spurious events
-    */
-   boolean settingDialog = false;
+   }
  
-   void initForService(Jws2Instance service, WsParamSetI jabap,
-           List<Argument> jabajobArgset)
+   void init(WsParamSetI p, List<ArgumentI> jobArgset)
    {
-     WsParamSetI p = null;
-     List<ArgumentI> jobArgset = null;
-     settingDialog = true;
-     { // instantiate the abstract proxy for Jaba objects
-       jobArgset = jabajobArgset == null ? null
-               : JabaParamStore.getJwsArgsfromJaba(jabajobArgset);
-       p = jabap; // (jabap != null) ? paramStore.getPreset(jabap.getName()) :
-                  // null;
-     }
-     Hashtable exnames = new Hashtable();
+     Hashtable<String, String> exnames = new Hashtable<>();
      for (int i = 0, iSize = setName.getItemCount(); i < iSize; i++)
      {
        exnames.put(setName.getItemAt(i), setName.getItemAt(i));
      }
-     servicePresets = new Hashtable();
+     servicePresets = new Hashtable<>();
      // Add the default entry - if not present already.
      if (!exnames.contains(SVC_DEF))
      {
        exnames.put(SVC_DEF, SVC_DEF);
        servicePresets.put(SVC_DEF, SVC_DEF);
      }
-     String curname = (p == null ? "" : p.getName());
+     // String curname = (p == null ? "" : p.getName());
      for (WsParamSetI pr : paramStore.getPresets())
      {
        if (!pr.isModifiable())
        }
      }
      settingDialog = false;
    }
  
-   @SuppressWarnings("unchecked")
    private void updateTable(WsParamSetI p, List<ArgumentI> jobArgset)
    {
      boolean setDefaultParams = false;
              OptionI opt = (OptionI) myarg;
              OptionBox ob = opanp.addOption(opt);
              ob.resetToDefault(setDefaultParams);
-             if (MAX_OPTWIDTH < ob.getPreferredSize().width)
+             if (maxOptWidth < ob.getPreferredSize().width)
              {
-               MAX_OPTWIDTH = ob.getPreferredSize().width;
+               maxOptWidth = ob.getPreferredSize().width;
              }
              ob.validate();
              cw += ob.getPreferredSize().width + 5;
      return modifiedElements.size() > 0;
    }
  
-   private Hashtable modifiedElements = new Hashtable();
    /**
     * reset gui and modification state settings
     */
      if (b && modifiedElements.size() > 0)
      {
        makeSetNameValid(!isUserPreset);
-       SetNamePanel.revalidate();
+       setNamePanel.revalidate();
      }
      updateButtonDisplay();
    }
      // sync the gui with the preset database
      for (int i = 0, iS = setName.getItemCount(); i < iS; i++)
      {
-       String snm = (String) setName.getItemAt(i);
+       String snm = setName.getItemAt(i);
        if (snm.equals(nm))
        {
          makeupdate = true;
      settingDialog = stn;
    }
  
+   /**
+    * Rebuilds the Options and Parameters panels
+    */
    @Override
    public void refreshParamLayout()
    {
-     // optsAndparams.setPreferredSize(null);
-     FlowLayout fl = new FlowLayout(FlowLayout.LEFT);
-     int sep = fl.getVgap();
-     boolean fh = true;
-     int os = 0,
-             s = jobOptions.getBorder().getBorderInsets(jobOptions).bottom
-                     + jobOptions.getBorder().getBorderInsets(jobOptions).top
-                     + 2 * sep;
-     /**
-      * final height for viewport
-      */
-     int finalh = s;
-     int panewidth = paramPane.getViewport().getSize().width - 120
-             - jobOptions.getBorder().getBorderInsets(jobOptions).left
-             + jobOptions.getBorder().getBorderInsets(jobOptions).right;
-     int w = 2 * fl.getHgap()
-             + (MAX_OPTWIDTH > OptsAndParamsPage.PARAM_WIDTH ? MAX_OPTWIDTH
-                     : OptsAndParamsPage.PARAM_WIDTH);
-     int hgap = fl.getHgap(), cw = hgap;
+     final int rightMargin = 40;
+     final int availableWidth = paramPane.getViewport().getSize().width
+             - rightMargin
+             - optionsPanel.getBorder().getBorderInsets(optionsPanel).left
+             + optionsPanel.getBorder().getBorderInsets(optionsPanel).right;
  
      if (opanp.getOptSet().size() > 0)
      {
+       int hgap = 5;
+       int currentWidth = hgap;
  
-       jobOptions.setLayout(new MigLayout("", "", ""));
-       jobOptions.removeAll();
+       /*
+        * layout constraint 'nogrid' prevents vertical column alignment,
+        * allowing controls to flow without extra space inserted to align
+        */
+       optionsPanel.setLayout(new MigLayout("nogrid", "", ""));
+       optionsPanel.removeAll();
+       JPanel lastAdded = null;
  
+       /*
+        * add each control in turn; if adding would overflow the right margin,
+        * remove and re-add the previous parameter with "wrap" (after) 
+        * in order to start a new row
+        */
        for (OptionBox pbox : opanp.getOptSet().values())
        {
          pbox.validate();
-         cw += pbox.getSize().width + hgap;
-         if (cw + 120 > panewidth)
-         {
-           jobOptions.add(pbox, "wrap");
-           // System.out.println("Wrap on "+pbox.option.getName());
-           cw = hgap + pbox.getSize().width;
-           fh = true;
-         }
-         else
+         int boxWidth = pbox.getSize().width;
+         currentWidth += boxWidth + hgap;
+         boolean wrapAfterLast = currentWidth > availableWidth
+                 && lastAdded != null;
+         // System.out.println(String.format(
+         // "%s width=%d, paneWidth=%d, currentWidth=%d, wrapAfterLast=%s",
+         // pbox.toString(), boxWidth, panewidth, currentWidth,
+         // wrapAfterLast));
+         if (wrapAfterLast)
          {
-           jobOptions.add(pbox);
-         }
-         if (fh)
-         {
-           finalh += pbox.getSize().height + fl.getVgap();
-           fh = false;
+           optionsPanel.remove(lastAdded);
+           optionsPanel.add(lastAdded, "wrap");
+           currentWidth = hgap + boxWidth;
          }
+         optionsPanel.add(pbox);
+         lastAdded = pbox;
        }
-       jobOptions.revalidate();
+       optionsPanel.revalidate();
      }
      else
      {
-       jobOptions.setVisible(false);
+       optionsPanel.setVisible(false);
      }
  
-     // Now layout the parameters assuming they occupy one column - to calculate
-     // total height of options+parameters
-     fl = new FlowLayout(FlowLayout.LEFT);
-     // helpful hint from
-     // http://stackoverflow.com/questions/2743177/top-alignment-for-flowlayout
-     fl.setAlignOnBaseline(true);
      if (opanp.getParamSet().size() > 0)
      {
-       paramList.removeAll();
-       paramList.setLayout(new MigLayout("", "", ""));
-       fh = true;
+       paramsPanel.removeAll();
+       paramsPanel.setLayout(new MigLayout("", "", ""));
+       int hgap = 5;
+       int currentWidth = hgap;
+       JPanel lastAdded = null;
        for (ParamBox pbox : opanp.getParamSet().values())
        {
          pbox.validate();
-         cw += pbox.getSize().width + hgap;
-         if (cw + 160 > panewidth)
-         {
-           paramList.add(pbox, "wrap");
-           cw = pbox.getSize().width + hgap;
-           fh = true;
-         }
-         else
+         int boxWidth = pbox.getSize().width;
+         currentWidth += boxWidth + hgap;
+         boolean wrapAfterLast = currentWidth > availableWidth
+                 && lastAdded != null;
+         if (wrapAfterLast)
          {
-           paramList.add(pbox);
+           paramsPanel.remove(lastAdded);
+           paramsPanel.add(lastAdded, "wrap");
+           currentWidth = pbox.getSize().width + hgap;
          }
-         if (fh)
-         {
-           finalh += pbox.getSize().height + fl.getVgap();
-           fh = false;
-         }
+         paramsPanel.add(pbox);
+         lastAdded = pbox;
        }
        /*
         * s = 2 * sep; for (ParamBox pbox : opanp.getParamSet().values()) {
         * pbox.validate(); s += sep +
         * .getBorder().getBorderInsets(paramList).bottom+paramList
         * .getBorder().getBorderInsets(paramList).top;
         */
-       paramList.revalidate();
+       paramsPanel.revalidate();
      }
      else
      {
-       paramList.setVisible(false);
+       paramsPanel.setVisible(false);
      }
      // TODO: waste some time trying to eliminate any unnecessary .validate calls
      // here
      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()
      return opanp.getCurrentSettings();
    }
  
-   String lastParmSet = null;
    /*
     * Hashtable<String, Object[]> editedParams = new Hashtable<String,
     * Object[]>();
      int n = 0;
      // remove any set names in the drop down menu that aren't either a reserved
      // setting, or a user defined or service preset.
-     Vector items = new Vector();
+     Vector<String> items = new Vector<>();
      while (n < setName.getItemCount())
      {
-       String item = (String) setName.getItemAt(n);
+       String item = setName.getItemAt(n);
        if (!item.equals(SVC_DEF) && !paramStore.presetExists(item))
        {
          setName.removeItemAt(n);
      initArgSetModified();
      syncSetNamesWithStore();
      setName.setSelectedItem(lastParmSet);
-     SetNamePanel.validate();
+     setNamePanel.validate();
      validate();
      settingDialog = false;
    }
     */
    protected void updateWebServiceMenus()
    {
+     if (Desktop.instance == null)
+     {
+       return;
+     }
      for (AlignFrame alignFrame : Desktop.getAlignFrames())
      {
        alignFrame.BuildWebServiceMenu();
    @Override
    public void itemStateChanged(ItemEvent e)
    {
-     if (e.getSource() == setName && e.getStateChange() == e.SELECTED)
+     if (e.getSource() == setName
+             && e.getStateChange() == ItemEvent.SELECTED)
      {
        final String setname = (String) setName.getSelectedItem();
-       System.out.println("Item state changed for " + setname
-               + " (handling ? " + !settingDialog + ")");
+       // System.out.println("Item state changed for " + setname
+       // + " (handling ? " + !settingDialog + ")");
        if (settingDialog)
        {
          // ignore event
  
    }
  
-   private void _renameExistingPreset(String oldName, String curSetName2)
-   {
-     paramStore.updatePreset(oldName, curSetName2, setDescr.getText(),
-             getJobParams());
-   }
    /**
     * store current settings as given name. You should then reset gui.
     * 
@@@ -56,13 -56,14 +56,14 @@@ import javax.xml.stream.XMLStreamReader
   */
  public class WsParamSetManager implements ParamManager
  {
+   private static final String WS_PARAM_FILES = "WS_PARAM_FILES";
    Hashtable<String, ParamDatastoreI> paramparsers = new Hashtable<>();
  
    @Override
    public WsParamSetI[] getParameterSet(String name, String serviceUrl,
            boolean modifiable, boolean unmodifiable)
    {
-     String files = Cache.getProperty("WS_PARAM_FILES");
+     String files = Cache.getProperty(WS_PARAM_FILES);
      if (files == null)
      {
        return null;
        } catch (IOException e)
        {
          Cache.log.info("Failed to parse parameter file " + pfile
-                 + " (Check that all JALVIEW_WSPARAMFILES entries are valid!)",
+                 + " (Check that all " + WS_PARAM_FILES
+                 + " entries are valid!)",
                  e);
        }
      }
      }
      if (filename == null)
      {
 +      // TODO: JAL-3048 webservice - not required for Jalview-JS
 +
        JalviewFileChooser chooser = new JalviewFileChooser("wsparams",
                "Web Service Parameter File");
        chooser.setFileView(new JalviewFileView());
      }
      if (outfile != null)
      {
-       String paramFiles = jalview.bin.Cache.getDefault("WS_PARAM_FILES",
+       String paramFiles = jalview.bin.Cache.getDefault(WS_PARAM_FILES,
                filename);
        if (paramFiles.indexOf(filename) == -1)
        {
          }
          paramFiles = paramFiles.concat(filename);
        }
-       Cache.setProperty("WS_PARAM_FILES", paramFiles);
+       Cache.setProperty(WS_PARAM_FILES, paramFiles);
  
        WebServiceParameterSet paramxml = new WebServiceParameterSet();
  
      {
        return;
      }
-     String paramFiles = jalview.bin.Cache.getDefault("WS_PARAM_FILES", "");
+     String paramFiles = jalview.bin.Cache.getDefault(WS_PARAM_FILES, "");
      if (paramFiles.indexOf(filename) > -1)
      {
        String nparamFiles = new String();
            nparamFiles = nparamFiles.concat("|").concat(fl);
          }
        }
-       jalview.bin.Cache.setProperty("WS_PARAM_FILES", nparamFiles);
+       jalview.bin.Cache.setProperty(WS_PARAM_FILES, nparamFiles);
      }
  
      try
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);
+         }
+       }
+     }
+   }
+ }
@@@ -112,7 -112,7 +112,7 @@@ public abstract class AlignFile extend
     * @param sourceType
     *          What type of file to read from (File, URL, Pasted String)
     */
 -  public AlignFile(String dataObject, DataSourceType sourceType)
 +  public AlignFile(Object dataObject, DataSourceType sourceType)
            throws IOException
    {
      this(true, dataObject, sourceType);
     *          What type of file to read from (File, URL)
     * @throws IOException
     */
 -  public AlignFile(boolean parseImmediately, String dataObject,
 +  public AlignFile(boolean parseImmediately, Object dataObject,
            DataSourceType sourceType) throws IOException
    {
 +    // BH allows File or String
      super(dataObject, sourceType);
      initData();
      if (parseImmediately)
     */
    protected void initData()
    {
-     seqs = new Vector<SequenceI>();
-     annotations = new Vector<AlignmentAnnotation>();
-     seqGroups = new ArrayList<SequenceGroup>();
+     seqs = new Vector<>();
+     annotations = new Vector<>();
+     seqGroups = new ArrayList<>();
      parseCalled = false;
    }
  
    @Override
    public void setSeqs(SequenceI[] s)
    {
-     seqs = new Vector<SequenceI>();
+     seqs = new Vector<>();
  
      for (int i = 0; i < s.length; i++)
      {
    {
      if (newickStrings == null)
      {
-       newickStrings = new Vector<String[]>();
+       newickStrings = new Vector<>();
      }
      newickStrings.addElement(new String[] { treeName, newickString });
    }
    {
      seqs.add(seq);
    }
+   /**
+    * Used only for hmmer statistics, so should probably be removed at some
+    * point. TODO remove this
+    * 
+    * @return
+    */
+   public Vector<AlignmentAnnotation> getAnnotations()
+   {
+     return annotations;
+   }
  }
   */
  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
@@@ -31,7 -29,7 +29,7 @@@
  
    void setNewlineString(String newline);
  
 -  void setExportSettings(AlignExportSettingI exportSettings);
 +  void setExportSettings(AlignExportSettingsI exportSettings);
  
    void configureForView(AlignmentViewPanel viewpanel);
  
@@@ -306,7 -306,7 +306,7 @@@ public enum FileFormat implements FileF
        else
        {
          StructureImportSettings.setShowSeqFeatures(true);
 -        return new MCview.PDBfile(
 +        return new mc_view.PDBfile(
                  StructureImportSettings.isVisibleChainAnnotation(),
                  StructureImportSettings.isProcessSecondaryStructure(),
                  StructureImportSettings.isExternalSecondaryStructure(),
      @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;
   */
  package jalview.io;
  
 +import java.io.File;
 +import java.io.IOException;
 +import java.util.StringTokenizer;
 +import java.util.Vector;
 +
 +import javax.swing.SwingUtilities;
 +
  import jalview.api.ComplexAlignFile;
  import jalview.api.FeatureSettingsModelI;
  import jalview.api.FeaturesDisplayedI;
@@@ -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;
@@@ -71,8 -74,6 +76,8 @@@
  
    boolean raiseGUI = true;
  
 +  private File selectedFile;
 +
    /**
     * default constructor always raised errors in GUI dialog boxes
     */
      this.raiseGUI = raiseGUI;
    }
  
 -  public void LoadFile(AlignViewport viewport, String file,
 +  public void LoadFile(AlignViewport viewport, Object file,
            DataSourceType protocol, FileFormatI format)
    {
      this.viewport = viewport;
 -    LoadFile(file, protocol, format);
 +    if (file instanceof File) {
 +      this.selectedFile = (File) file;
 +      file = selectedFile.getPath();
 +    }
 +    LoadFile(file.toString(), protocol, format);
    }
  
    public void LoadFile(String file, DataSourceType protocol,
    }
  
    /**
 +   * Load alignment from (file, protocol) of type format and wait till loaded
 +   * 
 +   * @param file
 +   * @param sourceType
 +   * @param format
 +   * @return alignFrame constructed from file contents
 +   */
 +  public AlignFrame LoadFileWaitTillLoaded(File file,
 +          DataSourceType sourceType, FileFormatI format)
 +  {
 +    this.selectedFile = file;
 +    this.file = file.getPath();
 +    this.protocol = sourceType;
 +    this.format = format;
 +    return _LoadFileWaitTillLoaded();
 +  }
 +
 +  /**
     * Load alignment from FileParse source of type format and wait till loaded
     * 
     * @param source
    }
  
    /**
 -   * start thread and wait until finished, then return the alignFrame that's
 -   * (hopefully) been read.
 +   * runs the 'run' method (in this thread), then return the alignFrame that's
 +   * (hopefully) been read
     * 
     * @return
     */
    protected AlignFrame _LoadFileWaitTillLoaded()
    {
 -    Thread loader = new Thread(this);
 -    loader.start();
 -
 -    while (loader.isAlive())
 -    {
 -      try
 -      {
 -        Thread.sleep(500);
 -      } catch (Exception ex)
 -      {
 -        System.out.println(
 -                "Exception caught while waiting for FileLoader thread");
 -        ex.printStackTrace();
 -      }
 -    }
 +    this.run();
  
      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)
      {
-       st = new StringTokenizer(historyItems, "\t");
+       StringTokenizer st = new StringTokenizer(historyItems, TAB);
  
        while (st.hasMoreTokens())
        {
-         recent.addElement(st.nextToken().trim());
+         String trimmed = st.nextToken().trim();
 -        if (!recent.contains(trimmed))
 -        {
 -          recent.add(trimmed);
 -        }
++      recent.add(trimmed);
        }
      }
  
-     if (recent.contains(file))
+     /*
+      * if file was already in the list, it moves to the top
+      */
+     if (recent.contains(filePath))
      {
-       recent.remove(file);
+       recent.remove(filePath);
      }
  
-     StringBuffer newHistory = new StringBuffer(file);
+     StringBuilder newHistory = new StringBuilder(filePath);
      for (int i = 0; i < recent.size() && i < 10; i++)
      {
-       newHistory.append("\t");
-       newHistory.append(recent.elementAt(i));
+       newHistory.append(TAB);
+       newHistory.append(recent.get(i));
      }
  
-     Cache.setProperty(type, newHistory.toString());
+     String newProperty = newHistory.toString();
+     Cache.setProperty(propertyName, newProperty);
  
-     if (protocol == DataSourceType.FILE)
-     {
-       Cache.setProperty("DEFAULT_FILE_FORMAT", format.getName());
-     }
+     return newProperty;
    }
  
    @Override
            format = new IdentifyFile().identify(source, false);
            // identify stream and rewind rather than close
          }
 +        else if (selectedFile != null) {
 +          format = new IdentifyFile().identify(selectedFile, protocol);
 +        }
          else
          {
            format = new IdentifyFile().identify(file, protocol);
                    "IMPLEMENTATION ERROR: Cannot read consecutive Jalview XML projects from a stream.");
            // We read the data anyway - it might make sense.
          }
 -        alignFrame = new Jalview2XML(raiseGUI).loadJalviewAlign(file);
 +        // BH 2018 switch to File object here instead of filename
 +        alignFrame = new Jalview2XML(raiseGUI).loadJalviewAlign(selectedFile == null ? file : selectedFile);
        }
        else
        {
                        file.lastIndexOf("."));
                String tempStructureFileStr = createNamedJvTempFile(
                        urlLeafName, structExt);
 -              UrlDownloadClient.download(file, tempStructureFileStr);
 -              al = fa.readFile(tempStructureFileStr, DataSourceType.FILE,
 +              
 +              // BH - switching to File object here so as to hold
 +              // ._bytes array directly
 +              File tempFile = new File(tempStructureFileStr);
 +              UrlDownloadClient.download(file, tempFile);
 +              
 +              al = fa.readFile(tempFile, DataSourceType.FILE,
                        format);
                source = fa.getAlignFile();
              }
              else
              {
 -              al = fa.readFile(file, protocol, format);
 +              if (selectedFile == null) {
 +                al = fa.readFile(file, protocol, format);
 +                
 +              } else {
 +                al = fa.readFile(selectedFile, protocol, format);
 +                             }
                source = fa.getAlignFile(); // keep reference for later if
 +              
                                            // necessary.
              }
            }
              }
              // append to existing alignment
              viewport.addAlignment(al, title);
+             if (source instanceof HMMFile)
+             {
+               AlignmentI alignment = viewport.getAlignment();
+               SequenceI seq = alignment
+                       .getSequenceAt(alignment.getHeight() - 1);
+               if (seq.hasHMMProfile())
+               {
+                 /* 
+                  * fudge: move HMM consensus sequence from last to first
+                  */
+                 alignment.deleteSequence(alignment.getAbsoluteHeight() - 1);
+                 alignment.insertSequenceAt(0, seq);
+               }
+               viewport.getAlignPanel().adjustAnnotationHeight();
+               viewport.updateSequenceIdColours();
+             }
            }
            else
            {
              if (!(protocol == DataSourceType.PASTE))
              {
                alignFrame.setFileName(file, format);
 +              alignFrame.setFileObject(selectedFile); // BH 2018 SwingJS
              }
              if (proxyColourScheme != null)
              {
          }
        }
  
-       updateRecentlyOpened();
+       updateRecentlyOpened(file, protocol);
+       if (protocol == DataSourceType.FILE && format != null)
+       {
+         Cache.setProperty("DEFAULT_FILE_FORMAT", format.getName());
+       }
  
      } catch (Exception er)
      {
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";
+   }
+ }
@@@ -20,7 -20,6 +20,7 @@@
   */
  package jalview.io;
  
 +import java.io.File;
  import java.io.IOException;
  
  /**
   */
  public class IdentifyFile
  {
 +  
 +  public FileFormatI identify(Object file, DataSourceType protocol) throws FileFormatException
 +  {
 +    // BH 2018
 +    return (file instanceof File ? identify((File) file, protocol) : identify((String) file, protocol));
 +    
 +  }
 +
 +  public FileFormatI identify(File file, DataSourceType sourceType)
 +          throws FileFormatException
 +  {
 +    // BH 2018
 +    String emessage = "UNIDENTIFIED FILE PARSING ERROR";
 +    FileParse parser = null;
 +    try
 +    {
 +      parser = new FileParse(file, sourceType);
 +      if (parser.isValid())
 +      {
 +        return identify(parser);
 +      }
 +    } catch (Exception e)
 +    {
 +      System.err.println("Error whilst identifying " + file);
 +      e.printStackTrace(System.err);
 +      emessage = e.getMessage();
 +    }
 +    if (parser != null)
 +    {
 +      throw new FileFormatException(parser.errormessage);
 +    }
 +    throw new FileFormatException(emessage);
 +  }
 +
    /**
     * Identify a datasource's file content.
     *
@@@ -90,7 -55,7 +90,7 @@@
        }
      } catch (Exception e)
      {
 -      System.err.println("Error whilst identifying");
 +      System.err.println("Error whilst identifying " + file);
        e.printStackTrace(System.err);
        emessage = e.getMessage();
      }
            if (source.inFile != null)
            {
              String fileStr = source.inFile.getName();
 -            // possibly a Jalview archive.
 -            if (fileStr.lastIndexOf(".jar") > -1
 -                    || fileStr.lastIndexOf(".zip") > -1)
 +            if (fileStr.contains(".jar")
 +                    || fileStr.contains(".zip") || fileStr.contains(".jvp"))
              {
 +              // possibly a Jalview archive (but check further)
                reply = FileFormat.Jalview;
              }
            }
            if (!lineswereskipped && data.startsWith("PK"))
            {
 -            reply = FileFormat.Jalview; // archive.
 +            reply = FileFormat.Jalview; // archive
              break;
            }
          }
            reply = FileFormat.ScoreMatrix;
            break;
          }
+         if (data.startsWith("HMMER3"))
+         {
+           reply = FileFormat.HMMER3;
+           break;
+         }
          if (data.startsWith("H ") && !aaIndexHeaderRead)
          {
            aaIndexHeaderRead = true;
      return true;
    }
  
 +  /**
 +   * 
 +   * @param args
 +   * @j2sIgnore
 +   */
    public static void main(String[] args)
    {
      for (int i = 0; args != null && i < args.length; i++)
        System.err.println("Usage: <Filename> [<Filename> ...]");
      }
    }
 +
 + 
  }
@@@ -20,6 -20,7 +20,6 @@@
   */
  package jalview.io;
  
 -import java.util.Arrays;
  import java.util.Collection;
  import java.util.Comparator;
  import java.util.LinkedHashMap;
@@@ -27,6 -28,7 +27,7 @@@ import java.util.List
  import java.util.Map;
  
  import jalview.api.FeatureColourI;
+ import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.DBRefEntry;
  import jalview.datamodel.DBRefSource;
  import jalview.datamodel.GeneLociI;
@@@ -57,7 -59,9 +58,7 @@@ public class SequenceAnnotationRepor
  
    private static String linkImageURL;
  
 -  private static final String[][] PRIMARY_SOURCES = new String[][] {
 -      DBRefSource.CODINGDBS, DBRefSource.DNACODINGDBS,
 -      DBRefSource.PROTEINDBS };
 + // public static final String[][] PRIMARY_SOURCES  moved to DBRefSource.java
  
    /*
     * Comparator to order DBRefEntry by Source + accession id (case-insensitive),
@@@ -79,8 -83,8 +80,8 @@@
        }
        String s1 = ref1.getSource();
        String s2 = ref2.getSource();
 -      boolean s1Primary = isPrimarySource(s1);
 -      boolean s2Primary = isPrimarySource(s2);
 +      boolean s1Primary = DBRefSource.isPrimarySource(s1);
 +      boolean s2Primary = DBRefSource.isPrimarySource(s2);
        if (s1Primary && !s2Primary)
        {
          return -1;
        return comp;
      }
  
 -    private boolean isPrimarySource(String source)
 -    {
 -      for (String[] primary : PRIMARY_SOURCES)
 -      {
 -        for (String s : primary)
 -        {
 -          if (source.equals(s))
 -          {
 -            return true;
 -          }
 -        }
 -      }
 -      return false;
 -    }
 +//    private boolean isPrimarySource(String source)
 +//    {
 +//      for (String[] primary : DBRefSource.PRIMARY_SOURCES)
 +//      {
 +//        for (String s : primary)
 +//        {
 +//          if (source.equals(s))
 +//          {
 +//            return true;
 +//          }
 +//        }
 +//      }
 +//      return false;
 +//    }
    };
  
    private boolean forTooltip;
        sb.append(tmp);
        maxWidth = Math.max(maxWidth, tmp.length());
      }
      SequenceI ds = sequence;
      while (ds.getDatasetSequence() != null)
      {
        ds = ds.getDatasetSequence();
      }
  
+     /*
+      * add any annotation scores
+      */
+     AlignmentAnnotation[] anns = ds.getAnnotation();
+     for (int i = 0; anns != null && i < anns.length; i++)
+     {
+       AlignmentAnnotation aa = anns[i];
+       if (aa != null && aa.hasScore() && aa.sequenceRef != null)
+       {
+         sb.append("<br>").append(aa.label).append(": ")
+                 .append(aa.getScore());
+       }
+     }
      if (showDbRefs)
      {
        maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
          maxWidth = Math.max(maxWidth, sz);
        }
      }
+     if (sequence.getAnnotation("Search Scores") != null)
+     {
+       sb.append("<br>");
+       String eValue = " E-Value: "
+               + sequence.getAnnotation("Search Scores")[0].getEValue();
+       String bitScore = " Bit Score: "
+               + sequence.getAnnotation("Search Scores")[0].getBitScore();
+       sb.append(eValue);
+       sb.append("<br>");
+       sb.append(bitScore);
+       maxWidth = Math.max(maxWidth, eValue.length());
+       maxWidth = Math.max(maxWidth, bitScore.length());
+     }
+     sb.append("<br>");
      sb.append("</i>");
      return maxWidth;
    }
  
    protected int appendDbRefs(final StringBuilder sb, SequenceI ds,
            boolean summary)
    {
 -    DBRefEntry[] dbrefs = ds.getDBRefs();
 +    List<DBRefEntry> dbrefs = ds.getDBRefs();
      if (dbrefs == null)
      {
        return 0;
      }
  
      // note this sorts the refs held on the sequence!
 -    Arrays.sort(dbrefs, comparator);
 +    dbrefs.sort(comparator);
      boolean ellipsis = false;
      String source = null;
      String lastSource = null;
   */
  package jalview.io;
  
 -import jalview.analysis.Rna;
 -import jalview.datamodel.AlignmentAnnotation;
 -import jalview.datamodel.AlignmentI;
 -import jalview.datamodel.Annotation;
 -import jalview.datamodel.DBRefEntry;
 -import jalview.datamodel.DBRefSource;
 -import jalview.datamodel.Mapping;
 -import jalview.datamodel.Sequence;
 -import jalview.datamodel.SequenceFeature;
 -import jalview.datamodel.SequenceI;
 -import jalview.schemes.ResidueProperties;
 -import jalview.util.Comparison;
 -import jalview.util.DBRefUtils;
 -import jalview.util.Format;
 -import jalview.util.MessageManager;
 -
  import java.io.BufferedReader;
  import java.io.FileReader;
  import java.io.IOException;
@@@ -39,21 -55,6 +39,21 @@@ import com.stevesoft.pat.Regex
  import fr.orsay.lri.varna.exceptions.ExceptionUnmatchedClosingParentheses;
  import fr.orsay.lri.varna.factories.RNAFactory;
  import fr.orsay.lri.varna.models.rna.RNA;
 +import jalview.analysis.Rna;
 +import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.Annotation;
 +import jalview.datamodel.DBRefEntry;
 +import jalview.datamodel.DBRefSource;
 +import jalview.datamodel.Mapping;
 +import jalview.datamodel.Sequence;
 +import jalview.datamodel.SequenceFeature;
 +import jalview.datamodel.SequenceI;
 +import jalview.schemes.ResidueProperties;
 +import jalview.util.Comparison;
 +import jalview.util.DBRefUtils;
 +import jalview.util.Format;
 +import jalview.util.MessageManager;
  
  // import org.apache.log4j.*;
  
@@@ -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";
  
  
    StringBuffer out; // output buffer
  
-   AlignmentI al;
+   private AlignmentI al;
  
    public StockholmFile()
    {
    }
  
    /**
-    * Creates a new StockholmFile object for output.
+    * Creates a new StockholmFile object for output
     */
    public StockholmFile(AlignmentI al)
    {
        // logger.debug("Stockholm version: " + version);
      }
  
-     // We define some Regexes here that will be used regularily later
+     // We define some Regexes here that will be used regularly later
      rend = new Regex("^\\s*\\/\\/"); // Find the end of an alignment
      p = new Regex("(\\S+)\\/(\\d+)\\-(\\d+)"); // split sequence id in
      // id/from/to
      Regex openparen = new Regex("(<|\\[)", "(");
      Regex closeparen = new Regex("(>|\\])", ")");
  
 -    // Detect if file is RNA by looking for bracket types
 -    Regex detectbrackets = new Regex("(<|>|\\[|\\]|\\(|\\))");
 +//    // Detect if file is RNA by looking for bracket types
 +//    Regex detectbrackets = new Regex("(<|>|\\[|\\]|\\(|\\))");
  
      rend.optimize();
      p.optimize();
          // End of the alignment, pass stuff back
          this.noSeqs = seqs.size();
  
 -        String seqdb, dbsource = null;
 +        String dbsource = null;
          Regex pf = new Regex("PF[0-9]{5}(.*)"); // Finds AC for Pfam
          Regex rf = new Regex("RF[0-9]{5}(.*)"); // Finds AC for Rfam
          if (getAlignmentProperty("AC") != null)
              if (features.containsKey(this.id2type(type)))
              {
                // logger.debug("Found content for " + this.id2type(type));
-               content = (Hashtable) features.get(this.id2type(type));
+               content = (Hashtable) features
+                       .get(this.id2type(type));
              }
              else
              {
                // logger.debug("Creating new content holder for " +
                // this.id2type(type));
                content = new Hashtable();
-               features.put(this.id2type(type), content);
+               features.put(id2type(type), content);
              }
              String ns = (String) content.get(ANNOTATION);
  
            Vector<AlignmentAnnotation> annotation, String label,
            String annots)
    {
-     String convert1, convert2 = null;
-     // convert1 = OPEN_PAREN.replaceAll(annots);
-     // convert2 = CLOSE_PAREN.replaceAll(convert1);
+         String convert1, convert2 = null;
+     // String convert1 = OPEN_PAREN.replaceAll(annots);
+     // String convert2 = CLOSE_PAREN.replaceAll(convert1);
      // annots = convert2;
  
      String type = label;
      type = id2type(type);
  
      boolean isrnass = false;
      if (type.equalsIgnoreCase("secondary structure"))
      {
        ss = true;
      for (int i = 0; i < annots.length(); i++)
      {
        String pos = annots.substring(i, i + 1);
+       if (UNDERSCORE == pos.charAt(0))
+       {
+         pos = " ";
+       }
        Annotation ann;
        ann = new Annotation(pos, "", ' ', 0f); // 0f is 'valid' null - will not
        // be written out
      int max = 0;
      int maxid = 0;
      int in = 0;
 -    Hashtable dataRef = null;
 +    int slen = s.length;
 +    SequenceI seq;
 +    Hashtable<String, String> dataRef = null;
      boolean isAA = s[in].isProtein();
 -    while ((in < s.length) && (s[in] != null))
 +    while ((in < slen) && ((seq = s[in]) != null))
      {
 -
 -      String tmp = printId(s[in], jvSuffix);
 -      max = Math.max(max, s[in].getLength());
 +      String tmp = printId(seq, jvSuffix);
 +      max = Math.max(max, seq.getLength());
  
        if (tmp.length() > maxid)
        {
          maxid = tmp.length();
        }
 -      if (s[in].getDBRefs() != null)
 +      List<DBRefEntry> seqrefs = seq.getDBRefs();
 +      int ndb;
 +      if (seqrefs != null && (ndb = seqrefs.size()) > 0)
        {
          if (dataRef == null)
          {
 -          dataRef = new Hashtable();
 +          dataRef = new Hashtable<>();
          }
 -        List<DBRefEntry> primrefs = s[in].getPrimaryDBRefs();
 +        List<DBRefEntry> primrefs = seq.getPrimaryDBRefs();
          if (primrefs.size() >= 1)
          {
            dataRef.put(tmp, dbref_to_ac_record(primrefs.get(0)));
          }
          else
          {
 -          for (int idb = 0; idb < s[in].getDBRefs().length; idb++)
 +          for (int idb = 0; idb < seq.getDBRefs().size(); idb++)
            {
 -            DBRefEntry dbref = s[in].getDBRefs()[idb];
 +            DBRefEntry dbref = seq.getDBRefs().get(idb);
              dataRef.put(tmp, dbref_to_ac_record(dbref));
              // if we put in a uniprot or EMBL record then we're done:
              if (isAA && DBRefSource.UNIPROT
      // output database accessions
      if (dataRef != null)
      {
 -      Enumeration en = dataRef.keys();
 +      Enumeration<String> en = dataRef.keys();
        while (en.hasMoreElements())
        {
          Object idd = en.nextElement();
 -        String type = (String) dataRef.remove(idd);
 +        String type = dataRef.remove(idd);
          out.append(new Format("%-" + (maxid - 2) + "s")
                  .form("#=GS " + idd.toString() + " "));
          if (isAA && type.contains("UNIPROT")
      }
  
      // output annotations
 -    while (i < s.length && s[i] != null)
 +    while (i < slen && (seq = s[i]) != null)
      {
 -      AlignmentAnnotation[] alAnot = s[i].getAnnotation();
 +      AlignmentAnnotation[] alAnot = seq.getAnnotation();
        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")
 -              .form(printId(s[i], jvSuffix) + " "));
 -      out.append(s[i].getSequenceAsString());
 +              .form(printId(seq, jvSuffix) + " "));
 +      out.append(seq.getSequenceAsString());
        out.append(newline);
        i++;
      }
  
      // alignment annotation
      AlignmentAnnotation aa;
 -    if (al.getAlignmentAnnotation() != null)
 +    AlignmentAnnotation[] an = al.getAlignmentAnnotation();
 +    if (an != null)
      {
 -      for (int ia = 0; ia < al.getAlignmentAnnotation().length; ia++)
 +      for (int ia = 0, na = an.length; ia < na; ia++)
        {
 -        aa = al.getAlignmentAnnotation()[ia];
 +        aa = an[ia];
          if (aa.autoCalculated || !aa.visible || aa.sequenceRef != null)
          {
            continue;
          }
 -        String seq = "";
 +        String sseq = "";
          String label;
          String key = "";
          if (aa.label.equals("seq"))
          out.append(
                  new Format("%-" + maxid + "s").form("#=GC " + label + " "));
          boolean isrna = aa.isValidStruc();
 -        for (int j = 0; j < aa.annotations.length; j++)
 +        for (int j = 0, nj = aa.annotations.length; j < nj; j++)
          {
 -          seq += outputCharacter(key, j, isrna, aa.annotations, null);
 +          sseq += outputCharacter(key, j, isrna, aa.annotations, null);
          }
 -        out.append(seq);
 +        out.append(sseq);
          out.append(newline);
        }
      }
      return out.toString();
    }
  
    /**
     * add an annotation character to the output row
     * 
              : seq;
    }
  
+   /**
+    * make a friendly ID string.
+    * 
+    * @param dataName
+    * @return truncated dataName to after last '/'
+    */
+   private String safeName(String dataName)
+   {
+     int b = 0;
+     while ((b = dataName.indexOf("/")) > -1 && b < dataName.length())
+     {
+       dataName = dataName.substring(b + 1).trim();
+     }
+     int e = (dataName.length() - dataName.indexOf(".")) + 1;
+     dataName = dataName.substring(1, e).trim();
+     return dataName;
+   }
+   
+   
    public String print()
    {
      out = new StringBuffer();
  
      }
    }
+   
    protected static String id2type(String id)
    {
      if (typeIds.containsKey(id))
              "Warning : Unknown Stockholm annotation type: " + type);
      return key;
    }
-   /**
-    * make a friendly ID string.
-    * 
-    * @param dataName
-    * @return truncated dataName to after last '/'
-    */
-   private String safeName(String dataName)
-   {
-     int b = 0;
-     while ((b = dataName.indexOf("/")) > -1 && b < dataName.length())
-     {
-       dataName = dataName.substring(b + 1).trim();
-     }
-     int e = (dataName.length() - dataName.indexOf(".")) + 1;
-     dataName = dataName.substring(1, e).trim();
-     return dataName;
-   }
  }
@@@ -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;
@@@ -52,7 -52,7 +51,7 @@@ public class ParsePackedSe
    public Object[] getAlignment(JalviewDataset context,
            Iterable<DataProvider> files) throws Exception
    {
 -    List<Object> rslt = new ArrayList<Object>();
 +    List<Object> rslt = new ArrayList<>();
      if (context == null)
      {
        context = new JalviewDataset();
          // if not, create one.
          if (context.featureColours == null)
          {
 -          context.featureColours = new HashMap<String, FeatureColourI>();
 +          context.featureColours = new HashMap<>();
          }
          try
          {
     * would be created.
     * 
     * @param args
 +   * @j2sIgnore
     */
    public static void main(String args[])
    {
      // make data providers from the set of keys/files
      int i = 0;
 -    List<DataProvider> dp = new ArrayList<DataProvider>();
 +    List<DataProvider> dp = new ArrayList<>();
      while ((i + 1) < args.length)
      {
        String type = args[i++];
@@@ -27,6 -27,8 +27,8 @@@ import jalview.api.SplitContainerI
  import jalview.bin.Cache;
  import jalview.gui.JvSwingUtils;
  import jalview.gui.Preferences;
+ import jalview.hmmer.HmmerCommand;
+ import jalview.io.FileFormatException;
  import jalview.io.FileFormats;
  import jalview.schemes.ResidueColourScheme;
  import jalview.util.MessageManager;
@@@ -42,6 -44,7 +44,7 @@@ import java.awt.event.FocusEvent
  import java.awt.event.KeyEvent;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
+ import java.io.IOException;
  import java.util.HashMap;
  import java.util.Map;
  
@@@ -61,18 -64,17 +64,20 @@@ import javax.swing.event.ChangeEvent
  import javax.swing.event.MenuEvent;
  import javax.swing.event.MenuListener;
  
 +@SuppressWarnings("serial")
  public class GAlignFrame extends JInternalFrame
  {
    protected JMenuBar alignFrameMenuBar = new JMenuBar();
  
    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();
  
@@@ -80,9 -82,7 +85,9 @@@
  
    protected JMenu sortByAnnotScore = new JMenu();
  
 -  protected JLabel statusBar = new JLabel();
 +  public JLabel statusBar = new JLabel(); // BH 2019 was protected, but not
 +                                          // sufficient for
 +                                          // AlignFrame.printWriter
  
    protected JMenu outputTextboxMenu = new JMenu();
  
  
    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;
    {
      try
      {
 +
 +      // for Web-page embedding using id=align-frame-div
 +      setName("jalview-alignment");
 +
        jbInit();
        setJMenuBar(alignFrameMenuBar);
  
            @Override
            public void actionPerformed(ActionEvent e)
            {
 -            outputText_actionPerformed(e);
 +            outputText_actionPerformed(e.getActionCommand());
            }
          });
  
        System.err.println(e.toString());
      }
  
 -    if (!Platform.isAMac())
 +    if (Platform.allowMnemonics()) // was "not mac and not JS"
      {
        closeMenuItem.setMnemonic('C');
        outputTextboxMenu.setMnemonic('T');
    private void jbInit() throws Exception
    {
      initColourMenu();
+   
      JMenuItem saveAs = new JMenuItem(
              MessageManager.getString("action.save_as"));
      ActionListener al = new ActionListener()
        @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()
                      | jalview.util.ShortcutKeyMaskExWrapper.SHIFT_DOWN_MASK,
              false);
      addMenuActionAndAccelerator(keyStroke, saveAs, al);
+   
      closeMenuItem.setText(MessageManager.getString("action.close"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_W,
              jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false);
        }
      };
      addMenuActionAndAccelerator(keyStroke, closeMenuItem, al);
+   
      JMenu editMenu = new JMenu(MessageManager.getString("action.edit"));
      JMenu viewMenu = new JMenu(MessageManager.getString("action.view"));
      JMenu annotationsMenu = new JMenu(
      JMenu calculateMenu = new JMenu(
              MessageManager.getString("action.calculate"));
      webService.setText(MessageManager.getString("action.web_service"));
+     initHMMERMenu();
      JMenuItem selectAllSequenceMenuItem = new JMenuItem(
              MessageManager.getString("action.select_all"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_A,
        }
      };
      addMenuActionAndAccelerator(keyStroke, selectAllSequenceMenuItem, al);
+   
      JMenuItem deselectAllSequenceMenuItem = new JMenuItem(
              MessageManager.getString("action.deselect_all"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);
        }
      };
      addMenuActionAndAccelerator(keyStroke, deselectAllSequenceMenuItem, al);
+   
      JMenuItem invertSequenceMenuItem = new JMenuItem(
              MessageManager.getString("action.invert_sequence_selection"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_I,
        }
      };
      addMenuActionAndAccelerator(keyStroke, invertSequenceMenuItem, al);
+   
      JMenuItem grpsFromSelection = new JMenuItem(
              MessageManager.getString("action.make_groups_selection"));
      grpsFromSelection.addActionListener(new ActionListener()
        }
      };
      addMenuActionAndAccelerator(keyStroke, remove2LeftMenuItem, al);
+   
      JMenuItem remove2RightMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_right"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_R,
        }
      };
      addMenuActionAndAccelerator(keyStroke, remove2RightMenuItem, al);
+   
      JMenuItem removeGappedColumnMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_empty_columns"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_E,
        }
      };
      addMenuActionAndAccelerator(keyStroke, removeGappedColumnMenuItem, al);
+   
      JMenuItem removeAllGapsMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_all_gaps"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_E,
        }
      };
      addMenuActionAndAccelerator(keyStroke, removeAllGapsMenuItem, al);
+   
      JMenuItem justifyLeftMenuItem = new JMenuItem(
              MessageManager.getString("action.left_justify_alignment"));
      justifyLeftMenuItem.addActionListener(new ActionListener()
          sortGroupMenuItem_actionPerformed(e);
        }
      });
+     JMenuItem sortEValueMenuItem = new JMenuItem(
+             MessageManager.getString("action.by_evalue"));
+     sortEValueMenuItem.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         sortEValueMenuItem_actionPerformed(e);
+       }
+     });
+     JMenuItem sortBitScoreMenuItem = new JMenuItem(
+             MessageManager.getString("action.by_bit_score"));
+     sortBitScoreMenuItem.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         sortBitScoreMenuItem_actionPerformed(e);
+       }
+     });
+   
      JMenuItem removeRedundancyMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_redundancy"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_D,
      };
      addMenuActionAndAccelerator(keyStroke, removeRedundancyMenuItem, al);
  
+     JMenuItem filterByEValue = new JMenuItem(
+             MessageManager.getString("action.filter_by_evalue"));
+     filterByEValue.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         filterByEValue_actionPerformed();
+       }
+     });
+     JMenuItem filterByScore = new JMenuItem(
+             MessageManager.getString("action.filter_by_score"));
+     filterByScore.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         filterByScore_actionPerformed();
+       }
+     });
+   
      JMenuItem pairwiseAlignmentMenuItem = new JMenuItem(
              MessageManager.getString("action.pairwise_alignment"));
      pairwiseAlignmentMenuItem.addActionListener(new ActionListener()
          pairwiseAlignmentMenuItem_actionPerformed(e);
        }
      });
+   
      this.getContentPane().setLayout(new BorderLayout());
      alignFrameMenuBar.setFont(new java.awt.Font("Verdana", 0, 11));
      statusBar.setBackground(Color.white);
      statusBar.setFont(new java.awt.Font("Verdana", 0, 11));
      statusBar.setBorder(BorderFactory.createLineBorder(Color.black));
      statusBar.setText(MessageManager.getString("label.status_bar"));
      outputTextboxMenu
              .setText(MessageManager.getString("label.out_to_textbox"));
  
      annotationPanelMenuItem.setActionCommand("");
      annotationPanelMenuItem
              .setText(MessageManager.getString("label.show_annotations"));
      final JCheckBoxMenuItem sortAnnByLabel = new JCheckBoxMenuItem(
              MessageManager.getString("label.sort_annotations_by_label"));
  
      sortAnnBySequence.setSelected(
              sortAnnotationsBy == SequenceAnnotationOrder.SEQUENCE_AND_LABEL);
      sortAnnBySequence.addActionListener(new ActionListener()
          colourTextMenuItem_actionPerformed(e);
        }
      });
+   
      JMenuItem htmlMenuItem = new JMenuItem(
              MessageManager.getString("label.html"));
      htmlMenuItem.addActionListener(new ActionListener()
          htmlMenuItem_actionPerformed(e);
        }
      });
+   
      JMenuItem createBioJS = new JMenuItem(
              MessageManager.getString("label.biojs_html_export"));
      createBioJS.addActionListener(new java.awt.event.ActionListener()
          bioJSMenuItem_actionPerformed(e);
        }
      });
+   
      JMenuItem overviewMenuItem = new JMenuItem(
              MessageManager.getString("label.overview_window"));
      overviewMenuItem.addActionListener(new ActionListener()
          overviewMenuItem_actionPerformed(e);
        }
      });
+   
      undoMenuItem.setEnabled(false);
      undoMenuItem.setText(MessageManager.getString("action.undo"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_Z,
        }
      };
      addMenuActionAndAccelerator(keyStroke, undoMenuItem, al);
+   
      redoMenuItem.setEnabled(false);
      redoMenuItem.setText(MessageManager.getString("action.redo"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_Y,
        }
      };
      addMenuActionAndAccelerator(keyStroke, redoMenuItem, al);
+   
      wrapMenuItem.setText(MessageManager.getString("label.wrap"));
      wrapMenuItem.addActionListener(new ActionListener()
      {
          wrapMenuItem_actionPerformed(e);
        }
      });
+   
      JMenuItem printMenuItem = new JMenuItem(
              MessageManager.getString("action.print"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_P,
        }
      };
      addMenuActionAndAccelerator(keyStroke, printMenuItem, al);
+   
      renderGapsMenuItem
              .setText(MessageManager.getString("action.show_gaps"));
      renderGapsMenuItem.setState(true);
          renderGapsMenuItem_actionPerformed(e);
        }
      });
+   
      JMenuItem findMenuItem = new JMenuItem(
              MessageManager.getString("action.find"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F,
  
      showSeqFeatures.setText(
              MessageManager.getString("label.show_sequence_features"));
      showSeqFeatures.addActionListener(new ActionListener()
      {
        @Override
              .setText(MessageManager.getString("label.show_database_refs"));
      showDbRefsMenuitem.addActionListener(new ActionListener()
      {
+   
        @Override
        public void actionPerformed(ActionEvent e)
        {
          showDbRefs_actionPerformed(e);
        }
+   
      });
      showNpFeatsMenuitem.setText(
              MessageManager.getString("label.show_non_positional_features"));
      showNpFeatsMenuitem.addActionListener(new ActionListener()
      {
+   
        @Override
        public void actionPerformed(ActionEvent e)
        {
          showNpFeats_actionPerformed(e);
        }
+   
      });
      showGroupConservation
              .setText(MessageManager.getString("label.group_conservation"));
      showGroupConservation.addActionListener(new ActionListener()
      {
+   
        @Override
        public void actionPerformed(ActionEvent e)
        {
          showGroupConservation_actionPerformed(e);
        }
+   
      });
  
      showGroupConsensus
              .setText(MessageManager.getString("label.group_consensus"));
      showGroupConsensus.addActionListener(new ActionListener()
      {
+   
        @Override
        public void actionPerformed(ActionEvent e)
        {
          showGroupConsensus_actionPerformed(e);
        }
+   
      });
      showConsensusHistogram.setText(
              MessageManager.getString("label.show_consensus_histogram"));
      showConsensusHistogram.addActionListener(new ActionListener()
      {
+   
        @Override
        public void actionPerformed(ActionEvent e)
        {
          showConsensusHistogram_actionPerformed(e);
        }
+   
      });
      showSequenceLogo
              .setText(MessageManager.getString("label.show_consensus_logo"));
      showSequenceLogo.addActionListener(new ActionListener()
      {
+   
        @Override
        public void actionPerformed(ActionEvent e)
        {
          showSequenceLogo_actionPerformed(e);
        }
+   
      });
      normaliseSequenceLogo
              .setText(MessageManager.getString("label.norm_consensus_logo"));
      normaliseSequenceLogo.addActionListener(new ActionListener()
      {
+   
        @Override
        public void actionPerformed(ActionEvent e)
        {
          normaliseSequenceLogo_actionPerformed(e);
        }
+   
      });
      applyAutoAnnotationSettings
              .setText(MessageManager.getString("label.apply_all_groups"));
          applyAutoAnnotationSettings_actionPerformed(e);
        }
      });
+   
      ButtonGroup buttonGroup = new ButtonGroup();
      final JRadioButtonMenuItem showAutoFirst = new JRadioButtonMenuItem(
              MessageManager.getString("label.show_first"));
          sortAnnotations_actionPerformed();
        }
      });
+   
      JMenuItem deleteGroups = new JMenuItem(
              MessageManager.getString("action.undefine_groups"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_U,
        }
      };
      addMenuActionAndAccelerator(keyStroke, deleteGroups, al);
+   
      JMenuItem annotationColumn = new JMenuItem(
              MessageManager.getString("action.select_by_annotation"));
      annotationColumn.addActionListener(new ActionListener()
          annotationColumn_actionPerformed(e);
        }
      });
+   
      JMenuItem createGroup = new JMenuItem(
              MessageManager.getString("action.create_group"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_G,
        }
      };
      addMenuActionAndAccelerator(keyStroke, createGroup, al);
+   
      JMenuItem unGroup = new JMenuItem(
              MessageManager.getString("action.remove_group"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_G,
        }
      };
      addMenuActionAndAccelerator(keyStroke, unGroup, al);
+   
      copy.setText(MessageManager.getString("action.copy"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_C,
              jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false);
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        copy_actionPerformed(e);
 +        copy_actionPerformed();
        }
      };
      addMenuActionAndAccelerator(keyStroke, copy, al);
+   
      cut.setText(MessageManager.getString("action.cut"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_X,
              jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false);
        @Override
        public void actionPerformed(ActionEvent e)
        {
 -        cut_actionPerformed(e);
 +        cut_actionPerformed();
        }
      };
      addMenuActionAndAccelerator(keyStroke, cut, al);
+   
      JMenuItem delete = new JMenuItem(
              MessageManager.getString("action.delete"));
      delete.addActionListener(new ActionListener()
        @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)
        {
-         pasteNew_actionPerformed(e);
+         try
+         {
+           pasteNew_actionPerformed(e);
+         } catch (IOException | InterruptedException e1)
+         {
+           // TODO Auto-generated catch block
+           e1.printStackTrace();
+         }
        }
      };
      addMenuActionAndAccelerator(keyStroke, pasteNew, al);
+   
      JMenuItem pasteThis = new JMenuItem(
              MessageManager.getString("label.to_this_alignment"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_V,
        @Override
        public void actionPerformed(ActionEvent e)
        {
-         pasteThis_actionPerformed(e);
+         try
+         {
+           pasteThis_actionPerformed(e);
+         } catch (IOException | InterruptedException e1)
+         {
+           // TODO Auto-generated catch block
+           e1.printStackTrace();
+         }
        }
      };
      addMenuActionAndAccelerator(keyStroke, pasteThis, al);
+   
      JMenuItem createPNG = new JMenuItem("PNG");
      createPNG.addActionListener(new ActionListener()
      {
      });
      createPNG.setActionCommand(
              MessageManager.getString("label.save_png_image"));
      JMenuItem font = new JMenuItem(MessageManager.getString("action.font"));
      font.addActionListener(new ActionListener()
      {
          createEPS(null);
        }
      });
+   
      JMenuItem createSVG = new JMenuItem("SVG");
      createSVG.addActionListener(new ActionListener()
      {
          createSVG(null);
        }
      });
+   
      JMenuItem loadTreeMenuItem = new JMenuItem(
              MessageManager.getString("label.load_associated_tree"));
      loadTreeMenuItem.setActionCommand(
          loadTreeMenuItem_actionPerformed(e);
        }
      });
+   
      scaleAbove.setVisible(false);
      scaleAbove.setText(MessageManager.getString("action.scale_above"));
      scaleAbove.addActionListener(new ActionListener()
              .setText(MessageManager.getString("label.automatic_scrolling"));
      followHighlightMenuItem.addActionListener(new ActionListener()
      {
+   
        @Override
        public void actionPerformed(ActionEvent e)
        {
          followHighlight_actionPerformed();
        }
+   
      });
+   
      sortByTreeMenu
              .setText(MessageManager.getString("action.by_tree_order"));
      sort.setText(MessageManager.getString("action.sort"));
        {
          buildTreeSortMenu();
        }
+   
        @Override
        public void menuDeselected(MenuEvent e)
        {
        }
+   
        @Override
        public void menuCanceled(MenuEvent e)
        {
      sort.add(sortByAnnotScore);
      sort.addMenuListener(new javax.swing.event.MenuListener()
      {
+   
        @Override
        public void menuCanceled(MenuEvent e)
        {
        }
+   
        @Override
        public void menuDeselected(MenuEvent e)
        {
        }
+   
        @Override
        public void menuSelected(MenuEvent e)
        {
      /*
       * Translate as cDNA with sub-menu of translation tables
       */
 -    showTranslation.setText(MessageManager
 -            .getString("label.translate_cDNA"));
 +    showTranslation
 +            .setText(MessageManager.getString("label.translate_cDNA"));
      boolean first = true;
      for (final GeneticCodeI table : GeneticCodes.getInstance()
              .getCodeTables())
          showReverse_actionPerformed(true);
        }
      });
+   
      JMenuItem extractScores = new JMenuItem(
              MessageManager.getString("label.extract_scores"));
      extractScores.addActionListener(new ActionListener()
      });
      extractScores.setVisible(true);
      // JBPNote: TODO: make gui for regex based score extraction
+   
      // for show products actions see AlignFrame.canShowProducts
      showProducts.setText(MessageManager.getString("label.get_cross_refs"));
+   
      runGroovy.setText(MessageManager.getString("label.run_groovy"));
      runGroovy.setToolTipText(
              MessageManager.getString("label.run_groovy_tip"));
          runGroovy_actionPerformed();
        }
      });
 -  
 -    JMenuItem openFeatureSettings = new JMenuItem(
 +
 +    openFeatureSettings = new JMenuItem(
              MessageManager.getString("action.feature_settings"));
      openFeatureSettings.addActionListener(new ActionListener()
      {
          featureSettings_actionPerformed(e);
        }
      });
 +
 +    /*
 +     * add sub-menu of database we can fetch from
 +     */
      JMenuItem fetchSequence = new JMenuItem(
              MessageManager.getString("label.fetch_sequences"));
      fetchSequence.addActionListener(new ActionListener()
        @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()
      {
          listenToViewSelections_actionPerformed(e);
        }
      });
+   
      JMenu addSequenceMenu = new JMenu(
              MessageManager.getString("label.add_sequences"));
      JMenuItem addFromFile = new JMenuItem(
          hiddenMarkers_actionPerformed(e);
        }
      });
+   
      JMenuItem invertColSel = new JMenuItem(
              MessageManager.getString("action.invert_column_selection"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_I,
        }
      };
      addMenuActionAndAccelerator(keyStroke, invertColSel, al);
+   
      showComplementMenuItem.setVisible(false);
      showComplementMenuItem.addActionListener(new ActionListener()
      {
          showComplement_actionPerformed(showComplementMenuItem.getState());
        }
      });
+   
      tabbedPane.addChangeListener(new javax.swing.event.ChangeListener()
      {
        @Override
            tabbedPane_mousePressed(e);
          }
        }
+   
        @Override
        public void mouseReleased(MouseEvent e)
        {
          tabbedPane_focusGained(e);
        }
      });
+   
      JMenuItem save = new JMenuItem(MessageManager.getString("action.save"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S,
              jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false);
        }
      };
      addMenuActionAndAccelerator(keyStroke, save, al);
+   
      reload.setEnabled(false);
      reload.setText(MessageManager.getString("action.reload"));
      reload.addActionListener(new ActionListener()
          reload_actionPerformed(e);
        }
      });
+   
      JMenuItem newView = new JMenuItem(
              MessageManager.getString("action.new_view"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_T,
        }
      };
      addMenuActionAndAccelerator(keyStroke, newView, al);
+   
      tabbedPane.setToolTipText("<html><i>"
              + MessageManager.getString("label.rename_tab_eXpand_reGroup")
              + "</i></html>");
+   
      formatMenu.setText(MessageManager.getString("action.format"));
      JMenu selectMenu = new JMenu(MessageManager.getString("action.select"));
  
          idRightAlign_actionPerformed(e);
        }
      });
+   
      gatherViews.setEnabled(false);
      gatherViews.setText(MessageManager.getString("action.gather_views"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_G, 0, false);
        }
      };
      addMenuActionAndAccelerator(keyStroke, gatherViews, al);
+   
      expandViews.setEnabled(false);
      expandViews.setText(MessageManager.getString("action.expand_views"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_X, 0, false);
        }
      };
      addMenuActionAndAccelerator(keyStroke, expandViews, al);
+   
      JMenuItem pageSetup = new JMenuItem(
              MessageManager.getString("action.page_setup"));
      pageSetup.addActionListener(new ActionListener()
          selectHighlightedColumns_actionPerformed(actionEvent);
        }
      };
+     JMenuItem Filter = new JMenuItem(
+             MessageManager.getString("action.select_highlighted_columns"));
+     selectHighlighted.setToolTipText(
+             MessageManager.getString("tooltip.select_highlighted_columns"));
+     al = new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent actionEvent)
+       {
+         selectHighlightedColumns_actionPerformed(actionEvent);
+       }
+     };
      selectHighlighted.addActionListener(al);
      JMenu tooltipSettingsMenu = new JMenu(
              MessageManager.getString("label.sequence_id_tooltip"));
      JMenu autoAnnMenu = new JMenu(
              MessageManager.getString("label.autocalculated_annotation"));
+   
      JMenu exportImageMenu = new JMenu(
              MessageManager.getString("label.export_image"));
      JMenu fileMenu = new JMenu(MessageManager.getString("action.file"));
      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);
      // editMenu.add(justifyRightMenuItem);
      // editMenu.addSeparator();
      editMenu.add(padGapsMenuitem);
+     editMenu.addSeparator();
+     editMenu.add(filterByEValue);
+     editMenu.add(filterByScore);
+   
      showMenu.add(showAllColumns);
      showMenu.add(showAllSeqs);
      showMenu.add(showAllhidden);
      viewMenu.add(alignmentProperties);
      viewMenu.addSeparator();
      viewMenu.add(overviewMenuItem);
+   
      annotationsMenu.add(annotationPanelMenuItem);
      annotationsMenu.addSeparator();
      annotationsMenu.add(showAllAlAnnotations);
      sort.add(sortLengthMenuItem);
      sort.add(sortGroupMenuItem);
      sort.add(sortPairwiseMenuItem);
+     sort.add(sortEValueMenuItem);
+     sort.add(sortBitScoreMenuItem);
      sort.add(sortByTreeMenu);
      calculateMenu.add(sort);
      calculateMenu.add(calculateTree);
      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);
 -    exportImageMenu.add(htmlMenuItem);
 +    if (!Platform.isJS())
 +    {
 +      exportImageMenu.add(htmlMenuItem);
 +    }
      exportImageMenu.add(epsFile);
      exportImageMenu.add(createPNG);
 -    exportImageMenu.add(createBioJS);
 -    exportImageMenu.add(createSVG);
 +    if (!Platform.isJS())
 +    {
 +      exportImageMenu.add(createBioJS);
 +      exportImageMenu.add(createSVG);
 +    }
      addSequenceMenu.add(addFromFile);
      addSequenceMenu.add(addFromText);
      addSequenceMenu.add(addFromURL);
      this.getContentPane().add(statusPanel, java.awt.BorderLayout.SOUTH);
      statusPanel.add(statusBar, null);
      this.getContentPane().add(tabbedPane, java.awt.BorderLayout.CENTER);
+   
      formatMenu.add(font);
      formatMenu.addSeparator();
      formatMenu.add(wrapMenuItem);
      // selectMenu.add(listenToViewSelections);
    }
  
+   /**
+    * Constructs the entries on the HMMER menu
+    */
+   protected void initHMMERMenu()
+   {
+     /*
+      * hmmbuild
+      */
+     JMenu hmmBuild = new JMenu(MessageManager.getString("label.hmmbuild"));
+     JMenuItem hmmBuildSettings = new JMenuItem(
+             MessageManager.getString("label.edit_settings_and_run"));
+     hmmBuildSettings.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         hmmBuild_actionPerformed(false);
+       }
+     });
+     JMenuItem hmmBuildRun = new JMenuItem(MessageManager.formatMessage(
+             "label.action_with_default_settings", "hmmbuild"));
+     hmmBuildRun.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         hmmBuild_actionPerformed(true);
+       }
+     });
+     hmmBuild.add(hmmBuildRun);
+     hmmBuild.add(hmmBuildSettings);
+     /*
+      * hmmalign
+      */
+     JMenu hmmAlign = new JMenu(MessageManager.getString("label.hmmalign"));
+     JMenuItem hmmAlignRun = new JMenuItem(MessageManager.formatMessage(
+             "label.action_with_default_settings", "hmmalign"));
+     hmmAlignRun.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         hmmAlign_actionPerformed(true);
+       }
+     });
+     JMenuItem hmmAlignSettings = new JMenuItem(
+             MessageManager.getString("label.edit_settings_and_run"));
+     hmmAlignSettings.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         hmmAlign_actionPerformed(false);
+       }
+     });
+     hmmAlign.add(hmmAlignRun);
+     hmmAlign.add(hmmAlignSettings);
+     /*
+      * hmmsearch
+      */
+     JMenu hmmSearch = new JMenu(
+             MessageManager.getString("label.hmmsearch"));
+     JMenuItem hmmSearchSettings = new JMenuItem(
+             MessageManager.getString("label.edit_settings_and_run"));
+     hmmSearchSettings.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         hmmSearch_actionPerformed(false);
+       }
+     });
+     JMenuItem hmmSearchRun = new JMenuItem(MessageManager.formatMessage(
+             "label.action_with_default_settings", "hmmsearch"));
+     hmmSearchRun.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         hmmSearch_actionPerformed(true);
+       }
+     });
+     JMenuItem addDatabase = new JMenuItem(
+             MessageManager.getString("label.add_database"));
+     addDatabase.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         try
+         {
+           addDatabase_actionPerformed();
+         } catch (IOException e1)
+         {
+           e1.printStackTrace();
+         }
+       }
+     });
+     hmmSearch.add(hmmSearchRun);
+     hmmSearch.add(hmmSearchSettings);
+     // hmmSearch.add(addDatabase);
+     /*
+      * jackhmmer
+      */
+     JMenu jackhmmer = new JMenu(
+             MessageManager.getString("label.jackhmmer"));
+     JMenuItem jackhmmerSettings = new JMenuItem(
+             MessageManager.getString("label.edit_settings_and_run"));
+     jackhmmerSettings.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         jackhmmer_actionPerformed(false);
+       }
+     });
+     JMenuItem jackhmmerRun = new JMenuItem(MessageManager.formatMessage(
+             "label.action_with_default_settings", "jackhmmer"));
+     jackhmmerRun.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         jackhmmer_actionPerformed(true);
+       }
+     });
+     /*
+     JMenuItem addDatabase = new JMenuItem(
+             MessageManager.getString("label.add_database"));
+     addDatabase.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         try
+         {
+           addDatabase_actionPerformed();
+         } catch (IOException e1)
+         {
+           e1.printStackTrace();
+         }
+       }
+     });
+     */
+     jackhmmer.add(jackhmmerRun);
+     jackhmmer.add(jackhmmerSettings);
+     // hmmSearch.add(addDatabase);
+     /*
+      * top level menu
+      */
+     hmmerMenu.setText(MessageManager.getString("action.hmmer"));
+     hmmerMenu.setEnabled(HmmerCommand.isHmmerAvailable());
+     hmmerMenu.add(hmmBuild);
+     hmmerMenu.add(hmmAlign);
+     hmmerMenu.add(hmmSearch);
+     hmmerMenu.add(jackhmmer);
+   }
    protected void loadVcf_actionPerformed()
    {
    }
    {
    }
  
 -  protected void outputText_actionPerformed(ActionEvent e)
 +  protected void outputText_actionPerformed(String formatName)
    {
    }
  
    {
    }
  
+   protected void sortEValueMenuItem_actionPerformed(ActionEvent e)
+   {
+   }
+   protected void sortBitScoreMenuItem_actionPerformed(ActionEvent e)
+   {
+   }
    protected void removeRedundancyMenuItem_actionPerformed(ActionEvent e)
    {
    }
    {
    }
  
 -  protected void copy_actionPerformed(ActionEvent e)
 +  protected void copy_actionPerformed()
    {
    }
  
 -  protected void cut_actionPerformed(ActionEvent e)
 +  protected void cut_actionPerformed()
    {
    }
  
 -  protected void delete_actionPerformed(ActionEvent e)
 +  protected void delete_actionPerformed()
    {
    }
  
    protected void pasteNew_actionPerformed(ActionEvent e)
+           throws IOException, InterruptedException
    {
    }
  
    protected void pasteThis_actionPerformed(ActionEvent e)
+           throws IOException, InterruptedException
    {
    }
  
    {
    }
  
+   protected void hmmBuild_actionPerformed(boolean withDefaults)
+   {
+   }
+   protected void hmmSearch_actionPerformed(boolean withDefaults)
+   {
+   }
+   protected void jackhmmer_actionPerformed(boolean b)
+   {
+   }
+   protected void addDatabase_actionPerformed()
+           throws FileFormatException, IOException
+   {
+   }
+   protected void hmmAlign_actionPerformed(boolean withDefaults)
+   {
+   }
    public void createPNG(java.io.File f)
    {
    }
    {
    }
  
+   protected void filterByEValue_actionPerformed()
+   {
+   }
+   protected void filterByScore_actionPerformed()
+   {
+   }
    protected void scaleRight_actionPerformed(ActionEvent e)
    {
    }
    {
    }
  
 -  protected void saveAs_actionPerformed(ActionEvent e)
 +  protected void saveAs_actionPerformed()
    {
    }
  
  
    }
  
 -  public void fetchSequence_actionPerformed(ActionEvent e)
 +  public void fetchSequence_actionPerformed()
    {
  
    }
    }
  
    public void associatedData_actionPerformed(ActionEvent e)
+           throws IOException, InterruptedException
    {
  
    }
@@@ -49,7 -49,6 +49,6 @@@ import java.awt.Insets
  import java.awt.Rectangle;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
- import java.awt.event.FocusEvent;
  import java.awt.event.KeyEvent;
  import java.awt.event.KeyListener;
  import java.awt.event.MouseAdapter;
@@@ -57,13 -56,16 +56,16 @@@ import java.awt.event.MouseEvent
  import java.util.Arrays;
  import java.util.List;
  
+ import javax.swing.AbstractButton;
  import javax.swing.AbstractCellEditor;
  import javax.swing.BorderFactory;
+ import javax.swing.BoxLayout;
  import javax.swing.ButtonGroup;
  import javax.swing.DefaultListCellRenderer;
  import javax.swing.JButton;
  import javax.swing.JCheckBox;
  import javax.swing.JComboBox;
+ import javax.swing.JComponent;
  import javax.swing.JFileChooser;
  import javax.swing.JLabel;
  import javax.swing.JPanel;
@@@ -87,6 -89,8 +89,8 @@@ import javax.swing.event.ChangeListener
  import javax.swing.table.TableCellEditor;
  import javax.swing.table.TableCellRenderer;
  
+ import net.miginfocom.swing.MigLayout;
  /**
   * Base class for the Preferences panel.
   * 
@@@ -161,10 -165,15 +165,15 @@@ public class GPreferences extends JPane
  
    protected JCheckBox showConsensLogo = new JCheckBox();
  
+   protected JCheckBox showInformationHistogram = new JCheckBox();
+   protected JCheckBox showHMMLogo = new JCheckBox();
    protected JCheckBox showDbRefTooltip = new JCheckBox();
  
    protected JCheckBox showNpTooltip = new JCheckBox();
  
    /*
     * Structure tab and components
     */
     */
    protected JComboBox<Object> epsRendering = new JComboBox<>();
  
 +  protected JComboBox<Object> htmlRendering = new JComboBox<>();
 +
 +  protected JComboBox<Object> svgRendering = new JComboBox<>();
 +
    protected JLabel userIdWidthlabel = new JLabel();
  
    protected JCheckBox autoIdWidth = new JCheckBox();
    protected JCheckBox sortByTree = new JCheckBox();
  
    /*
+    * hmmer tab and components
+    */
+   protected JPanel hmmerTab;
+   protected JCheckBox hmmrTrimTermini;
+   protected AbstractButton hmmerBackgroundUniprot;
+   protected AbstractButton hmmerBackgroundAlignment;
+   protected JTextField hmmerSequenceCount;
+   protected JTextField hmmerPath;
+   protected JTextField cygwinPath;
+   /*
     * Web Services tab
     */
    protected JPanel wsTab = new JPanel();
      tabbedPane.add(initConnectionsTab(),
              MessageManager.getString("label.connections"));
  
 -    tabbedPane.add(initBackupsTab(),
 -            MessageManager.getString("label.backups"));
 +      if (!Platform.isJS()) 
 +      {
 +        tabbedPane.add(initBackupsTab(), 
 +                      MessageManager.getString("label.backups"));
 +      }
  
      tabbedPane.add(initLinksTab(),
              MessageManager.getString("label.urllinks"));
      tabbedPane.add(initEditingTab(),
              MessageManager.getString("label.editing"));
  
+     tabbedPane.add(initHMMERTab(), MessageManager.getString("label.hmmer"));
      /*
       * See WsPreferences for the real work of configuring this tab.
       */
 -    wsTab.setLayout(new BorderLayout());
 -    tabbedPane.add(wsTab, MessageManager.getString("label.web_services"));
 +    if (!Platform.isJS())
 +    {
 +      wsTab.setLayout(new BorderLayout());
 +      tabbedPane.add(wsTab, MessageManager.getString("label.web_services"));
 +    }
  
      /*
       * Handler to validate a tab before leaving it - currently only for
-      * Structure.
+      * Structure
       */
      tabbedPane.addChangeListener(new ChangeListener()
      {
    }
  
    /**
-    * 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
     */
    {
      JPanel outputTab = new JPanel();
      outputTab.setLayout(null);
 -    JLabel epsLabel = new JLabel();
 +
 +    JLabel epsLabel = new JLabel(
 +            MessageManager.formatMessage("label.rendering_style", "EPS"));
      epsLabel.setFont(LABEL_FONT);
      epsLabel.setHorizontalAlignment(SwingConstants.RIGHT);
 -    epsLabel.setText(MessageManager.getString("label.eps_rendering_style"));
 -    epsLabel.setBounds(new Rectangle(9, 31, 140, 24));
 +    epsLabel.setBounds(new Rectangle(9, 31, 160, 24));
      epsRendering.setFont(LABEL_FONT);
 -    epsRendering.setBounds(new Rectangle(154, 34, 187, 21));
 +    epsRendering.setBounds(new Rectangle(174, 34, 187, 21));
 +    JLabel htmlLabel = new JLabel(
 +            MessageManager.formatMessage("label.rendering_style", "HTML"));
 +    htmlLabel.setFont(LABEL_FONT);
 +    htmlLabel.setHorizontalAlignment(SwingConstants.RIGHT);
 +    htmlLabel.setBounds(new Rectangle(9, 55, 160, 24));
 +    htmlRendering.setFont(LABEL_FONT);
 +    htmlRendering.setBounds(new Rectangle(174, 58, 187, 21));
 +    JLabel svgLabel = new JLabel(
 +            MessageManager.formatMessage("label.rendering_style", "SVG"));
 +    svgLabel.setFont(LABEL_FONT);
 +    svgLabel.setHorizontalAlignment(SwingConstants.RIGHT);
 +    svgLabel.setBounds(new Rectangle(9, 79, 160, 24));
 +    svgRendering.setFont(LABEL_FONT);
 +    svgRendering.setBounds(new Rectangle(174, 82, 187, 21));
 +
      JLabel jLabel1 = new JLabel();
      jLabel1.setFont(LABEL_FONT);
      jLabel1.setHorizontalAlignment(SwingConstants.CENTER);
      jLabel1.setText(MessageManager.getString("label.append_start_end"));
      jLabel1.setFont(LABEL_FONT);
 +
      fastajv.setFont(LABEL_FONT);
      fastajv.setHorizontalAlignment(SwingConstants.LEFT);
      clustaljv.setText(MessageManager.getString("label.clustal") + "     ");
      TitledBorder titledBorder2 = new TitledBorder(
              MessageManager.getString("label.file_output"));
      jPanel11.setBorder(titledBorder2);
 -    jPanel11.setBounds(new Rectangle(30, 72, 196, 182));
 +    jPanel11.setBounds(new Rectangle(30, 120, 196, 182));
      GridLayout gridLayout3 = new GridLayout();
      jPanel11.setLayout(gridLayout3);
      gridLayout3.setRows(8);
              MessageManager.getString("label.automatically_set_id_width"));
      autoIdWidth.setToolTipText(JvSwingUtils.wrapTooltip(true, MessageManager
              .getString("label.adjusts_width_generated_eps_png")));
 -    autoIdWidth.setBounds(new Rectangle(228, 96, 188, 23));
 +    autoIdWidth.setBounds(new Rectangle(228, 144, 320, 23));
      autoIdWidth.addActionListener(new ActionListener()
      {
  
      userIdWidthlabel.setToolTipText(
              JvSwingUtils.wrapTooltip(true, MessageManager.getString(
                      "label.manually_specify_width_left_column")));
 -    userIdWidthlabel.setBounds(new Rectangle(236, 120, 168, 23));
 +    userIdWidthlabel.setBounds(new Rectangle(236, 168, 320, 23));
      userIdWidth.setFont(JvSwingUtils.getTextAreaFont());
      userIdWidth.setText("");
 -    userIdWidth.setBounds(new Rectangle(232, 144, 84, 23));
 +    userIdWidth.setBounds(new Rectangle(232, 192, 84, 23));
      userIdWidth.addActionListener(new ActionListener()
      {
  
      modellerOutput.setFont(LABEL_FONT);
      modellerOutput
              .setText(MessageManager.getString("label.use_modeller_output"));
 -    modellerOutput.setBounds(new Rectangle(228, 226, 168, 23));
 +    modellerOutput.setBounds(new Rectangle(228, 274, 320, 23));
      embbedBioJSON.setFont(LABEL_FONT);
      embbedBioJSON.setText(MessageManager.getString("label.embbed_biojson"));
 -    embbedBioJSON.setBounds(new Rectangle(228, 200, 250, 23));
 +    embbedBioJSON.setBounds(new Rectangle(228, 248, 250, 23));
  
      jPanel11.add(jLabel1);
      jPanel11.add(blcjv);
      outputTab.add(userIdWidth);
      outputTab.add(userIdWidthlabel);
      outputTab.add(modellerOutput);
 -    outputTab.add(embbedBioJSON);
 -    outputTab.add(epsLabel);
 -    outputTab.add(epsRendering);
 +    if (!Platform.isJS())
 +    {
 +      /*
 +       * JalviewJS doesn't support Lineart option or SVG output
 +       */
 +      outputTab.add(embbedBioJSON);
 +      outputTab.add(epsLabel);
 +      outputTab.add(epsRendering);
 +      outputTab.add(htmlLabel);
 +      outputTab.add(htmlRendering);
 +      outputTab.add(svgLabel);
 +      outputTab.add(svgRendering);
 +    }
      outputTab.add(jPanel11);
      return outputTab;
    }
      protColourLabel.setHorizontalAlignment(SwingConstants.LEFT);
      protColourLabel.setText(
              MessageManager.getString("label.prot_alignment_colour") + " ");
-     JvSwingUtils.addtoLayout(coloursTab,
+     GPreferences.addtoLayout(coloursTab,
              MessageManager
                      .getString("label.default_colour_scheme_for_alignment"),
              protColourLabel, protColour);
      nucColourLabel.setHorizontalAlignment(SwingConstants.LEFT);
      nucColourLabel.setText(
              MessageManager.getString("label.nuc_alignment_colour") + " ");
-     JvSwingUtils.addtoLayout(coloursTab,
+     GPreferences.addtoLayout(coloursTab,
              MessageManager
                      .getString("label.default_colour_scheme_for_alignment"),
              nucColourLabel, nucColour);
      annotationShding.setBorder(new TitledBorder(
              MessageManager.getString("label.annotation_shading_default")));
      annotationShding.setLayout(new GridLayout(1, 2));
-     JvSwingUtils.addtoLayout(annotationShding,
+     GPreferences.addtoLayout(annotationShding,
              MessageManager.getString(
                      "label.default_minimum_colour_annotation_shading"),
              mincolourLabel, minColour);
-     JvSwingUtils.addtoLayout(annotationShding,
+     GPreferences.addtoLayout(annotationShding,
              MessageManager.getString(
                      "label.default_maximum_colour_annotation_shading"),
              maxcolourLabel, maxColour);
        {
          if (e.getClickCount() == 2)
          {
-           String chosen = openFileChooser();
+           String chosen = openFileChooser(false);
            if (chosen != null)
            {
              chimeraPath.setText(chosen);
      docFieldPref.setBounds(new Rectangle(10, ypos, 450, 120));
      structureTab.add(docFieldPref);
  
 +    /*
 +     * hide Chimera options in JalviewJS
 +     */
 +    if (Platform.isJS()) 
 +    {
 +      pathLabel.setVisible(false);
 +      chimeraPath.setVisible(false);
 +      viewerLabel.setVisible(false);
 +      structViewer.setVisible(false);
 +    }
 +    
      return structureTab;
    }
  
     * 
     * @return
     */
-   protected String openFileChooser()
+   protected String openFileChooser(boolean forFolder)
    {
      String choice = null;
      JFileChooser chooser = new JFileChooser();
+     if (forFolder)
+     {
+       chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+     }
  
      // chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(
      return choice;
    }
  
-   /**
-    * Validate the structure tab preferences; if invalid, set focus on this tab.
-    * 
-    * @param e
-    */
-   protected boolean validateStructure(FocusEvent e)
-   {
-     if (!validateStructure())
-     {
-       e.getComponent().requestFocusInWindow();
-       return false;
-     }
-     return true;
-   }
    protected boolean validateStructure()
    {
      return false;
      visualTab.add(fontNameCB);
      visualTab.add(fontSizeCB);
      visualTab.add(fontStyleCB);
 +    
 +    if (Platform.isJS())
 +    {
 +      startupCheckbox.setVisible(false);
 +      startupFileTextfield.setVisible(false);
 +    }
 +    
      return visualTab;
    }
  
      BackupFilesPresetEntry savedPreset = BackupFilesPresetEntry
              .getSavedBackupEntry();
      enableBackupFiles
 -            .setSelected(Cache.getDefault(BackupFiles.ENABLED, true));
 +            .setSelected(Cache.getDefault(BackupFiles.ENABLED, !Platform.isJS()));
  
      BackupFilesPresetEntry backupfilesCustomEntry = BackupFilesPresetEntry
              .createBackupFilesPresetEntry(Cache
      }
  
    }
+   protected void validateHmmerPath()
+   {
+   }
+   protected void validateCygwinPath()
+   {
+   }
+   /**
+    * A helper method to add a panel containing a label and a component to a
+    * panel
+    * 
+    * @param panel
+    * @param tooltip
+    * @param label
+    * @param valBox
+    */
+   protected static void addtoLayout(JPanel panel, String tooltip,
+           JComponent label, JComponent valBox)
+   {
+     JPanel laypanel = new JPanel(new GridLayout(1, 2));
+     JPanel labPanel = new JPanel(new BorderLayout());
+     JPanel valPanel = new JPanel();
+     labPanel.setBounds(new Rectangle(7, 7, 158, 23));
+     valPanel.setBounds(new Rectangle(172, 7, 270, 23));
+     labPanel.add(label, BorderLayout.WEST);
+     valPanel.add(valBox);
+     laypanel.add(labPanel);
+     laypanel.add(valPanel);
+     valPanel.setToolTipText(tooltip);
+     labPanel.setToolTipText(tooltip);
+     valBox.setToolTipText(tooltip);
+     panel.add(laypanel);
+     panel.validate();
+   }
  }
  
@@@ -20,7 -20,6 +20,6 @@@
   */
  package jalview.jbgui;
  
- import jalview.bin.Jalview;
  import jalview.gui.JvSwingUtils;
  import jalview.util.MessageManager;
  
@@@ -75,7 -74,7 +74,7 @@@ public class GUserDefinedColours extend
  
    JLabel jLabel1 = new JLabel();
  
 -  protected JTextField schemeName = new JTextField();
 +  public JTextField schemeName = new JTextField();
  
    BorderLayout borderLayout1 = new BorderLayout();
  
  
    protected JPanel casePanel = new JPanel();
  
 -  protected JCheckBox caseSensitive = new JCheckBox();
 +  public JCheckBox caseSensitive = new JCheckBox();
  
 -  protected JCheckBox lcaseColour = new JCheckBox();
 +  public JCheckBox lcaseColour = new JCheckBox();
  
    protected List<JButton> selectedButtons;
  
    {
    }
  
 -  protected boolean savebutton_actionPerformed()
 +  protected void savebutton_actionPerformed()
    {
 -    return false;
    }
  
    /**
@@@ -41,6 -41,7 +41,7 @@@ import jalview.datamodel.AlignmentI
  import jalview.datamodel.DBRefEntry;
  import jalview.datamodel.GeneLocus;
  import jalview.datamodel.GraphLine;
+ import jalview.datamodel.HiddenMarkovModel;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.Point;
  import jalview.datamodel.RnaViewerModel;
@@@ -72,6 -73,7 +73,7 @@@ import jalview.gui.TreePanel
  import jalview.io.BackupFiles;
  import jalview.io.DataSourceType;
  import jalview.io.FileFormat;
+ import jalview.io.HMMFile;
  import jalview.io.NewickFile;
  import jalview.math.Matrix;
  import jalview.math.MatrixI;
@@@ -96,9 -98,9 +98,9 @@@ import jalview.viewmodel.ViewportRanges
  import jalview.viewmodel.seqfeatures.FeatureRendererModel;
  import jalview.viewmodel.seqfeatures.FeatureRendererSettings;
  import jalview.viewmodel.seqfeatures.FeaturesDisplayed;
- import jalview.ws.jws2.Jws2Discoverer;
+ import jalview.ws.api.ServiceWithParameters;
+ import jalview.ws.jws2.PreferredServiceRegistry;
  import jalview.ws.jws2.dm.AAConSettings;
- import jalview.ws.jws2.jabaws2.Jws2Instance;
  import jalview.ws.params.ArgumentI;
  import jalview.ws.params.AutoCalcSetting;
  import jalview.ws.params.WsParamSetI;
@@@ -154,7 -156,6 +156,7 @@@ import java.awt.Color
  import java.awt.Font;
  import java.awt.Rectangle;
  import java.io.BufferedReader;
 +import java.io.ByteArrayInputStream;
  import java.io.DataInputStream;
  import java.io.DataOutputStream;
  import java.io.File;
@@@ -211,19 -212,12 +213,21 @@@ import javax.xml.stream.XMLStreamReader
   */
  public class Jalview2XML
  {
 +
 +  // BH 2018 we add the .jvp binary extension to J2S so that
 +  // it will declare that binary when we do the file save from the browser
 +
 +  static
 +  {
 +    Platform.addJ2SBinaryType(".jvp?");
 +  }
 +
    private static final String VIEWER_PREFIX = "viewer_";
  
    private static final String RNA_PREFIX = "rna_";
  
+   private static final String HMMER_PREFIX = "hmmer_";
    private static final String UTF_8 = "UTF-8";
  
    /**
        public boolean isResolvable()
        {
          return super.isResolvable() && mp.getTo() != null;
 -      };
 +      }
  
        @Override
        boolean resolve()
        } catch (Exception foo)
        {
        }
 -      ;
        jout.close();
      } catch (Exception ex)
      {
      try
      {
        // create backupfiles object and get new temp filename destination
 -      BackupFiles backupfiles = new BackupFiles(jarFile);
 -      FileOutputStream fos = new FileOutputStream(
 -              backupfiles.getTempFilePath());
 +      boolean doBackup = BackupFiles.getEnabled();
 +      BackupFiles backupfiles = doBackup ? new BackupFiles(jarFile) : null;
 +      FileOutputStream fos = new FileOutputStream(doBackup ? 
 +              backupfiles.getTempFilePath() : jarFile);
  
        JarOutputStream jout = new JarOutputStream(fos);
        List<AlignFrame> frames = new ArrayList<>();
        } catch (Exception foo)
        {
        }
        jout.close();
        boolean success = true;
  
 -      backupfiles.setWriteSuccess(success);
 -      success = backupfiles.rollBackupsAndRenameTempFile();
 +      if (doBackup)
 +      {
 +        backupfiles.setWriteSuccess(success);
 +        success = backupfiles.rollBackupsAndRenameTempFile();
 +      }
  
        return success;
      } catch (Exception ex)
          jseq.getFeatures().add(features);
        }
  
+       /*
+        * save PDB entries for sequence
+        */
        if (jdatasq.getAllPDBEntries() != null)
        {
          Enumeration<PDBEntry> en = jdatasq.getAllPDBEntries().elements();
  
        saveRnaViewers(jout, jseq, jds, viewIds, ap, storeDS);
  
+       if (jds.hasHMMProfile())
+       {
+         saveHmmerProfile(jout, jseq, jds);
+       }
        // jms.addJSeq(jseq);
        object.getJSeq().add(jseq);
      }
        // using save and then load
        try
        {
 +      fileName = fileName.replace('\\', '/');
          System.out.println("Writing jar entry " + fileName);
          JarEntry entry = new JarEntry(fileName);
          jout.putNextEntry(entry);
      }
      return object;
    }
+   /**
+    * Saves the HMMER profile associated with the sequence as a file in the jar,
+    * in HMMER format, and saves the name of the file as a child element of the
+    * XML sequence element
+    * 
+    * @param jout
+    * @param xmlSeq
+    * @param seq
+    */
+   protected void saveHmmerProfile(JarOutputStream jout, JSeq xmlSeq,
+           SequenceI seq)
+   {
+     HiddenMarkovModel profile = seq.getHMM();
+     if (profile == null)
+     {
+       warn("Want to save HMM profile for " + seq.getName()
+               + " but none found");
+       return;
+     }
+     HMMFile hmmFile = new HMMFile(profile);
+     String hmmAsString = hmmFile.print();
+     String jarEntryName = HMMER_PREFIX + nextCounter();
+     try
+     {
+       writeJarEntry(jout, jarEntryName, hmmAsString.getBytes());
+       xmlSeq.setHmmerProfile(jarEntryName);
+     } catch (IOException e)
+     {
+       warn("Error saving HMM profile: " + e.getMessage());
+     }
+   }
  
+     
    /**
     * Writes PCA viewer attributes and computed values to an XML model object and
     * adds it to the JalviewModel. Any exceptions are reported by logging.
    {
      if (jout != null)
      {
 +      jarEntryName = jarEntryName.replace('\\','/');
        System.out.println("Writing jar entry " + jarEntryName);
        jout.putNextEntry(new JarEntry(jarEntryName));
        DataOutputStream dout = new DataOutputStream(jout);
        }
        else if (!matchedFile.equals(pdbentry.getFile()))
        {
-         Cache.log.warn(
-                 "Probably lost some PDB-Sequence mappings for this structure file (which apparently has same PDB Entry code): "
-                         + pdbentry.getFile());
+         Cache.log.warn(
+                   "Probably lost some PDB-Sequence mappings for this structure file (which apparently has same PDB Entry code): "
+                           + pdbentry.getFile());
        }
        // record the
        // file so we
      if (calcIdParam.getVersion().equals("1.0"))
      {
        final String[] calcIds = calcIdParam.getServiceURL().toArray(new String[0]);
-       Jws2Instance service = Jws2Discoverer.getDiscoverer()
+       ServiceWithParameters service = PreferredServiceRegistry.getRegistry()
                .getPreferredServiceFor(calcIds);
        if (service != null)
        {
            argList = parmSet.getArguments();
            parmSet = null;
          }
-         AAConSettings settings = new AAConSettings(
+         AutoCalcSetting settings = new AAConSettings(
                  calcIdParam.isAutoUpdate(), service, parmSet, argList);
          av.setCalcIdSettingsFor(calcIdParam.getCalcId(), settings,
                  calcIdParam.isNeedsUpdate());
        }
        else
        {
-         warn("Cannot resolve a service for the parameters used in this project. Try configuring a JABAWS server.");
+         warn("Cannot resolve a service for the parameters used in this project. Try configuring a server in the Web Services preferences tab.");
          return false;
        }
      }
      vamsasSeq.setName(jds.getName());
      vamsasSeq.setSequence(jds.getSequenceAsString());
      vamsasSeq.setDescription(jds.getDescription());
 -    jalview.datamodel.DBRefEntry[] dbrefs = null;
 +    List<DBRefEntry> dbrefs = null;
      if (jds.getDatasetSequence() != null)
      {
        vamsasSeq.setDsseqid(seqHash(jds.getDatasetSequence()));
       */
      if (dbrefs != null)
      {
 -      for (int d = 0; d < dbrefs.length; d++)
 +      for (int d = 0, nd = dbrefs.size(); d < nd; d++)
        {
          DBRef dbref = new DBRef();
 -        DBRefEntry dbRefEntry = dbrefs[d];
 -        dbref.setSource(dbRefEntry.getSource());
 -        dbref.setVersion(dbRefEntry.getVersion());
 -        dbref.setAccessionId(dbRefEntry.getAccessionId());
 -        if (dbRefEntry instanceof GeneLocus)
 +        DBRefEntry ref = dbrefs.get(d);
 +        dbref.setSource(ref.getSource());
 +        dbref.setVersion(ref.getVersion());
 +        dbref.setAccessionId(ref.getAccessionId());
 +        if (ref instanceof GeneLocus)
          {
            dbref.setLocus(true);
          }
 -        if (dbRefEntry.hasMap())
 +        if (ref.hasMap())
          {
 -          Mapping mp = createVamsasMapping(dbRefEntry.getMap(), parentseq,
 +          Mapping mp = createVamsasMapping(ref.getMap(), parentseq,
                    jds, recurse);
            dbref.setMapping(mp);
          }
     * @param file
     *          - HTTP URL or filename
     */
 -  public AlignFrame loadJalviewAlign(final String file)
 +  public AlignFrame loadJalviewAlign(final Object file)
    {
  
      jalview.gui.AlignFrame af = null;
            public void run()
            {
              setLoadingFinishedForNewStructureViewers();
 -          };
 +          }
          });
        } catch (Exception x)
        {
      return af;
    }
  
 -  private jarInputStreamProvider createjarInputStreamProvider(
 -          final String file) throws MalformedURLException
 -  {
 -    URL url = null;
 -    errorMessage = null;
 -    uniqueSetSuffix = null;
 -    seqRefIds = null;
 -    viewportsAdded.clear();
 -    frefedSequence = null;
 -
 -    if (file.startsWith("http://"))
 -    {
 -      url = new URL(file);
 -    }
 -    final URL _url = url;
 -    return new jarInputStreamProvider()
 -    {
 -
 -      @Override
 -      public JarInputStream getJarInputStream() throws IOException
 -      {
 -        if (_url != null)
 -        {
 -          return new JarInputStream(_url.openStream());
 -        }
 -        else
 -        {
 -          return new JarInputStream(new FileInputStream(file));
 -        }
 -      }
 -
 -      @Override
 -      public String getFilename()
 -      {
 -        return file;
 -      }
 -    };
 -  }
 +      @SuppressWarnings("unused")
 +      private jarInputStreamProvider createjarInputStreamProvider(final Object ofile) throws MalformedURLException {
 +
 +              // BH 2018 allow for bytes already attached to File object
 +              try {
 +                      String file = (ofile instanceof File ? ((File) ofile).getCanonicalPath() : ofile.toString());
 +      byte[] bytes = Platform.isJS() ? Platform.getFileBytes((File) ofile)
 +              : null;
 +                      URL url = null;
 +                      errorMessage = null;
 +                      uniqueSetSuffix = null;
 +                      seqRefIds = null;
 +                      viewportsAdded.clear();
 +                      frefedSequence = null;
 +
 +                      if (file.startsWith("http://")) {
 +                              url = new URL(file);
 +                      }
 +                      final URL _url = url;
 +                      return new jarInputStreamProvider() {
 +
 +                              @Override
 +                              public JarInputStream getJarInputStream() throws IOException {
 +                                      if (bytes != null) {
 +//                                            System.out.println("Jalview2XML: opening byte jarInputStream for bytes.length=" + bytes.length);
 +                                              return new JarInputStream(new ByteArrayInputStream(bytes));
 +                                      }
 +                                      if (_url != null) {
 +//                                            System.out.println("Jalview2XML: opening url jarInputStream for " + _url);
 +                                              return new JarInputStream(_url.openStream());
 +                                      } else {
 +//                                            System.out.println("Jalview2XML: opening file jarInputStream for " + file);
 +                                              return new JarInputStream(new FileInputStream(file));
 +                                      }
 +                              }
 +
 +                              @Override
 +                              public String getFilename() {
 +                                      return file;
 +                              }
 +                      };
 +              } catch (IOException e) {
 +                      e.printStackTrace();
 +                      return null;
 +              }
 +      }
  
    /**
     * Recover jalview session from a jalview project archive. Caller may
  
          if (jarentry != null && jarentry.getName().endsWith(".xml"))
          {
 -          InputStreamReader in = new InputStreamReader(jin, UTF_8);
 -          // JalviewModel object = new JalviewModel();
 -
            JAXBContext jc = JAXBContext
                    .newInstance("jalview.xml.binding.jalview");
            XMLStreamReader streamReader = XMLInputFactory.newInstance()
                    .unmarshal(streamReader, JalviewModel.class);
            JalviewModel object = jbe.getValue();
  
 -          /*
 -          Unmarshaller unmar = new Unmarshaller(object);
 -          unmar.setValidation(false);
 -          object = (JalviewModel) unmar.unmarshal(in);
 -          */
            if (true) // !skipViewport(object))
            {
              _af = loadFromObject(object, file, true, jprovider);
              }
            }
          }
+         /*
+          * load any HMMER profile
+          */
+         // TODO fix this
+         String hmmJarFile = jseqs.get(i).getHmmerProfile();
+         if (hmmJarFile != null && jprovider != null)
+         {
+           loadHmmerProfile(jprovider, hmmJarFile, al.getSequenceAt(i));
+         }
        }
      } // end !multipleview
  
    }
  
    /**
+    * Loads a HMMER profile from a file stored in the project, and associates it
+    * with the specified sequence
+    * 
+    * @param jprovider
+    * @param hmmJarFile
+    * @param seq
+    */
+   protected void loadHmmerProfile(jarInputStreamProvider jprovider,
+           String hmmJarFile, SequenceI seq)
+   {
+     try
+     {
+       String hmmFile = copyJarEntry(jprovider, hmmJarFile, "hmm", null);
+       HMMFile parser = new HMMFile(hmmFile, DataSourceType.FILE);
+       HiddenMarkovModel hmmModel = parser.getHMM();
+       hmmModel = new HiddenMarkovModel(hmmModel, seq);
+       seq.setHMM(hmmModel);
+     } catch (IOException e)
+     {
+       warn("Error loading HMM profile for " + seq.getName() + ": "
+               + e.getMessage());
+     }
+   }
+   /**
     * Instantiate and link any saved RNA (Varna) viewers. The state of the Varna
     * panel is restored from separate jar entries, two (gapped and trimmed) per
     * sequence and secondary structure.
    {
      AlignFrame af = null;
      af = new AlignFrame(al, safeInt(view.getWidth()),
 -            safeInt(view.getHeight()), uniqueSeqSetId, viewId);
 +            safeInt(view.getHeight()), uniqueSeqSetId, viewId) 
 +//    {
 +//            
 +//            @Override
 +//            protected void processKeyEvent(java.awt.event.KeyEvent e) {
 +//                    System.out.println("Jalview2XML   AF " + e);
 +//                    super.processKeyEvent(e);
 +//                    
 +//            }
 +//            
 +//    }
 +    ;
  
      af.setFileName(file, FileFormat.Jalview);
  
      String id = object.getViewport().get(0).getSequenceSetId();
      if (skipList.containsKey(id))
      {
-       if (Cache.log != null && Cache.log.isDebugEnabled())
-       {
-         Cache.log.debug("Skipping seuqence set id " + id);
-       }
+       if (Cache.log != null && Cache.log.isDebugEnabled())
+         {
+           Cache.log.debug("Skipping seuqence set id " + id);
+         }
        return true;
      }
      return false;
          jmap.setTo(djs);
          incompleteSeqs.put(sqid, djs);
          seqRefIds.put(sqid, djs);
        }
        jalview.bin.Cache.log.debug("about to recurse on addDBRefs.");
        addDBRefs(djs, ms);
        }
        else
        {
-         Cache.log.debug("Ignoring " + jvobj.getClass() + " (ID = " + id);
+           Cache.log.debug("Ignoring " + jvobj.getClass() + " (ID = " + id);
        }
      }
    }
          maxcol = new Color(Integer.parseInt(colourModel.getRGB(), 16));
        } catch (Exception e)
        {
-         Cache.log.warn("Couldn't parse out graduated feature color.", e);
+           Cache.log.warn("Couldn't parse out graduated feature color.", e);
        }
    
        NoValueColour noCol = colourModel.getNoValueColour();
@@@ -29,12 -29,14 +29,14 @@@ import jalview.datamodel.AlignmentAnnot
  import jalview.datamodel.Annotation;
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.HiddenColumns;
+ import jalview.datamodel.HiddenMarkovModel;
  import jalview.datamodel.ProfilesI;
  import jalview.schemes.ColourSchemeI;
  import jalview.schemes.NucleotideColourScheme;
  import jalview.schemes.ResidueProperties;
  import jalview.schemes.ZappoColourScheme;
  import jalview.util.Platform;
+ import jalview.workers.InformationThread;
  
  import java.awt.BasicStroke;
  import java.awt.Color;
@@@ -43,6 -45,7 +45,6 @@@ import java.awt.FontMetrics
  import java.awt.Graphics;
  import java.awt.Graphics2D;
  import java.awt.Image;
 -import java.awt.font.LineMetrics;
  import java.awt.geom.AffineTransform;
  import java.awt.image.ImageObserver;
  import java.util.BitSet;
@@@ -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;
  
  
    private ProfilesI hconsensus;
  
 -  private Hashtable[] complementConsensus;
 +  private Hashtable<String, Object>[] complementConsensus;
  
 -  private Hashtable[] hStrucConsensus;
 +  private Hashtable<String, Object>[] hStrucConsensus;
  
    private boolean av_ignoreGapsConsensus;
  
+   private boolean av_ignoreBelowBackground;
    /**
     * attributes set from AwtRenderPanelI
     */
      complementConsensus = av.getComplementConsensusHash();
      hStrucConsensus = av.getRnaStructureConsensusHash();
      av_ignoreGapsConsensus = av.isIgnoreGapsConsensus();
+     av_ignoreBelowBackground = av.isIgnoreBelowBackground();
+     av_infoHeight = av.isInfoLetterHeight();
    }
  
    /**
     * Returns profile data; the first element is the profile type, the second is
     * the number of distinct values, the third the total count, and the remainder
      // properties/rendering attributes as a global 'alignment group' which holds
      // all vis settings for the alignment as a whole rather than a subset
      //
-     if (aa.autoCalculated && (aa.label.startsWith("Consensus")
-             || aa.label.startsWith("cDNA Consensus")))
+     if (InformationThread.HMM_CALC_ID.equals(aa.getCalcId()))
+     {
+       HiddenMarkovModel hmm = aa.sequenceRef.getHMM();
+       return AAFrequency.extractHMMProfile(hmm, column,
+               av_ignoreBelowBackground, av_infoHeight); // TODO check if this follows standard
+                                          // pipeline
+     }
+     if (aa.autoCalculated
+             && (aa.label.startsWith("Consensus") || aa.label
+                     .startsWith("cDNA Consensus")))
      {
        boolean forComplement = aa.label.startsWith("cDNA Consensus");
-       if (aa.groupRef != null && aa.groupRef.consensusData != null
+       if (aa.groupRef != null && aa.groupRef.getConsensusData() != null
                && aa.groupRef.isShowSequenceLogo())
        {
          // TODO? group consensus for cDNA complement
          return AAFrequency.extractProfile(
-                 aa.groupRef.consensusData.get(column),
-                 aa.groupRef.getIgnoreGapsConsensus());
+                 aa.groupRef.getConsensusData().get(column),
+                                               aa.groupRef.getIgnoreGapsConsensus());
        }
        // TODO extend annotation row to enable dynamic and static profile data to
        // be stored
      return null;
    }
  
 -  boolean rna = false;
 -
    /**
     * Render the annotation rows associated with an alignment.
     * 
      updateFromAwtRenderPanel(annotPanel, av);
      fm = g.getFontMetrics();
      AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
 -    int temp = 0;
 +    // int temp = 0;
      if (aa == null)
      {
        return false;
      boolean validRes = false;
      boolean validEnd = false;
      boolean labelAllCols = false;
 -    boolean centreColLabels;
 -    boolean centreColLabelsDef = av.isCentreColumnLabels();
 +//    boolean centreColLabels;
 +//    boolean centreColLabelsDef = av.isCentreColumnLabels();
      boolean scaleColLabel = false;
      final AlignmentAnnotation consensusAnnot = av
              .getAlignmentConsensusAnnotation();
              .getAlignmentStrucConsensusAnnotation();
      final AlignmentAnnotation complementConsensusAnnot = av
              .getComplementConsensusAnnotation();
  
      BitSet graphGroupDrawn = new BitSet();
      int charOffset = 0; // offset for a label
 -    float fmWidth, fmScaling = 1f; // scaling for a label to fit it into a
 -    // column.
 -    Font ofont = g.getFont();
      // \u03B2 \u03B1
      // debug ints
      int yfrom = 0, f_i = 0, yto = 0, f_to = 0;
      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)
        {
          continue;
        }
 -      centreColLabels = row.centreColLabels || centreColLabelsDef;
 +//      centreColLabels = row.centreColLabels || centreColLabelsDef;
        labelAllCols = row.showAllColLabels;
        scaleColLabel = row.scaleColLabel;
        lastSS = ' ';
              if (validCharWidth && validRes && displayChar != null
                      && (displayChar.length() > 0))
              {
 -
 -              fmWidth = fm.charsWidth(displayChar.toCharArray(), 0,
 +              Graphics2D gg = ((Graphics2D) g);
 +              float fmWidth = fm.charsWidth(displayChar.toCharArray(), 0,
                        displayChar.length());
 -              if (/* centreColLabels || */scaleColLabel)
 +
 +              /*
 +               * shrink label width to fit in column, if that is
 +               * both configured and necessary
 +               */
 +              boolean scaledToFit = false;
 +              float fmScaling = 1f;
 +              if (scaleColLabel && fmWidth > charWidth)
                {
 -                // fmWidth = fm.charsWidth(displayChar.toCharArray(), 0,
 -                // displayChar.length());
 -                //
 -                // if (scaleColLabel)
 -                // {
 -                // justify the label and scale to fit in column
 -                if (fmWidth > charWidth)
 -                {
 -                  // scale only if the current font isn't already small enough
 -                  fmScaling = charWidth;
 -                  fmScaling /= fmWidth;
 -                  g.setFont(ofont.deriveFont(AffineTransform
 -                          .getScaleInstance(fmScaling, 1.0)));
 -                  // and update the label's width to reflect the scaling.
 -                  fmWidth = charWidth;
 -                }
 -                // }
 +                scaledToFit = true;
 +                fmScaling = charWidth;
 +                fmScaling /= fmWidth;
 +                // and update the label's width to reflect the scaling.
 +                fmWidth = charWidth;
                }
 -              // TODO is it ok to use width of / show all characters here?
 -              // else
 -              // {
 -              // fmWidth = fm.charWidth(displayChar.charAt(0));
 -              // }
 +
                charOffset = (int) ((charWidth - fmWidth) / 2f);
  
                if (row_annotations[column].colour == null)
                {
 -                g.setColor(Color.black);
 +                gg.setColor(Color.black);
                }
                else
                {
 -                g.setColor(row_annotations[column].colour);
 +                gg.setColor(row_annotations[column].colour);
                }
  
 +              /*
 +               * draw the label, unless it is the same secondary structure
 +               * symbol (excluding RNA Helix) as the previous column
 +               */
 +              final int xPos = (x * charWidth) + charOffset;
 +              final int yPos = y + iconOffset;
 +
 +              /*
 +               * translate to drawing position _before_ applying any scaling
 +               */
 +              gg.translate(xPos, yPos);
 +              if (scaledToFit)
 +              {
 +                /*
 +                 * use a scaling transform to make the label narrower
 +                 * (JalviewJS doesn't have Font.deriveFont(AffineTransform))
 +                 */
 +                gg.transform(
 +                        AffineTransform.getScaleInstance(fmScaling, 1.0));
 +              }
                if (column == 0 || row.graph > 0)
                {
 -                g.drawString(displayChar, (x * charWidth) + charOffset,
 -                        y + iconOffset);
 +                gg.drawString(displayChar, 0, 0);
                }
                else if (row_annotations[column - 1] == null || (labelAllCols
                        || !displayChar.equals(
                        || (displayChar.length() < 2
                                && row_annotations[column].secondaryStructure == ' ')))
                {
 -                g.drawString(displayChar, x * charWidth + charOffset,
 -                        y + iconOffset);
 +                gg.drawString(displayChar, 0, 0);
 +              }
 +              if (scaledToFit)
 +              {
 +                /*
 +                 * undo scaling before translating back 
 +                 * (restoring saved transform does NOT work in JS PDFGraphics!)
 +                 */
 +                gg.transform(AffineTransform
 +                        .getScaleInstance(1D / fmScaling, 1.0));
                }
 -              g.setFont(ofont);
 +              gg.translate(-xPos, -yPos);
              }
            }
            if (row.hasIcons)
                if (x > -1)
                {
  
 -                int nb_annot = x - temp;
 +                // int nb_annot = x - temp;
                  // System.out.println("\t type :"+lastSS+"\t x :"+x+"\t nbre
                  // annot :"+nb_annot);
                  switch (lastSS)
                  case ')': // and opposite direction
                    drawStemAnnot(g, row_annotations, lastSSX, x, y,
                            iconOffset, startRes, column, validRes, validEnd);
 -                  temp = x;
 +                  // temp = x;
                    break;
  
                  case 'H':
                    drawNotCanonicalAnnot(g, nonCanColor, row_annotations,
                            lastSSX, x, y, iconOffset, startRes, column,
                            validRes, validEnd);
 -                  temp = x;
 +                  // temp = x;
                    break;
                  default:
                    g.setColor(Color.gray);
                    g.fillRect(lastSSX, y + 6 + iconOffset,
                            (x * charWidth) - lastSSX, 2);
 -                  temp = x;
 +                  // temp = x;
                    break;
                  }
                }
  
    public static final Color STEM_COLOUR = Color.blue;
  
 -  private Color sdNOTCANONICAL_COLOUR;
 +  // private Color sdNOTCANONICAL_COLOUR;
  
    void drawGlyphLine(Graphics g, Annotation[] row, int lastSSX, int x,
            int y, int iconOffset, int startRes, int column, boolean validRes,
      int x1 = lastSSX;
      int x2 = (x * charWidth);
  
 -    if (MAC)
 +    if (USE_FILL_ROUND_RECT)
      {
        int ofs = charWidth / 2;
        // Off by 1 offset when drawing rects and ovals
      }
    }
  
 +  @SuppressWarnings("unused")
    void drawBarGraph(Graphics g, AlignmentAnnotation _aa,
            Annotation[] aa_annotations, int sRes, int eRes, float min,
            float max, int y, boolean renderHistogram, boolean renderProfile,
            boolean isStructureProfile = profl[0] == AlignmentAnnotation.STRUCTURE_PROFILE;
            boolean isCdnaProfile = profl[0] == AlignmentAnnotation.CDNA_PROFILE;
            float ht = normaliseProfile ? y - _aa.graphHeight : y1;
 -          double htn = normaliseProfile ? _aa.graphHeight : (y2 - y1);// aa.graphHeight;
 -          double hght;
 -          float wdth;
 -          double ht2 = 0;
 -          char[] dc;
 +          final double normaliseFactor = normaliseProfile ? _aa.graphHeight
 +                  : (y2 - y1);
  
            /**
             * Render a single base for a sequence profile, a base pair for
             * structure profile, and a triplet for a cdna profile
             */
 -          dc = new char[isStructureProfile ? 2 : (isCdnaProfile ? 3 : 1)];
 +          char[] dc = new char[isStructureProfile ? 2
 +                  : (isCdnaProfile ? 3 : 1)];
 +
 +          // lm is not necessary - we can just use fm - could be off by no more
 +          // than 0.5 px
 +          // LineMetrics lm = g.getFontMetrics(ofont).getLineMetrics("Q", g);
 +          // System.out.println(asc + " " + dec + " " + (asc - lm.getAscent())
 +          // + " " + (dec - lm.getDescent()));
 +
 +          double asc = fm.getAscent();
 +          double dec = fm.getDescent();
 +          double fht = fm.getHeight();
  
 -          LineMetrics lm = g.getFontMetrics(ofont).getLineMetrics("Q", g);
            double scale = 1f / (normaliseProfile ? profl[2] : 100f);
 -          float ofontHeight = 1f / lm.getAscent();// magnify to fill box
 -          double scl = 0.0;
 +          // float ofontHeight = 1f / fm.getAscent();// magnify to fill box
  
            /*
             * Traverse the character(s)/percentage data in the array
             */
 -          int c = 3;
 -          int valuesProcessed = 0;
 +
 +          float ht2 = ht;
 +
            // profl[1] is the number of values in the profile
 -          while (valuesProcessed < profl[1])
 +          for (int i = 0, c = 3, last = profl[1]; i < last; i++)
            {
 +
 +            String s;
              if (isStructureProfile)
              {
                // todo can we encode a structure pair as an int, like codons?
                dc[0] = (char) profl[c++];
                dc[1] = (char) profl[c++];
 +              s = new String(dc);
              }
              else if (isCdnaProfile)
              {
 -              dc = CodingUtils.decodeCodon(profl[c++]);
 +              CodingUtils.decodeCodon2(profl[c++], dc);
 +              s = new String(dc);
              }
              else
              {
                dc[0] = (char) profl[c++];
 +              s = new String(dc);
              }
 -
 -            wdth = charWidth;
 -            wdth /= fm.charsWidth(dc, 0, dc.length);
 -
 -            ht += scl;
              // next profl[] position is profile % for the character(s)
 -            scl = htn * scale * profl[c++];
 -            lm = ofont.getLineMetrics(dc, 0, 1,
 -                    g.getFontMetrics().getFontRenderContext());
 -            Font font = ofont.deriveFont(AffineTransform
 -                    .getScaleInstance(wdth, scl / lm.getAscent()));
 -            g.setFont(font);
 -            lm = g.getFontMetrics().getLineMetrics(dc, 0, 1, g);
  
 -            // Debug - render boxes around characters
 -            // g.setColor(Color.red);
 -            // g.drawRect(x*av.charWidth, (int)ht, av.charWidth,
 -            // (int)(scl));
 -            // g.setColor(profcolour.findColour(dc[0]).darker());
 +            int percent = profl[c++];
 +            if (percent == 0)
 +            {
 +              // failsafe in case a count rounds down to 0%
 +              continue;
 +            }
 +            double newHeight = normaliseFactor * scale * percent;
  
              /*
               * Set character colour as per alignment colour scheme; use the
              if (isCdnaProfile)
              {
                final String codonTranslation = ResidueProperties
 -                      .codonTranslate(new String(dc));
 +                      .codonTranslate(s);
                colour = profcolour.findColour(codonTranslation.charAt(0),
                        column, null);
              }
              }
              g.setColor(colour == Color.white ? Color.lightGray : colour);
  
 -            hght = (ht + (scl - lm.getDescent()
 -                    - lm.getBaselineOffsets()[lm.getBaselineIndex()]));
 +            // Debug - render boxes around characters
 +            // g.setColor(Color.red);
 +            // g.drawRect(x*av.charWidth, (int)ht, av.charWidth,
 +            // (int)(scl));
 +            // g.setColor(profcolour.findColour(dc[0]).darker());
  
 -            g.drawChars(dc, 0, dc.length, x * charWidth, (int) hght);
 -            valuesProcessed++;
 +            double sx = 1f * charWidth / fm.charsWidth(dc, 0, dc.length);
 +            double sy = newHeight / asc;
 +            double newAsc = asc * sy;
 +            double newDec = dec * sy;
 +            // it is not necessary to recalculate lm for the new font.
 +            // note: lm.getBaselineOffsets()[lm.getBaselineIndex()]) must be 0
 +            // by definition. Was:
 +            // int hght = (int) (ht + (newAsc - newDec);
 +            // - lm.getBaselineOffsets()[lm.getBaselineIndex()]));
 +
 +            if (Platform.isJS())
 +            {
 +              /*
 +               * SwingJS does not implement font.deriveFont()
 +               * so use a scaling transform to draw instead,
 +               * this is off by a very small amount
 +               */
 +              final int hght = (int) (ht2 + (newAsc - newDec));
 +              Graphics2D gg = (Graphics2D) g;
 +              int xShift = (int) Math.round(x * charWidth / sx);
 +              int yShift = (int) Math.round(hght / sy);
 +              gg.transform(AffineTransform.getScaleInstance(sx, sy));
 +              gg.drawString(s, xShift, yShift);
 +              gg.transform(
 +                      AffineTransform.getScaleInstance(1D / sx, 1D / sy));
 +              ht2 += newHeight;
 +            }
 +            else
 +            /**
 +             * Java only
 +             * 
 +             * @j2sIgnore
 +             */
 +            {
 +              // Java ('normal') method is to scale the font to fit
 +
 +              final int hght = (int) (ht + (newAsc - newDec));
 +              Font font = ofont
 +                      .deriveFont(AffineTransform.getScaleInstance(sx, sy));
 +              g.setFont(font);
 +              g.drawChars(dc, 0, dc.length, x * charWidth, hght);
 +              ht += newHeight;
 +            }
            }
 -          g.setFont(ofont);
          }
        }
        x++;
@@@ -34,6 -34,13 +34,13 @@@ import java.util.Vector
  
  public class ResidueProperties
  {
+   // alphabet names used in Hidden Markov Model files
+   public static final String ALPHABET_RNA = "RNA";
+   public static final String ALPHABET_DNA = "DNA";
+   public static final String ALPHABET_AMINO = "amino";
    // Stores residue codes/names and colours and other things
    public static final int[] aaIndex; // aaHash version 2.1.1 and below
  
@@@ -50,6 -57,9 +57,9 @@@
    // lookup from modified amino acid (e.g. MSE) to canonical form (e.g. MET)
    public static final Map<String, String> modifications = new HashMap<>();
  
+   // residue background frequencies across different alphabets
+   public static final Map<String, Map<Character, Float>> backgroundFrequencies = new HashMap<>();
    static
    {
      aaIndex = new int[255];
  
    }
  
+   static
+   {
+     Map<Character, Float> amino = new HashMap<>();
+     amino.put('A', 0.0826f);
+     amino.put('Q', 0.0393f);
+     amino.put('L', 0.0965f);
+     amino.put('S', 0.0661f);
+     amino.put('R', 0.0553f);
+     amino.put('E', 0.0674f);
+     amino.put('K', 0.0582f);
+     amino.put('T', 0.0535f);
+     amino.put('N', 0.0406f);
+     amino.put('G', 0.0708f);
+     amino.put('M', 0.0241f);
+     amino.put('W', 0.0109f);
+     amino.put('D', 0.0546f);
+     amino.put('H', 0.0227f);
+     amino.put('F', 0.0386f);
+     amino.put('Y', 0.0292f);
+     amino.put('C', 0.0137f);
+     amino.put('I', 0.0593f);
+     amino.put('P', 0.0472f);
+     amino.put('V', 0.0686f);
+     backgroundFrequencies.put(ALPHABET_AMINO, amino);
+     // todo: these don't match https://www.ebi.ac.uk/uniprot/TrEMBLstats - what
+     // are they?
+   }
+   // TODO get correct frequencies
+   static
+   {
+     Map<Character, Float> dna = new HashMap<>();
+     dna.put('A', 0.25f);
+     dna.put('C', 0.25f);
+     dna.put('T', 0.25f);
+     dna.put('G', 0.25f);
+     backgroundFrequencies.put(ALPHABET_DNA, dna);
+   }
+   static
+   {
+     Map<Character, Float> rna = new HashMap<>();
+     rna.put('A', 0.25f);
+     rna.put('C', 0.25f);
+     rna.put('T', 0.25f);
+     rna.put('G', 0.25f);
+     backgroundFrequencies.put(ALPHABET_RNA, rna);
+   }
    public static String getCanonicalAminoAcid(String aA)
    {
      String canonical = modifications.get(aA);
  
    // main method generates perl representation of residue property hash
    // / cut here
 +  /**
 +   * @j2sIgnore
 +   * @param args
 +   */
    public static void main(String[] args)
    {
      Hashtable<String, Vector<String>> aaProps = new Hashtable<>();
   */
  package jalview.util;
  
 +import jalview.javascript.json.JSON;
 +
 +import java.awt.Toolkit;
  import java.awt.event.MouseEvent;
 +import java.io.BufferedReader;
 +import java.io.File;
 +import java.io.FileOutputStream;
 +import java.io.FileReader;
 +import java.io.IOException;
 +import java.io.InputStream;
 +import java.io.InputStreamReader;
 +import java.io.Reader;
 +import java.net.URL;
 +import java.util.Properties;
 +
 +import javax.swing.SwingUtilities;
 +
 +import org.json.simple.parser.JSONParser;
 +import org.json.simple.parser.ParseException;
  
  /**
   * System platform information used by Applet and Application
   */
  public class Platform
  {
 -  private static Boolean isAMac = null, isWindows = null;
 +
 +  private static boolean isJS = /** @j2sNative true || */
 +          false;
 +
 +  private static Boolean isNoJSMac = null, isNoJSWin = null, isMac = null,
 +          isWin = null;
  
    private static Boolean isHeadless = null;
  
    /**
 -   * sorry folks - Macs really are different
 +   * added to group mouse events into Windows and nonWindows (mac, unix, linux)
     * 
 -   * @return true if we do things in a special way.
 +   * @return
     */
 -  public static boolean isAMac()
 +  public static boolean isMac()
    {
 -    if (isAMac == null)
 -    {
 -      isAMac = System.getProperty("os.name").indexOf("Mac") > -1;
 -    }
 +    return (isMac == null
 +            ? (isMac = (System.getProperty("os.name").indexOf("Mac") >= 0))
 +            : isMac);
 +  }
 +
 +  /**
 +   * added to group mouse events into Windows and nonWindows (mac, unix, linux)
 +   * 
 +   * @return
 +   */
 +  public static boolean isWin()
 +  {
 +    return (isWin == null
 +            ? (isWin = (System.getProperty("os.name").indexOf("Win") >= 0))
 +            : isWin);
 +  }
  
 -    return isAMac.booleanValue();
 +  /**
 +   * 
 +   * @return true if HTML5 JavaScript
 +   */
 +  public static boolean isJS()
 +  {
 +    return isJS;
 +  }
  
 +  /**
 +   * sorry folks - Macs really are different
 +   * 
 +   * BH: disabled for SwingJS -- will need to check key-press issues
 +   * 
 +   * @return true if we do things in a special way.
 +   */
 +  public static boolean isAMacAndNotJS()
 +  {
 +    return (isNoJSMac == null ? (isNoJSMac = !isJS && isMac()) : isNoJSMac);
    }
  
    /**
-    * Check if we are on a Microsoft plaform...
+    * Check if we are on a Microsoft platform...
     * 
     * @return true if we have to cope with another platform variation
     */
 -  public static boolean isWindows()
 +  public static boolean isWindowsAndNotJS()
    {
 -    if (isWindows == null)
 -    {
 -      isWindows = System.getProperty("os.name").indexOf("Win") > -1;
 -    }
 -    return isWindows.booleanValue();
 +    return (isNoJSWin == null ? (isNoJSWin = !isJS && isWin()) : isNoJSWin);
    }
  
    /**
    /**
     * Answers true if the mouse event has Meta-down (Command key on Mac) or
     * Ctrl-down (on other o/s). Note this answers _false_ if the Ctrl key is
 -   * pressed instead of the Meta/Cmd key on Mac. To test for Ctrl-click on Mac,
 -   * you can use e.isPopupTrigger().
 +   * pressed instead of the Meta/Cmd key on Mac. To test for Ctrl-pressed on
 +   * Mac, you can use e.isPopupTrigger().
     * 
     * @param e
     * @return
     */
    public static boolean isControlDown(MouseEvent e)
    {
 -    boolean aMac = isAMac();
 -    return isControlDown(e, aMac);
 +    return isControlDown(e, isMac());
    }
  
    /**
     */
    protected static boolean isControlDown(MouseEvent e, boolean aMac)
    {
 -    if (aMac)
 +    if (!aMac)
      {
 -      /*
 -       * answer false for right mouse button
 -       */
 -      if (e.isPopupTrigger())
 +      return e.isControlDown();
 +
 +      // Jalview 2.11 code below: above is as amended for JalviewJS
 +      // /*
 +      // * answer false for right mouse button
 +      // */
 +      // if (e.isPopupTrigger())
 +      // {
 +      // return false;
 +      // }
 +      // return
 +      // (jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx() //
 +      // .getMenuShortcutKeyMaskEx()
 +      // & jalview.util.ShortcutKeyMaskExWrapper
 +      // .getModifiersEx(e)) != 0; // getModifiers()) != 0;
 +    }
 +    // answer false for right mouse button
 +    // shortcut key will be META for a Mac
 +    return !e.isPopupTrigger()
 +            && (Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()
 +                    & e.getModifiers()) != 0;
 +    // could we use e.isMetaDown() here?
 +  }
 +
 +  // BH: I don't know about that previous method. Here is what SwingJS uses.
 +  // Notice the distinction in mouse events. (BUTTON3_MASK == META)
 +  //
 +  // private static boolean isPopupTrigger(int id, int mods, boolean isWin) {
 +  // boolean rt = ((mods & InputEvent.BUTTON3_MASK) != 0);
 +  // if (isWin) {
 +  // if (id != MouseEvent.MOUSE_RELEASED)
 +  // return false;
 +  ////
 +  //// // Oddly, Windows returns InputEvent.META_DOWN_MASK on release, though
 +  //// // BUTTON3_DOWN_MASK for pressed. So here we just accept both.
 +  ////
 +  //// actually, we can use XXX_MASK, not XXX_DOWN_MASK and avoid this issue,
 +  // because
 +  //// J2S adds the appropriate extended (0x3FC0) and simple (0x3F) modifiers.
 +  ////
 +  // return rt;
 +  // } else {
 +  // // mac, linux, unix
 +  // if (id != MouseEvent.MOUSE_PRESSED)
 +  // return false;
 +  // boolean lt = ((mods & InputEvent.BUTTON1_MASK) != 0);
 +  // boolean ctrl = ((mods & InputEvent.CTRL_MASK) != 0);
 +  // return rt || (ctrl && lt);
 +  // }
 +  // }
 +  //
 +
 +  /**
 +   * Windows (not Mac, Linux, or Unix) and right button to test for the
 +   * right-mouse pressed event in Windows that would have opened a menu or a
 +   * Mac.
 +   * 
 +   * @param e
 +   * @return
 +   */
 +  public static boolean isWinRightButton(MouseEvent e)
 +  {
 +    // was !isAMac(), but that is true also for Linux and Unix and JS,
 +
 +    return isWin() && SwingUtilities.isRightMouseButton(e);
 +  }
 +
 +  /**
 +   * Windows (not Mac, Linux, or Unix) and middle button -- for mouse wheeling
 +   * without pressing the button.
 +   * 
 +   * @param e
 +   * @return
 +   */
 +  public static boolean isWinMiddleButton(MouseEvent e)
 +  {
 +    // was !isAMac(), but that is true also for Linux and Unix and JS
 +    return isWin() && SwingUtilities.isMiddleMouseButton(e);
 +  }
 +
 +  public static boolean allowMnemonics()
 +  {
 +    return !isMac();
 +  }
 +
 +  public final static int TIME_RESET = 0;
 +
 +  public final static int TIME_MARK = 1;
 +
 +  public static final int TIME_SET = 2;
 +
 +  public static final int TIME_GET = 3;
 +
 +  public static long time, mark, set, duration;
 +
 +  public static void timeCheck(String msg, int mode)
 +  {
 +    long t = System.currentTimeMillis();
 +    switch (mode)
 +    {
 +    case TIME_RESET:
 +      time = mark = t;
 +      if (msg != null)
 +      {
 +        System.err.println("Platform: timer reset\t\t\t" + msg);
 +      }
 +      break;
 +    case TIME_MARK:
 +      if (set > 0)
 +      {
 +        duration += (t - set);
 +      }
 +      else
        {
 -        return false;
 +        if (time == 0)
 +        {
 +          time = mark = t;
 +        }
 +        if (msg != null)
 +        {
 +          System.err.println("Platform: timer mark\t" + ((t - time) / 1000f)
 +                  + "\t" + ((t - mark) / 1000f) + "\t" + msg);
 +        }
 +        mark = t;
        }
 -      return (jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx() // .getMenuShortcutKeyMaskEx()
 -              & jalview.util.ShortcutKeyMaskExWrapper
 -                      .getModifiersEx(e)) != 0; // getModifiers()) != 0;
 +      break;
 +    case TIME_SET:
 +      set = t;
 +      break;
 +    case TIME_GET:
 +      if (msg != null)
 +      {
 +        System.err.println("Platform: timer dur\t" + ((t - time) / 1000f)
 +                + "\t" + ((duration) / 1000f) + "\t" + msg);
 +      }
 +      set = 0;
 +      break;
 +    }
 +  }
 +
 +  public static void cacheFileData(String path, Object data)
 +  {
 +    if (!isJS() || data == null)
 +    {
 +      return;
 +    }
 +    /**
 +     * @j2sNative
 +     * 
 +     *            swingjs.JSUtil.cacheFileData$S$O(path, data);
 +     * 
 +     */
 +  }
 +
 +  public static void cacheFileData(File file)
 +  {
 +    byte[] data;
 +    if (!isJS() || (data = Platform.getFileBytes(file)) == null)
 +    {
 +      return;
 +    }
 +    cacheFileData(file.toString(), data);
 +  }
 +
 +  public static byte[] getFileBytes(File f)
 +  {
 +    return /** @j2sNative f && swingjs.JSUtil.getFileBytes$java_io_File(f) || */
 +    null;
 +  }
 +
 +  public static byte[] getFileAsBytes(String fileStr)
 +  {
 +    byte[] bytes = null;
 +    // BH 2018 hack for no support for access-origin
 +    /**
 +     * @j2sNative bytes = swingjs.JSUtil.getFileAsBytes$O(fileStr)
 +     */
 +    cacheFileData(fileStr, bytes);
 +    return bytes;
 +  }
 +
 +  @SuppressWarnings("unused")
 +  public static String getFileAsString(String url)
 +  {
 +    String ret = null;
 +    /**
 +     * @j2sNative
 +     * 
 +     *            ret = swingjs.JSUtil.getFileAsString$S(url);
 +     * 
 +     * 
 +     */
 +    cacheFileData(url, ret);
 +    return ret;
 +  }
 +
 +  public static boolean setFileBytes(File f, String urlstring)
 +  {
 +    if (!isJS())
 +    {
 +      return false;
      }
 -    return e.isControlDown();
 +    @SuppressWarnings("unused")
 +    byte[] bytes = getFileAsBytes(urlstring);
 +    // TODO temporary doubling of ç§˜bytes and _bytes;
 +    // just remove _bytes when new transpiler has been installed
 +    /**
 +     * @j2sNative f.\u79d8bytes = f._bytes = bytes;
 +     */
 +    return true;
 +  }
 +
 +  public static void addJ2SBinaryType(String ext)
 +  {
 +    /**
 +     * @j2sNative
 +     * 
 +     *            J2S._binaryTypes.push("." + ext + "?");
 +     * 
 +     */
 +  }
 +
 +  /**
 +   * Encode the URI using JavaScript encodeURIComponent
 +   * 
 +   * @param value
 +   * @return encoded value
 +   */
 +  public static String encodeURI(String value)
 +  {
 +    /**
 +     * @j2sNative value = encodeURIComponent(value);
 +     */
 +    return value;
 +  }
 +
 +  /**
 +   * Open the URL using a simple window call if this is JavaScript
 +   * 
 +   * @param url
 +   * @return true if window has been opened
 +   */
 +  public static boolean openURL(String url)
 +  {
 +    if (!isJS())
 +    {
 +      return false;
 +    }
 +    /**
 +     * @j2sNative
 +     * 
 +     * 
 +     *            window.open(url);
 +     */
 +    return true;
 +  }
 +
 +  public static String getUniqueAppletID()
 +  {
 +    /**
 +     * @j2sNative return swingjs.JSUtil.getApplet$()._uniqueId;
 +     *
 +     */
 +    return null;
 +
 +  }
 +
 +  /**
 +   * Read the Info block for this applet.
 +   * 
 +   * @param prefix
 +   *          "jalview_"
 +   * @param p
 +   * @return unique id for this applet
 +   */
 +  public static void readInfoProperties(String prefix, Properties p)
 +  {
 +    if (!isJS())
 +    {
 +      return;
 +    }
 +    String id = getUniqueAppletID();
 +    String key = "", value = "";
 +    /**
 +     * @j2sNative var info = swingjs.JSUtil.getApplet$().__Info || {}; for (var
 +     *            key in info) { if (key.indexOf(prefix) == 0) { value = "" +
 +     *            info[key];
 +     */
 +
 +    System.out.println(
 +            "Platform id=" + id + " reading Info." + key + " = " + value);
 +    p.put(id + "_" + key, value);
 +
 +    /**
 +     * @j2sNative
 +     * 
 +     * 
 +     *            } }
 +     */
 +  }
 +
 +  public static void setAjaxJSON(URL url)
 +  {
 +    if (isJS())
 +    {
 +      JSON.setAjax(url);
 +    }
 +  }
 +
 +  public static Object parseJSON(InputStream response)
 +          throws IOException, ParseException
 +  {
 +    if (isJS())
 +    {
 +      return JSON.parse(response);
 +    }
 +
 +    BufferedReader br = null;
 +    try
 +    {
 +      br = new BufferedReader(new InputStreamReader(response, "UTF-8"));
 +      return new JSONParser().parse(br);
 +    } finally
 +    {
 +      if (br != null)
 +      {
 +        try
 +        {
 +          br.close();
 +        } catch (IOException e)
 +        {
 +          // ignore
 +        }
 +      }
 +    }
 +  }
 +
 +  public static Object parseJSON(String json) throws ParseException
 +  {
 +    return (isJS() ? JSON.parse(json)
 +            : new JSONParser().parse(json));
 +  }
 +
 +  public static Object parseJSON(Reader r)
 +          throws IOException, ParseException
 +  {
 +    if (r == null)
 +    {
 +      return null;
 +    }
 +
 +    if (!isJS())
 +    {
 +      return new JSONParser().parse(r);
 +    }
 +    // Using a file reader is not currently supported in SwingJS JavaScript
 +
 +    if (r instanceof FileReader)
 +    {
 +      throw new IOException(
 +              "StringJS does not support FileReader parsing for JSON -- but it could...");
 +    }
 +    return JSON.parse(r);
 +
 +  }
 +
 +  /**
 +   * Dump the input stream to an output file.
 +   * 
 +   * @param is
 +   * @param outFile
 +   * @throws IOException
 +   *                       if the file cannot be created or there is a problem
 +   *                       reading the input stream.
 +   */
 +  public static void streamToFile(InputStream is, File outFile)
 +          throws IOException
 +  {
 +    if (isJS() && /**
 +                   * @j2sNative outFile.setBytes$O && outFile.setBytes$O(is) &&
 +                   */
 +            true)
 +    {
 +      return;
 +    }
 +    FileOutputStream fio = new FileOutputStream(outFile);
 +    try
 +    {
 +      byte[] bb = new byte[32 * 1024];
 +      int l;
 +      while ((l = is.read(bb)) > 0)
 +      {
 +        fio.write(bb, 0, l);
 +      }
 +    } finally
 +    {
 +      fio.close();
 +    }
 +  }
 +
 +  /**
 +   * Add a known domain that implements access-control-allow-origin:*
 +   * 
 +   * These should be reviewed periodically.
 +   * 
 +   * @param domain
 +   *          for a service that is not allowing ajax
 +   * 
 +   * @author hansonr@stolaf.edu
 +   * 
 +   */
 +  public static void addJ2SDirectDatabaseCall(String domain)
 +  {
 +
 +    if (isJS())
 +    {
 +      System.out.println(
 +            "Platform adding known access-control-allow-origin * for domain "
 +                    + domain);
 +      /**
 +       * @j2sNative
 +       * 
 +       *            J2S.addDirectDatabaseCall(domain);
 +       */
 +    }
 +
 +  }
 +
 +  public static void getURLCommandArguments()
 +  {
 +
 +    /**
 +     * Retrieve the first query field as command arguments to Jalview. Include
 +     * only if prior to "?j2s" or "&j2s" or "#". Assign the applet's __Info.args
 +     * element to this value.
 +     * 
 +     * @j2sNative var a =
 +     *            decodeURI((document.location.href.replace("&","?").split("?j2s")[0]
 +     *            + "?").split("?")[1].split("#")[0]); a &&
 +     *            (J2S.thisApplet.__Info.args = a.split(" "));
 +     */
 +
    }
  
    /**
@@@ -24,7 -24,6 +24,7 @@@ import jalview.analysis.AnnotationSorte
  import jalview.analysis.Conservation;
  import jalview.analysis.TreeModel;
  import jalview.api.AlignCalcManagerI;
 +import jalview.api.AlignExportSettingsI;
  import jalview.api.AlignViewportI;
  import jalview.api.AlignmentViewPanel;
  import jalview.api.FeaturesDisplayedI;
@@@ -32,7 -31,6 +32,7 @@@ import jalview.api.ViewStyleI
  import jalview.commands.CommandI;
  import jalview.datamodel.AlignedCodonFrame;
  import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentExportData;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentView;
  import jalview.datamodel.Annotation;
@@@ -59,6 -57,7 +59,7 @@@ import jalview.viewmodel.styles.ViewSty
  import jalview.workers.AlignCalcManager;
  import jalview.workers.ComplementConsensusThread;
  import jalview.workers.ConsensusThread;
+ import jalview.workers.InformationThread;
  import jalview.workers.StrucConsensusThread;
  
  import java.awt.Color;
@@@ -103,6 -102,27 +104,27 @@@ public abstract class AlignmentViewpor
     * alignment displayed in the viewport. Please use get/setter
     */
    protected AlignmentI alignment;
+   
+   /*
+    * probably unused indicator that view is of a dataset rather than an
+    * alignment
+    */
+   protected boolean ignoreBelowBackGroundFrequencyCalculation = false;
+   protected boolean infoLetterHeight = false;
+   protected AlignmentAnnotation occupancy;
+   
+   /**
+    * results of alignment consensus analysis for visible portion of view
+    */
+   protected ProfilesI consensusProfiles;
+   /**
+    * HMM profile for the alignment
+    */
+   protected ProfilesI hmmProfiles;
  
    public AlignmentViewport(AlignmentI al)
    {
     * alignment
     */
    protected boolean isDataset = false;
+   
    public void setDataset(boolean b)
    {
      isDataset = b;
    protected boolean ignoreGapsInConsensusCalculation = false;
  
    protected ResidueShaderI residueShading = new ResidueShader();
+   
    @Override
    public void setGlobalColourScheme(ColourSchemeI cs)
    {
    {
      return residueShading;
    }
+   
    protected AlignmentAnnotation consensus;
  
    protected AlignmentAnnotation complementConsensus;
    /**
     * results of cDNA complement consensus visible portion of view
     */
 -  protected Hashtable[] hcomplementConsensus = null;
 +  protected Hashtable<String, Object>[] hcomplementConsensus = null;
  
    /**
     * 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 setComplementConsensusHash(Hashtable[] hconsensus)
 +  public void setComplementConsensusHash(
 +          Hashtable<String, Object>[] hconsensus)
    {
      this.hcomplementConsensus = hconsensus;
    }
    }
  
    @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 Hashtable[] getRnaStructureConsensusHash()
 +  public Hashtable<String, Object>[] getRnaStructureConsensusHash()
    {
      return hStrucConsensus;
    }
  
    @Override
 -  public void setRnaStructureConsensusHash(Hashtable[] hStrucConsensus)
 +  public void setRnaStructureConsensusHash(
 +          Hashtable<String, Object>[] hStrucConsensus)
    {
      this.hStrucConsensus = hStrucConsensus;
  
      }
    }
  
+   @Override
+   public void initInformationWorker(final AlignmentViewPanel ap)
+   {
+     if (calculator
+             .getRegisteredWorkersOfClass(InformationThread.class) == null)
+     {
+       calculator.registerWorker(new InformationThread(this, ap));
+     }
+   }
    // --------START Structure Conservation
    public void updateStrucConsensus(final AlignmentViewPanel ap)
    {
      strucConsensus = null;
      conservation = null;
      quality = null;
+     consensusProfiles = null;
      groupConsensus = null;
      groupConservation = null;
      hconsensus = null;
    protected boolean showConsensusHistogram = true;
  
    /**
+    * should hmm profile be rendered by default
+    */
+   protected boolean hmmShowSequenceLogo = false;
+   /**
+    * should hmm profile be rendered normalised to row height
+    */
+   protected boolean hmmNormaliseSequenceLogo = false;
+   /**
+    * should information histograms be rendered by default
+    */
+   protected boolean hmmShowHistogram = true;
+   /**
     * @return the showConsensusProfile
     */
    @Override
    }
  
    /**
+    * @return the showInformationProfile
+    */
+   @Override
+   public boolean isShowHMMSequenceLogo()
+   {
+     return hmmShowSequenceLogo;
+   }
+   /**
     * @param showSequenceLogo
     *          the new value
     */
      this.showSequenceLogo = showSequenceLogo;
    }
  
+   public void setShowHMMSequenceLogo(boolean showHMMSequenceLogo)
+   {
+     if (showHMMSequenceLogo != this.hmmShowSequenceLogo)
+     {
+       this.hmmShowSequenceLogo = showHMMSequenceLogo;
+       // TODO: updateAnnotation if description (tooltip) will show
+       // profile in place of information content?
+       // calculator.updateAnnotationFor(InformationThread.class);
+     }
+     this.hmmShowSequenceLogo = showHMMSequenceLogo;
+   }
    /**
     * @param showConsensusHistogram
     *          the showConsensusHistogram to set
    }
  
    /**
+    * @param showInformationHistogram
+    */
+   public void setShowInformationHistogram(boolean showInformationHistogram)
+   {
+     this.hmmShowHistogram = showInformationHistogram;
+   }
+   /**
     * @return the showGroupConservation
     */
    public boolean isShowGroupConservation()
    }
  
    /**
+    * 
+    * @return flag to indicate if the information content histogram should be
+    *         rendered by default
+    */
+   @Override
+   public boolean isShowInformationHistogram()
+   {
+     return this.hmmShowHistogram;
+   }
+   /**
     * when set, updateAlignment will always ensure sequences are of equal length
     */
    private boolean padGaps = false;
                  ignoreGapsInConsensusCalculation);
        }
      }
+   }
  
+   public void setIgnoreBelowBackground(boolean b, AlignmentViewPanel ap)
+   {
+     ignoreBelowBackGroundFrequencyCalculation = b;
+   }
+   public void setInfoLetterHeight(boolean b, AlignmentViewPanel ap)
+   {
+     infoLetterHeight = b;
    }
  
    private long sgrouphash = -1, colselhash = -1;
      return ignoreGapsInConsensusCalculation;
    }
  
+   @Override
+   public boolean isIgnoreBelowBackground()
+   {
+     return ignoreBelowBackGroundFrequencyCalculation;
+   }
+   @Override
+   public boolean isInfoLetterHeight()
+   {
+     return infoLetterHeight;
+   }
    // property change stuff
    // JBPNote Prolly only need this in the applet version.
    private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
        }
      } while (end < max);
  
 -    int[][] startEnd = new int[regions.size()][2];
 +    // int[][] startEnd = new int[regions.size()][2];
  
      return regions;
    }
  
      updateAllColourSchemes();
      calculator.restartWorkers();
-     // alignment.adjustSequenceAnnotations();
    }
  
    /**
                MessageManager.getString("label.consensus_descr"),
                new Annotation[1], 0f, 100f, AlignmentAnnotation.BAR_GRAPH);
        initConsensus(consensus);
        initGapCounts();
  
        initComplementConsensus();
      boolean showprf = isShowSequenceLogo();
      boolean showConsHist = isShowConsensusHistogram();
      boolean normLogo = isNormaliseSequenceLogo();
+     boolean showHMMPrf = isShowHMMSequenceLogo();
+     boolean showInfoHist = isShowInformationHistogram();
+     boolean normHMMLogo = isNormaliseHMMSequenceLogo();
  
      /**
       * TODO reorder the annotation rows according to group/sequence ordering on
       * alignment
       */
 -    boolean sortg = true;
 +    // boolean sortg = true;
  
      // remove old automatic annotation
      // add any new annotation
            sg.setshowSequenceLogo(showprf);
            sg.setShowConsensusHistogram(showConsHist);
            sg.setNormaliseSequenceLogo(normLogo);
+           sg.setShowHMMSequenceLogo(showHMMPrf);
+           sg.setShowInformationHistogram(showInfoHist);
+           sg.setNormaliseHMMSequenceLogo(normHMMLogo);
          }
          if (conv)
          {
    public void clearSequenceColours()
    {
      sequenceColours.clear();
 -  };
 +  }
  
    @Override
    public AlignViewportI getCodingComplement()
      return sq;
    }
  
+   public boolean hasReferenceAnnotation()
+   {
+     AlignmentAnnotation[] annots = this.alignment.getAlignmentAnnotation();
+     for (AlignmentAnnotation annot : annots)
+     {
+       if ("RF".equals(annot.label) || annot.label.contains("Reference"))
+       {
+         return true;
+       }
+     }
+     return false;
+   }
    @Override
    public void setCurrentTree(TreeModel tree)
    {
    }
  
    @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
        codingComplement.setUpdateStructures(needToUpdateStructureViews);
      }
    }
+   /**
+    * Filters out sequences with an eValue higher than the specified value. The
+    * filtered sequences are hidden or deleted. Sequences with no eValues are also
+    * filtered out.
+    * 
+    * @param eValue
+    * @param delete
+    */
+   public void filterByEvalue(double eValue)
+   {
+     for (SequenceI seq : alignment.getSequencesArray())
+     {
+       if ((seq.getAnnotation("Search Scores") == null
+               || seq.getAnnotation("Search Scores")[0].getEValue() > eValue)
+               && seq.getHMM() == null)
+       {
+         hideSequence(new SequenceI[] { seq });
+       }
+     }
+   }
+   /**
+    * Filters out sequences with an score lower than the specified value. The
+    * filtered sequences are hidden or deleted.
+    * 
+    * @param score
+    * @param delete
+    */
+   public void filterByScore(double score)
+   {
+     for (SequenceI seq : alignment.getSequencesArray())
+     {
+       if ((seq.getAnnotation("Search Scores") == null
+               || seq.getAnnotation("Search Scores")[0]
+                       .getBitScore() < score)
+               && seq.getHMM() == null)
+       {
+         hideSequence(new SequenceI[] { seq });
+       }
+     }
+   }
  }
@@@ -46,7 -46,7 +46,7 @@@ public class ConsensusThread extends Al
        return;
      }
      calcMan.notifyStart(this);
 -    long started = System.currentTimeMillis();
 +    // long started = System.currentTimeMillis();
      try
      {
        AlignmentAnnotation consensus = getConsensusAnnotation();
    protected void deriveConsensus(AlignmentAnnotation consensusAnnotation,
            ProfilesI hconsensus)
    {
      long nseq = getSequences().length;
      AAFrequency.completeConsensus(consensusAnnotation, hconsensus,
              hconsensus.getStartColumn(), hconsensus.getEndColumn() + 1,
@@@ -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;
@@@ -56,7 -54,6 +55,7 @@@ import java.util.Map
  import java.util.Map.Entry;
  
  import javax.xml.bind.JAXBContext;
 +import javax.xml.bind.JAXBElement;
  import javax.xml.bind.JAXBException;
  import javax.xml.stream.FactoryConfigurationError;
  import javax.xml.stream.XMLInputFactory;
@@@ -183,8 -180,8 +182,8 @@@ public abstract class EmblXmlSource ext
        XMLStreamReader streamReader = XMLInputFactory.newInstance()
                .createXMLStreamReader(is);
        javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
 -      jalview.xml.binding.embl.ROOT root = (jalview.xml.binding.embl.ROOT) um
 -              .unmarshal(streamReader);
 +      JAXBElement<ROOT> rootElement =  um.unmarshal(streamReader, ROOT.class);
 +      ROOT root = rootElement.getValue();
  
        /*
         * document root contains either "entry" or "entrySet"
@@@ -32,7 -32,6 +32,6 @@@ import jalview.datamodel.SequenceI
  import jalview.schemes.ResidueProperties;
  import jalview.util.StringUtils;
  import jalview.ws.seqfetcher.DbSourceProxyImpl;
- import jalview.xml.binding.embl.ROOT;
  import jalview.xml.binding.uniprot.DbReferenceType;
  import jalview.xml.binding.uniprot.Entry;
  import jalview.xml.binding.uniprot.FeatureType;
@@@ -48,7 -47,6 +47,7 @@@ import java.util.List
  import java.util.Vector;
  
  import javax.xml.bind.JAXBContext;
 +import javax.xml.bind.JAXBElement;
  import javax.xml.bind.JAXBException;
  import javax.xml.stream.FactoryConfigurationError;
  import javax.xml.stream.XMLInputFactory;
@@@ -503,10 -501,7 +502,10 @@@ public class Uniprot extends DbSourcePr
        XMLStreamReader streamReader = XMLInputFactory.newInstance()
                .createXMLStreamReader(is);
        javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
 -      jalview.xml.binding.uniprot.Uniprot uniprot = (jalview.xml.binding.uniprot.Uniprot) um.unmarshal(streamReader);
 +      JAXBElement<jalview.xml.binding.uniprot.Uniprot> uniprotElement = 
 +                um.unmarshal(streamReader, jalview.xml.binding.uniprot.Uniprot.class);
 +      jalview.xml.binding.uniprot.Uniprot uniprot = uniprotElement.getValue();
 +      
        if (uniprot != null && !uniprot.getEntry().isEmpty())
        {
          entries = uniprot.getEntry();
@@@ -47,7 -47,7 +47,7 @@@ import compbio.metadata.RunnerConfig
  public class JabaParamStore implements ParamDatastoreI
  {
  
-   Hashtable<String, JabaWsParamSet> editedParams = new Hashtable<String, JabaWsParamSet>();
+   Hashtable<String, JabaWsParamSet> editedParams = new Hashtable<>();
  
    private Jws2Instance service;
  
    @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)
        {
    public static List<ArgumentI> getJwsArgsfromJaba(List jabargs,
            boolean sortByOpt)
    {
-     List<ArgumentI> rgs = new ArrayList<ArgumentI>();
-     List<String> rgnames = new ArrayList<String>();
+     List<ArgumentI> rgs = new ArrayList<>();
+     List<String> rgnames = new ArrayList<>();
      for (Object rg : jabargs)
      {
        ArgumentI narg = null;
      boolean found = false;
      for (String url : urls)
      {
-       if (service.getServiceTypeURI().equals(url)
+       if (service.getNameURI().equals(url)
                || service.getUri().equalsIgnoreCase(url))
        {
          found = true;
      wsp.setDescription(descr);
      wsp.setApplicableUrls(urls.clone());
  
-     List<String> lines = new ArrayList<String>();
+     List<String> lines = new ArrayList<>();
      StringTokenizer st = new StringTokenizer(parameterfile, "\n");
      while (st.hasMoreTokens())
      {
@@@ -22,34 -22,25 +22,25 @@@ package jalview.ws.jws2
  
  import jalview.bin.Cache;
  import jalview.gui.AlignFrame;
- import jalview.gui.Desktop;
- import jalview.gui.JvSwingUtils;
  import jalview.util.MessageManager;
  import jalview.ws.WSMenuEntryProviderI;
+ import jalview.ws.api.ServiceWithParameters;
  import jalview.ws.jws2.jabaws2.Jws2Instance;
  import jalview.ws.params.ParamDatastoreI;
  
- import java.awt.Color;
- import java.awt.event.ActionEvent;
- import java.awt.event.ActionListener;
  import java.beans.PropertyChangeEvent;
  import java.beans.PropertyChangeListener;
  import java.beans.PropertyChangeSupport;
  import java.net.MalformedURLException;
  import java.net.URL;
  import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.HashMap;
  import java.util.HashSet;
- import java.util.Hashtable;
  import java.util.List;
- import java.util.Map;
  import java.util.Set;
  import java.util.StringTokenizer;
  import java.util.Vector;
  
  import javax.swing.JMenu;
- import javax.swing.JMenuItem;
  
  import compbio.ws.client.Services;
  
@@@ -249,6 -240,7 +240,6 @@@ public class Jws2Discoverer implements 
        } catch (Exception e)
        {
        }
 -      ;
        for (JabaWsServerQuery squery : qrys)
        {
          if (squery.isRunning())
          for (Jws2Instance svc : services)
          {
            svcs[ipos] = svc;
-           spos[ipos++] = 1000 * svcUrls.indexOf(svc.getHost()) + 1
-                   + svctypes.indexOf(svc.serviceType);
+           spos[ipos++] = 1000 * svcUrls.indexOf(svc.getHostURL()) + 1
+                   + svctypes.indexOf(svc.getName());
          }
          jalview.util.QuickSort.sort(spos, svcs);
          services = new Vector<>();
          for (Jws2Instance svc : svcs)
          {
-           if (!ignoredServices.contains(svc.serviceType))
+           if (!ignoredServices.contains(svc.getName()))
            {
              services.add(svc);
            }
    @Override
    public void attachWSMenuEntry(JMenu wsmenu, final AlignFrame alignFrame)
    {
+     if (running || services == null || services.size() == 0)
+     {
+       return;
+     }
      // dynamically regenerate service list.
      populateWSMenuEntry(wsmenu, alignFrame, null);
    }
  
    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)
        {
          testUrls.add(url);
        }
 -      ;
      }
      Thread runner = getDiscoverer()
              .startDiscoverer(new PropertyChangeListener()
                    System.out.println("Changesupport: There are now "
                            + getDiscoverer().services.size() + " services");
                    int i = 1;
-                   for (Jws2Instance instance : getDiscoverer().services)
+                   for (ServiceWithParameters instance : getDiscoverer().services)
                    {
                      System.out.println("Service " + i++ + " "
-                             + instance.getClass() + "@" + instance.getHost()
+                             + instance.getClass() + "@"
+                             + instance.getHostURL()
                              + ": " + instance.getActionText());
                    }
  
        } catch (InterruptedException e)
        {
        }
 -      ;
      }
      try
      {
      return urls;
    }
  
-   public Vector<Jws2Instance> getServices()
+   public Vector<ServiceWithParameters> getServices()
    {
      return (services == null) ? new Vector<>()
              : new Vector<>(services);
    }
  
    /**
-    * pick the user's preferred service based on a set of URLs (jaba server
-    * locations) and service URIs (specifying version and service interface
-    * class)
-    * 
-    * @param serviceURL
-    * @return null or best match for given uri/ls.
-    */
-   public Jws2Instance getPreferredServiceFor(String[] serviceURLs)
-   {
-     HashSet<String> urls = new HashSet<>();
-     urls.addAll(Arrays.asList(serviceURLs));
-     Jws2Instance match = null;
-     if (services != null)
-     {
-       for (Jws2Instance svc : services)
-       {
-         if (urls.contains(svc.getServiceTypeURI()))
-         {
-           if (match == null)
-           {
-             // for moment we always pick service from server ordered first in
-             // user's preferences
-             match = svc;
-           }
-           if (urls.contains(svc.getUri()))
-           {
-             // stop and return - we've matched type URI and URI for service
-             // endpoint
-             return svc;
-           }
-         }
-       }
-     }
-     return match;
-   }
-   Map<String, Map<String, String>> preferredServiceMap = new HashMap<>();
-   /**
-    * get current preferred service of the given type, or global default
-    * 
-    * @param af
-    *          null or a specific alignFrame
-    * @param serviceType
-    *          Jws2Instance.serviceType for service
-    * @return null if no service of this type is available, the preferred service
-    *         for the serviceType and af if specified and if defined.
-    */
-   public Jws2Instance getPreferredServiceFor(AlignFrame af,
-           String serviceType)
-   {
-     String serviceurl = null;
-     synchronized (preferredServiceMap)
-     {
-       String afid = (af == null) ? "" : af.getViewport().getSequenceSetId();
-       Map<String, String> prefmap = preferredServiceMap.get(afid);
-       if (afid.length() > 0 && prefmap == null)
-       {
-         // recover global setting, if any
-         prefmap = preferredServiceMap.get("");
-       }
-       if (prefmap != null)
-       {
-         serviceurl = prefmap.get(serviceType);
-       }
-     }
-     Jws2Instance response = null;
-     for (Jws2Instance svc : services)
-     {
-       if (svc.serviceType.equals(serviceType))
-       {
-         if (serviceurl == null || serviceurl.equals(svc.getHost()))
-         {
-           response = svc;
-           break;
-         }
-       }
-     }
-     return response;
-   }
-   public void setPreferredServiceFor(AlignFrame af, String serviceType,
-           String serviceAction, Jws2Instance selectedServer)
-   {
-     String afid = (af == null) ? "" : af.getViewport().getSequenceSetId();
-     if (preferredServiceMap == null)
-     {
-       preferredServiceMap = new HashMap<>();
-     }
-     Map<String, String> prefmap = preferredServiceMap.get(afid);
-     if (prefmap == null)
-     {
-       prefmap = new HashMap<>();
-       preferredServiceMap.put(afid, prefmap);
-     }
-     prefmap.put(serviceType, selectedServer.getHost());
-     prefmap.put(serviceAction, selectedServer.getHost());
-   }
-   public void setPreferredServiceFor(String serviceType,
-           String serviceAction, Jws2Instance selectedServer)
-   {
-     setPreferredServiceFor(null, serviceType, serviceAction,
-             selectedServer);
-   }
-   /**
     * Set a URL to try before any others. For use with command-line parameter to
     * configure a local Jabaws installation without the need to add to property
     * files.
@@@ -27,7 -27,12 +27,12 @@@ import jalview.gui.Desktop
  import jalview.gui.JvOptionPane;
  import jalview.gui.JvSwingUtils;
  import jalview.util.MessageManager;
- import jalview.ws.jws2.jabaws2.Jws2Instance;
+ import jalview.ws.WSMenuEntryProviderI;
+ import jalview.ws.api.JalviewServiceEndpointProviderI;
+ import jalview.ws.api.MultipleSequenceAlignmentI;
+ import jalview.ws.api.ServiceWithParameters;
+ import jalview.ws.gui.MsaWSThread;
+ import jalview.ws.params.ArgumentI;
  import jalview.ws.params.WsParamSetI;
  
  import java.awt.event.ActionEvent;
@@@ -40,23 -45,28 +45,28 @@@ import javax.swing.JMenu
  import javax.swing.JMenuItem;
  import javax.swing.ToolTipManager;
  
- import compbio.data.msa.MsaWS;
- import compbio.metadata.Argument;
  /**
-  * DOCUMENT ME!
+  * MsaWSClient
+  * 
+  * Instantiates web service menu items for multiple alignment services, and
+  * holds logic for constructing a web service thread.
   * 
-  * @author $author$
+  * TODO remove dependency on Jws2Client methods for creating AACon service UI
+  * elements.
+  * 
+  * @author Jim Procter et al
   * @version $Revision$
   */
- public class MsaWSClient extends Jws2Client
+ public class MsaWSClient extends Jws2Client implements WSMenuEntryProviderI
  {
    /**
-    * server is a WSDL2Java generated stub for an archetypal MsaWSI service.
+    * server is a proxy class implementing the core methods for submitting,
+    * monitoring and retrieving results from a multiple sequence alignment
+    * service
     */
-   MsaWS server;
+   MultipleSequenceAlignmentI server;
  
-   public MsaWSClient(Jws2Instance sh, String altitle,
+   public MsaWSClient(ServiceWithParameters sh, String altitle,
            jalview.datamodel.AlignmentView msa, boolean submitGaps,
            boolean preserveOrder, AlignmentI seqdataset,
            AlignFrame _alignFrame)
@@@ -66,7 -76,8 +76,8 @@@
      // TODO Auto-generated constructor stub
    }
  
-   public MsaWSClient(Jws2Instance sh, WsParamSetI preset, String altitle,
+   public MsaWSClient(ServiceWithParameters sh, WsParamSetI preset,
+           String altitle,
            jalview.datamodel.AlignmentView msa, boolean submitGaps,
            boolean preserveOrder, AlignmentI seqdataset,
            AlignFrame _alignFrame)
     *          DOCUMENT ME!
     */
  
-   public MsaWSClient(Jws2Instance sh, WsParamSetI preset,
-           List<Argument> arguments, boolean editParams, String altitle,
+   public MsaWSClient(ServiceWithParameters sh, WsParamSetI preset,
+           List<ArgumentI> arguments, boolean editParams, String altitle,
            jalview.datamodel.AlignmentView msa, boolean submitGaps,
            boolean preserveOrder, AlignmentI seqdataset,
            AlignFrame _alignFrame)
        return;
      }
  
-     if (!(sh.service instanceof MsaWS))
+     if (!(sh instanceof JalviewServiceEndpointProviderI
+             && ((JalviewServiceEndpointProviderI) sh)
+                     .getEndpoint() instanceof MultipleSequenceAlignmentI))
      {
        // redundant at mo - but may change
        JvOptionPane.showMessageDialog(Desktop.desktop,
                MessageManager.formatMessage(
                        "label.service_called_is_not_msa_service",
                        new String[]
-                       { sh.serviceType }),
+                       { sh.getName() }),
                MessageManager.getString("label.internal_jalview_error"),
                JvOptionPane.WARNING_MESSAGE);
  
        return;
      }
-     server = (MsaWS) sh.service;
+     serviceHandle = sh;
+     server = (MultipleSequenceAlignmentI) ((JalviewServiceEndpointProviderI) sh)
+             .getEndpoint();
      if ((wsInfo = setWebService(sh, false)) == null)
      {
        JvOptionPane.showMessageDialog(Desktop.desktop, MessageManager
                .formatMessage("label.msa_service_is_unknown", new String[]
-               { sh.serviceType }),
+               { sh.getName() }),
                MessageManager.getString("label.internal_jalview_error"),
                JvOptionPane.WARNING_MESSAGE);
  
      }
      else
      {
 +      wsInfo.setVisible(false);
        JvOptionPane.showMessageDialog(alignFrame,
                MessageManager.getString("info.invalid_msa_input_mininfo"),
                MessageManager.getString("info.invalid_msa_notenough"),
                JvOptionPane.INFORMATION_MESSAGE);
      }
    }
  
 -  public static void main(String[] args)
 -  {
 -    System.out.println("A".matches("(-*[a-zA-Z]-*){1}[a-zA-Z-]*"));
 -  }
 -
    protected String getServiceActionKey()
    {
      return "MsaWS";
  
    @Override
    public void attachWSMenuEntry(JMenu rmsawsmenu,
-           final Jws2Instance service, final AlignFrame alignFrame)
+           final ServiceWithParameters service, final AlignFrame alignFrame)
    {
-     if (registerAAConWSInstance(rmsawsmenu, service, alignFrame))
+     if (Jws2ClientFactory.registerAAConWSInstance(rmsawsmenu,
+                     service, alignFrame))
      {
        // Alignment dependent analysis calculation WS gui
        return;
      }
+     serviceHandle = service;
      setWebService(service, true); // headless
+     attachWSMenuEntry(rmsawsmenu, alignFrame);
+   }
+   @Override
+   public void attachWSMenuEntry(JMenu wsmenu, AlignFrame alignFrame)
+   {
      boolean finished = true, submitGaps = false;
-     JMenu msawsmenu = rmsawsmenu;
+     /**
+      * temp variables holding msa service submenu or root service menu
+      */
+     JMenu msawsmenu = wsmenu;
+     JMenu rmsawsmenu = wsmenu;
      String svcname = WebServiceName;
      if (svcname.endsWith("WS"))
      {
        rmsawsmenu.add(msawsmenu);
        calcName = "";
      }
-     boolean hasparams = service.hasParameters();
+     boolean hasparams = serviceHandle.hasParameters();
+     ServiceWithParameters service = (ServiceWithParameters) serviceHandle;
      do
      {
        String action = "Align ";
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,21 -25,27 +25,30 @@@ 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;
  
  public class AAFrequencyTest
  {
+   HiddenMarkovModel hmm;
  
    @BeforeClass(alwaysRun = true)
    public void setUpJvOptionPane()
      JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
    }
  
+   @BeforeClass(alwaysRun = true)
+   public void setUp() throws IOException, MalformedURLException
+   {
+     /*
+      * load a dna (ACGT) HMM file to a HiddenMarkovModel
+      */
+     HMMFile hmmFile = new HMMFile(new FileParse(
+             "test/jalview/io/test_MADE1_hmm.txt", DataSourceType.FILE));
+     hmm = hmmFile.getHMM();
+   }
    @Test(groups = { "Functional" })
    public void testCalculate_noProfile()
    {
      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;
+   }
 -
  }
@@@ -27,7 -27,6 +27,6 @@@ import static org.testng.AssertJUnit.as
  import static org.testng.AssertJUnit.assertSame;
  import static org.testng.AssertJUnit.assertTrue;
  
- import jalview.analysis.AlignmentUtils.DnaVariant;
  import jalview.datamodel.AlignedCodonFrame;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentAnnotation;
@@@ -41,7 -40,6 +40,6 @@@ import jalview.datamodel.SearchResultsI
  import jalview.datamodel.Sequence;
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceI;
- import jalview.datamodel.features.SequenceFeatures;
  import jalview.gui.JvOptionPane;
  import jalview.io.AppletFormatAdapter;
  import jalview.io.DataSourceType;
@@@ -51,12 -49,10 +49,10 @@@ import jalview.io.FormatAdapter
  import jalview.io.gff.SequenceOntologyI;
  import jalview.util.MapList;
  import jalview.util.MappingUtils;
- import jalview.ws.params.InvalidArgumentException;
  
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.Arrays;
- import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.TreeMap;
@@@ -975,7 -971,7 +971,7 @@@ public class AlignmentUtilsTest
      assertTrue(AlignmentUtils.haveCrossRef(seq2, seq1));
  
      // now the other way round
 -    seq1.setDBRefs(null);
 +      seq1.setDBRefs(null);
      seq2.addDBRef(new DBRefEntry("EMBL", "1", "A12345"));
      assertTrue(AlignmentUtils.haveCrossRef(seq1, seq2));
      assertTrue(AlignmentUtils.haveCrossRef(seq2, seq1));
      DBRefEntry dna1xref = new DBRefEntry("UNIPROT", "ENSEMBL", "pep1",
              new Mapping(mapfordna1));
      dna1.addDBRef(dna1xref);
 -    assertEquals(2, dna1.getDBRefs().length); // to self and to pep1
 +    assertEquals(2, dna1.getDBRefs().size()); // to self and to pep1
      DBRefEntry dna2xref = new DBRefEntry("UNIPROT", "ENSEMBL", "pep2",
              new Mapping(mapfordna2));
      dna2.addDBRef(dna2xref);
 -    assertEquals(2, dna2.getDBRefs().length); // to self and to pep2
 +    assertEquals(2, dna2.getDBRefs().size()); // to self and to pep2
  
      /*
       * execute method under test:
       * verify CDS has a dbref with mapping to peptide
       */
      assertNotNull(cds1Dss.getDBRefs());
 -    assertEquals(2, cds1Dss.getDBRefs().length);
 -    dbref = cds1Dss.getDBRefs()[0];
 +    assertEquals(2, cds1Dss.getDBRefs().size());
 +    dbref = cds1Dss.getDBRefs().get(0);
      assertEquals(dna1xref.getSource(), dbref.getSource());
      // version is via ensembl's primary ref
      assertEquals(dna1xref.getVersion(), dbref.getVersion());
       */
      assertNotNull(pep1.getDBRefs());
      // FIXME pep1.getDBRefs() is 1 - is that the correct behaviour ?
 -    assertEquals(2, pep1.getDBRefs().length);
 -    dbref = pep1.getDBRefs()[1];
 +    assertEquals(2, pep1.getDBRefs().size());
 +    dbref = pep1.getDBRefs().get(1);
      assertEquals("ENSEMBL", dbref.getSource());
      assertEquals("0", dbref.getVersion());
      assertEquals("CDS|dna1", dbref.getAccessionId());
      /*
       * verify cDNA has added a dbref with mapping to CDS
       */
 -    assertEquals(3, dna1.getDBRefs().length);
 -    DBRefEntry dbRefEntry = dna1.getDBRefs()[2];
 +    assertEquals(3, dna1.getDBRefs().size());
 +    DBRefEntry dbRefEntry = dna1.getDBRefs().get(2);
      assertSame(cds1Dss, dbRefEntry.getMap().getTo());
      MapList dnaToCdsMapping = new MapList(new int[] { 4, 6, 10, 12 },
              new int[] { 1, 6 }, 1, 1);
      assertEquals(dnaToCdsMapping, dbRefEntry.getMap().getMap());
 -    assertEquals(3, dna2.getDBRefs().length);
 -    dbRefEntry = dna2.getDBRefs()[2];
 +    assertEquals(3, dna2.getDBRefs().size());
 +    dbRefEntry = dna2.getDBRefs().get(2);
      assertSame(cds2Dss, dbRefEntry.getMap().getTo());
      dnaToCdsMapping = new MapList(new int[] { 1, 3, 7, 9, 13, 15 },
              new int[] { 1, 9 }, 1, 1);
      /*
       * verify CDS has added a dbref with mapping to cDNA
       */
 -    assertEquals(2, cds1Dss.getDBRefs().length);
 -    dbRefEntry = cds1Dss.getDBRefs()[1];
 +    assertEquals(2, cds1Dss.getDBRefs().size());
 +    dbRefEntry = cds1Dss.getDBRefs().get(1);
      assertSame(dna1.getDatasetSequence(), dbRefEntry.getMap().getTo());
      MapList cdsToDnaMapping = new MapList(new int[] { 1, 6 }, new int[] {
          4, 6, 10, 12 }, 1, 1);
      assertEquals(cdsToDnaMapping, dbRefEntry.getMap().getMap());
 -    assertEquals(2, cds2Dss.getDBRefs().length);
 -    dbRefEntry = cds2Dss.getDBRefs()[1];
 +    assertEquals(2, cds2Dss.getDBRefs().size());
 +    dbRefEntry = cds2Dss.getDBRefs().get(1);
      assertSame(dna2.getDatasetSequence(), dbRefEntry.getMap().getTo());
      cdsToDnaMapping = new MapList(new int[] { 1, 9 }, new int[] { 1, 3, 7,
          9, 13, 15 }, 1, 1);
@@@ -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;
@@@ -75,28 -73,28 +74,28 @@@ public class CrossRefTes
      DBRefEntry ref8 = new DBRefEntry("PFAM", "1", "A123");
      // ENSEMBL is a source of either dna or protein sequence data
      DBRefEntry ref9 = new DBRefEntry("ENSEMBL", "1", "A123");
 -    DBRefEntry[] refs = new DBRefEntry[] { ref1, ref2, ref3, ref4, ref5,
 -        ref6, ref7, ref8, ref9 };
 +    List<DBRefEntry> refs = Arrays.asList(new DBRefEntry[] { ref1, ref2, ref3, ref4, ref5,
 +            ref6, ref7, ref8, ref9 });
  
      /*
       * Just the DNA refs:
       */
 -    DBRefEntry[] found = DBRefUtils.selectDbRefs(true, refs);
 -    assertEquals(4, found.length);
 -    assertSame(ref5, found[0]);
 -    assertSame(ref6, found[1]);
 -    assertSame(ref7, found[2]);
 -    assertSame(ref9, found[3]);
 +    List<DBRefEntry> found = DBRefUtils.selectDbRefs(true, refs);
 +    assertEquals(4, found.size());
 +    assertSame(ref5, found.get(0));
 +    assertSame(ref6, found.get(1));
 +    assertSame(ref7, found.get(2));
 +    assertSame(ref9, found.get(3));
  
      /*
       * Just the protein refs:
       */
      found = DBRefUtils.selectDbRefs(false, refs);
 -    assertEquals(4, found.length);
 -    assertSame(ref1, found[0]);
 -    assertSame(ref2, found[1]);
 -    assertSame(ref4, found[2]);
 -    assertSame(ref9, found[3]);
 +    assertEquals(4, found.size());
 +    assertSame(ref1, found.get(0));
 +    assertSame(ref2, found.get(1));
 +    assertSame(ref4, found.get(2));
 +    assertSame(ref9, found.get(3));
    }
  
    /**
       * and others to dna coding databases
       */
      sources.clear();
 -    seq.setDBRefs(null);
 +      seq.setDBRefs(null);
      seq.addDBRef(new DBRefEntry("UNIPROT", "0", "A1234"));
      seq.addDBRef(new DBRefEntry("EMBLCDS", "0", "E2347"));
      SequenceI seq2 = new Sequence("Seq2", "MGKYQARLSS");
      CrossRef testee = new CrossRef(al.getSequencesArray(), al);
      AlignedCodonFrame acf = new AlignedCodonFrame();
      boolean found = testee.searchDataset(true, dna1, dbref, result, acf,
 -            true);
 +            true, DBRefUtils.SEARCH_MODE_FULL);
      assertFalse(found);
      assertTrue(result.isEmpty());
      assertTrue(acf.isEmpty());
      acf = new AlignedCodonFrame();
      dbref = new DBRefEntry("UNIPROT", "0", "Q9ZTS2");
      found = testee.searchDataset(!dna1.isProtein(), dna1, dbref, result,
 -            acf, false); // search dataset with a protein xref from a dna
 +            acf, false, DBRefUtils.SEARCH_MODE_FULL); // search dataset with a protein xref from a dna
                           // sequence to locate the protein product
      assertTrue(found);
      assertEquals(1, result.size());
      acf = new AlignedCodonFrame();
      dbref = new DBRefEntry("UNIPROT", "0", "Q9ZTS2");
      found = testee.searchDataset(!pep1.isProtein(), pep1, dbref, result,
 -            acf, false); // search dataset with a protein's direct dbref to
 +            acf, false, DBRefUtils.SEARCH_MODE_FULL); // search dataset with a protein's direct dbref to
                           // locate dna sequences with matching xref
      assertTrue(found);
      assertEquals(1, result.size());
      /*
       * verify mappings added to Uniprot-to-EMBL dbrefs
       */
 -    Mapping mapping = p0ce19.getDBRefs()[0].getMap();
 +    Mapping mapping = p0ce19.getDBRefs().get(0).getMap();
      assertSame(j03321, mapping.getTo());
 -    mapping = p0ce19.getDBRefs()[1].getMap();
 +    mapping = p0ce19.getDBRefs().get(1).getMap();
      assertSame(x06707, mapping.getTo());
 -    mapping = p0ce20.getDBRefs()[0].getMap();
 +    mapping = p0ce20.getDBRefs().get(0).getMap();
      assertSame(j03321, mapping.getTo());
 -    mapping = p0ce20.getDBRefs()[1].getMap();
 +    mapping = p0ce20.getDBRefs().get(1).getMap();
      assertSame(x06707, mapping.getTo());
  
      /*
       * verify dbrefs on EMBL are mapped to alignment seqs
       */
 -    assertSame(p0ce19, j03321.getDBRefs()[0].getMap().getTo());
 -    assertSame(p0ce20, j03321.getDBRefs()[1].getMap().getTo());
 -    assertSame(p0ce19, x06707.getDBRefs()[0].getMap().getTo());
 -    assertSame(p0ce20, x06707.getDBRefs()[1].getMap().getTo());
 +    
 +    assertSame(p0ce19, j03321.getDBRefs().get(0).getMap().getTo());
 +    assertSame(p0ce20, j03321.getDBRefs().get(1).getMap().getTo());
 +    assertSame(p0ce19, x06707.getDBRefs().get(0).getMap().getTo());
 +    assertSame(p0ce20, x06707.getDBRefs().get(1).getMap().getTo());
  
      /*
       * verify new dbref on EMBL dbref mapping is copied to the
       * original Uniprot sequence
       */
 -    assertEquals(4, p0ce19.getDBRefs().length);
 -    assertEquals("PIR", p0ce19.getDBRefs()[3].getSource());
 -    assertEquals("S01875", p0ce19.getDBRefs()[3].getAccessionId());
 +    assertEquals(4, p0ce19.getDBRefs().size());
 +    assertEquals("PIR", p0ce19.getDBRefs().get(3).getSource());
 +    assertEquals("S01875", p0ce19.getDBRefs().get(3).getAccessionId());
    }
  
    @Test(groups = "Functional")
@@@ -1173,14 -1173,14 +1173,14 @@@ public class AlignmentTes
      /*
       * verify peptide.cdsdbref.peptidedbref is now mapped to peptide dataset
       */
 -    DBRefEntry[] dbRefs = pep.getDBRefs();
 -    assertEquals(2, dbRefs.length);
 -    assertSame(dna, dbRefs[0].map.to);
 -    assertSame(cds, dbRefs[1].map.to);
 -    assertEquals(1, dna.getDBRefs().length);
 -    assertSame(pep.getDatasetSequence(), dna.getDBRefs()[0].map.to);
 -    assertEquals(1, cds.getDBRefs().length);
 -    assertSame(pep.getDatasetSequence(), cds.getDBRefs()[0].map.to);
 +    List<DBRefEntry> dbRefs = pep.getDBRefs();
 +    assertEquals(2, dbRefs.size());
 +    assertSame(dna, dbRefs.get(0).map.to);
 +    assertSame(cds, dbRefs.get(1).map.to);
 +    assertEquals(1, dna.getDBRefs().size());
 +    assertSame(pep.getDatasetSequence(), dna.getDBRefs().get(0).map.to);
 +    assertEquals(1, cds.getDBRefs().size());
 +    assertSame(pep.getDatasetSequence(), cds.getDBRefs().get(0).map.to);
    }
  
    @Test(groups = { "Functional" })
              "Temperature Factor", null, false, seq, null);
      assertNotNull(ala);
      assertEquals(seq, ala.sequenceRef);
-     assertEquals("", ala.calcId);
+     assertEquals("", ala.getCalcId());
+   }
+   @Test(groups = {"Functional"})
+   public void testUpdateFromOrAddAnnotation()
+   {
+     SequenceI seq = new Sequence("seq1", "FRMLPSRT-A--L-");
+     AlignmentI alignment = new Alignment(new SequenceI[] { seq });
+     AlignmentAnnotation ala = alignment.findOrCreateAnnotation(
+             "Temperature Factor", null, false, seq, null);
+     assertNotNull(ala);
+     assertEquals(seq, ala.sequenceRef);
+     assertEquals("", ala.getCalcId());
+     // Assuming findOrCreateForNullCalcId passed then this should work
+     assertTrue(ala == alignment.updateFromOrCopyAnnotation(ala));
+     AlignmentAnnotation updatedAla = new AlignmentAnnotation(ala);
+     updatedAla.description = "updated Description";
+     Assert.assertTrue(
+             ala == alignment.updateFromOrCopyAnnotation(updatedAla));
+     Assert.assertEquals(ala.toString(), updatedAla.toString());
+     updatedAla.calcId = "newCalcId";
+     AlignmentAnnotation newUpdatedAla = alignment
+             .updateFromOrCopyAnnotation(updatedAla);
+     Assert.assertTrue(updatedAla != newUpdatedAla);
+     Assert.assertEquals(updatedAla.toString(), newUpdatedAla.toString());
    }
  
    @Test(groups = "Functional")
@@@ -34,7 -34,6 +34,6 @@@ import jalview.commands.EditCommand.Act
  import jalview.datamodel.PDBEntry.Type;
  import jalview.gui.JvOptionPane;
  import jalview.util.MapList;
- import jalview.ws.params.InvalidArgumentException;
  
  import java.io.File;
  import java.util.ArrayList;
@@@ -299,8 -298,6 +298,6 @@@ public class SequenceTes
       * invalid inputs
       */
      assertNull(sq.findPositions(6, 5));
-     assertNull(sq.findPositions(0, 5));
-     assertNull(sq.findPositions(-1, 5));
  
      /*
       * all gapped ranges
      assertEquals(new Range(11, 12), sq.findPositions(5, 10)); // DE
      assertEquals(new Range(8, 13), sq.findPositions(1, 13)); // the lot
      assertEquals(new Range(8, 13), sq.findPositions(1, 99));
+     /**
+      * now try on a sequence with no gaps
+      */
+     sq.createDatasetSequence();
+     assertEquals(new Range(8, 13),
+             sq.getDatasetSequence().findPositions(1, 99));
+     assertEquals(new Range(8, 13),
+             sq.getDatasetSequence().findPositions(0, 99));
    }
  
    /**
      assertNotNull(newDs);
      assertNotSame(ds, newDs);
      assertNotNull(sq.getDBRefs());
 -    assertEquals(1, sq.getDBRefs().length);
 -    assertNotSame(dbr1, sq.getDBRefs()[0]);
 -    assertEquals(dbr1, sq.getDBRefs()[0]);
 +    assertEquals(1, sq.getDBRefs().size());
 +    assertNotSame(dbr1, sq.getDBRefs().get(0));
 +    assertEquals(dbr1, sq.getDBRefs().get(0));
  
      /*
       * internal delete with sequence features
      assertEquals(4, sq.getEnd());
      assertSame(ds, PA.getValue(sq, "datasetSequence"));
      assertNotNull(sq.getDBRefs());
 -    assertEquals(1, sq.getDBRefs().length);
 -    assertSame(dbr1, sq.getDBRefs()[0]);
 +    assertEquals(1, sq.getDBRefs().size());
 +    assertSame(dbr1, sq.getDBRefs().get(0));
    }
  
    @Test(groups = { "Functional" })
              new AlignmentAnnotation("Test annot", "Test annot description",
                      annots));
      Assert.assertEquals(sq.getDescription(), "Test sequence description..");
 -    Assert.assertEquals(sq.getDBRefs().length, 5); // DBRefs are on dataset
 +    Assert.assertEquals(sq.getDBRefs().size(), 5); // DBRefs are on dataset
                                                     // sequence
      Assert.assertEquals(sq.getAllPDBEntries().size(), 4);
      Assert.assertNotNull(sq.getAnnotation());
      Assert.assertEquals(sq.getAnnotation()[0].annotations.length, 2);
 -    Assert.assertEquals(sq.getDatasetSequence().getDBRefs().length, 5); // same
 +    Assert.assertEquals(sq.getDatasetSequence().getDBRefs().size(), 5); // same
                                                                          // as
                                                                          // sq.getDBRefs()
      Assert.assertEquals(sq.getDatasetSequence().getAllPDBEntries().size(),
  
      Assert.assertEquals(derived.getDescription(),
              "Test sequence description..");
 -    Assert.assertEquals(derived.getDBRefs().length, 5); // come from dataset
 +    Assert.assertEquals(derived.getDBRefs().size(), 5); // come from dataset
      Assert.assertEquals(derived.getAllPDBEntries().size(), 4);
      Assert.assertNotNull(derived.getAnnotation());
      Assert.assertEquals(derived.getAnnotation()[0].annotations.length, 2);
 -    Assert.assertEquals(derived.getDatasetSequence().getDBRefs().length, 5);
 +    Assert.assertEquals(derived.getDatasetSequence().getDBRefs().size(), 5);
      Assert.assertEquals(derived.getDatasetSequence().getAllPDBEntries()
              .size(), 4);
      Assert.assertNotNull(derived.getDatasetSequence().getAnnotation());
      // but that doesn't distinguish it from an aligned sequence
      // which has not yet generated a dataset sequence
      // NB getDBRef looks inside dataset sequence if not null
 -    DBRefEntry[] dbrefs = copy.getDBRefs();
 -    assertEquals(1, dbrefs.length);
 -    assertFalse(dbrefs[0] == seq1.getDBRefs()[0]);
 -    assertTrue(dbrefs[0].equals(seq1.getDBRefs()[0]));
 +    List<DBRefEntry> dbrefs = copy.getDBRefs();
 +    assertEquals(1, dbrefs.size());
 +    assertFalse(dbrefs.get(0) == seq1.getDBRefs().get(0));
 +    assertTrue(dbrefs.get(0).equals(seq1.getDBRefs().get(0)));
    }
  
    @Test(groups = { "Functional" })
  
      // getDBRef looks inside dataset sequence and this is shared,
      // so holds the same dbref objects
 -    DBRefEntry[] dbrefs = copy.getDBRefs();
 -    assertEquals(1, dbrefs.length);
 -    assertSame(dbrefs[0], seq1.getDBRefs()[0]);
 +    List<DBRefEntry> dbrefs = copy.getDBRefs();
 +    assertEquals(1, dbrefs.size());
 +    assertSame(dbrefs.get(0), seq1.getDBRefs().get(0));
    }
  
    /**
      assertNull(sq.getDBRefs());
      DBRefEntry dbref = new DBRefEntry("Uniprot", "1", "P00340");
      sq.addDBRef(dbref);
 -    assertEquals(1, sq.getDBRefs().length);
 -    assertSame(dbref, sq.getDBRefs()[0]);
 +    assertEquals(1, sq.getDBRefs().size());
 +    assertSame(dbref, sq.getDBRefs().get(0));
  
      /*
       * change of version - new entry
       */
      DBRefEntry dbref2 = new DBRefEntry("Uniprot", "2", "P00340");
      sq.addDBRef(dbref2);
 -    assertEquals(2, sq.getDBRefs().length);
 -    assertSame(dbref, sq.getDBRefs()[0]);
 -    assertSame(dbref2, sq.getDBRefs()[1]);
 +    assertEquals(2, sq.getDBRefs().size());
 +    assertSame(dbref, sq.getDBRefs().get(0));
 +    assertSame(dbref2, sq.getDBRefs().get(1));
  
      /*
       * matches existing entry - not added
       */
      sq.addDBRef(new DBRefEntry("UNIPROT", "1", "p00340"));
 -    assertEquals(2, sq.getDBRefs().length);
 +    assertEquals(2, sq.getDBRefs().size());
  
      /*
       * different source = new entry
       */
      DBRefEntry dbref3 = new DBRefEntry("UniRef", "1", "p00340");
      sq.addDBRef(dbref3);
 -    assertEquals(3, sq.getDBRefs().length);
 -    assertSame(dbref3, sq.getDBRefs()[2]);
 +    assertEquals(3, sq.getDBRefs().size());
 +    assertSame(dbref3, sq.getDBRefs().get(2));
  
      /*
       * different ref = new entry
       */
      DBRefEntry dbref4 = new DBRefEntry("UniRef", "1", "p00341");
      sq.addDBRef(dbref4);
 -    assertEquals(4, sq.getDBRefs().length);
 -    assertSame(dbref4, sq.getDBRefs()[3]);
 +    assertEquals(4, sq.getDBRefs().size());
 +    assertSame(dbref4, sq.getDBRefs().get(3));
  
      /*
       * matching ref with a mapping - map updated
          1, 1 }, 3, 1));
      dbref5.setMap(map);
      sq.addDBRef(dbref5);
 -    assertEquals(4, sq.getDBRefs().length);
 -    assertSame(dbref4, sq.getDBRefs()[3]);
 +    assertEquals(4, sq.getDBRefs().size());
 +    assertSame(dbref4, sq.getDBRefs().get(3));
      assertSame(map, dbref4.getMap());
  
      /*
      DBRefEntry dbref6 = new DBRefEntry(dbref2.getSource(), "3",
              dbref2.getAccessionId());
      sq.addDBRef(dbref6);
 -    assertEquals(4, sq.getDBRefs().length);
 -    assertSame(dbref2, sq.getDBRefs()[1]);
 +    assertEquals(4, sq.getDBRefs().size());
 +    assertSame(dbref2, sq.getDBRefs().get(1));
      assertEquals("3", dbref2.getVersion());
  
      /*
      DBRefEntry dbref7 = new DBRefEntry(dbref3.getSource(), "3",
              dbref3.getAccessionId());
      sq.addDBRef(dbref7);
 -    assertEquals(4, sq.getDBRefs().length);
 -    assertSame(dbref3, sq.getDBRefs()[2]);
 +    assertEquals(4, sq.getDBRefs().size());
 +    assertSame(dbref3, sq.getDBRefs().get(2));
      assertEquals("3", dbref2.getVersion());
    }
  
      assertTrue(primaryDBRefs.isEmpty());
  
      // empty dbrefs
 -    sq.setDBRefs(new DBRefEntry[] {});
 +      sq.setDBRefs(null);
      primaryDBRefs = sq.getPrimaryDBRefs();
      assertTrue(primaryDBRefs.isEmpty());
  
@@@ -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;
@@@ -33,8 -33,6 +32,8 @@@ import org.testng.annotations.BeforeCla
  import org.testng.annotations.BeforeMethod;
  import org.testng.annotations.Test;
  
 +import junit.extensions.PA;
 +
  public class PDBFTSPanelTest
  {
  
    {
      PDBFTSPanel searchPanel = new PDBFTSPanel(null);
      JInternalFrame mainFrame = searchPanel.getMainFrame();
 -    JComboBox<String> txt_search = searchPanel.getTxtSearch();
 +//    JComboBox<String> txt_search = PA.gsearchPanel.getTxtSearch();
  
      assertTrue(mainFrame.getTitle().length() == 20);
      assertTrue(mainFrame.getTitle()
              .equalsIgnoreCase("PDB Sequence Fetcher"));
 -    txt_search.setSelectedItem("ABC");
 +    PA.invokeMethod(PA.getValue(searchPanel, "txt_search"), "setSelectedItem(java.lang.String)", "ABC");
 +  //  txt_search.setSelectedItem("ABC");
      try
      {
        // wait for web-service to handle response
@@@ -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;
      /*
       * wait for Consensus thread to complete
       */
 -    synchronized (this)
 +    do
      {
 -      while (af.getViewport().getConsensusSeq() == null)
 +      try
 +      {
 +        Thread.sleep(50);
 +      } catch (InterruptedException x)
        {
 -        try
 -        {
 -          wait(50);
 -        } catch (InterruptedException e)
 -        {
 -        }
        }
 -    }
 +    } while (af.getViewport().getCalcManager().isWorking());
    }
  
    public static void setUpJvOptionPane()
    @Test(groups = "Functional")
    public void testChangeColour_background_groupsAndThresholds()
    {
-     AlignViewport av = af.getViewport();
+     AlignViewportI av = af.getViewport();
      AlignmentI al = av.getAlignment();
  
      /*
    @Test(groups = "Functional")
    public void testColourThresholdActions()
    {
-     AlignViewport av = af.getViewport();
+     AlignViewportI av = af.getViewport();
      AlignmentI al = av.getAlignment();
  
      /*
  
      /*
       * inspect the colour of 
 -     * FER_CAPAN.9(I), column 14 (14 base 0)
 +     * FER_CAPAN.9(I), column 15 (14 base 0)
       * FER_CAPAN.10(SER), column 16 (15 base 0)
       */
      SequenceI ferCapan = al.findName("FER_CAPAN");
      SliderPanel sp = SliderPanel.getSliderPanel();
      assertTrue(sp.isForConservation());
      assertEquals(sp.getValue(), 30); // initial slider setting
 +    c = rs.findColour('I', 14, ferCapan);
 +    Color i_faded = new Color(255, 255, 255);
 +    assertEquals(c, i_faded);
      sp.valueChanged(10);
      assertSame(rs, av.getResidueShading());
 +    assertEquals(rs.getConservationInc(), 10);
      c = rs.findColour('I', 14, ferCapan);
 -    Color i_faded = new Color(196, 186, 196);
 +    i_faded = new Color(196, 186, 196);
      assertEquals(c, i_faded);
      c = rs.findColour('S', 15, ferCapan);
      Color s_faded = new Color(144, 225, 144);
    @Test(groups = "Functional")
    public void testNewView_colourThresholds()
    {
-     AlignViewport av = af.getViewport();
+     AlignViewportI av = af.getViewport();
      AlignmentI al = av.getAlignment();
  
      /*
       */
      af.newView_actionPerformed(null);
      assertEquals(af.alignPanel.getViewName(), "View 1");
-     AlignViewport av2 = af.getViewport();
+     AlignmentViewport av2 = af.getViewport();
      assertNotSame(av, av2);
      assertSame(av2, af.alignPanel.av);
      rs = av2.getResidueShading();
@@@ -27,6 -27,7 +27,7 @@@ import static org.testng.AssertJUnit.as
  import static org.testng.AssertJUnit.assertSame;
  import static org.testng.AssertJUnit.assertTrue;
  
+ import jalview.api.AlignViewportI;
  import java.util.ArrayList;
  import java.util.List;
  
@@@ -54,6 -55,7 +55,7 @@@ import jalview.schemes.ColourSchemeI
  import jalview.schemes.PIDColourScheme;
  import jalview.structure.StructureSelectionManager;
  import jalview.util.MapList;
+ import jalview.viewmodel.AlignmentViewport;
  import jalview.viewmodel.ViewportRanges;
  
  public class AlignViewportTest
@@@ -68,7 -70,7 +70,7 @@@
  
    AlignmentI al;
  
-   AlignViewport testee;
+   AlignmentViewport testee;
  
    @BeforeClass(alwaysRun = true)
    public static void setUpBeforeClass() throws Exception
       * wait for Conservation thread to complete
       */
      AlignViewport viewport = af.getViewport();
 +    waitForCalculations(viewport);
 +    AlignmentAnnotation[] anns = viewport.getAlignment()
 +            .getAlignmentAnnotation();
 +    assertNotNull("No annotations found", anns);
 +    assertEquals("More than one annotation found", 1, anns.length);
 +    assertTrue("Annotation is not Quality",
 +            anns[0].description.startsWith("Alignment Quality"));
 +    Annotation[] annotations = anns[0].annotations;
 +    assertNotNull("Quality annotations are null", annotations);
 +    assertNotNull("Quality in column 1 is null", annotations[0]);
 +    assertTrue("No quality value in column 1", annotations[0].value > 10f);
 +  }
 +
 +  /**
 +   * Wait for consensus etc calculation threads to complete
 +   * 
 +   * @param viewport
 +   */
 +  protected void waitForCalculations(AlignViewport viewport)
 +  {
      synchronized (this)
      {
        while (viewport.getCalcManager().isWorking())
          }
        }
      }
 -    AlignmentAnnotation[] anns = viewport.getAlignment()
 -            .getAlignmentAnnotation();
 -    assertNotNull("No annotations found", anns);
 -    assertEquals("More than one annotation found", 1, anns.length);
 -    assertTrue("Annotation is not Quality",
 -            anns[0].description.startsWith("Alignment Quality"));
 -    Annotation[] annotations = anns[0].annotations;
 -    assertNotNull("Quality annotations are null", annotations);
 -    assertNotNull("Quality in column 1 is null", annotations[0]);
 -    assertTrue("No quality value in column 1", annotations[0].value > 10f);
    }
  
    @Test(groups = { "Functional" })
    {
      AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
              "examples/uniref50.fa", DataSourceType.FILE);
-     AlignViewport av = af.getViewport();
+     AlignViewportI av = af.getViewport();
      SequenceGroup sg1 = new SequenceGroup();
      SequenceGroup sg2 = new SequenceGroup();
      SequenceGroup sg3 = new SequenceGroup();
      jalview.bin.Cache.setProperty("SHOW_OCCUPANCY", Boolean.FALSE.toString());
      AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
              "examples/uniref50.fa", DataSourceType.FILE);
-     AlignViewport av = af.getViewport();
-     Assert.assertNull(av.getAlignmentGapAnnotation(), "Preference did not disable occupancy row.");
+     AlignViewportI av = af.getViewport();
+     Assert.assertNull(av.getAlignmentGapAnnotation(),
+             "Preference did not disable occupancy row.");
      int c = 0;
      for (AlignmentAnnotation aa : av.getAlignment().findAnnotations(null,
              null, "Occupancy"))
      af = new FileLoader().LoadFileWaitTillLoaded(
              "examples/uniref50.fa", DataSourceType.FILE);
      av = af.getViewport();
-     Assert.assertNotNull(av.getAlignmentGapAnnotation(), "Preference did not enable occupancy row.");
+     Assert.assertNotNull(av.getAlignmentGapAnnotation(),
+             "Preference did not enable occupancy row.");
      c = 0;
      for (AlignmentAnnotation aa : av.getAlignment().findAnnotations(null,
              null, av.getAlignmentGapAnnotation().label))
      String fasta = ">s1\nA-C\n>s2\nA-C\n>s3\nA-D\n>s4\n--D\n";
      AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(fasta,
              DataSourceType.PASTE);
 -    AlignmentViewport testme = af.getViewport();
 +    AlignViewport testme = af.getViewport();
 +    waitForCalculations(testme);
      SequenceI cons = testme.getConsensusSeq();
      assertEquals("A-C", cons.getSequenceAsString());
    }
@@@ -28,23 -28,6 +28,23 @@@ import static org.testng.AssertJUnit.as
  import static org.testng.AssertJUnit.assertNull;
  import static org.testng.AssertJUnit.assertTrue;
  
 +import java.awt.Component;
 +import java.awt.Container;
 +import java.io.IOException;
 +import java.util.ArrayList;
 +import java.util.Collections;
 +import java.util.Iterator;
 +import java.util.List;
 +
 +import javax.swing.JMenu;
 +import javax.swing.JMenuItem;
 +import javax.swing.JPopupMenu;
 +import javax.swing.JSeparator;
 +
 +import org.testng.annotations.BeforeClass;
 +import org.testng.annotations.BeforeMethod;
 +import org.testng.annotations.Test;
 +
  import jalview.bin.Cache;
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.AlignmentI;
@@@ -65,6 -48,23 +65,6 @@@ import jalview.urls.desktop.DesktopUrlP
  import jalview.util.MessageManager;
  import jalview.util.UrlConstants;
  
 -import java.awt.Component;
 -import java.awt.Container;
 -import java.io.IOException;
 -import java.util.ArrayList;
 -import java.util.Collections;
 -import java.util.Iterator;
 -import java.util.List;
 -
 -import javax.swing.JMenu;
 -import javax.swing.JMenuItem;
 -import javax.swing.JPopupMenu;
 -import javax.swing.JSeparator;
 -
 -import org.testng.annotations.BeforeClass;
 -import org.testng.annotations.BeforeMethod;
 -import org.testng.annotations.Test;
 -
  public class PopupMenuTest
  {
  
      testee.configureReferenceAnnotationsMenu(menu, seqs);
      assertTrue(menu.isEnabled());
      String s = MessageManager.getString("label.add_annotations_for");
 -    String expected = "<html><style> p.ttip {width: 350; text-align: left; word-wrap: break-word;}</style><p class=\"ttip\">"
 -            + s + "<br/>Jmol/secondary structure<br/>PDB/Temp</p></html>";
 +    String expected = "<html><style> div.ttip {width:350px;white-space:pre-wrap;padding:2px;overflow-wrap:break-word;}</style>"
 +            + "<div class=\"ttip\">" + s
 +            + "<br/>Jmol/secondary structure<br/>PDB/Temp </div></html>";
      assertEquals(expected, menu.getToolTipText());
    }
  
      testee.configureReferenceAnnotationsMenu(menu, seqs);
      assertTrue(menu.isEnabled());
      String s = MessageManager.getString("label.add_annotations_for");
 -    String expected = "<html><style> p.ttip {width: 350; text-align: left; word-wrap: break-word;}</style><p class=\"ttip\">"
 -            + s + "<br/>Jmol/secondary structure<br/>PDB/Temp</p></html>";
 +    String expected = "<html><style> div.ttip {width:350px;white-space:pre-wrap;padding:2px;overflow-wrap:break-word;}</style>"
 +            + "<div class=\"ttip\">" + s
 +            + "<br/>Jmol/secondary structure<br/>PDB/Temp </div></html>";
      assertEquals(expected, menu.getToolTipText());
    }
  
      // PDB.secondary structure on Sequence0
      AlignmentAnnotation annotation = new AlignmentAnnotation(
              "secondary structure", "", 0);
+     annotation.annotations = new Annotation[] { new Annotation(2f) };
      annotation.setCalcId("PDB");
      seqs.get(0).getDatasetSequence().addAlignmentAnnotation(annotation);
      if (addToSequence)
      // PDB.Temp on Sequence1
      annotation = new AlignmentAnnotation("Temp", "", 0);
      annotation.setCalcId("PDB");
+     annotation.annotations = new Annotation[] { new Annotation(2f) };
      seqs.get(1).getDatasetSequence().addAlignmentAnnotation(annotation);
      if (addToSequence)
      {
      // JMOL.secondary structure on Sequence0
      annotation = new AlignmentAnnotation("secondary structure", "", 0);
      annotation.setCalcId("Jmol");
+     annotation.annotations = new Annotation[] { new Annotation(2f) };
      seqs.get(0).getDatasetSequence().addAlignmentAnnotation(annotation);
      if (addToSequence)
      {
@@@ -31,7 -31,6 +31,6 @@@ import jalview.datamodel.Sequence
  import jalview.datamodel.SequenceI;
  import jalview.fts.api.FTSData;
  import jalview.jbgui.GStructureChooser.FilterOption;
- import jalview.ws.params.InvalidArgumentException;
  
  import java.util.Collection;
  import java.util.Vector;
@@@ -94,7 -93,7 +93,7 @@@ public class StructureChooserTes
      assertEquals(
              "text:XYZ_1 OR text:XYZ_2 OR text:XYZ_3 OR text:XYZ_4 OR text:4kqy",
              query);
 -    seq.setDBRefs(null);
 +      seq.setDBRefs(null);
      query = StructureChooser.buildQuery(seq);
      assertEquals("text:4kqy", query);
  
@@@ -37,7 -37,7 +37,7 @@@ public class FileFormatsTes
              .getName())));
      assertTrue(formats.isIdentifiable(formats.forName(FileFormat.Jnet
              .getName())));
 -    assertFalse(formats.isIdentifiable(formats.forName(FileFormat.Jalview
 +    assertTrue(formats.isIdentifiable(formats.forName(FileFormat.Jalview
              .getName())));
      assertFalse(formats.isIdentifiable(null));
  
@@@ -55,7 -55,7 +55,7 @@@
    @Test(groups = "Functional")
    public void testGetReadableFormats()
    {
-     String expected = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview]";
+     String expected = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3]";
      FileFormats formats = FileFormats.getInstance();
      assertEquals(formats.getReadableFormats().toString(), expected);
    }
    @Test(groups = "Functional")
    public void testGetWritableFormats()
    {
-     String expected = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP]";
+     String expected = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP, HMMER3]";
      FileFormats formats = FileFormats.getInstance();
      assertEquals(formats.getWritableFormats(true).toString(), expected);
-     expected = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP, Jalview]";
+     expected = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP, Jalview, HMMER3]";
      assertEquals(formats.getWritableFormats(false).toString(), expected);
    }
  
    @Test(groups = "Functional")
    public void testDeregisterFileFormat()
    {
-     String writable = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP]";
-     String readable = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview]";
+     String writable = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP, HMMER3]";
+     String readable = "[Fasta, PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3]";
      FileFormats formats = FileFormats.getInstance();
      assertEquals(formats.getWritableFormats(true).toString(), writable);
      assertEquals(formats.getReadableFormats().toString(), readable);
  
      formats.deregisterFileFormat(FileFormat.Fasta.getName());
-     writable = "[PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP]";
-     readable = "[PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview]";
+     writable = "[PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP, HMMER3]";
+     readable = "[PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3]";
      assertEquals(formats.getWritableFormats(true).toString(), writable);
      assertEquals(formats.getReadableFormats().toString(), readable);
  
@@@ -89,8 -89,8 +89,8 @@@
       * re-register the format: it gets added to the end of the list
       */
      formats.registerFileFormat(FileFormat.Fasta);
-     writable = "[PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP, Fasta]";
-     readable = "[PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, Fasta]";
+     writable = "[PFAM, Stockholm, PIR, BLC, AMSA, JSON, PileUp, MSF, Clustal, PHYLIP, HMMER3, Fasta]";
+     readable = "[PFAM, Stockholm, PIR, BLC, AMSA, HTML, RNAML, JSON, PileUp, MSF, Clustal, PHYLIP, GFF or Jalview features, PDB, mmCIF, Jalview, HMMER3, Fasta]";
      assertEquals(formats.getWritableFormats(true).toString(), writable);
      assertEquals(formats.getReadableFormats().toString(), readable);
    }
@@@ -26,6 -26,17 +26,6 @@@ import static org.testng.AssertJUnit.as
  import static org.testng.AssertJUnit.assertTrue;
  import static org.testng.AssertJUnit.fail;
  
 -import jalview.datamodel.Alignment;
 -import jalview.datamodel.AlignmentAnnotation;
 -import jalview.datamodel.AlignmentI;
 -import jalview.datamodel.Annotation;
 -import jalview.datamodel.DBRefEntry;
 -import jalview.datamodel.Sequence;
 -import jalview.datamodel.SequenceFeature;
 -import jalview.datamodel.SequenceI;
 -import jalview.gui.JvOptionPane;
 -import jalview.util.DBRefUtils;
 -
  import java.io.File;
  import java.util.Arrays;
  import java.util.BitSet;
@@@ -39,17 -50,6 +39,17 @@@ import org.testng.Assert
  import org.testng.annotations.BeforeClass;
  import org.testng.annotations.Test;
  
 +import jalview.datamodel.Alignment;
 +import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.Annotation;
 +import jalview.datamodel.DBRefEntry;
 +import jalview.datamodel.Sequence;
 +import jalview.datamodel.SequenceFeature;
 +import jalview.datamodel.SequenceI;
 +import jalview.gui.JvOptionPane;
 +import jalview.util.DBRefUtils;
 +
  public class StockholmFileTest
  {
  
      AlignmentI fromStockholm = af.readFile(toStockholm,
              DataSourceType.PASTE, FileFormat.Stockholm);
      SequenceI importedSeq = fromStockholm.getSequenceAt(0);
 -    assertTrue(importedSeq.getDBRefs().length == 1,
 +    assertTrue(importedSeq.getDBRefs()
 +            .size() == 1,
              "Expected just one database reference to be added to sequence.");
      assertTrue(
 -            importedSeq.getDBRefs()[0].getAccessionId().indexOf(" ") == -1,
 +            importedSeq.getDBRefs().get(0).getAccessionId().indexOf(
 +                    " ") == -1,
              "Spaces were found in accession ID.");
      List<DBRefEntry> dbrefs = DBRefUtils.searchRefs(importedSeq.getDBRefs(),
              "P00224");
                            || (seq_original[i].getSequenceFeatures() != null && seq_new[in]
                                    .getSequenceFeatures() != null));
            // compare sequence features
-           if (seq_original[i].getSequenceFeatures() != null
+           if (!ignoreFeatures
+                   && seq_original[i].getSequenceFeatures() != null
                    && seq_new[in].getSequenceFeatures() != null)
            {
-             System.out.println("There are feature!!!");
+             System.out.println("Checking feature equivalence.");
              sequenceFeatures_original = seq_original[i]
                      .getSequenceFeatures();
              sequenceFeatures_new = seq_new[in].getSequenceFeatures();
@@@ -51,11 -51,11 +51,12 @@@ import jalview.datamodel.AlignmentAnnot
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.DBRefEntry;
  import jalview.datamodel.GeneLocus;
+ import jalview.datamodel.HiddenMarkovModel;
  import jalview.datamodel.HiddenSequences;
  import jalview.datamodel.Mapping;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.PDBEntry.Type;
 +import jalview.datamodel.Sequence.DBModList;
  import jalview.datamodel.SequenceCollectionI;
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceGroup;
@@@ -64,7 -64,6 +65,6 @@@ import jalview.datamodel.features.Featu
  import jalview.datamodel.features.FeatureMatcherSet;
  import jalview.datamodel.features.FeatureMatcherSetI;
  import jalview.gui.AlignFrame;
- import jalview.gui.AlignViewport;
  import jalview.gui.AlignmentPanel;
  import jalview.gui.Desktop;
  import jalview.gui.JvOptionPane;
@@@ -91,6 -90,8 +91,8 @@@ import jalview.util.matcher.Condition
  import jalview.viewmodel.AlignmentViewport;
  import jalview.viewmodel.seqfeatures.FeatureRendererModel;
  
+ import junit.extensions.PA;
  @Test(singleThreaded = true)
  public class Jalview2xmlTests extends Jalview2xmlBase
  {
              af.getViewport()
                      .getGlobalColourScheme() instanceof RNAHelicesColour,
              "Couldn't apply RNA helices colourscheme");
 -    assertTrue(af.saveAlignment(tfile, FileFormat.Jalview),
 +    af.saveAlignment(tfile, FileFormat.Jalview);
 +    assertTrue(af.isSaveAlignmentSuccessful(),
              "Failed to store as a project.");
      af.closeMenuItem_actionPerformed(true);
      af = null;
              DataSourceType.FILE);
      assertNotNull(af, "Didn't read input file " + inFile);
      af.loadJalviewDataFile(inAnnot, DataSourceType.FILE, null, null);
-     AlignViewport viewport = af.getViewport();
+     AlignViewportI viewport = af.getViewport();
      assertSame(viewport.getGlobalColourScheme().getClass(),
              TCoffeeColourScheme.class, "Didn't set T-coffee colourscheme");
      assertNotNull(
                              .getSchemeName()),
              "Recognise T-Coffee score from string");
  
 -    assertTrue(af.saveAlignment(tfile, FileFormat.Jalview),
 +    af.saveAlignment(tfile, FileFormat.Jalview);
 +    assertTrue(af.isSaveAlignmentSuccessful(),
              "Failed to store as a project.");
      af.closeMenuItem_actionPerformed(true);
      af = null;
      sg.addSequence(af.getViewport().getAlignment().getSequenceAt(1), false);
      sg.addSequence(af.getViewport().getAlignment().getSequenceAt(2), true);
      af.alignPanel.alignmentChanged();
 -    assertTrue(af.saveAlignment(tfile, FileFormat.Jalview),
 +    af.saveAlignment(tfile, FileFormat.Jalview);
 +    assertTrue(af.isSaveAlignmentSuccessful(),
              "Failed to store as a project.");
      af.closeMenuItem_actionPerformed(true);
      af = null;
      AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
              "examples/uniref50.fa", DataSourceType.FILE);
  
-     AlignViewport av = af.getViewport();
+     AlignViewportI av = af.getViewport();
      AlignmentI al = av.getAlignment();
  
      /*
      File tfile = File.createTempFile("JalviewTest", ".jvp");
      tfile.deleteOnExit();
      String filePath = tfile.getAbsolutePath();
 -    assertTrue(af.saveAlignment(filePath, FileFormat.Jalview),
 +    af.saveAlignment(filePath, FileFormat.Jalview);
 +    assertTrue(af.isSaveAlignmentSuccessful(),
              "Failed to store as a project.");
  
      /*
    }
  
    /**
+    * Load an HMM profile to an alignment, and confirm it is correctly restored
+    * when reloaded from project
+    * 
+    * @throws IOException
+    */
+   @Test(groups = { "Functional" })
+   public void testStoreAndRecoverHmmProfile() throws IOException
+   {
+     Desktop.instance.closeAll_actionPerformed(null);
+     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+             "examples/uniref50.fa", DataSourceType.FILE);
+   
+     AlignViewportI av = af.getViewport();
+     AlignmentI al = av.getAlignment();
+     /*
+      * mimic drag and drop of hmm file on to alignment
+      */
+     AlignFrame af2 = new FileLoader().LoadFileWaitTillLoaded(
+             "examples/uniref50.hmm", DataSourceType.FILE);
+     al.insertSequenceAt(0,
+             af2.getViewport().getAlignment().getSequenceAt(0));
+     /*
+      * check it loaded in
+      */
+     SequenceI hmmSeq = al.getSequenceAt(0);
+     assertTrue(hmmSeq.hasHMMProfile());
+     HiddenMarkovModel hmm = hmmSeq.getHMM();
+     assertSame(hmm.getConsensusSequence(), hmmSeq);
+     /*
+      * save project, close windows, reload project, verify
+      */
+     File tfile = File.createTempFile("testStoreAndRecoverHmmProfile",
+             ".jvp");
+     tfile.deleteOnExit();
+     new Jalview2XML(false).saveState(tfile);
+     Desktop.instance.closeAll_actionPerformed(null);
+     af = new FileLoader().LoadFileWaitTillLoaded(tfile.getAbsolutePath(),
+             DataSourceType.FILE);
+     Assert.assertNotNull(af, "Failed to reload project");
+     hmmSeq = al.getSequenceAt(0);
+     assertTrue(hmmSeq.hasHMMProfile());
+     assertSame(hmm.getConsensusSequence(), hmmSeq);
+     Mapping mapToHmmConsensus = (Mapping) PA.getValue(hmm,
+             "mapToHmmConsensus");
+     assertNotNull(mapToHmmConsensus);
+     assertSame(mapToHmmConsensus.getTo(), hmmSeq.getDatasetSequence());
+   }
+   /**
     * pre 2.11 - jalview 2.10 erroneously created new dataset entries for each
     * view (JAL-3171) this test ensures we can import and merge those views
     */
              DataSourceType.FILE);
      AlignmentViewPanel rap = Desktop.getAlignmentPanels(null)[0];
      SequenceI rpep = rap.getAlignment().getSequenceAt(0);
 +    DBModList<DBRefEntry> dbrefs = rpep.getDBRefs();
      assertEquals(rpep.getName(), "P30419");
 -    DBRefEntry[] dbrefs = rpep.getDBRefs();
 -    assertEquals(dbrefs.length, 3);
 -    DBRefEntry dbRef = dbrefs[0];
 +    assertEquals(dbrefs.size(), 3);
 +    DBRefEntry dbRef = dbrefs.get(0);
      assertFalse(dbRef instanceof GeneLocus);
      assertNull(dbRef.getMap());
      assertEquals(dbRef, dbref1);
       * restored dbrefs with mapping have a different 'map to'
       * sequence but otherwise match the original dbrefs
       */
 -    dbRef = dbrefs[1];
 +    dbRef = dbrefs.get(1);
      assertFalse(dbRef instanceof GeneLocus);
      assertTrue(dbRef.equalRef(dbref2));
      assertNotNull(dbRef.getMap());
      /*
       * GeneLocus map.to is null so can compare Mapping objects
       */
 -    dbRef = dbrefs[2];
 +    dbRef = dbrefs.get(2);
      assertTrue(dbRef instanceof GeneLocus);
      assertEquals(dbRef, dbref3);
    }
@@@ -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);