Merge branch 'develop' into features/JAL-4134_use_annotation_row_for_colours_and_groups
authorJames Procter <j.procter@dundee.ac.uk>
Fri, 19 May 2023 17:07:01 +0000 (18:07 +0100)
committerJames Procter <j.procter@dundee.ac.uk>
Fri, 19 May 2023 17:07:01 +0000 (18:07 +0100)
 Conflicts:
src/jalview/datamodel/Alignment.java
src/jalview/datamodel/ContactMatrix.java
src/jalview/datamodel/ContactMatrixI.java
src/jalview/datamodel/SeqDistanceContactMatrix.java
src/jalview/datamodel/annotations/AnnotationRowBuilder.java
src/jalview/ext/jmol/JmolParser.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/TreeCanvas.java
src/jalview/project/Jalview2XML.java
src/jalview/renderer/ContactMapRenderer.java
src/jalview/ws/datamodel/alphafold/PAEContactMatrix.java
src/jalview/ws/dbsources/EBIAlfaFold.java
src/jalview/xml/binding/jalview/MatrixType.java
test/jalview/analysis/AverageDistanceEngineTest.java
test/jalview/project/Jalview2xmlTests.java

15 files changed:
1  2 
resources/lang/Messages.properties
src/jalview/datamodel/ContactMatrix.java
src/jalview/datamodel/ContactMatrixI.java
src/jalview/datamodel/SeqDistanceContactMatrix.java
src/jalview/ext/jmol/JmolParser.java
src/jalview/gui/AnnotationPanel.java
src/jalview/gui/StructureChooser.java
src/jalview/gui/TreeCanvas.java
src/jalview/project/Jalview2XML.java
src/jalview/renderer/ContactMapRenderer.java
src/jalview/ws/datamodel/alphafold/PAEContactMatrix.java
src/jalview/ws/dbsources/EBIAlfaFold.java
src/jalview/xml/binding/jalview/MatrixType.java
test/jalview/analysis/AverageDistanceEngineTest.java
test/jalview/project/Jalview2xmlTests.java

@@@ -220,10 -220,10 +220,10 @@@ label.colourScheme_nucleotideambiguity 
  label.colourScheme_t-coffeescores = T-Coffee Scores
  label.colourScheme_rnahelices = By RNA Helices
  label.colourScheme_sequenceid = Sequence ID Colour
- label.colourScheme_gecos\:flower = gecos Flower
- label.colourScheme_gecos\:blossom = gecos Blossom
- label.colourScheme_gecos\:sunset = gecos Sunset
- label.colourScheme_gecos\:ocean = gecos Ocean
+ label.colourScheme_gecos-flower = gecos Flower
+ label.colourScheme_gecos-blossom = gecos Blossom
+ label.colourScheme_gecos-sunset = gecos Sunset
+ label.colourScheme_gecos-ocean = gecos Ocean
  label.blc = BLC
  label.fasta = Fasta
  label.msf = MSF
@@@ -449,7 -449,6 +449,7 @@@ label.input_cut_paste_params = Cut & Pa
  label.alignment_output_command = Alignment output - {0}
  label.annotations = Annotations
  label.structure_options = Structure Options
 +label.structure_import_options = Structure Import Options
  label.features = Features
  label.overview_params = Overview {0}
  label.paste_newick_file = Paste Newick file
@@@ -1449,7 -1448,5 +1449,7 @@@ label.tftype_default = Defaul
  label.tftype_plddt = pLDDT
  label.optional = (optional)
  label.choose_tempfac_type = Choose Temperature Factor type
 +label.interpret_tempfac_as = Interpret Temperature Factor as
  label.add_pae_matrix_file = Add PAE matrix file
  label.nothing_selected = Nothing selected
 +label.working_ellipsis = Working ... 
@@@ -1,12 -1,10 +1,12 @@@
  package jalview.datamodel;
  
  import java.awt.Color;
 +import java.math.BigInteger;
  import java.util.ArrayList;
  import java.util.BitSet;
  import java.util.HashMap;
  import java.util.List;
 +import java.util.Spliterator;
  import java.util.StringTokenizer;
  
  import jalview.bin.Console;
@@@ -150,6 -148,20 +150,6 @@@ public abstract class ContactMatrix imp
    }
  
    @Override
 -  public boolean hasReferenceSeq()
 -  {
 -    // TODO Auto-generated method stub
 -    return false;
 -  }
 -
 -  @Override
 -  public SequenceI getReferenceSeq()
 -  {
 -    // TODO Auto-generated method stub
 -    return null;
 -  }
 -
 -  @Override
    public String getAnnotLabel()
    {
      return "Contact Matrix";
    {
      return "Contact Matrix";
    }
--
 -  List<BitSet> groups = null;
 -
 -  @Override
 -  public void updateGroups(List<BitSet> colGroups)
 -  {
 -    groups = colGroups;
 -    colorMap = new HashMap<>();
 -  }
 -
 -  @Override
 -  public boolean hasGroups()
 -  {
 -    return groups != null && groups.size() > 0;
 -  }
 -
 -  @Override
 -  public List<BitSet> getGroups()
 -  {
 -    return groups;
 -  }
 -
 -  @Override
 -  public BitSet getGroupsFor(int column)
 -  {
 -    for (BitSet gp : groups)
 -    {
 -      if (gp.get(column))
 -      {
 -        return gp;
 -      }
 -    }
 -    return ContactMatrixI.super.getGroupsFor(column);
 -  }
 -
 -  HashMap<BitSet, Color> colorMap = new HashMap<>();
 -
 +  GroupSet grps = new GroupSet();
    @Override
 -  public Color getColourForGroup(BitSet bs)
 +  public GroupSetI getGroupSet()
    {
 -    if (bs == null)
 -    {
 -      return Color.white;
 -    }
 -    Color groupCol = colorMap.get(bs);
 -    if (groupCol == null)
 -    {
 -      return Color.white;
 -    }
 -    return groupCol;
 +    return grps;
    }
--
    @Override
 -  public void setColorForGroup(BitSet bs, Color color)
 +  public void setGroupSet(GroupSet makeGroups)
    {
 -    colorMap.put(bs, color);
 +    grps = makeGroups;
    }
--
    public static String contactToFloatString(ContactMatrixI cm)
    {
      StringBuilder sb = new StringBuilder();
      float[][] vals = new float[cols][rows];
      StringTokenizer tabsep = new StringTokenizer(values, "" + '\t');
      int c = 0, r = 0;
--
      while (tabsep.hasMoreTokens())
      {
        double elem = Double.valueOf(tabsep.nextToken());
        }
        if (c >= vals.length)
        {
--
          break;
        }
      }
        Console.warn(
                "Ignoring additional elements for Float string to contact matrix parsing.");
      }
--
      return vals;
    }
  }
@@@ -14,6 -14,10 +14,6 @@@ public interface ContactMatrix
  
    float getMax();
  
 -  boolean hasReferenceSeq();
 -
 -  SequenceI getReferenceSeq();
 -
    String getAnnotDescr();
  
    String getAnnotLabel();
    String getType();
  
    int getWidth();
--
    int getHeight();
 +  public GroupSetI getGroupSet();
 +
 +  /// proxy methods to simplify use of the interface
 +  /// Mappable contact matrices can override these to perform mapping
 +
 +  default public boolean hasGroupSet()
 +  {
 +    return getGroupSet() != null;
 +  }
  
    default boolean hasGroups()
    {
 -    return false;
 +    return hasGroupSet() && getGroupSet().hasGroups();
    }
  
    default BitSet getGroupsFor(int column)
    {
 -    BitSet colbitset = new BitSet();
 -    colbitset.set(column);
 -    return colbitset;
 +    if (!hasGroupSet())
 +    {
 +      BitSet colbitset = new BitSet();
 +      colbitset.set(column);
 +      return colbitset;
 +    }
 +    return getGroupSet().getGroupsFor(column);
    }
  
    default List<BitSet> getGroups()
    {
 -    return Arrays.asList();
 +    if (!hasGroupSet())
 +    {
 +      return Arrays.asList();
 +    }
 +    return getGroupSet().getGroups();
    }
  
    default boolean hasTree()
    {
 -    return false;
 +    return hasGroupSet() ? getGroupSet().hasTree() : false;
    }
  
    /**
     */
    default String getNewick()
    {
 -    return null;
 +    return hasGroupSet() ? getGroupSet().getNewick() : null;
    }
  
    default String getTreeMethod()
    {
 -    return null;
 +    return hasGroupSet() ? getGroupSet().getTreeMethod() : null;
    }
  
    default boolean hasCutHeight()
    {
 -    return false;
 +    return hasGroupSet() ? getGroupSet().hasCutHeight() : false;
    }
  
    default double getCutHeight()
    {
 -    return 0;
 +    return hasGroupSet() ? getGroupSet().getCutHeight() : 0;
    }
  
 -  void updateGroups(List<BitSet> colGroups);
 +  default void updateGroups(List<BitSet> colGroups)
 +  {
 +    if (hasGroupSet())
 +    {
 +      getGroupSet().updateGroups(colGroups);
 +    }
 +  }
  
 -  void setColorForGroup(BitSet bs, Color color);
 +  default void setColorForGroup(BitSet bs, Color color)
 +  {
 +    if (hasGroupSet())
 +    {
 +      getGroupSet().setColorForGroup(bs, color);
 +    }
 +  }
  
    default Color getColourForGroup(BitSet bs)
    {
 -    return Color.white;
 -  };
 +    if (hasGroupSet())
 +    {
 +      return getGroupSet().getColourForGroup(bs);
 +    }
 +    else
 +    {
 +      return Color.white;
 +    }
 +  }
 +
 +  void setGroupSet(GroupSet makeGroups);
  }
