Merge branch 'releases/Release_2_10_4_Branch' into develop
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 7 May 2018 13:59:27 +0000 (14:59 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 7 May 2018 13:59:27 +0000 (14:59 +0100)
Conflicts:
.classpath

1  2 
.classpath
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/gui/Desktop.java
src/jalview/gui/SeqPanel.java
src/jalview/util/MapList.java
test/jalview/io/Jalview2xmlTests.java
test/jalview/util/MapListTest.java

diff --combined .classpath
@@@ -48,6 -48,8 +48,7 @@@
        <classpathentry kind="lib" path="lib/VARNAv3-93.jar"/>
        <classpathentry kind="lib" path="lib/jfreesvg-2.1.jar"/>
        <classpathentry kind="lib" path="lib/quaqua-filechooser-only-8.0.jar"/>
 -      <classpathentry kind="lib" path="lib/htsjdk-1.133.jar"/>
+       <classpathentry kind="lib" path="lib/VAqua4.jar"/>
        <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/plugin"/>
        <classpathentry kind="lib" path="lib/xml-apis.jar"/>
        <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
@@@ -66,7 -68,6 +67,7 @@@
        <classpathentry kind="con" path="org.testng.TESTNG_CONTAINER"/>
        <classpathentry kind="lib" path="lib/biojava-core-4.1.0.jar"/>
        <classpathentry kind="lib" path="lib/biojava-ontology-4.1.0.jar"/>
 +      <classpathentry kind="lib" path="lib/htsjdk-2.12.0.jar"/>
        <classpathentry kind="lib" path="lib/groovy-all-2.4.12-indy.jar"/>
        <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
        <classpathentry kind="output" path="classes"/>
@@@ -242,6 -242,7 +242,6 @@@ label.documentation = Documentatio
  label.about = About...
  label.show_sequence_limits = Show Sequence Limits
  action.feature_settings = Feature Settings...
 -label.feature_settings = Feature Settings
  label.all_columns = All Columns
  label.all_sequences = All Sequences
  label.selected_columns = Selected Columns 
@@@ -273,7 -274,6 +273,7 @@@ label.chimera_missing = Chimera structu
  label.chimera_failed = Error opening Chimera - is it installed?\nCheck path in Preferences, Structure
  label.min_colour = Minimum Colour
  label.max_colour = Maximum Colour
 +label.no_colour = No Colour
  label.use_original_colours = Use Original Colours
  label.threshold_minmax = Threshold is min/max
  label.represent_group_with = Represent Group with {0}
@@@ -281,9 -281,9 +281,9 @@@ label.selection = Selectio
  label.group_colour = Group Colour
  label.sequence = Sequence
  label.view_pdb_structure = View PDB Structure
 -label.min = Min:
 -label.max = Max:
 -label.colour_by_label = Colour by label
 +label.min_value = Min value
 +label.max_value = Max value
 +label.no_value = No value
  label.new_feature = New Feature
  label.match_case = Match Case
  label.view_alignment_editor = View in alignment editor
@@@ -368,8 -368,6 +368,8 @@@ label.optimise_order = Optimise Orde
  label.seq_sort_by_score = Sequence sort by Score
  label.load_colours = Load Colours
  label.save_colours = Save Colours
 +label.load_colours_tooltip = Load feature colours and filters from file
 +label.save_colours_tooltip = Save feature colours and filters to file
  label.fetch_das_features = Fetch DAS Features
  label.selected_database_to_fetch_from = Selected {0} database {1} to fetch from {2} 
  label.database_param = Database: {0}
@@@ -402,10 -400,6 +402,6 @@@ label.view_name_original = Origina
  label.enter_view_name = Enter View Name
  label.enter_label = Enter label
  label.enter_label_for_the_structure = Enter a label for the structure
- label.pdb_entry_is_already_displayed = {0} is already displayed.\nDo you want to re-use this viewer ?
- label.map_sequences_to_visible_window = Map Sequences to Visible Window: {0}
- label.add_pdbentry_to_view = Do you want to add {0} to the view called\n{1}\n
- label.align_to_existing_structure_view = Align to existing structure view
  label.pdb_entries_couldnt_be_retrieved = The following pdb entries could not be retrieved from the PDB\:\n{0}\nPlease retry, or try downloading them manually.
  label.couldnt_load_file = Couldn't load file
  label.couldnt_find_pdb_id_in_file = Couldn't find a PDB id in the file supplied. Please enter an Id to identify this structure.
@@@ -492,10 -486,6 +488,10 @@@ label.settings_for_type = Settings for 
  label.view_full_application = View in Full Application
  label.load_associated_tree = Load Associated Tree...
  label.load_features_annotations = Load Features/Annotations...
 +label.load_vcf = Load SNP variants from plain text or indexed VCF data
 +label.load_vcf_file = Load VCF File
 +label.searching_vcf = Loading VCF variants...
 +label.added_vcf = Added {0} VCF variants to {1} sequence(s)
  label.export_features = Export Features...
  label.export_annotations = Export Annotations...
  label.to_upper_case = To Upper Case
@@@ -534,6 -524,7 +530,6 @@@ label.threshold_feature_above_threshol
  label.threshold_feature_below_threshold = Below Threshold
  label.adjust_threshold = Adjust threshold
  label.toggle_absolute_relative_display_threshold = Toggle between absolute and relative display threshold.
 -label.display_features_same_type_different_label_using_different_colour = Display features of the same type with a different label using a different colour. (e.g. domain features)
  label.select_colour_minimum_value = Select Colour for Minimum Value
  label.select_colour_maximum_value = Select Colour for Maximum Value
  label.open_url_param = Open URL {0}
@@@ -785,7 -776,7 +781,7 @@@ label.pairwise_aligned_sequences = Pair
  label.original_data_for_params = Original Data for {0}
  label.points_for_params = Points for {0}
  label.transformed_points_for_params = Transformed points for {0}
 -label.graduated_color_for_params = Graduated Feature Colour for {0}
 +label.variable_color_for = Variable Feature Colour for {0}
  label.select_background_colour = Select Background Colour
  label.invalid_font = Invalid Font
  label.separate_multiple_accession_ids = Enter one or more accession IDs separated by a semi-colon ";"
@@@ -872,7 -863,7 +868,7 @@@ label.msa_service_is_unknown = The Mult
  label.service_called_is_not_seq_search_service = The Service called \n{0}\nis not a \nSequence Search Service\!
  label.seq_search_service_is_unknown = The Sequence Search Service named {0} is unknown
  label.feature_type = Feature Type
 -label.display = Display
 +label.show = Show
  label.service_url = Service URL
  label.copied_sequences = Copied sequences
  label.cut_sequences = Cut Sequences
@@@ -1218,7 -1209,6 +1214,6 @@@ label.pdb_sequence_fetcher = PDB Sequen
  label.result = result
  label.results = results
  label.structure_chooser = Structure Chooser
- label.select = Select : 
  label.invert = Invert 
  label.select_pdb_file = Select PDB File
  info.select_filter_option = Select Filter Option/Manual Entry
@@@ -1325,41 -1315,9 +1320,41 @@@ label.select_hidden_colour = Select hid
  label.overview = Overview
  label.reset_to_defaults = Reset to defaults
  label.oview_calc = Recalculating overview...
 +label.feature_details = Feature details
 +label.matchCondition_contains = Contains
 +label.matchCondition_notcontains = Does not contain
 +label.matchCondition_matches = Matches
 +label.matchCondition_notmatches = Does not match
 +label.matchCondition_present = Is present
 +label.matchCondition_notpresent = Is not present
 +label.matchCondition_eq = =
 +label.matchCondition_ne = not =
 +label.matchCondition_lt = <
 +label.matchCondition_le = <=
 +label.matchCondition_gt = >
 +label.matchCondition_ge = >=
 +label.numeric_required = The value should be numeric
 +label.filter = Filter
 +label.filters = Filters
 +label.join_conditions = Join conditions with
 +label.score = Score
 +label.colour_by_label = Colour by label
 +label.variable_colour = Variable colour...
 +label.select_colour = Select colour
  option.enable_disable_autosearch = When ticked, search is performed automatically
  option.autosearch = Autosearch
  label.retrieve_ids = Retrieve IDs
 +label.display_settings_for = Display settings for {0} features
 +label.simple = Simple
 +label.simple_colour = Simple Colour
 +label.colour_by_text = Colour by text
 +label.graduated_colour = Graduated Colour
 +label.by_text_of = By text of
 +label.by_range_of = By range of
 +label.filters_tooltip = Click to set or amend filters
 +label.or = Or
 +label.and = And
 +label.sequence_feature_colours = Sequence Feature Colours
  label.best_quality = Best Quality
  label.best_resolution = Best Resolution
  label.most_protein_chain = Most Protein Chain
@@@ -226,6 -226,7 +226,6 @@@ label.automatic_scrolling = Desplazamie
  label.documentation = Documentación
  label.about = Acerca de...
  label.show_sequence_limits = Mostrar los límites de la secuencia
 -label.feature_settings = Ajustar funciones...
  label.all_columns = Todas las columnas
  label.all_sequences = Todas las secuencias
  label.selected_columns = Columnas seleccionadas
@@@ -242,7 -243,6 +242,7 @@@ label.apply_all_groups = Aplicar a todo
  label.autocalculated_annotation = Anotación autocalculada
  label.min_colour = Color mínimo
  label.max_colour = Color máximo
 +label.no_colour = Sin color
  label.use_original_colours = Usar colores originales
  label.threshold_minmax = El umbral es mín/máx
  label.represent_group_with = Representar al grupo con
@@@ -250,9 -250,8 +250,9 @@@ label.selection = Selecciona
  label.group_colour = Color del grupo
  label.sequence = Secuencia
  label.view_pdb_structure = Ver estructura PDB
 -label.min = Mín:
 -label.max = Máx:
 +label.max_value = Valor máximo
 +label.min_value = Valor mínimo
 +label.no_value = Sin valor
  label.colour_by_label = Color por etiquetas
  label.new_feature = Nueva función
  label.match_case = Hacer corresponder mayúsculas y minúsculas
@@@ -337,8 -336,6 +337,8 @@@ label.optimise_order = Optimizar orde
  label.seq_sort_by_score = Ordenar las secuencias por puntuación
  label.load_colours = Cargar colores
  label.save_colours = Guardar colores
 +label.load_colours_tooltip = Cargar colores y filtros desde fichero
 +label.save_colours_tooltip = Guardar colores y filtros en fichero
  label.fetch_das_features = Recuperar funciones DAS
  label.selected_database_to_fetch_from = Seleccionada {0} Base de datos {1} para buscar de {2} 
  label.database_param = Base de datos: {0}
@@@ -370,10 -367,6 +370,6 @@@ label.ignore_unmatched_dropped_files = 
  label.enter_view_name = Introduzca un nombre para la vista
  label.enter_label = Introducir etiqueta
  label.enter_label_for_the_structure = Introducir una etiqueta para la estructura
- label.pdb_entry_is_already_displayed = {0} Ya est\u00E1 mostrado.\nQuieres volver a usar este visor?
- label.map_sequences_to_visible_window = Mapa de secuencias en ventana visible: {0}
- label.add_pdbentry_to_view = Quieres a\u00F1adir {0} a la vista llamada\n{1}\n
- label.align_to_existing_structure_view = Alinear a una estructura ya existente
  label.pdb_entries_couldnt_be_retrieved = Las siguientes entradas pdb no pueden ser extra\u00EDdas del PDB\:\n{0}\nPor favor, prueba descarg\u00E1ndolas manualmente.
  label.couldnt_load_file = No se pudo cargar el fichero
  label.couldnt_find_pdb_id_in_file = No se pudo encontrar un Id PDB en el fichero suministrado. Por favor, introduzca un Id para identificar esta estructura.
@@@ -459,10 -452,6 +455,10 @@@ label.settings_for_type = Ajustes para 
  label.view_full_application = Ver en la aplicación completa 
  label.load_associated_tree = Cargar árbol asociado ...
  label.load_features_annotations = Cargar características/anotaciones ...
 +label.load_vcf = Cargar variantes SNP desde fichero VCF texto o tab-indexado
 +label.load_vcf_file = Cargar fichero VCF
 +label.searching_vcf = Cargando variantes VCF...
 +label.added_vcf= {0} variantes VCF añadidas a {1} secuencia(s)
  label.export_features = Exportar características...
  label.export_annotations = Exportar anotaciones ...
  label.to_upper_case = Pasar a mayúsculas
@@@ -496,6 -485,7 +492,6 @@@ label.threshold_feature_above_threshol
  label.threshold_feature_below_threshold = Por debajo del umbral
  label.adjust_threshold = Ajustar umbral
  label.toggle_absolute_relative_display_threshold = Cambiar entre mostrar el umbral absoluto y el relativo.
 -label.display_features_same_type_different_label_using_different_colour = Mostrar las características del mismo tipo con una etiqueta diferente y empleando un color distinto (p.e. características del dominio)
  label.select_colour_minimum_value = Seleccionar el color para el valor mínimo
  label.select_colour_maximum_value = Seleccionar el color para el valor máximo
  label.open_url_param = Abrir URL {0}
@@@ -715,7 -705,7 +711,7 @@@ label.pairwise_aligned_sequences = Secu
  label.original_data_for_params = Datos originales de {0}
  label.points_for_params = Puntos de {0}
  label.transformed_points_for_params = Puntos transformados de {0}
 -label.graduated_color_for_params = Color graduado para la característica de {0}
 +label.variable_color_for = Color variable para la característica de {0}
  label.select_background_colour = Seleccionar color de fondo
  label.invalid_font = Fuente no válida
  label.separate_multiple_accession_ids = Separar los accession id con un punto y coma ";"
@@@ -798,7 -788,7 +794,7 @@@ label.msa_service_is_unknown = El Servi
  label.service_called_is_not_seq_search_service = El Servicio llamando \n{0}\nno es un \nServicio de B\u00FAsqueda de Secuencias\!
  label.seq_search_service_is_unknown = El Servicio de Búsqueda de Sencuencias llamado {0} es desconocido
  label.feature_type = Tipo de característisca
 -label.display = Representación
 +label.show = Mostrar
  label.service_url = URL del servicio
  label.copied_sequences = Secuencias copiadas
  label.cut_sequences = Cortar secuencias
@@@ -1178,7 -1168,6 +1174,6 @@@ label.structures_filter=Filtro de Estru
  label.scale_protein_to_cdna=Adaptar proteína a cDNA
  label.scale_protein_to_cdna_tip=Hacer a los residuos de proteínas de la misma anchura que los codones en ventanas divididas
  status.loading_cached_pdb_entries=Cargando Entradas PDB en Caché
- label.select=Seleccionar :
  label.select_by_annotation=Seleccionar/Ocultar Columnas por Anotación
  action.select_by_annotation=Seleccionar/Ocultar Columnas por Anotación...
  action.export_features=Exportar Características
@@@ -1326,41 -1315,9 +1321,41 @@@ label.select_hidden_colour = Selecciona
  label.overview = Resumen
  label.reset_to_defaults = Restablecen a los predeterminados
  label.oview_calc = Recalculando resumen
 +label.feature_details = Detalles de característica 
 +label.matchCondition_contains = Contiene
 +label.matchCondition_notcontains = No contiene
 +label.matchCondition_matches = Es igual a
 +label.matchCondition_notmatches = No es igual a
 +label.matchCondition_present = Está presente
 +label.matchCondition_notpresent = No está presente
 +label.matchCondition_eq = =
 +label.matchCondition_ne = not =
 +label.matchCondition_lt = <
 +label.matchCondition_le = <=
 +label.matchCondition_gt = >
 +label.matchCondition_ge = >=
 +label.numeric_required = Valor numérico requerido
 +label.filter = Filtro
 +label.filters = Filtros
 +label.join_conditions = Combinar condiciones con
 +label.score = Puntuación
 +label.colour_by_label = Colorear por texto
 +label.variable_colour = Color variable...
 +label.select_colour = Seleccionar color
  option.enable_disable_autosearch = Marcar para buscar automáticamente
  option.autosearch = Auto búsqueda
  label.retrieve_ids = Recuperar IDs
 +label.display_settings_for = Visualización de características {0}
 +label.simple = Simple
 +label.simple_colour = Color simple
 +label.colour_by_text = Colorear por texto
 +label.graduated_colour = Color graduado
 +label.by_text_of = Por texto de
 +label.by_range_of = Por rango de
 +label.filters_tooltip = Haga clic para configurar o modificar los filtros
 +label.or = O
 +label.and = Y
 +label.sequence_feature_colours = Colores de características de las secuencias
  label.best_quality = Mejor Calidad
  label.best_resolution = Mejor Resolución
  label.most_protein_chain = Más Cadena de Proteína
@@@ -900,6 -900,8 +900,6 @@@ public class Desktop extends jalview.jb
            menuItem.removeActionListener(menuItem.getActionListeners()[0]);
          }
          windowMenu.remove(menuItem);
 -
 -        System.gc();
        };
      });
  
      {
        ssm.resetAll();
      }
 -    System.gc();
    }
  
    @Override
    {
      Cache.setProperty(EXPERIMENTAL_FEATURES, Boolean.toString(selected));
    }
