JAL-3081 sort annotations by order read from project after reloading
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Sun, 17 Nov 2019 13:38:27 +0000 (13:38 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Sun, 17 Nov 2019 13:38:27 +0000 (13:38 +0000)
src/jalview/analysis/AnnotationSorter.java
src/jalview/project/Jalview2XML.java
test/jalview/analysis/AnnotationSorterTest.java

index 83f3adf..2e04cb1 100644 (file)
@@ -28,6 +28,7 @@ import jalview.datamodel.SequenceI;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -40,6 +41,70 @@ import java.util.Map;
 public class AnnotationSorter
 {
   /**
+   * A special comparator for use when reloading from project, that supports
+   * reordering annotations to match their order in the project. It incorporates
+   * special handling of autocalculated annotations, that are recreated on load:
+   * <ul>
+   * <li>these are matched based on autocalc flag and label ("Consensus") etc
+   * rather than object identity</li>
+   * <li>on successful match, visibility and height settings are applied from
+   * those saved in the project</li>
+   * </ul>
+   */
+  private final class ReloadComparator
+          implements Comparator<AlignmentAnnotation>
+  {
+    private List<AlignmentAnnotation> order;
+
+    ReloadComparator(List<AlignmentAnnotation> theOrder)
+    {
+      order = theOrder;
+    }
+
+    @Override
+    public int compare(AlignmentAnnotation o1,
+            AlignmentAnnotation o2)
+    {
+      int i1 = findPosition(o1);
+      int i2 = findPosition(o2);
+      return Integer.compare(i1, i2);
+    }
+
+    /**
+     * A helper method that returns the position of the annotation in the
+     * {@code order} list, by object identity, or failing that, by matched
+     * autocalc label ("Consensus" etc). Returns -1 if not found. This can
+     * happen if, for example, Consensus was saved but since turned off in
+     * Preferences.
+     * 
+     * @param aa
+     * @return
+     */
+    private int findPosition(AlignmentAnnotation aa)
+    {
+      int i = order.indexOf(aa);
+      if (i == -1 && aa.autoCalculated && aa.label != null)
+      {
+        for (int j = 0; j < order.size(); j++)
+        {
+          AlignmentAnnotation ann = order.get(j);
+          if (aa.label.equals(ann.label))
+          {
+            i = j;
+            aa.visible = ann.visible;
+            if (ann.graphHeight >= 0)
+            {
+              aa.graphHeight = ann.graphHeight;
+            }
+            break;
+          }
+        }
+      }
+      return i;
+    }
+  }
+
+  /**
    * enum for annotation sort options. The text description is used in the
    * Preferences drop-down options. The enum name is saved in the preferences
    * file.
@@ -452,4 +517,27 @@ public class AnnotationSorter
     }
     return Integer.compare(index1.intValue(), index2.intValue());
   }
+
+  /**
+   * Sort annotations to match the order of the provided list. This is intended
+   * only for use when reloading a project, in order to set the saved order
+   * after constructing a viewport (which might sort differently based on user
+   * preferences for sort order).
+   * <p>
+   * There is some special handling specific to auto-calculated annotations.
+   * These are saved in the project, to preserve their visibility and height
+   * properties, but are recalculated when the viewport is reconstructed.
+   * <ul>
+   * <li>Autocalculated annotations are matched to the list by label
+   * "Consensus/Quality/Conservation/Occupancy"</li>
+   * <li>those that are matched have their visibility and height set</li>
+   * </ul>
+   * 
+   * @param addedAnnotation
+   */
+  public void sort(List<AlignmentAnnotation> annotations)
+  {
+    Arrays.sort(alignment.getAlignmentAnnotation(),
+            new ReloadComparator(annotations));
+  }
 }
index c017d11..eaabd47 100644 (file)
@@ -24,6 +24,7 @@ import static jalview.math.RotatableMatrix.Axis.X;
 import static jalview.math.RotatableMatrix.Axis.Y;
 import static jalview.math.RotatableMatrix.Axis.Z;
 
+import jalview.analysis.AnnotationSorter;
 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 import jalview.analysis.Conservation;
 import jalview.analysis.PCA;
@@ -3622,7 +3623,7 @@ public class Jalview2XML
 
     // ////////////////////////////////
     // LOAD ANNOTATIONS
-    List<JvAnnotRow> autoAlan = new ArrayList<>();
+    List<AlignmentAnnotation> addedAnnotation = new ArrayList<>();
 
     /*
      * store any annotations which forward reference a group's ID
@@ -3713,7 +3714,11 @@ public class Jalview2XML
             }
           }
         }
-        jalview.datamodel.AlignmentAnnotation jaa = null;
+
+        /*
+         * construct the Jalview AlignmentAnnotation, add to alignment
+         */
+        AlignmentAnnotation jaa = null;
 
         if (annotation.isGraph())
         {
@@ -3811,9 +3816,10 @@ public class Jalview2XML
           jaa.autoCalculated = true; // means annotation will be marked for
           // update at end of load.
         }
-        if (annotation.getGraphHeight() != null)
+        Integer graphHeight = annotation.getGraphHeight();
+        if (graphHeight != null)
         {
-          jaa.graphHeight = annotation.getGraphHeight().intValue();
+          jaa.graphHeight = graphHeight.intValue();
         }
         jaa.belowAlignment = annotation.isBelowAlignment();
         jaa.setCalcId(annotation.getCalcId());
@@ -3825,17 +3831,16 @@ public class Jalview2XML
             jaa.setProperty(prop.getName(), prop.getValue());
           }
         }