@@@ -5,21 -5,16 +5,19 @@@ import java.util.BitSet
  import java.util.HashMap;
  import java.util.List;
  
 +import jalview.util.MapList;
 +import jalview.ws.datamodel.alphafold.MappableContactMatrix;
  /**
   * Dummy contact matrix based on sequence distance
   * 
   * @author jprocter
   *
   */
 -public class SeqDistanceContactMatrix implements ContactMatrixI
 +public class SeqDistanceContactMatrix
 +        extends MappableContactMatrix<SeqDistanceContactMatrix>
 +        implements ContactMatrixI
  {
    private static final String SEQUENCE_DISTANCE = "SEQUENCE_DISTANCE";
--
    private int width = 0;
  
    public SeqDistanceContactMatrix(int width)
    }
  
    @Override
 -  public boolean hasReferenceSeq()
 -  {
 -    // TODO Auto-generated method stub
 -    return false;
 -  }
 -
 -  @Override
 -  public SequenceI getReferenceSeq()
 -  {
 -    // TODO Auto-generated method stub
 -    return null;
 -  }
 -
 -  @Override
    public String getAnnotDescr()
    {
      return "Sequence distance matrix";
    {
      return width;
    }
--
 -  private List<BitSet> groups = null;
 -
 -  @Override
 -  public void updateGroups(List<BitSet> colGroups)
 -  {
 -    groups = colGroups;
 -  }
 -
    @Override
 -  public boolean hasGroups()
 +  protected double getElementAt(int _column, int i)
    {
 -    return groups != null;
 +    return Math.abs(_column - i);
    }
--
 -  @Override
 -  public List<BitSet> getGroups()
 -  {
 -    return groups;
 -  }
 -
 -  HashMap<BitSet, Color> colorMap = new HashMap<>();
 -
    @Override
 -  public Color getColourForGroup(BitSet bs)
 +  protected SeqDistanceContactMatrix newMappableContactMatrix(
 +          SequenceI newRefSeq, MapList newFromMapList)
    {
 -    if (bs == null)
 -    {
 -      return Color.white;
 -    }
 -    Color groupCol = colorMap.get(bs);
 -    if (groupCol == null)
 -    {
 -      return Color.white;
 -    }
 -    return groupCol;
 -  }
  
 -  @Override
 -  public void setColorForGroup(BitSet bs, Color color)
 -  {
 -    colorMap.put(bs, color);
 +    return new SeqDistanceContactMatrix(width);
    }
  }
@@@ -47,7 -47,6 +47,6 @@@ import jalview.datamodel.SequenceI
  import jalview.datamodel.annotations.AlphaFoldAnnotationRowBuilder;
  import jalview.datamodel.annotations.AnnotationRowBuilder;
  import jalview.io.DataSourceType;
- import jalview.io.FileFormatException;
  import jalview.io.FileParse;
  import jalview.io.StructureFile;
  import jalview.schemes.ResidueProperties;
@@@ -89,6 -88,11 +88,10 @@@ public class JmolParser extends Structu
      super(inFile, sourceType, tempfacType);
    }
  
+   public JmolParser(FileParse fp, boolean doXferSettings) throws IOException
+   {
+     super(fp, doXferSettings);
+   }
 -
    public JmolParser(FileParse fp) throws IOException
    {
      super(fp);
    @Override
    public void parse() throws IOException
    {
+     parse(true);
+   }
+   @Override
+   public void parse(boolean doXferSettings) throws IOException
+   {
      setChains(new Vector<PDBChain>());
      Viewer jmolModel = getJmolData();
      jmolModel.openReader(getDataName(), getDataName(), getReader());
                        ? PDBEntry.Type.MMCIF.toString()
                        : "PDB");
  
-       transformJmolModelToJalview(jmolModel.ms);
+       transformJmolModelToJalview(jmolModel.ms, doXferSettings);
      }
    }
  
      return false;
    }
  
-   public void transformJmolModelToJalview(ModelSet ms) throws IOException
+   public void transformJmolModelToJalview(ModelSet ms,
+           boolean localDoXferSettings) throws IOException
    {
      try
      {
          }
          lastID = tmpatom.resNumIns.trim();
        }
-       if (isParseImmediately())
+       if (isParseImmediately() && localDoXferSettings)
        {
          // configure parsing settings from the static singleton
          xferSettings();
        {
          try
          {
-           Console.info("retrieving pAE for " + pdbId);
+           Console.info("Retrieving PAE for " + pdbId);
            File paeFile = EBIAlfaFold.fetchAlphaFoldPAE(pdbId, null);
            this.setPAEMatrix(paeFile.getAbsolutePath());
          } catch (Throwable t)
        // add a PAEMatrix if set (either by above or otherwise)
        if (hasPAEMatrix())
        {
 -        Alignment al = new Alignment(prot.toArray(new SequenceI[0]));
 -        EBIAlfaFold.addAlphaFoldPAE(al, new File(this.getPAEMatrix()), 0,
 -                null, false, false, null);
 -
 -        if (al.getAlignmentAnnotation() != null)
 +        try
          {
 -          for (AlignmentAnnotation alann : al.getAlignmentAnnotation())
 +          Alignment al = new Alignment(prot.toArray(new SequenceI[0]));
 +          EBIAlfaFold.addAlphaFoldPAE(al, new File(this.getPAEMatrix()), 0,
-                   null, false, false);
++                  null, false, false, null);
 +
 +          if (al.getAlignmentAnnotation() != null)
            {
 -            annotations.add(alann);
 +            for (AlignmentAnnotation alann : al.getAlignmentAnnotation())
 +            {
 +              annotations.add(alann);
 +            }
            }
 +        } catch (Throwable ff)
 +        {
 +          Console.error("Couldn't import PAE Matrix from " + getPAEMatrix(),
 +                  ff);
 +          warningMessage += "Couldn't import PAE Matrix"
 +                  + getNewlineString() + ff.getLocalizedMessage()
 +                  + getNewlineString();
          }
        }
      } catch (OutOfMemoryError er)
      {
        System.out.println(
@@@ -656,8 -656,7 +656,8 @@@ public class AnnotationPanel extends JP
        fr = Math.min(cXci.cStart, cXci.cEnd);
        to = Math.max(cXci.cStart, cXci.cEnd);
  
 -      if (evt.isControlDown())
 +      // double click selects the whole group
 +      if (evt.getClickCount() == 2)
        {
          ContactMatrixI matrix = av.getContactMatrix(clicked);
  
            {
              SequenceI rseq = clicked.sequenceRef;
              BitSet grp = matrix.getGroupsFor(currentX);
 +            // TODO: cXci needs to be mapped to real groups
              for (int c = fr; c <= to; c++)
              {
                BitSet additionalGrp = matrix.getGroupsFor(c);
        {
          // select corresponding range in segment under mouse
          {
 -          for (int c = fr; c <= to; c++)
 +          int[] rng = forCurrentX.getMappedPositionsFor(fr, to);
 +          if (rng != null)
            {
 -            av.getColumnSelection().addElement(c);
 +            av.getColumnSelection().addRangeOfElements(rng, true);
            }
            av.getColumnSelection().addElement(currentX);
          }
          // PAE SPECIFIC
          // and also select everything lower than the max range adjacent
          // (kind of works)
 -        if (PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
 +        if (evt.isControlDown()
 +                && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
          {
            int c = fr - 1;
            ContactRange cr = forCurrentX.getRangeFor(fr, to);
              if (// cr.getMin() <= cval &&
              cval <= thresh)
              {
 -              av.getColumnSelection().addElement(c--);
 -            }
 -            else
 -            {
 -              break;
 +              int[] cols = forCurrentX.getMappedPositionsFor(c, c);
 +              if (cols != null)
 +              {
 +                av.getColumnSelection().addRangeOfElements(cols, true);
 +              }
 +              else
 +              {
 +                break;
 +              }
              }
 +            c--;
            }
            c = to;
            while (c < forCurrentX.getContactHeight())
              if (// cr.getMin() <= cval &&
              cval <= thresh)
              {
 -              av.getColumnSelection().addElement(c++);
 +              int[] cols = forCurrentX.getMappedPositionsFor(c, c);
 +              if (cols != null)
 +              {
 +                av.getColumnSelection().addRangeOfElements(cols, true);
 +              }
              }
              else
              {
                break;
              }
 +            c++;
 +
            }
          }
        }
        int fr, to;
        fr = Math.min(lastXci.cStart, lastXci.cEnd);
        to = Math.max(lastXci.cStart, lastXci.cEnd);
 -      jalview.bin.Console.trace("Marking " + fr + " to " + to);
 -      for (int c = fr; c <= to; c++)
 +      int[] mappedPos = forFromX.getMappedPositionsFor(fr, to);
 +      if (mappedPos != null)
        {
 -        if (cma.sequenceRef != null)
 +        jalview.bin.Console.trace("Marking " + fr + " to " + to
 +                + " mapping to sequence positions " + mappedPos[0] + " to "
 +                + mappedPos[1]);
 +        for (int pair = 0; pair < mappedPos.length; pair += 2)
          {
 -          int col = cma.sequenceRef.findIndex(c);
 -          av.getColumnSelection().addElement(col);
 -        }
 -        else
 -        {
 -          av.getColumnSelection().addElement(c);
 +          for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
 +          // {
 +          // if (cma.sequenceRef != null)
 +          // {
 +          // int col = cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
 +          // av.getColumnSelection().addElement(col);
 +          // }
 +          // else
 +          {
 +            av.getColumnSelection().addElement(c);
 +          }
          }
        }
 +      // and again for most recent corner of drag
        fr = Math.min(cXci.cStart, cXci.cEnd);
        to = Math.max(cXci.cStart, cXci.cEnd);
 -      jalview.bin.Console.trace("Marking " + fr + " to " + to);
 -      for (int c = fr; c <= to; c++)
 +      mappedPos = forFromX.getMappedPositionsFor(fr, to);
 +      if (mappedPos != null)
        {
 -        if (cma.sequenceRef != null)
 +        for (int pair = 0; pair < mappedPos.length; pair += 2)
          {
 -          int col = cma.sequenceRef.findIndex(c);
 -          av.getColumnSelection().addElement(col);
 -        }
 -        else
 -        {
 -          av.getColumnSelection().addElement(c);
 +          jalview.bin.Console.trace("Marking " + fr + " to " + to
 +                  + " mapping to sequence positions " + mappedPos[pair]
 +                  + " to " + mappedPos[pair + 1]);
 +          for (int c = mappedPos[pair]; c <= mappedPos[pair + 1]; c++)
 +          {
 +            // if (cma.sequenceRef != null)
 +            // {
 +            // int col =
 +            // cma.sequenceRef.findIndex(cma.sequenceRef.getStart()+c);
 +            // av.getColumnSelection().addElement(col);
 +            // }
 +            // else
 +            {
 +              av.getColumnSelection().addElement(c);
 +            }
 +          }
          }
        }
        fr = Math.min(lastX, currentX);
          ContactRange cr = clist.getRangeFor(ci.cStart, ci.cEnd);
          tooltip = "Contact from " + clist.getPosition() + ", [" + ci.cStart
                  + " - " + ci.cEnd + "]" + "<br/>Mean:" + cr.getMean();
 +
          int col = ann.sequenceRef.findPosition(column);
 +        int[][] highlightPos;
 +        int[] mappedPos = clist.getMappedPositionsFor(ci.cStart, ci.cEnd);
 +        if (mappedPos != null)
 +        {
 +          highlightPos = new int[1 + mappedPos.length][2];
 +          highlightPos[0] = new int[] { col, col };
 +          for (int p = 0, h = 0; p < mappedPos.length; h++, p += 2)
 +          {
 +            highlightPos[h][0] = ann.sequenceRef
 +                    .findPosition(mappedPos[p] - 1);
 +            highlightPos[h][1] = ann.sequenceRef
 +                    .findPosition(mappedPos[p + 1] - 1);
 +          }
 +        }
 +        else
 +        {
 +          highlightPos = new int[][] { new int[] { col, col } };
 +        }
          ap.getStructureSelectionManager()
 -                .highlightPositionsOn(ann.sequenceRef, new int[][]
 -                { new int[] { col, col },
 -                    new int[]
 -                    { ci.cStart, ci.cEnd } }, null);
 +                .highlightPositionsOn(ann.sequenceRef, highlightPos, null);
        }
      }
      return tooltip;
          return;
        }
      }
-     imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
-             + 1) * av.getCharWidth();
+     updateFadedImageWidth();
      if (imgWidth < 1)
      {
        return;
      g.drawImage(image, 0, 0, this);
    }
  