+   /**
+    * 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
+    * @param structureViewerClass
+    *          if not null, only return viewers of this class
+    * @return
+    */
+   public List<StructureViewerBase> getStructureViewers(
+           AlignmentPanel apanel,
+           Class<? extends StructureViewerBase> structureViewerClass)
+   {
+     List<StructureViewerBase> result = new ArrayList<>();
+     JInternalFrame[] frames = Desktop.instance.getAllFrames();
+     for (JInternalFrame frame : frames)
+     {
+       if (frame instanceof StructureViewerBase)
+       {
+         if (structureViewerClass == null
+                 || structureViewerClass.isInstance(frame))
+         {
+           if (apanel == null
+                   || ((StructureViewerBase) frame).isLinkedWith(apanel))
+           {
+             result.add((StructureViewerBase) frame);
+           }
+         }
+       }
+     }
+     return result;
+   }
  }
@@@ -59,6 -59,7 +59,6 @@@ import java.awt.event.MouseListener
  import java.awt.event.MouseMotionListener;
  import java.awt.event.MouseWheelEvent;
  import java.awt.event.MouseWheelListener;
 -import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  
@@@ -75,11 -76,12 +75,11 @@@ import javax.swing.ToolTipManager
  public class SeqPanel extends JPanel
          implements MouseListener, MouseMotionListener, MouseWheelListener,
          SequenceListener, SelectionListener
 -
  {
 -  /** DOCUMENT ME!! */
 +  private static final int MAX_TOOLTIP_LENGTH = 300;
 +
    public SeqCanvas seqCanvas;
  
 -  /** DOCUMENT ME!! */
    public AlignmentPanel ap;
  
    /*
    SearchResultsI lastSearchResults;
  
    /**
 -   * Creates a new SeqPanel object.
 +   * Creates a new SeqPanel object
     * 
 -   * @param avp
 -   *          DOCUMENT ME!
 -   * @param p
 -   *          DOCUMENT ME!
 +   * @param viewport
 +   * @param alignPanel
     */
 -  public SeqPanel(AlignViewport av, AlignmentPanel ap)
 +  public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
    {
      linkImageURL = getClass().getResource("/images/link.gif");
      seqARep = new SequenceAnnotationReport(linkImageURL.toString());
      ToolTipManager.sharedInstance().registerComponent(this);
      ToolTipManager.sharedInstance().setInitialDelay(0);
      ToolTipManager.sharedInstance().setDismissDelay(10000);
 -    this.av = av;
 +    this.av = viewport;
      setBackground(Color.white);
  
 -    seqCanvas = new SeqCanvas(ap);
 +    seqCanvas = new SeqCanvas(alignPanel);
      setLayout(new BorderLayout());
      add(seqCanvas, BorderLayout.CENTER);
  
 -    this.ap = ap;
 +    this.ap = alignPanel;
  
 -    if (!av.isDataset())
 +    if (!viewport.isDataset())
      {
        addMouseMotionListener(this);
        addMouseListener(this);
        addMouseWheelListener(this);
 -      ssm = av.getStructureSelectionManager();
 +      ssm = viewport.getStructureSelectionManager();
        ssm.addStructureViewerListener(this);
        ssm.addSelectionListener(this);
      }
        List<SequenceFeature> features = ap.getFeatureRenderer()
                .findFeaturesAtColumn(sequence, column + 1);
        seqARep.appendFeatures(tooltipText, pos, features,
 -              this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
 +              this.ap.getSeqPanel().seqCanvas.fr);
      }
      if (tooltipText.length() == 6) // <html>
      {
      }
      else
      {
 +      if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
 +      {
 +        tooltipText.setLength(MAX_TOOLTIP_LENGTH);
 +        tooltipText.append("...");
 +      }
        String textString = tooltipText.toString();
        if (lastTooltip == null || !lastTooltip.equals(textString))
        {
      final int column = findColumn(evt);
      final int seq = findSeq(evt);
      SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 -    List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
 +    List<SequenceFeature> features = ap.getFeatureRenderer()
              .findFeaturesAtColumn(sequence, column + 1);
 -    List<String> links = new ArrayList<>();
 -    for (SequenceFeature sf : allFeatures)
 -    {
 -      if (sf.links != null)
 -      {
 -        for (String link : sf.links)
 -        {
 -          links.add(link);
 -        }
 -      }
 -    }
  
 -    PopupMenu pop = new PopupMenu(ap, null, links);
 +    PopupMenu pop = new PopupMenu(ap, null, features);
      pop.show(this, evt.getX(), evt.getY());
    }
  
  
      return true;
    }
+   /**
+    * 
+    * @return null or last search results handled by this panel
+    */
+   public SearchResultsI getLastSearchResults()
+   {
+     return lastSearchResults;
+   }
  }