-        if (jaa.autoCalculated)
-        {
-          autoAlan.add(new JvAnnotRow(i, jaa));
-        }
-        else
-        // if (!autoForView)
+        if (!jaa.autoCalculated)
         {
-          // add autocalculated group annotation and any user created annotation
-          // for the view
+          // TODO ensure Consensus etc is enabled if found in project?
+          /*
+           * add autocalculated group annotation and any user created annotation
+           * for the view
+           */
           al.addAnnotation(jaa);
         }
+        addedAnnotation.add(jaa);
       }
     }
     // ///////////////////////
@@ -4028,10 +4033,16 @@ public class Jalview2XML
     if (isnewview)
     {
       af = loadViewport(file, jseqs, hiddenSeqs, al, jalviewModel, view,
-              uniqueSeqSetId, viewId, autoAlan);
-      // TODO resort annotations here to their order in the project?
+              uniqueSeqSetId, viewId);
+      // TODO restore autocalc preferences if overridden earlier?
+      /*
+       * resort annotations to their order in the project
+       * (also sets height and visibility for autocalc'd annotation)
+       */
       av = af.getViewport();
+      new AnnotationSorter(av).sort(addedAnnotation);
       ap = af.alignPanel;
+      ap.adjustAnnotationHeight();
     }
 
     /*
@@ -4839,7 +4850,7 @@ public class Jalview2XML
   AlignFrame loadViewport(String file, List<JSeq> JSEQ,
           List<SequenceI> hiddenSeqs, AlignmentI al,
           JalviewModel jm, Viewport view, String uniqueSeqSetId,
-          String viewId, List<JvAnnotRow> autoAlan)
+          String viewId)
   {
     AlignFrame af = null;
     af = new AlignFrame(al, safeInt(view.getWidth()),
@@ -5196,8 +5207,6 @@ public class Jalview2XML
               safeInt(view.getWidth()), safeInt(view.getHeight()));
       // recompute any autoannotation
       af.alignPanel.updateAnnotation(false, true);
-      reorderAutoannotation(af, al, autoAlan);
-      af.sortAnnotations(false);
       af.alignPanel.alignmentChanged();
     }
     else
@@ -5324,106 +5333,6 @@ public class Jalview2XML
     return cs;
   }
 
-  private void reorderAutoannotation(AlignFrame af, AlignmentI al,
-          List<JvAnnotRow> autoAlan)
-  {
-    // copy over visualization settings for autocalculated annotation in the
-    // view
-    if (al.getAlignmentAnnotation() != null)
-    {
-      /**
-       * Kludge for magic autoannotation names (see JAL-811)
-       */
-      String[] magicNames = new String[] { "Consensus", "Quality",
-          "Conservation" };
-      JvAnnotRow nullAnnot = new JvAnnotRow(-1, null);
-      Hashtable<String, JvAnnotRow> visan = new Hashtable<>();
-      for (String nm : magicNames)
-      {
-        visan.put(nm, nullAnnot);
-      }
-      for (JvAnnotRow auan : autoAlan)
-      {
-        visan.put(auan.template.label
-                + (auan.template.getCalcId() == null ? ""
-                        : "\t" + auan.template.getCalcId()),
-                auan);
-      }
-      int hSize = al.getAlignmentAnnotation().length;
-      List<JvAnnotRow> reorder = new ArrayList<>();
-      // work through any autoCalculated annotation already on the view
-      // removing it if it should be placed in a different location on the
-      // annotation panel.
-      List<String> remains = new ArrayList<>(visan.keySet());
-      for (int h = 0; h < hSize; h++)
-      {
-        jalview.datamodel.AlignmentAnnotation jalan = al
-                .getAlignmentAnnotation()[h];
-        if (jalan.autoCalculated)
-        {
-          String k;
-          JvAnnotRow valan = visan.get(k = jalan.label);
-          if (jalan.getCalcId() != null)
-          {
-            valan = visan.get(k = jalan.label + "\t" + jalan.getCalcId());
-          }
-
-          if (valan != null)
-          {
-            // delete the auto calculated row from the alignment
-            al.deleteAnnotation(jalan, false);
-            remains.remove(k);
-            hSize--;
-            h--;
-            if (valan != nullAnnot)
-            {
-              if (jalan != valan.template)
-              {
-                // newly created autoannotation row instance
-                // so keep a reference to the visible annotation row
-                // and copy over all relevant attributes
-                if (valan.template.graphHeight >= 0)
-
-                {
-                  jalan.graphHeight = valan.template.graphHeight;
-                }
-                jalan.visible = valan.template.visible;
-              }
-              reorder.add(new JvAnnotRow(valan.order, jalan));
-            }
-          }
-        }
-      }
-      // Add any (possibly stale) autocalculated rows that were not appended to
-      // the view during construction
-      for (String other : remains)
-      {
-        JvAnnotRow othera = visan.get(other);
-        if (othera != nullAnnot && othera.template.getCalcId() != null
-                && othera.template.getCalcId().length() > 0)
-        {
-          reorder.add(othera);
-        }
-      }
-      // now put the automatic annotation in its correct place
-      int s = 0, srt[] = new int[reorder.size()];
-      JvAnnotRow[] rws = new JvAnnotRow[reorder.size()];
-      for (JvAnnotRow jvar : reorder)
-      {
-        rws[s] = jvar;
-        srt[s++] = jvar.order;
-      }
-      reorder.clear();
-      jalview.util.QuickSort.sort(srt, rws);
-      // and re-insert the annotation at its correct position
-      for (JvAnnotRow jvar : rws)
-      {
-        al.addAnnotation(jvar.template, jvar.order);
-      }
-      af.alignPanel.adjustAnnotationHeight();
-    }
-  }
-
   Hashtable skipList = null;
 
   /**
index de57b1b..fa8af3d 100644 (file)
@@ -33,6 +33,8 @@ import jalview.datamodel.SequenceI;
 import jalview.gui.AlignViewport;
 import jalview.gui.JvOptionPane;
 
+import java.util.Arrays;
+import java.util.List;
 import java.util.Random;
 
 import org.testng.annotations.BeforeClass;
@@ -138,6 +140,7 @@ public class AnnotationSorterTest
     av.setShowAutocalculatedAbove(false);
     AnnotationSorter testee = new AnnotationSorter(av);
     testee.sort(SequenceAnnotationOrder.SEQUENCE_AND_LABEL, false);
+    anns = al.getAlignmentAnnotation();
     assertEquals("label5", anns[0].label); // for sequence 0
     assertEquals("label0", anns[1].label); // for sequence 1
     assertEquals("iron", anns[2].label); // sequence 3 /iron
@@ -169,6 +172,7 @@ public class AnnotationSorterTest
     av.setShowAutocalculatedAbove(true);
     AnnotationSorter testee = new AnnotationSorter(av);
     testee.sort(SequenceAnnotationOrder.SEQUENCE_AND_LABEL, false);
+    anns = al.getAlignmentAnnotation();
     assertEquals("Quality", anns[0].label); // autocalc annotations
     assertEquals("Consensus", anns[1].label); // retain ordering
     assertEquals("label5", anns[2].label); // for sequence 0
@@ -209,6 +213,7 @@ public class AnnotationSorterTest
     av.setShowAutocalculatedAbove(false);
     AnnotationSorter testee = new AnnotationSorter(av);
     testee.sort(SequenceAnnotationOrder.LABEL_AND_SEQUENCE, false);
+    anns = al.getAlignmentAnnotation();
     assertEquals("IRON", anns[0].label); // IRON / sequence 0
     assertEquals("iron", anns[1].label); // iron / sequence 3
     assertEquals("label0", anns[2].label); // label0 / sequence 1
@@ -240,6 +245,7 @@ public class AnnotationSorterTest
     av.setShowAutocalculatedAbove(true);
     AnnotationSorter testee = new AnnotationSorter(av);
     testee.sort(SequenceAnnotationOrder.LABEL_AND_SEQUENCE, false);
+    anns = al.getAlignmentAnnotation();
     assertEquals("Quality", anns[0].label); // autocalc annotations
     assertEquals("Consensus", anns[1].label); // retain ordering
     assertEquals("IRON", anns[2].label); // IRON / sequence 0
@@ -272,6 +278,7 @@ public class AnnotationSorterTest
     av.setShowAutocalculatedAbove(true);
     AnnotationSorter testee = new AnnotationSorter(av);
     testee.sort(SequenceAnnotationOrder.NONE, false);
+    anns = al.getAlignmentAnnotation();
     assertEquals("Quality", anns[0].label); // autocalc annotations
     assertEquals("Consensus", anns[1].label); // retain ordering
     assertEquals("label0", anns[2].label);
@@ -468,6 +475,7 @@ public class AnnotationSorterTest
     av.setShowAutocalculatedAbove(true);
     AnnotationSorter testee = new AnnotationSorter(av);
     testee.sort(SequenceAnnotationOrder.CUSTOM, false);
+    anns = al.getAlignmentAnnotation();
     assertEquals("label0", anns[0].label); // all unchanged
     assertEquals("structure", anns[1].label);
     assertEquals("iron", anns[2].label);
@@ -502,6 +510,7 @@ public class AnnotationSorterTest
     av.setShowAutocalculatedAbove(true);
     AnnotationSorter testee = new AnnotationSorter(av);
     testee.sort(SequenceAnnotationOrder.LABEL_AND_SEQUENCE, true);
+    anns = al.getAlignmentAnnotation();
     assertEquals("Quality", anns[0].label); // moved to top
     assertEquals("Consensus", anns[1].label); // moved to top
     assertEquals("label0", anns[2].label); // the rest unchanged
@@ -524,4 +533,42 @@ public class AnnotationSorterTest
     assertEquals("Quality", anns[5].label); // moved to bottom
     assertEquals("Consensus", anns[6].label); // moved to bottom
   }
+
+  /**
+   * Test sorting by annotation order
+   */
+  @Test(groups = { "Functional" })
+  public void testSortByAnnotation()
+  {
+    AlignmentI al = av.getAlignment();
+    AlignmentAnnotation[] anns = al.getAlignmentAnnotation();
+  
+    // @formatter:off
+    anns[0].sequenceRef = al.getSequenceAt(1); anns[0].label = "label0";
+    anns[1].sequenceRef = al.getSequenceAt(3); anns[1].label = "structure";
+    anns[2].sequenceRef = al.getSequenceAt(3); anns[2].label = "iron";
+    anns[3].autoCalculated = true;             anns[3].label = "Quality";
+    anns[4].autoCalculated = true;             anns[4].label = "Consensus";
+    anns[5].sequenceRef = al.getSequenceAt(0); anns[5].label = "label5";
+    anns[6].sequenceRef = al.getSequenceAt(3); anns[6].label = "IRP";
+    // @formatter:on
+  
+    av.setShowAutocalculatedAbove(false);
+    AnnotationSorter testee = new AnnotationSorter(av);
+    List<AlignmentAnnotation> reorder = Arrays.asList(anns[2], anns[6],
+            anns[3], anns[0], anns[5], anns[1], anns[4]);
+    testee.sort(reorder);
+
+    /*
+     * should now be ordered as specified by the list
+     */
+    anns = al.getAlignmentAnnotation();
+    assertEquals("iron", anns[0].label);
+    assertEquals("IRP", anns[1].label);
+    assertEquals("Quality", anns[2].label);
+    assertEquals("label0", anns[3].label);
+    assertEquals("label5", anns[4].label);
+    assertEquals("structure", anns[5].label);
+    assertEquals("Consensus", anns[6].label);
+  }
 }