+   public void updateFadedImageWidth()
+   {
+     imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
+             + 1) * av.getCharWidth();
+   }
    /**
     * set true to enable redraw timing debug output on stderr
     */
    @Override
    public int getFadedImageWidth()
    {
+     updateFadedImageWidth();
      return imgWidth;
    }
  
@@@ -28,9 -28,11 +28,11 @@@ import java.io.File
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashSet;
+ import java.util.LinkedHashMap;
  import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.Locale;
+ import java.util.Map;
  import java.util.concurrent.Callable;
  import java.util.concurrent.Executors;
  
@@@ -45,11 -47,16 +47,16 @@@ import javax.swing.table.AbstractTableM
  
  import com.stevesoft.pat.Regex;
  
+ import jalview.analysis.AlignmentUtils;
+ import jalview.api.AlignmentViewPanel;
  import jalview.api.structures.JalviewStructureDisplayI;
  import jalview.bin.Cache;
  import jalview.bin.Console;
  import jalview.bin.Jalview;
+ import jalview.datamodel.AlignmentAnnotation;
+ import jalview.datamodel.AlignmentI;
  import jalview.datamodel.PDBEntry;
+ import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  import jalview.ext.jmol.JmolParser;
  import jalview.fts.api.FTSData;
@@@ -60,11 -67,11 +67,12 @@@ import jalview.fts.core.FTSRestRequest
  import jalview.fts.core.FTSRestResponse;
  import jalview.fts.service.pdb.PDBFTSRestClient;
  import jalview.fts.service.threedbeacons.TDB_FTSData;
+ import jalview.gui.StructureViewer.ViewerType;
  import jalview.gui.structurechooser.PDBStructureChooserQuerySource;
  import jalview.gui.structurechooser.StructureChooserQuerySource;
  import jalview.gui.structurechooser.ThreeDBStructureChooserQuerySource;
  import jalview.io.DataSourceType;
 +import jalview.io.FileFormatException;
  import jalview.io.JalviewFileChooser;
  import jalview.io.JalviewFileView;
  import jalview.jbgui.FilterOption;
@@@ -77,8 -84,6 +85,8 @@@ import jalview.util.Platform
  import jalview.util.StringUtils;
  import jalview.ws.DBRefFetcher;
  import jalview.ws.DBRefFetcher.FetchFinishedListenerI;
 +import jalview.ws.datamodel.alphafold.PAEContactMatrix;
 +import jalview.ws.dbsources.EBIAlfaFold;
  import jalview.ws.seqfetcher.DbSourceProxy;
  import jalview.ws.sifts.SiftsSettings;
  
@@@ -702,7 -707,7 +710,7 @@@ public class StructureChooser extends G
    }
  
    /**
 -   * Handles action event for btn_pdbFromFile
 +   * Handles action event for btn_paeMatrixFile
     */
    @Override
    protected void paeMatrixFile_actionPerformed()
              "label.load_pae_matrix_file_associate_with_structure",
              pdbFile.getName()));
  
 +    // TODO convert to Callable/Promise
      int value = chooser.showOpenDialog(null);
      if (value == JalviewFileChooser.APPROVE_OPTION)
      {
 -      localPdbPaeMatrixFileName = chooser.getSelectedFile().getPath();
 +      String fileName = chooser.getSelectedFile().getPath();
 +      try
 +      {
 +        PAEContactMatrix.validateContactMatrixFile(fileName);
 +      } catch (Exception thr)
 +      {
 +        JvOptionPane.showInternalMessageDialog(this, MessageManager
 +                .formatMessage("label.couldnt_load_file", new Object[]
 +                { fileName }) + "<br>" + thr.getLocalizedMessage(),
 +                MessageManager.getString("label.error_loading_file"),
 +                JvOptionPane.WARNING_MESSAGE);
 +        Console.error("Couldn't import " + fileName + " as a PAE matrix",
 +                thr);
 +        return;
 +      }
 +      localPdbPaeMatrixFileName = fileName;
        Cache.setProperty("LAST_DIRECTORY", localPdbPaeMatrixFileName);
      }
      validateAssociationFromFile();
        {
          pdbFileString = MessageManager.getString("label.none");
          pdbFileTooltip = MessageManager.getString("label.nothing_selected");
 +        setPdbOptionsEnabled(false);
        }
      }
      else
      {
        btn_pdbFromFile.setEnabled(false);
 +      setPdbOptionsEnabled(false);
        // lbl_fromFileStatus.setIcon(errorImage);
        pdbFileString = MessageManager.getString("label.none");
        pdbFileTooltip = MessageManager.getString("label.nothing_selected");
      final StructureSelectionManager ssm = ap.getStructureSelectionManager();
  
      final int preferredHeight = pnl_filter.getHeight();
 +    btn_add.setEnabled(false);
 +    btn_newView.setEnabled(false);
 +    btn_cancel.setEnabled(false);
 +    actionsPanel.setEnabled(false);
  
 +    final String progress = MessageManager
 +            .getString("label.working_ellipsis");
 +    setProgressBar(progress, progress.hashCode());
      Runnable viewStruc = new Runnable()
      {
        @Override
                    .getCmb_assSeq().getSelectedItem();
            SequenceI userSelectedSeq = assSeqOpt.getSequence();
            if (userSelectedSeq != null)
 +          {
              selectedSequence = userSelectedSeq;
 +          }
            String pdbFilename = selectedPdbFileName;
  
            StructureChooser.openStructureFileForSequence(ssm, sc, ap,
-                   selectedSequence, true, pdbFilename, tft, paeFilename);
+                   selectedSequence, true, pdbFilename, tft, paeFilename,
+                   true);
          }
          SwingUtilities.invokeLater(new Runnable()
          {
            @Override
            public void run()
            {
 +            setProgressBar("Complete.", progress.hashCode());
              closeAction(preferredHeight);
              mainFrame.dispose();
            }
            StructureSelectionManager ssm, final PDBEntry[] pdbEntriesToView,
            final AlignmentPanel alignPanel, SequenceI[] sequences)
    {
+     return launchStructureViewer(ssm, pdbEntriesToView, alignPanel,
+             sequences, null);
+   }
+   private StructureViewer launchStructureViewer(
+           StructureSelectionManager ssm, final PDBEntry[] pdbEntriesToView,
+           final AlignmentPanel alignPanel, SequenceI[] sequences,
+           ViewerType viewerType)
+   {
      long progressId = sequences.hashCode();
      setProgressBar(MessageManager
              .getString("status.launching_3d_structure_viewer"), progressId);
                MessageManager.getString(
                        "status.fetching_3d_structures_for_selected_entries"),
                progressId);
-       theViewer.viewStructures(pdbEntriesToView, sequences, alignPanel);
+       theViewer.viewStructures(pdbEntriesToView, sequences, alignPanel,
+               viewerType);
      }
      else
      {
                "status.fetching_3d_structures_for",
                pdbEntriesToView[0].getId()), progressId);
        // Can we pass a pre-computeMappinged pdbFile?