@@@ -116,8 -116,17 +116,17 @@@ public class MapLis
    {
      int hashCode = 31 * fromRatio;
      hashCode = 31 * hashCode + toRatio;
-     hashCode = 31 * hashCode + fromShifts.toArray().hashCode();
-     hashCode = 31 * hashCode + toShifts.toArray().hashCode();
+     for (int[] shift : fromShifts)
+     {
+       hashCode = 31 * hashCode + shift[0];
+       hashCode = 31 * hashCode + shift[1];
+     }
+     for (int[] shift : toShifts)
+     {
+       hashCode = 31 * hashCode + shift[0];
+       hashCode = 31 * hashCode + shift[1];
+     }
      return hashCode;
    }
  
     */
    public boolean isFromForwardStrand()
    {
 +    return isForwardStrand(getFromRanges());
 +  }
 +
 +  /**
 +   * Returns true if mapping is to forward strand, false if to reverse strand.
 +   * Result is just based on the first 'to' range that is not a single position.
 +   * Default is true unless proven to be false. Behaviour is not well defined if
 +   * the mapping has a mixture of forward and reverse ranges.
 +   * 
 +   * @return
 +   */
 +  public boolean isToForwardStrand()
 +  {
 +    return isForwardStrand(getToRanges());
 +  }
 +
 +  /**
 +   * A helper method that returns true unless at least one range has start > end.
 +   * Behaviour is undefined for a mixture of forward and reverse ranges.
 +   * 
 +   * @param ranges
 +   * @return
 +   */
 +  private boolean isForwardStrand(List<int[]> ranges)
 +  {
      boolean forwardStrand = true;
 -    for (int[] range : getFromRanges())
 +    for (int[] range : ranges)
      {
        if (range[1] > range[0])
        {
              || (fromRatio == 3 && toRatio == 1);
    }
  
 +  /**
 +   * Returns a map which is the composite of this one and the input map. That
 +   * is, the output map has the fromRanges of this map, and its toRanges are the
 +   * toRanges of this map as transformed by the input map.
 +   * <p>
 +   * Returns null if the mappings cannot be traversed (not all toRanges of this
 +   * map correspond to fromRanges of the input), or if this.toRatio does not
 +   * match map.fromRatio.
 +   * 
 +   * <pre>
 +   * Example 1:
 +   *    this:   from [1-100] to [501-600]
 +   *    input:  from [10-40] to [60-90]
 +   *    output: from [10-40] to [560-590]
 +   * Example 2 ('reverse strand exons'):
 +   *    this:   from [1-100] to [2000-1951], [1000-951] // transcript to loci
 +   *    input:  from [1-50]  to [41-90] // CDS to transcript
 +   *    output: from [10-40] to [1960-1951], [1000-971] // CDS to gene loci
 +   * </pre>
 +   * 
 +   * @param map
 +   * @return
 +   */
 +  public MapList traverse(MapList map)
 +  {
 +    if (map == null)
 +    {
 +      return null;
 +    }
 +
 +    /*
 +     * compound the ratios by this rule:
 +     * A:B with M:N gives A*M:B*N
 +     * reduced by greatest common divisor
 +     * so 1:3 with 3:3 is 3:9 or 1:3
 +     * 1:3 with 3:1 is 3:3 or 1:1
 +     * 1:3 with 1:3 is 1:9
 +     * 2:5 with 3:7 is 6:35
 +     */
 +    int outFromRatio = getFromRatio() * map.getFromRatio();
 +    int outToRatio = getToRatio() * map.getToRatio();
 +    int gcd = MathUtils.gcd(outFromRatio, outToRatio);
 +    outFromRatio /= gcd;
 +    outToRatio /= gcd;
 +
 +    List<int[]> toRanges = new ArrayList<>();
 +    for (int[] range : getToRanges())
 +    {
 +      int[] transferred = map.locateInTo(range[0], range[1]);
 +      if (transferred == null)
 +      {
 +        return null;
 +      }
 +      toRanges.add(transferred);
 +    }
 +
 +    return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio);
 +  }
 +
  }