-       theViewer.viewStructures(pdbEntriesToView[0], sequences, alignPanel);
+       theViewer.viewStructures(pdbEntriesToView[0], sequences, alignPanel,
+               viewerType);
      }
      setProgressBar(null, progressId);
      // remember the last viewer we used...
    public static void openStructureFileForSequence(
            StructureSelectionManager ssm, StructureChooser sc,
            AlignmentPanel ap, SequenceI seq, boolean prompt,
-           String sFilename, TFType tft, String paeFilename)
+           String sFilename, TFType tft, String paeFilename,
+           boolean doXferSettings)
    {
-     boolean headless = false;
+     openStructureFileForSequence(ssm, sc, ap, seq, prompt, sFilename, tft,
+             paeFilename, false, true, doXferSettings, null);
+   }
+   public static void openStructureFileForSequence(
+           StructureSelectionManager ssm, StructureChooser sc,
+           AlignmentPanel ap, SequenceI seq, boolean prompt,
+           String sFilename, TFType tft, String paeFilename,
+           boolean forceHeadless, boolean showRefAnnotations,
+           boolean doXferSettings, ViewerType viewerType)
+   {
+     boolean headless = forceHeadless;
      if (sc == null)
      {
-       headless = true;
+       // headless = true;
+       prompt = false;
        sc = new StructureChooser(new SequenceI[] { seq }, seq, ap, false);
      }
      if (ssm == null)
  
      PDBEntry fileEntry = new AssociatePdbFileWithSeq().associatePdbWithSeq(
              sFilename, DataSourceType.FILE, seq, prompt, Desktop.instance,
-             tft, paeFilename);
+             tft, paeFilename, doXferSettings);
  
-     StructureViewer sViewer = sc.launchStructureViewer(ssm,
-             new PDBEntry[]
-             { fileEntry }, ap, new SequenceI[] { seq });
+     // if headless, "false" in the sc constructor above will avoid GUI behaviour
+     // in sc.launchStructureViewer()
+     if (!headless && !(viewerType == null))
+       sc.launchStructureViewer(ssm, new PDBEntry[] { fileEntry }, ap,
+               new SequenceI[]
+               { seq }, viewerType);
  
      if (headless)
        sc.mainFrame.dispose();
+     if (showRefAnnotations)
+       showReferenceAnnotationsForSequence(ap.alignFrame, seq);
+   }
+   public static void showReferenceAnnotationsForSequence(AlignFrame af,
+           SequenceI sequence)
+   {
+     AlignViewport av = af.getCurrentView();
+     AlignmentI al = av.getAlignment();
+     List<SequenceI> forSequences = new ArrayList<>();
+     forSequences.add(sequence);
+     final Map<SequenceI, List<AlignmentAnnotation>> candidates = new LinkedHashMap<>();
+     AlignmentUtils.findAddableReferenceAnnotations(forSequences, null,
+             candidates, al);
+     final SequenceGroup selectionGroup = av.getSelectionGroup();
+     AlignmentUtils.addReferenceAnnotations(candidates, al, selectionGroup);
+     for (AlignmentViewPanel ap : af.getAlignPanels())
+     {
+       // required to readjust the height and position of the PAE
+       // annotation
+       ap.adjustAnnotationHeight();
+     }
    }
  }
@@@ -54,7 -54,6 +54,7 @@@ import jalview.analysis.Conservation
  import jalview.analysis.TreeModel;
  import jalview.api.AlignViewportI;
  import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.Annotation;
  import jalview.datamodel.BinaryNode;
  import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.ContactMatrixI;