@@@ -23,13 -23,11 +23,13 @@@ package jalview.io
  import static org.testng.Assert.assertEquals;
  import static org.testng.Assert.assertFalse;
  import static org.testng.Assert.assertNotNull;
 +import static org.testng.Assert.assertNull;
  import static org.testng.Assert.assertSame;
  import static org.testng.Assert.assertTrue;
  
  import jalview.api.AlignViewportI;
  import jalview.api.AlignmentViewPanel;
 +import jalview.api.FeatureColourI;
  import jalview.api.ViewStyleI;
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.AlignmentI;
@@@ -37,17 -35,12 +37,17 @@@ import jalview.datamodel.HiddenSequence
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.PDBEntry.Type;
  import jalview.datamodel.SequenceCollectionI;
 +import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
 +import jalview.datamodel.features.FeatureMatcher;
 +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.FeatureRenderer;
  import jalview.gui.Jalview2XML;
  import jalview.gui.JvOptionPane;
  import jalview.gui.PopupMenu;
@@@ -57,16 -50,13 +57,16 @@@ import jalview.schemes.AnnotationColour
  import jalview.schemes.BuriedColourScheme;
  import jalview.schemes.ColourSchemeI;
  import jalview.schemes.ColourSchemeProperty;
 +import jalview.schemes.FeatureColour;
  import jalview.schemes.JalviewColourScheme;
  import jalview.schemes.RNAHelicesColour;
  import jalview.schemes.StrandColourScheme;
  import jalview.schemes.TCoffeeColourScheme;
  import jalview.structure.StructureImportSettings;
 +import jalview.util.matcher.Condition;
  import jalview.viewmodel.AlignmentViewport;
  
 +import java.awt.Color;
  import java.io.File;
  import java.io.IOException;
  import java.util.ArrayList;
@@@ -257,6 -247,31 +257,31 @@@ public class Jalview2xmlTests extends J
  
    }
  
+   /**
+    * Test for JAL-2223 - multiple mappings in View Mapping report
+    * 
+    * @throws Exception
+    */
+   @Test(groups = { "Functional" })
+   public void noDuplicatePdbMappingsMade() throws Exception
+   {
+     StructureImportSettings.setProcessSecondaryStructure(true);
+     StructureImportSettings.setVisibleChainAnnotation(true);
+     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+             "examples/exampleFile_2_7.jar", DataSourceType.FILE);
+     assertNotNull(af, "Didn't read in the example file correctly.");
+     // locate Jmol viewer
+     // count number of PDB mappings the structure selection manager holds -
+     String pdbFile = af.getCurrentView().getStructureSelectionManager()
+             .findFileForPDBId("1A70");
+     assertEquals(
+             af.getCurrentView().getStructureSelectionManager()
+                     .getMapping(pdbFile).length,
+             2, "Expected only two mappings for 1A70");
+   }
    @Test(groups = { "Functional" })
    public void viewRefPdbAnnotation() throws Exception
    {
      assertTrue(rs.conservationApplied());
      assertEquals(rs.getConservationInc(), 30);
    }
 +
 +  /**
 +   * Test save and reload of feature colour schemes and filter settings
 +   * 
 +   * @throws IOException
 +   */
 +  @Test(groups = { "Functional" })
 +  public void testSaveLoadFeatureColoursAndFilters() throws IOException
 +  {
 +    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
 +            ">Seq1\nACDEFGHIKLM", DataSourceType.PASTE);
 +    SequenceI seq1 = af.getViewport().getAlignment().getSequenceAt(0);
 +
 +    /*
 +     * add some features to the sequence
 +     */
 +    int score = 1;
 +    addFeatures(seq1, "type1", score++);
 +    addFeatures(seq1, "type2", score++);
 +    addFeatures(seq1, "type3", score++);
 +    addFeatures(seq1, "type4", score++);
 +    addFeatures(seq1, "type5", score++);
 +
 +    /*
 +     * set colour schemes for features
 +     */
 +    FeatureRenderer fr = af.getFeatureRenderer();
 +    fr.findAllFeatures(true);
 +
 +    // type1: red
 +    fr.setColour("type1", new FeatureColour(Color.red));
 +
 +    // type2: by label
 +    FeatureColourI byLabel = new FeatureColour();
 +    byLabel.setColourByLabel(true);
 +    fr.setColour("type2", byLabel);
 +
 +    // type3: by score above threshold
 +    FeatureColourI byScore = new FeatureColour(Color.BLACK, Color.BLUE, 1,
 +            10);
 +    byScore.setAboveThreshold(true);
 +    byScore.setThreshold(2f);
 +    fr.setColour("type3", byScore);
 +
 +    // type4: by attribute AF
 +    FeatureColourI byAF = new FeatureColour();
 +    byAF.setColourByLabel(true);
 +    byAF.setAttributeName("AF");
 +    fr.setColour("type4", byAF);
 +
 +    // type5: by attribute CSQ:PolyPhen below threshold
 +    FeatureColourI byPolyPhen = new FeatureColour(Color.BLACK, Color.BLUE,
 +            1, 10);
 +    byPolyPhen.setBelowThreshold(true);
 +    byPolyPhen.setThreshold(3f);
 +    byPolyPhen.setAttributeName("CSQ", "PolyPhen");
 +    fr.setColour("type5", byPolyPhen);
 +
 +    /*
 +     * set filters for feature types
 +     */
 +
 +    // filter type1 features by (label contains "x")
 +    FeatureMatcherSetI filterByX = new FeatureMatcherSet();
 +    filterByX.and(FeatureMatcher.byLabel(Condition.Contains, "x"));
 +    fr.setFeatureFilter("type1", filterByX);
 +
 +    // filter type2 features by (score <= 2.4 and score > 1.1)
 +    FeatureMatcherSetI filterByScore = new FeatureMatcherSet();
 +    filterByScore.and(FeatureMatcher.byScore(Condition.LE, "2.4"));
 +    filterByScore.and(FeatureMatcher.byScore(Condition.GT, "1.1"));
 +    fr.setFeatureFilter("type2", filterByScore);
 +
 +    // filter type3 features by (AF contains X OR CSQ:PolyPhen != 0)
 +    FeatureMatcherSetI filterByXY = new FeatureMatcherSet();
 +    filterByXY
 +            .and(FeatureMatcher.byAttribute(Condition.Contains, "X", "AF"));
 +    filterByXY.or(FeatureMatcher.byAttribute(Condition.NE, "0", "CSQ",
 +            "PolyPhen"));
 +    fr.setFeatureFilter("type3", filterByXY);
 +
 +    /*
 +     * save as Jalview project
 +     */
 +    File tfile = File.createTempFile("JalviewTest", ".jvp");
 +    tfile.deleteOnExit();
 +    String filePath = tfile.getAbsolutePath();
 +    assertTrue(af.saveAlignment(filePath, FileFormat.Jalview),
 +            "Failed to store as a project.");
 +
 +    /*
 +     * close current alignment and load the saved project
 +     */
 +    af.closeMenuItem_actionPerformed(true);
 +    af = null;
 +    af = new FileLoader()
 +            .LoadFileWaitTillLoaded(filePath, DataSourceType.FILE);
 +    assertNotNull(af, "Failed to import new project");
 +
 +    /*
 +     * verify restored feature colour schemes and filters
 +     */
 +    fr = af.getFeatureRenderer();
 +    FeatureColourI fc = fr.getFeatureStyle("type1");
 +    assertTrue(fc.isSimpleColour());
 +    assertEquals(fc.getColour(), Color.red);
 +    fc = fr.getFeatureStyle("type2");
 +    assertTrue(fc.isColourByLabel());
 +    fc = fr.getFeatureStyle("type3");
 +    assertTrue(fc.isGraduatedColour());
 +    assertNull(fc.getAttributeName());
 +    assertTrue(fc.isAboveThreshold());
 +    assertEquals(fc.getThreshold(), 2f);
 +    fc = fr.getFeatureStyle("type4");
 +    assertTrue(fc.isColourByLabel());
 +    assertTrue(fc.isColourByAttribute());
 +    assertEquals(fc.getAttributeName(), new String[] { "AF" });
 +    fc = fr.getFeatureStyle("type5");
 +    assertTrue(fc.isGraduatedColour());
 +    assertTrue(fc.isColourByAttribute());
 +    assertEquals(fc.getAttributeName(), new String[] { "CSQ", "PolyPhen" });
 +    assertTrue(fc.isBelowThreshold());
 +    assertEquals(fc.getThreshold(), 3f);
 +
 +    assertEquals(fr.getFeatureFilter("type1").toStableString(),
 +            "Label Contains x");
 +    assertEquals(fr.getFeatureFilter("type2").toStableString(),
 +            "(Score LE 2.4) AND (Score GT 1.1)");
 +    assertEquals(fr.getFeatureFilter("type3").toStableString(),
 +            "(AF Contains X) OR (CSQ:PolyPhen NE 0.0)");
 +  }
 +
 +  private void addFeature(SequenceI seq, String featureType, int score)
 +  {
 +    SequenceFeature sf = new SequenceFeature(featureType, "desc", 1, 2,
 +            score, "grp");
 +    sf.setValue("AF", score);
 +    sf.setValue("CSQ", new HashMap<String, String>()
 +    {
 +      {
 +        put("PolyPhen", Integer.toString(score));
 +      }
 +    });
 +    seq.addSequenceFeature(sf);
 +  }
 +
 +  /**
 +   * Adds two features of the given type to the given sequence, also setting the
 +   * score as the value of attribute "AF" and sub-attribute "CSQ:PolyPhen"
 +   * 
 +   * @param seq
 +   * @param featureType
 +   * @param score
 +   */
 +  private void addFeatures(SequenceI seq, String featureType, int score)
 +  {
 +    addFeature(seq, featureType, score++);
 +    addFeature(seq, featureType, score);
 +  }
  }