@@@ -68,7 -67,6 +68,7 @@@ import jalview.schemes.ColourSchemeI
  import jalview.structure.SelectionSource;
  import jalview.util.Format;
  import jalview.util.MessageManager;
 +import jalview.ws.datamodel.MappableContactMatrixI;
  
  /**
   * DOCUMENT ME!
@@@ -1037,41 -1035,6 +1037,41 @@@ public class TreeCanvas extends JPanel 
              cm.setColorForGroup(gp, colors.get(gp));
            }
          }
 +        // stash colors in linked annotation row.
 +        // doesn't work yet. TESTS!
 +        int sstart = aa.sequenceRef != null ? aa.sequenceRef.getStart() - 1
 +                : 0;
 +        Annotation ae;
 +        Color gpcol = null;
 +        int[] seqpos = null;
 +        for (BitSet gp : colors.keySet())
 +        {
 +          gpcol = colors.get(gp);
 +          for (int p = gp.nextSetBit(0); p >= 0
 +                  && p < Integer.MAX_VALUE; p = gp.nextSetBit(p + 1))
 +          {
 +            if (cm instanceof MappableContactMatrixI)
 +            {
 +              MappableContactMatrixI mcm = (MappableContactMatrixI) cm;
 +              seqpos = mcm.getMappedPositionsFor(aa.sequenceRef, p);
 +              if (seqpos == null)
 +              {
 +                // no mapping for this column.
 +                continue;
 +              }
 +              // TODO: handle ranges...
 +              ae = aa.getAnnotationForPosition(seqpos[0]);
 +            }
 +            else
 +            {
 +              ae = aa.getAnnotationForPosition(p + sstart);
 +            }
 +            if (ae != null)
 +            {
 +              ae.colour = gpcol.brighter().darker();
 +            }
 +          }
 +        }
        }
      }
  
        return false;
      }
      ColumnSelection cs = av.getColumnSelection();
--
      HiddenColumns hc = av.getAlignment().getHiddenColumns();
      int offp = (rseq != null) ? rseq.findIndex(rseq.getStart() + colm)
              : colm;
      }
      return false;
    }
--
    private BitSet createColumnGroupFor(Vector<BinaryNode> l, Color col)
    {
      BitSet gp = new BitSet();
            Color col)
    {
      SequenceI rseq = tp.assocAnnotation.sequenceRef;
      if (av == null || av.getAlignment() == null)
      {
        // alignment is closed
        {
          continue;
        }
 +      // TODO - sort indices for faster lookup
        ColumnSelection cs = av.getColumnSelection();
        HiddenColumns hc = av.getAlignment().getHiddenColumns();
 +      ContactMatrixI cm = av.getContactMatrix(tp.assocAnnotation);
 +      MappableContactMatrixI mcm = null;
 +      int offp;
 +      if (cm instanceof MappableContactMatrixI)
 +      {
 +        mcm = (MappableContactMatrixI) cm;
 +        int[] seqpos = mcm.getMappedPositionsFor(
 +                tp.assocAnnotation.sequenceRef, colm);
 +        if (seqpos == null)
 +        {
 +          // no mapping for this column.
 +          continue;
 +        }
 +        // TODO: handle ranges...
 +        offp = seqpos[0];
 +      }
 +      else
        {
 -        int offp = (rseq != null) ? rseq.findIndex(rseq.getStart() + colm)
 +        offp = (rseq != null) ? rseq.findIndex(rseq.getStart() + colm)
                  : colm;
 -
 -        if (!av.hasHiddenColumns() || hc.isVisible(offp - 1))
 +      }
 +      if (!av.hasHiddenColumns() || hc.isVisible(offp - 1))
 +      {
 +        if (cs.contains(offp - 1))
          {
 -          if (cs.contains(offp - 1))
 -          {
 -            cs.removeElement(offp - 1);
 -          }
 -          else
 -          {
 -            cs.addElement(offp - 1);
 -          }
 +          cs.removeElement(offp - 1);
 +        }
 +        else
 +        {
 +          cs.addElement(offp - 1);
          }
        }
      }
@@@ -96,7 -96,6 +96,7 @@@ import jalview.datamodel.ContactMatrixI
  import jalview.datamodel.DBRefEntry;
  import jalview.datamodel.GeneLocus;
  import jalview.datamodel.GraphLine;
 +import jalview.datamodel.GroupSet;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.Point;
  import jalview.datamodel.RnaViewerModel;
@@@ -153,7 -152,6 +153,7 @@@ import jalview.viewmodel.ViewportRanges
  import jalview.viewmodel.seqfeatures.FeatureRendererModel;
  import jalview.viewmodel.seqfeatures.FeatureRendererSettings;
  import jalview.viewmodel.seqfeatures.FeaturesDisplayed;
 +import jalview.ws.datamodel.MappableContactMatrixI;
  import jalview.ws.datamodel.alphafold.PAEContactMatrix;
  import jalview.ws.jws2.Jws2Discoverer;
  import jalview.ws.jws2.dm.AAConSettings;
@@@ -196,7 -194,6 +196,7 @@@ import jalview.xml.binding.jalview.Jalv
  import jalview.xml.binding.jalview.JalviewModel.Viewport.Overview;
  import jalview.xml.binding.jalview.JalviewUserColours;
  import jalview.xml.binding.jalview.JalviewUserColours.Colour;
 +import jalview.xml.binding.jalview.MapListType;
  import jalview.xml.binding.jalview.MapListType.MapListFrom;
  import jalview.xml.binding.jalview.MapListType.MapListTo;
  import jalview.xml.binding.jalview.Mapping;
@@@ -2326,7 -2323,8 +2326,7 @@@ public class Jalview2XM
                {
                  for (BitSet gp : cm.getGroups())
                  {
 -                  BigInteger val = new BigInteger(gp.toByteArray());
 -                  xmlmat.getGroups().add(val.toString());
 +                  xmlmat.getGroups().add(stringifyBitset(gp));
                  }
                }
                if (cm.hasTree())
                {
                  xmlmat.setCutHeight(cm.getCutHeight());
                }
--
                // set/get properties
 +              if (cm instanceof MappableContactMatrixI)
 +              {
 +                jalview.util.MapList mlst = ((MappableContactMatrixI) cm)
 +                        .getMapFor(annotation.sequenceRef);
 +                if (mlst != null)
 +                {
 +                  MapListType mp = new MapListType();
 +                  List<int[]> r = mlst.getFromRanges();
 +                  for (int[] range : r)
 +                  {
 +                    MapListFrom mfrom = new MapListFrom();
 +                    mfrom.setStart(range[0]);
 +                    mfrom.setEnd(range[1]);
 +                    // mp.addMapListFrom(mfrom);
 +                    mp.getMapListFrom().add(mfrom);
 +                  }
 +                  r = mlst.getToRanges();
 +                  for (int[] range : r)
 +                  {
 +                    MapListTo mto = new MapListTo();
 +                    mto.setStart(range[0]);
 +                    mto.setEnd(range[1]);
 +                    // mp.addMapListTo(mto);
 +                    mp.getMapListTo().add(mto);
 +                  }
 +                  mp.setMapFromUnit(
 +                          BigInteger.valueOf(mlst.getFromRatio()));
 +                  mp.setMapToUnit(BigInteger.valueOf(mlst.getToRatio()));
 +                  xmlmat.setMapping(mp);
 +                }
 +              }
 +              // and add to model
                an.getContactmatrix().add(xmlmat);
              }
            }
  
    }
  
 +  private String stringifyBitset(BitSet gp)
 +  {
 +    StringBuilder sb = new StringBuilder();
 +    for (long val : gp.toLongArray())
 +    {
 +      if (sb.length() > 0)
 +      {
 +        sb.append(",");
 +      }
 +      sb.append(val);
 +    }
 +    return sb.toString();
 +  }
 +
 +  private BitSet deStringifyBitset(String stringified)
 +  {
 +    if ("".equals(stringified) || stringified == null)
 +    {
 +      return new BitSet();
 +    }
 +    String[] longvals = stringified.split(",");
 +    long[] newlongvals = new long[longvals.length];
 +    for (int lv = 0; lv < longvals.length; lv++)
 +    {
 +      try
 +      {
 +        newlongvals[lv] = Long.valueOf(longvals[lv]);
 +      } catch (Exception x)
 +      {
 +        errorMessage += "Couldn't destringify bitset from: '" + stringified
 +                + "'";
 +        newlongvals[lv] = 0;
 +      }
 +    }
 +    return BitSet.valueOf(newlongvals);
 +
 +  }
    private CalcIdParam createCalcIdParam(String calcId, AlignViewport av)
    {
      AutoCalcSetting settings = av.getCalcIdSettingsFor(calcId);
                            .fromFloatStringToContacts(xmlmat.getElements(),
                                    xmlmat.getCols().intValue(),
                                    xmlmat.getRows().intValue());
 +                  jalview.util.MapList mapping = null;
 +                  if (xmlmat.getMapping() != null)
 +                  {
 +                    MapListType m = xmlmat.getMapping();
 +                    // Mapping m = dr.getMapping();
 +                    int fr[] = new int[m.getMapListFrom().size() * 2];
 +                    Iterator<MapListFrom> from = m.getMapListFrom()
 +                            .iterator();// enumerateMapListFrom();
 +                    for (int _i = 0; from.hasNext(); _i += 2)
 +                    {
 +                      MapListFrom mf = from.next();
 +                      fr[_i] = mf.getStart();
 +                      fr[_i + 1] = mf.getEnd();
 +                    }
 +                    int fto[] = new int[m.getMapListTo().size() * 2];
 +                    Iterator<MapListTo> to = m.getMapListTo().iterator();// enumerateMapListTo();
 +                    for (int _i = 0; to.hasNext(); _i += 2)
 +                    {
 +                      MapListTo mf = to.next();
 +                      fto[_i] = mf.getStart();
 +                      fto[_i + 1] = mf.getEnd();
 +                    }
  
 -                  PAEContactMatrix newpae = new PAEContactMatrix(
 -                          jaa.sequenceRef, elements);
 +                    mapping = new jalview.util.MapList(fr, fto,
 +                            m.getMapFromUnit().intValue(),
 +                            m.getMapToUnit().intValue());
 +                  }
                    List<BitSet> newgroups = new ArrayList<BitSet>();
                    if (xmlmat.getGroups().size() > 0)
                    {
                      for (String sgroup : xmlmat.getGroups())
                      {
 -                      try
 -                      {
 -                        BigInteger group = new BigInteger(sgroup);
 -                        newgroups.add(BitSet.valueOf(group.toByteArray()));
 -                      } catch (NumberFormatException nfe)
 -                      {
 -                        Console.error(
 -                                "Problem parsing groups for a contact matrix (\""
 -                                        + sgroup + "\"",
 -                                nfe);
 -                      }
 +                      newgroups.add(deStringifyBitset(sgroup));
                      }
                    }
                    String nwk = xmlmat.getNewick().size() > 0
                      Console.log.info(
                              "Ignoring additional clusterings for contact matrix");
                    }
--
                    String treeMethod = xmlmat.getTreeMethod();
                    double thresh = xmlmat.getCutHeight() != null
                            ? xmlmat.getCutHeight()
                            : 0;
 -                  newpae.restoreGroups(newgroups, treeMethod, nwk, thresh);
 +                  GroupSet grpset = new GroupSet();
 +                  grpset.restoreGroups(newgroups, treeMethod, nwk, thresh);
 +                  PAEContactMatrix newpae = new PAEContactMatrix(
 +                          jaa.sequenceRef, mapping, elements, grpset);
                    jaa.sequenceRef.addContactListFor(jaa, newpae);
                  }
                }
@@@ -35,12 -35,12 +35,10 @@@ public abstract class ContactMapRendere
       * shown when no data available from map
       */
      Color no_data;
--
      /**
       * shown for region not currently visible - should normally not see this
       */
      Color hidden;