@@@ -391,7 -391,9 +391,9 @@@ public class MapListTes
      MapList ml7 = new MapList(codons, protein, 3, 1); // toShifts differ
  
      assertTrue(ml.equals(ml));
+     assertEquals(ml.hashCode(), ml.hashCode());
      assertTrue(ml.equals(ml1));
+     assertEquals(ml.hashCode(), ml1.hashCode());
      assertTrue(ml1.equals(ml));
  
      assertFalse(ml.equals(null));
      assertEquals(1, merged.size());
      assertArrayEquals(new int[] { 9, 0 }, merged.get(0));
    }
 +
 +  /**
 +   * Test the method that compounds ('traverses') two mappings
 +   */
 +  @Test(groups = "Functional")
 +  public void testTraverse()
 +  {
 +    /*
 +     * simple 1:1 plus 1:1 forwards
 +     */
 +    MapList ml1 = new MapList(new int[] { 3, 4, 8, 12 }, new int[] { 5, 8,
 +        11, 13 }, 1, 1);
 +    MapList ml2 = new MapList(new int[] { 1, 50 }, new int[] { 40, 45, 70,
 +        75, 90, 127 }, 1, 1);
 +    MapList compound = ml1.traverse(ml2);
 +
 +    assertEquals(compound.getFromRatio(), 1);
 +    assertEquals(compound.getToRatio(), 1);
 +    List<int[]> fromRanges = compound.getFromRanges();
 +    assertEquals(fromRanges.size(), 2);
 +    assertArrayEquals(new int[] { 3, 4 }, fromRanges.get(0));
 +    assertArrayEquals(new int[] { 8, 12 }, fromRanges.get(1));
 +    List<int[]> toRanges = compound.getToRanges();
 +    assertEquals(toRanges.size(), 2);
 +    // 5-8 maps to 44-45,70-71
 +    // 11-13 maps to 74-75,90
 +    assertArrayEquals(new int[] { 44, 45, 70, 71 }, toRanges.get(0));
 +    assertArrayEquals(new int[] { 74, 75, 90, 90 }, toRanges.get(1));
 +
 +    /*
 +     * 1:1 over 1:1 backwards ('reverse strand')
 +     */
 +    ml1 = new MapList(new int[] { 1, 50 }, new int[] { 70, 119 }, 1, 1);
 +    ml2 = new MapList(new int[] { 1, 500 },
 +            new int[] { 1000, 901, 600, 201 }, 1, 1);
 +    compound = ml1.traverse(ml2);
 +
 +    assertEquals(compound.getFromRatio(), 1);
 +    assertEquals(compound.getToRatio(), 1);
 +    fromRanges = compound.getFromRanges();
 +    assertEquals(fromRanges.size(), 1);
 +    assertArrayEquals(new int[] { 1, 50 }, fromRanges.get(0));
 +    toRanges = compound.getToRanges();
 +    assertEquals(toRanges.size(), 1);
 +    assertArrayEquals(new int[] { 931, 901, 600, 582 }, toRanges.get(0));
 +
 +    /*
 +     * 1:1 plus 1:3 should result in 1:3
 +     */
 +    ml1 = new MapList(new int[] { 1, 30 }, new int[] { 11, 40 }, 1, 1);
 +    ml2 = new MapList(new int[] { 1, 100 }, new int[] { 1, 50, 91, 340 },
 +            1, 3);
 +    compound = ml1.traverse(ml2);
 +
 +    assertEquals(compound.getFromRatio(), 1);
 +    assertEquals(compound.getToRatio(), 3);
 +    fromRanges = compound.getFromRanges();
 +    assertEquals(fromRanges.size(), 1);
 +    assertArrayEquals(new int[] { 1, 30 }, fromRanges.get(0));
 +    // 11-40 maps to 31-50,91-160
 +    toRanges = compound.getToRanges();
 +    assertEquals(toRanges.size(), 1);
 +    assertArrayEquals(new int[] { 31, 50, 91, 160 }, toRanges.get(0));
 +
 +    /*
 +     * 3:1 plus 1:1 should result in 3:1
 +     */
 +    ml1 = new MapList(new int[] { 1, 30 }, new int[] { 11, 20 }, 3, 1);
 +    ml2 = new MapList(new int[] { 1, 100 }, new int[] { 1, 15, 91, 175 },
 +            1, 1);
 +    compound = ml1.traverse(ml2);
 +
 +    assertEquals(compound.getFromRatio(), 3);
 +    assertEquals(compound.getToRatio(), 1);
 +    fromRanges = compound.getFromRanges();
 +    assertEquals(fromRanges.size(), 1);
 +    assertArrayEquals(new int[] { 1, 30 }, fromRanges.get(0));
 +    // 11-20 maps to 11-15, 91-95
 +    toRanges = compound.getToRanges();
 +    assertEquals(toRanges.size(), 1);
 +    assertArrayEquals(new int[] { 11, 15, 91, 95 }, toRanges.get(0));
 +
 +    /*
 +     * 1:3 plus 3:1 should result in 1:1
 +     */
 +    ml1 = new MapList(new int[] { 21, 40 }, new int[] { 13, 72 }, 1, 3);
 +    ml2 = new MapList(new int[] { 1, 300 }, new int[] { 51, 70, 121, 200 },
 +            3, 1);
 +    compound = ml1.traverse(ml2);
 +
 +    assertEquals(compound.getFromRatio(), 1);
 +    assertEquals(compound.getToRatio(), 1);
 +    fromRanges = compound.getFromRanges();
 +    assertEquals(fromRanges.size(), 1);
 +    assertArrayEquals(new int[] { 21, 40 }, fromRanges.get(0));
 +    // 13-72 maps 3:1 to 55-70, 121-124
 +    toRanges = compound.getToRanges();
 +    assertEquals(toRanges.size(), 1);
 +    assertArrayEquals(new int[] { 55, 70, 121, 124 }, toRanges.get(0));
 +
 +    /*
 +     * 3:1 plus 1:3 should result in 1:1
 +     */
 +    ml1 = new MapList(new int[] { 31, 90 }, new int[] { 13, 32 }, 3, 1);
 +    ml2 = new MapList(new int[] { 11, 40 }, new int[] { 41, 50, 71, 150 },
 +            1, 3);
 +    compound = ml1.traverse(ml2);
 +
 +    assertEquals(compound.getFromRatio(), 1);
 +    assertEquals(compound.getToRatio(), 1);
 +    fromRanges = compound.getFromRanges();
 +    assertEquals(fromRanges.size(), 1);
 +    assertArrayEquals(new int[] { 31, 90 }, fromRanges.get(0));
 +    // 13-32 maps to 47-50,71-126
 +    toRanges = compound.getToRanges();
 +    assertEquals(toRanges.size(), 1);
 +    assertArrayEquals(new int[] { 47, 50, 71, 126 }, toRanges.get(0));
 +
 +    /*
 +     * method returns null if not all regions are mapped through
 +     */
 +    ml1 = new MapList(new int[] { 1, 50 }, new int[] { 101, 150 }, 1, 1);
 +    ml2 = new MapList(new int[] { 131, 180 }, new int[] { 201, 250 }, 1, 3);
 +    compound = ml1.traverse(ml2);
 +    assertNull(compound);
 +  }
 +
 +  /**
 +   * Test that method that inspects for the (first) forward or reverse 'to' range.
 +   * Single position ranges are ignored.
 +   */
 +  @Test(groups = { "Functional" })
 +  public void testIsToForwardsStrand()
 +  {
 +    // [3-9] declares forward strand
 +    MapList ml = new MapList(new int[] { 20, 11 },
 +            new int[]
 +            { 2, 2, 3, 9, 12, 11 }, 1, 1);
 +    assertTrue(ml.isToForwardStrand());
 +
 +    // [11-5] declares reverse strand ([13-14] is ignored)
 +    ml = new MapList(new int[] { 20, 11 },
 +            new int[]
 +            { 2, 2, 11, 5, 13, 14 }, 1, 1);
 +    assertFalse(ml.isToForwardStrand());
 +
 +    // all single position ranges - defaults to forward strand
 +    ml = new MapList(new int[] { 3, 1 }, new int[] { 2, 2, 4, 4, 6, 6 }, 1,
 +            1);
 +    assertTrue(ml.isToForwardStrand());
 +  }
  }