--
      /**
       * linear shading scheme min/max
       */
          x++;
          continue;
        }
 +      // ContactListI from viewport can map column -> group
        Color gpcol = (cm == null) ? Color.white
 -              : cm.getColourForGroup(cm.getGroupsFor(column));
 +              : contacts.getColourForGroup(); // cm.getColourForGroup(cm.getGroupsFor(column));
        // feature still in development - highlight or omit regions hidden in
        // the alignment - currently marks them as red rows
        boolean maskHiddenCols = false;
 -      // TODO: pass visible column mask to the ContactGeometry object so it maps
 +      // TODO: optionally pass visible column mask to the ContactGeometry object
 +      // so it maps
        // only visible contacts to geometry
        // Bean holding mapping from contact list to pixels
 +      // TODO: allow bracketing/limiting of range on contacts to render (like
 +      // visible column mask but more flexible?)
 +
 +      // COntactListI provides mapping for column -> cm-groupmapping
        final ContactGeometry cgeom = new ContactGeometry(contacts,
                _aa.graphHeight);
  
        for (int ht = y2, eht = y2
                - _aa.graphHeight; ht >= eht; ht -= cgeom.pixels_step)
        {
          ContactGeometry.contactInterval ci = cgeom.mapFor(y2 - ht,
                  y2 - ht + cgeom.pixels_step);
          // cstart = (int) Math.floor(((double) y2 - ht) * contacts_per_pixel);
          boolean rowsel = false, containsHidden = false;
          if (columnSelection != null)
          {
 -          if (_aa.sequenceRef == null)
 -          {
 -            rowsel = columnSelection.intersects(ci.cStart, ci.cEnd);
 -          }
 -          else
 -          {
 -            // TODO check we have correctly mapped cstart to local sequence
 -            // numbering
 -            int s = _aa.sequenceRef.findIndex(ci.cStart);
 -            int e = _aa.sequenceRef.findIndex(ci.cEnd);
 -            if (maskHiddenCols && hasHiddenColumns)
 -            {
 -              // TODO: turn into function and create test !!
 -              Iterator<int[]> viscont = hiddenColumns
 -                      .getVisContigsIterator(s, e, false);
 -              containsHidden = !viscont.hasNext();
 -            }
 -            if (s > 0 && s < _aa.sequenceRef.getLength())
 -            {
 -              rowsel = columnSelection.intersects(s, e);
 -            }
 -
 -          }
 +          rowsel = cgeom.intersects(ci, columnSelection, hiddenColumns,
 +                  maskHiddenCols);
          }
          // TODO: show selected region
          if (colsel || rowsel)
                    (int) (((float) (col.getBlue() + gpcol.getBlue())) / 2f));
          }
          g.setColor(col);
--
          if (cgeom.pixels_step > 1)
          {
            g.fillRect(x * charWidth, ht, charWidth, 1 + cgeom.pixels_step);
@@@ -1,52 -1,53 +1,56 @@@
  package jalview.ws.datamodel.alphafold;
  
  import java.awt.Color;
 +import java.io.BufferedInputStream;
 +import java.io.File;
 +import java.io.FileInputStream;
 +import java.io.IOException;
  import java.util.ArrayList;
  import java.util.BitSet;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
 +import java.util.Map.Entry;
 +
 +import org.json.simple.JSONObject;
  
  import jalview.analysis.AverageDistanceEngine;
  import jalview.bin.Console;
 +import jalview.datamodel.Annotation;
  import jalview.datamodel.BinaryNode;
  import jalview.datamodel.ContactListI;
  import jalview.datamodel.ContactListImpl;
  import jalview.datamodel.ContactListProviderI;
  import jalview.datamodel.ContactMatrixI;
 +import jalview.datamodel.GroupSet;
 +import jalview.datamodel.GroupSetI;
 +import jalview.datamodel.Mapping;
 +import jalview.datamodel.SequenceDummy;
  import jalview.datamodel.SequenceI;
 +import jalview.io.DataSourceType;
 +import jalview.io.FileFormatException;
 +import jalview.io.FileParse;
 +import jalview.util.MapList;
  import jalview.util.MapUtils;
 +import jalview.ws.dbsources.EBIAlfaFold;
  
 -public class PAEContactMatrix implements ContactMatrixI
 +public class PAEContactMatrix extends
 +        MappableContactMatrix<PAEContactMatrix> implements ContactMatrixI
  {
 -  SequenceI refSeq = null;
 -
 -  /**
 -   * the length that refSeq is expected to be (excluding gaps, of course)
 -   */
 -  int length;
    int maxrow = 0, maxcol = 0;
  
 -  int[] indices1, indices2;
    float[][] elements;
  
    float maxscore;
  
 -  private void setRefSeq(SequenceI _refSeq)
 -  {
 -    refSeq = _refSeq;
 -    while (refSeq.getDatasetSequence() != null)
 -    {
 -      refSeq = refSeq.getDatasetSequence();
 -    }
 -    length = _refSeq.getEnd() - _refSeq.getStart() + 1;
 -  }
    @SuppressWarnings("unchecked")
    public PAEContactMatrix(SequenceI _refSeq, Map<String, Object> pae_obj)
 +          throws FileFormatException
    {
      setRefSeq(_refSeq);
      // convert the lists to primitive arrays and store
    }
  
    /**
 +   * new matrix with specific mapping to a reference sequence
 +   * 
 +   * @param newRefSeq
 +   * @param newFromMapList
 +   * @param elements2
 +   * @param grps2
 +   */
 +  public PAEContactMatrix(SequenceI newRefSeq, MapList newFromMapList,
 +          float[][] elements2, GroupSet grps2)
 +  {
 +    this(newRefSeq, elements2);
 +    toSeq = newFromMapList;
 +    grps = grps2;
 +  }
 +
 +  /**
     * parse a sane JSON representation of the pAE
     * 
     * @param pae_obj
    @SuppressWarnings("unchecked")
    private void parse_version_2_pAE(Map<String, Object> pae_obj)
    {
 -    // this is never going to be reached by the integer rounding.. or is it ?
 -    maxscore = ((Double) MapUtils.getFirst(pae_obj,
 -            "max_predicted_aligned_error", "max_pae")).floatValue();
 +    maxscore = -1;
 +    // look for a maxscore element - if there is one...
 +    try
 +    {
 +      // this is never going to be reached by the integer rounding.. or is it ?
 +      maxscore = ((Double) MapUtils.getFirst(pae_obj,
 +              "max_predicted_aligned_error", "max_pae")).floatValue();
 +    } catch (Throwable t)
 +    {
 +      // ignore if a key is not found.
 +    }
      List<List<Long>> scoreRows = ((List<List<Long>>) MapUtils
              .getFirst(pae_obj, "predicted_aligned_error", "pae"));
      elements = new float[scoreRows.size()][scoreRows.size()];
        while (scores.hasNext())
        {
          Object d = scores.next();
          if (d instanceof Double)
 +        {
            elements[row][col++] = ((Double) d).longValue();
 +        }
          else
 +        {
            elements[row][col++] = (float) ((Long) d).longValue();
 +        }
 +
 +        if (maxscore < elements[row][col - 1])
 +        {
 +          maxscore = elements[row][col - 1];
 +        }
        }
        row++;
        col = 0;
      // dataset refSeq
      Iterator<Long> rows = ((List<Long>) pae_obj.get("residue1")).iterator();
      Iterator<Long> cols = ((List<Long>) pae_obj.get("residue2")).iterator();
 +    // two pass - to allocate the elements array
 +    while (rows.hasNext())
 +    {
 +      int row = rows.next().intValue();
 +      int col = cols.next().intValue();
 +      if (maxrow < row)
 +      {
 +        maxrow = row;
 +      }
 +      if (maxcol < col)
 +      {
 +        maxcol = col;
 +      }
 +
 +    }
 +    rows = ((List<Long>) pae_obj.get("residue1")).iterator();
 +    cols = ((List<Long>) pae_obj.get("residue2")).iterator();
      Iterator<Double> scores = ((List<Double>) pae_obj.get("distance"))
              .iterator();
 -    // assume square matrix
 -    elements = new float[length][length];
 +    elements = new float[maxrow][maxcol];
      while (scores.hasNext())
      {
        float escore = scores.next().floatValue();
    }
  
    @Override
 -  public ContactListI getContactList(final int _column)
 +  public ContactListI getContactList(final int column)
    {
-     // final int _column;
-     // if (toSeq != null)
-     // {
-     // int[] word = toSeq.locateInTo(column, column);
-     // if (word == null)
-     // {
-     // return null;
-     // }
-     // _column = word[0];
-     // }
-     // else
-     // {
-     // _column = column;
-     // }
 -    if (_column < 0 || _column >= elements.length)
 +    if (column < 0 || column >= elements.length)
      {
        return null;
      }
        @Override
        public int getPosition()
        {
 -        return _column;
 +        return column;
        }
  
        @Override
        }
  
        @Override
 -      public double getContactAt(int column)
 +      public double getContactAt(int mcolumn)
        {
 -        if (column < 0 || column >= elements[_column].length)
 +        if (mcolumn < 0 || mcolumn >= elements[column].length)
          {
            return -1;
          }
 -        return elements[_column][column];
 +        return elements[column][mcolumn];
        }
      });
    }
  
    @Override
 -  public float getMin()
 +  protected double getElementAt(int _column, int i)
    {
 -    return 0;
 +    return elements[_column][i];
    }
  
    @Override
 -  public float getMax()
 -  {
 -    return maxscore;
 -  }
 -
 -  @Override
 -  public boolean hasReferenceSeq()
 +  public float getMin()
    {
 -    return (refSeq != null);
 +    return 0;
    }
  
    @Override
 -  public SequenceI getReferenceSeq()
 +  public float getMax()
    {
 -    return refSeq;
 +    return maxscore;
    }
  
    @Override
    {
      return length;
    }
--
 -  List<BitSet> groups = null;
 -
 -  @Override
 -  public boolean hasGroups()
 -  {
 -    return groups != null;
 -  }
 -
 -  String newick = null;
 -
 -  @Override
 -  public String getNewick()
 -  {
 -    return newick;
 -  }
 -
 -  @Override
 -  public boolean hasTree()
 -  {
 -    return newick != null && newick.length() > 0;
 -  }
 -
 -  boolean abs;
 -
 -  double thresh;
 -
 -  String treeType = null;
 -
 -  public void makeGroups(float thresh, boolean abs)
 +  public static void validateContactMatrixFile(String fileName)
 +          throws FileFormatException, IOException
    {
 -    AverageDistanceEngine clusterer = new AverageDistanceEngine(null, null,
 -            this);
 -    double height = clusterer.findHeight(clusterer.getTopNode());
 -    newick = new jalview.io.NewickFile(clusterer.getTopNode(), false, true)
 -            .print();
 -    treeType = "UPGMA";
 -    Console.trace("Newick string\n" + newick);
 -
 -    List<BinaryNode> nodegroups;
 -    if (abs ? height > thresh : 0 < thresh && thresh < 1)
 +    FileInputStream infile = null;
 +    try
      {
 -      float cut = abs ? (float) (thresh / height) : thresh;
 -      Console.debug("Threshold " + cut + " for height=" + height);
 -
 -      nodegroups = clusterer.groupNodes(cut);
 -    }
 -    else
 +      infile = new FileInputStream(new File(fileName));
 +    } catch (Throwable t)
      {
 -      nodegroups = new ArrayList<BinaryNode>();
 -      nodegroups.add(clusterer.getTopNode());
 +      new IOException("Couldn't open " + fileName, t);
      }
 -    this.abs = abs;
 -    this.thresh = thresh;
 -    groups = new ArrayList<>();
 -    for (BinaryNode root : nodegroups)
 +    JSONObject paeDict = null;
 +    try
      {
 -      BitSet gpset = new BitSet();
 -      for (BinaryNode leaf : clusterer.findLeaves(root))
 -      {
 -        gpset.set((Integer) leaf.element());
 -      }
 -      groups.add(gpset);
 -    }
 -  }
 -
 -  @Override
 -  public void updateGroups(List<BitSet> colGroups)
 -  {
 -    if (colGroups != null)
 +      paeDict = EBIAlfaFold.parseJSONtoPAEContactMatrix(infile);
 +    } catch (Throwable t)
      {
 -      groups = colGroups;
 +      new FileFormatException("Couldn't parse " + fileName
 +              + " as a JSON dict or array containing a dict");
      }
 -  }
  
 -  @Override
 -  public BitSet getGroupsFor(int column)
 -  {
 -    for (BitSet gp : groups)
 +    PAEContactMatrix matrix = new PAEContactMatrix(
 +            new SequenceDummy("Predicted"), (Map<String, Object>) paeDict);
 +    if (matrix.getWidth() <= 0)
      {
 -      if (gp.get(column))
 -      {
 -        return gp;
 -      }
 -    }
 -    return ContactMatrixI.super.getGroupsFor(column);
 -  }
 -
 -  HashMap<BitSet, Color> colorMap = new HashMap<>();
 -
 -  @Override
 -  public Color getColourForGroup(BitSet bs)
 -  {
 -    if (bs == null)
 -    {
 -      return Color.white;
 -    }
 -    Color groupCol = colorMap.get(bs);
 -    if (groupCol == null)
 -    {
 -      return Color.white;
 +      throw new FileFormatException(
 +              "No data in PAE matrix read from '" + fileName + "'");
      }
 -    return groupCol;
 -  }
 -
 -  @Override
 -  public void setColorForGroup(BitSet bs, Color color)
 -  {
 -    colorMap.put(bs, color);
    }
--
 -  public void restoreGroups(List<BitSet> newgroups, String treeMethod,
 -          String tree, double thresh2)
 -  {
 -    treeType = treeMethod;
 -    groups = newgroups;
 -    thresh = thresh2;
 -    newick = tree;
 -
 -  }
 -
 -  @Override
 -  public boolean hasCutHeight()
 -  {
 -    return groups != null && thresh != 0;
 -  }
 -
 -  @Override
 -  public double getCutHeight()
 -  {
 -    return thresh;
 -  }
 -
    @Override
 -  public String getTreeMethod()
 +  protected PAEContactMatrix newMappableContactMatrix(SequenceI newRefSeq,
 +          MapList newFromMapList)
    {
 -    return treeType;
 +    PAEContactMatrix pae = new PAEContactMatrix(newRefSeq, newFromMapList,
 +            elements, new GroupSet(grps));
 +    return pae;
    }
  }
@@@ -44,7 -44,6 +44,7 @@@ import jalview.datamodel.AlignmentAnnot
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.ContactMatrixI;
  import jalview.datamodel.DBRefEntry;
 +import jalview.datamodel.GroupSet;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceI;
@@@ -295,11 -294,12 +295,12 @@@ public class EBIAlfaFold extends EbiFil
            AlignmentI pdbAlignment, String retrievalUrl) throws IOException
    {
      File pae = fetchAlphaFoldPAE(id, retrievalUrl);
-     addAlphaFoldPAE(pdbAlignment, pae, 0, null, false, false);
+     addAlphaFoldPAE(pdbAlignment, pae, 0, null, false, false, null);
    }
  
    public static void addAlphaFoldPAE(AlignmentI pdbAlignment, File pae,
-           int index, String id, boolean isStruct, boolean isStructId)
+           int index, String id, boolean isStruct, boolean isStructId,
+           String label)
    {
      FileInputStream paeInput = null;
      try
  
      if (isStruct)
      {
+       // ###### WRITE A TEST for this bit of the logic addAlphaFoldPAE with
+       // different params.
        StructureSelectionManager ssm = StructureSelectionManager
                .getStructureSelectionManager(Desktop.instance);
        if (ssm != null)
        {
          String structFilename = isStructId ? ssm.findFileForPDBId(id) : id;
-         addPAEToStructure(ssm, structFilename, pae);
+         addPAEToStructure(ssm, structFilename, pae, label);
        }
  
      }
        try
        {
          if (!importPaeJSONAsContactMatrixToSequence(pdbAlignment, paeInput,
-                 index, id))
+                 index, id, label))
          {
            Console.warn("Could not import contact matrix from '"
                    + pae.getAbsolutePath() + "' to sequence.");
    }
  
    public static void addPAEToStructure(StructureSelectionManager ssm,
-           String structFilename, File pae)
+           String structFilename, File pae, String label)
    {
      FileInputStream paeInput = null;
      try
  
        try
        {
-         if (!importPaeJSONAsContactMatrixToStructure(smArray, paeInput))
+         if (!importPaeJSONAsContactMatrixToStructure(smArray, paeInput,
+                 label))
          {
            Console.warn("Could not import contact matrix from '"
                    + pae.getAbsolutePath() + "' to structure.");
     */
    public static boolean importPaeJSONAsContactMatrixToSequence(
            AlignmentI pdbAlignment, InputStream pae_input, int index,
-           String seqId) throws IOException, ParseException
+           String seqId, String label) throws IOException, ParseException
    {
      SequenceI sequence = null;
      if (seqId == null)
        return false;
      }
      return importPaeJSONAsContactMatrixToSequence(pdbAlignment, pae_input,
-             sequence);
+             sequence, label);
    }
  
    public static boolean importPaeJSONAsContactMatrixToSequence(
            AlignmentI pdbAlignment, InputStream pae_input,
-           SequenceI sequence) throws IOException, ParseException
+           SequenceI sequence, String label)
+           throws IOException, ParseException
    {
      JSONObject paeDict = parseJSONtoPAEContactMatrix(pae_input);
      if (paeDict == null)
      }
      ContactMatrixI matrix = new PAEContactMatrix(sequence,
              (Map<String, Object>) paeDict);
 -    ((PAEContactMatrix) matrix).makeGroups(5f, true);
 +    matrix.setGroupSet(GroupSet.makeGroups(matrix, 5f, true));
  
      AlignmentAnnotation cmannot = sequence.addContactList(matrix);
+     if (label != null)
+       cmannot.label = label;
      pdbAlignment.addAnnotation(cmannot);
  
      return true;
      return paeDict;
    }
  
+   // ###### TEST THIS
    public static boolean importPaeJSONAsContactMatrixToStructure(
-           StructureMapping[] smArray, InputStream paeInput)
+           StructureMapping[] smArray, InputStream paeInput, String label)
            throws IOException, ParseException
    {
      boolean someDone = false;
      for (StructureMapping sm : smArray)
      {
        boolean thisDone = importPaeJSONAsContactMatrixToStructure(sm,
-               paeInput);
+               paeInput, label);
        someDone |= thisDone;
      }
      return someDone;
    }
  
    public static boolean importPaeJSONAsContactMatrixToStructure(
-           StructureMapping sm, InputStream paeInput)
+           StructureMapping sm, InputStream paeInput, String label)
            throws IOException, ParseException
    {
      JSONObject pae_obj = parseJSONtoPAEContactMatrix(paeInput);
        return false;
      }
  
-     ContactMatrixI matrix = new PAEContactMatrix(sm.getSequence(),
+     SequenceI seq = sm.getSequence();
+     ContactMatrixI matrix = new PAEContactMatrix(seq,
              (Map<String, Object>) pae_obj);
 -    ((PAEContactMatrix) matrix).makeGroups(5f, true);
 -    AlignmentAnnotation cmannot = seq.addContactList(matrix);
 +    matrix.setGroupSet(GroupSet.makeGroups(matrix, 5f, true));
 +    AlignmentAnnotation cmannot = sm.getSequence().addContactList(matrix);
-     sm.getSequence().addAlignmentAnnotation(cmannot);
+     /* this already happens in Sequence.addContactList()
+      seq.addAlignmentAnnotation(cmannot);
+      */
      return true;
    }
  
@@@ -2,9 -2,9 +2,10 @@@
  // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 
  // See <a href="http://java.sun.com/xml/jaxb">http://java.sun.com/xml/jaxb</a> 
  // Any modifications to this file will be lost upon recompilation of the source schema. 
 -// Generated on: 2023.03.17 at 05:31:44 PM GMT 
 +// Generated on: 2023.05.13 at 06:58:41 PM BST 
  //
  
++
  package jalview.xml.binding.jalview;
  
  import java.math.BigInteger;
@@@ -16,6 -16,6 +17,7 @@@ import javax.xml.bind.annotation.XmlAtt
  import javax.xml.bind.annotation.XmlElement;
  import javax.xml.bind.annotation.XmlType;
  
++
  /**
   * <p>
   * Java class for MatrixType complex type.
@@@ -33,7 -33,6 +35,7 @@@
   *         &lt;element name="groups" type="{http://www.w3.org/2001/XMLSchema}string" maxOccurs="unbounded" minOccurs="0"/>
   *         &lt;element name="newick" type="{http://www.w3.org/2001/XMLSchema}string" maxOccurs="unbounded" minOccurs="0"/>
   *         &lt;element name="property" type="{www.vamsas.ac.uk/jalview/version2}property" maxOccurs="unbounded" minOccurs="0"/>
 + *         &lt;element name="mapping" type="{www.vamsas.ac.uk/jalview/version2}mapListType" minOccurs="0"/>
   *       &lt;/sequence>
   *       &lt;attribute name="type" use="required" type="{http://www.w3.org/2001/XMLSchema}string" />
   *       &lt;attribute name="rows" use="required" type="{http://www.w3.org/2001/XMLSchema}integer" />
@@@ -52,7 -51,7 +54,7 @@@
  @XmlType(
    name = "MatrixType",
    propOrder =
 -  { "elements", "groups", "newick", "property" })
 +  { "elements", "groups", "newick", "property", "mapping" })
  public class MatrixType
  {
  
@@@ -65,8 -64,6 +67,8 @@@
  
    protected List<Property> property;
  
 +  protected MapListType mapping;
 +
    @XmlAttribute(name = "type", required = true)
    protected String type;
  
    }
  
    /**
 +   * Gets the value of the mapping property.
 +   * 
 +   * @return possible object is {@link MapListType }
 +   * 
 +   */
 +  public MapListType getMapping()
 +  {
 +    return mapping;
 +  }
 +
 +  /**
 +   * Sets the value of the mapping property.
 +   * 
 +   * @param value
 +   *          allowed object is {@link MapListType }
 +   * 
 +   */
 +  public void setMapping(MapListType value)
 +  {
 +    this.mapping = value;
 +  }
 +
 +  /**
     * Gets the value of the type property.
     * 
     * @return possible object is {@link String }
@@@ -45,7 -45,7 +45,7 @@@ public class AverageDistanceEngineTes
      Cache.loadProperties("test/jalview/bin/TestProps.jvprops");
    }
  
 -  @Test
 +  @Test(groups = { "Functional" })
    public void testUPGMAEngine() throws Exception
    {
      AlignFrame af = new FileLoader(false).LoadFileWaitTillLoaded(
        }
        System.out.println("\\");
      }
--
    }
  
  }
@@@ -32,9 -32,7 +32,9 @@@ import java.awt.Color
  import java.awt.Rectangle;
  import java.io.File;
  import java.io.IOException;
 +import java.math.BigInteger;
  import java.util.ArrayList;
 +import java.util.BitSet;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Locale;
@@@ -61,7 -59,6 +61,7 @@@ import jalview.datamodel.ContactMatrix
  import jalview.datamodel.ContactMatrixI;
  import jalview.datamodel.DBRefEntry;
  import jalview.datamodel.GeneLocus;
 +import jalview.datamodel.GroupSet;
  import jalview.datamodel.HiddenSequences;
  import jalview.datamodel.Mapping;
  import jalview.datamodel.PDBEntry;
@@@ -103,7 -100,6 +103,7 @@@ import jalview.util.MapList
  import jalview.util.matcher.Condition;
  import jalview.viewmodel.AlignmentViewport;
  import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 +import jalview.ws.datamodel.MappableContactMatrixI;
  import jalview.ws.datamodel.alphafold.PAEContactMatrix;
  
  @Test(singleThreaded = true)
@@@ -114,6 -110,8 +114,8 @@@ public class Jalview2xmlTests extends J
    @BeforeClass(alwaysRun = true)
    public void setUpJvOptionPane()
    {
+     if (Desktop.instance != null)
+       Desktop.instance.closeAll_actionPerformed(null);
      JvOptionPane.setInteractiveMode(false);
      JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
    }
      float[][] vals = ContactMatrix.fromFloatStringToContacts(content,
              sq.getLength(), sq.getLength());
      assertEquals(vals[3][4], paevals[3][4]);
 -    dummyMat.makeGroups(0.5f, false);
 +    dummyMat.setGroupSet(GroupSet.makeGroups(dummyMat, 0.5f, false));
      Assert.assertNotSame(dummyMat.getNewick(), "");
      AlignmentAnnotation paeCm = sq.addContactList(dummyMat);
      al.addAnnotation(paeCm);
 +    // verify store/restore of group bitsets
 +    for (BitSet gp : dummyMat.getGroups())
 +    {
 +      StringBuilder sb = new StringBuilder();
 +      for (long val : gp.toLongArray())
 +      {
 +        if (sb.length() > 0)
 +        {
 +          sb.append(",");
 +        }
 +        sb.append(val);
 +      }
 +      String[] longvals = sb.toString().split(",");
 +      long[] newlongvals = new long[longvals.length];
 +      for (int lv = 0; lv < longvals.length; lv++)
 +      {
 +        try
 +        {
 +          newlongvals[lv] = Long.valueOf(longvals[lv]);
 +        } catch (Exception x)
 +        {
 +          Assert.fail("failed to deserialise bitset element ");
 +        }
 +      }
 +      BitSet newGp = BitSet.valueOf(newlongvals);
 +      assertTrue(gp.equals(newGp));
 +    }
      File tfile = File.createTempFile("testStoreAndRecoverPAEmatrix",
              ".jvp");
      new Jalview2XML(false).saveState(tfile);
      ContactMatrixI restoredMat = newSeq
              .getContactMatrixFor(newSeq.getAnnotation()[0]);
      Assert.assertNotNull(restoredMat);
 +    MapList oldMap = ((MappableContactMatrixI) dummyMat).getMapFor(sq);
 +    MapList newMap = ((MappableContactMatrixI) restoredMat)
 +            .getMapFor(newSeq);
 +    Assert.assertEquals(oldMap.getFromRanges(), newMap.getFromRanges());
 +    Assert.assertEquals(oldMap.getToRanges(), newMap.getToRanges());
 +    Assert.assertEquals(oldMap.getFromRatio(), newMap.getFromRatio());
 +    Assert.assertEquals(oldMap.getToRatio(), newMap.getToRatio());
      for (i = sq.getLength() - 1; i >= 0; i--)
      {
        ContactListI oldCM = dummyMat.getContactList(i),
                newCM = restoredMat.getContactList(i);
        for (int j = oldCM.getContactHeight(); j >= 0; j--)
        {
 -        Assert.assertEquals(oldCM.getContactAt(j), newCM.getContactAt(j));
 +        double old_j = oldCM.getContactAt(j);
 +        double new_j = newCM.getContactAt(j);
 +        Assert.assertEquals(old_j, new_j);
        }
      }
      Assert.assertEquals(restoredMat.hasGroups(), dummyMat.hasGroups());
      Assert.assertEquals(restoredMat.getGroups(), dummyMat.getGroups());
      Assert.assertEquals(restoredMat.hasTree(), dummyMat.hasTree());
      Assert.assertEquals(restoredMat.getNewick(), dummyMat.getNewick());
--
    }
  
  }