3253-omnibus save JAL-3253-applet-SwingJS-omnibus
authorBobHanson <hansonr@stolaf.edu>
Thu, 9 Apr 2020 17:53:04 +0000 (12:53 -0500)
committerBobHanson <hansonr@stolaf.edu>
Thu, 9 Apr 2020 17:53:04 +0000 (12:53 -0500)
83 files changed:
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/CrossRef.java
src/jalview/analysis/scoremodels/FeatureDistanceModel.java
src/jalview/api/AlignViewControllerGuiI.java
src/jalview/api/AlignViewportI.java
src/jalview/api/FeatureRenderer.java
src/jalview/api/SplitContainerI.java
src/jalview/api/ViewStyleI.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/AlignViewport.java
src/jalview/appletgui/SeqPanel.java
src/jalview/bin/Jalview.java
src/jalview/bin/JalviewAppLoader.java
src/jalview/controller/AlignViewController.java
src/jalview/controller/FeatureSettingsControllerGuiI.java
src/jalview/datamodel/DBRefEntry.java
src/jalview/datamodel/GeneLociI.java
src/jalview/datamodel/GeneLocus.java [new file with mode: 0644]
src/jalview/datamodel/MappedFeatures.java [new file with mode: 0644]
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/ext/ensembl/EnsemblCdna.java
src/jalview/ext/ensembl/EnsemblFeatures.java
src/jalview/ext/ensembl/EnsemblGene.java
src/jalview/ext/ensembl/EnsemblInfo.java
src/jalview/ext/ensembl/EnsemblLookup.java
src/jalview/ext/ensembl/EnsemblMap.java
src/jalview/ext/ensembl/EnsemblProtein.java
src/jalview/ext/ensembl/EnsemblRestClient.java
src/jalview/ext/ensembl/EnsemblSeqProxy.java
src/jalview/ext/ensembl/EnsemblSequenceFetcher.java
src/jalview/ext/htsjdk/VCFReader.java
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/ext/paradise/Annotate3D.java
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/ext/so/SequenceOntology.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AnnotationExporter.java
src/jalview/gui/CrossRefAction.java
src/jalview/gui/Desktop.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/IdPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/SeqPanel.java
src/jalview/gui/SplashScreen.java
src/jalview/gui/SplitFrame.java
src/jalview/gui/VamsasApplication.java
src/jalview/io/FeaturesFile.java
src/jalview/io/FileLoader.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/gff/Gff2Helper.java
src/jalview/io/gff/Gff3Helper.java
src/jalview/io/gff/GffHelperBase.java
src/jalview/io/gff/GffHelperI.java
src/jalview/io/gff/SequenceOntologyLite.java
src/jalview/io/vcf/VCFLoader.java
src/jalview/project/Jalview2XML.java
src/jalview/structure/SequenceListener.java
src/jalview/structure/StructureSelectionManager.java
src/jalview/util/MapList.java
src/jalview/util/Platform.java
src/jalview/util/StringUtils.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
src/jalview/viewmodel/styles/ViewStyle.java
src/jalview/ws/DBRefFetcher.java
src/jalview/ws/dbsources/EmblXmlSource.java
swingjs/SwingJS-site.zip
swingjs/net.sf.j2s.core-j11.jar
swingjs/timestamp
swingjs/ver/3.2.9/SwingJS-site.zip
swingjs/ver/3.2.9/net.sf.j2s.core-j11.jar
swingjs/ver/3.2.9/timestamp
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/datamodel/SequenceFeatureTest.java
test/jalview/ext/ensembl/EnsemblSeqProxyTest.java
test/jalview/ext/jmol/JmolViewerTest.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/io/FeaturesFileTest.java
test/jalview/io/FileLoaderTest.java
test/jalview/io/SequenceAnnotationReportTest.java
test/jalview/structure/Mapping.java

index 2946ba2..0c40873 100644 (file)
@@ -20,8 +20,7 @@
  */
 package jalview.analysis;
 
-import static jalview.io.gff.GffConstants.CLINICAL_SIGNIFICANCE;
-
+import jalview.commands.RemoveGapColCommand;
 import jalview.datamodel.AlignedCodon;
 import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignedCodonFrame.SequenceToSequenceMapping;
@@ -37,7 +36,6 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.SequenceFeatures;
-import jalview.io.gff.Gff3Helper;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.schemes.ResidueProperties;
 import jalview.util.Comparison;
@@ -45,10 +43,7 @@ import jalview.util.DBRefUtils;
 import jalview.util.IntRangeComparator;
 import jalview.util.MapList;
 import jalview.util.MappingUtils;
-import jalview.util.StringUtils;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -1607,7 +1602,7 @@ public class AlignmentUtils
     {
       for (int ix = 0, nx = xrefs.size(); ix < nx; ix++)
       {
-         DBRefEntry xref = xrefs.get(ix);
+        DBRefEntry xref = xrefs.get(ix);
         String xrefName = xref.getSource() + "|" + xref.getAccessionId();
         // case-insensitive test, consistent with DBRefEntry.equalRef()
         if (xrefName.equalsIgnoreCase(name))
@@ -1737,31 +1732,36 @@ public class AlignmentUtils
 
           cdsSeqs.add(cdsSeq);
 
-          if (!dataset.getSequences().contains(cdsSeqDss))
-          {
-            // check if this sequence is a newly created one
-            // so needs adding to the dataset
-            dataset.addSequence(cdsSeqDss);
-          }
-
           /*
-           * add a mapping from CDS to the (unchanged) mapped to range
+           * build the mapping from CDS to protein
            */
-          List<int[]> cdsRange = Collections.singletonList(new int[] { 1,
-              cdsSeq.getLength() });
+          List<int[]> cdsRange = Collections
+                  .singletonList(new int[]
+                  { cdsSeq.getStart(),
+                      cdsSeq.getLength() + cdsSeq.getStart() - 1 });
           MapList cdsToProteinMap = new MapList(cdsRange,
                   mapList.getToRanges(), mapList.getFromRatio(),
                   mapList.getToRatio());
-          AlignedCodonFrame cdsToProteinMapping = new AlignedCodonFrame();
-          cdsToProteinMapping.addMap(cdsSeqDss, proteinProduct,
-                  cdsToProteinMap);
 
-          /*
-           * guard against duplicating the mapping if repeating this action
-           */
-          if (!mappings.contains(cdsToProteinMapping))
+          if (!dataset.getSequences().contains(cdsSeqDss))
           {
-            mappings.add(cdsToProteinMapping);
+            /*
+             * if this sequence is a newly created one, add it to the dataset
+             * and made a CDS to protein mapping (if sequence already exists,
+             * CDS-to-protein mapping _is_ the transcript-to-protein mapping)
+             */
+            dataset.addSequence(cdsSeqDss);
+            AlignedCodonFrame cdsToProteinMapping = new AlignedCodonFrame();
+            cdsToProteinMapping.addMap(cdsSeqDss, proteinProduct,
+                  cdsToProteinMap);
+
+            /*
+             * guard against duplicating the mapping if repeating this action
+             */
+            if (!mappings.contains(cdsToProteinMapping))
+            {
+              mappings.add(cdsToProteinMapping);
+            }
           }
 
           propagateDBRefsToCDS(cdsSeqDss, dnaSeq.getDatasetSequence(),
@@ -1871,7 +1871,7 @@ public class AlignmentUtils
       return;
     }
 
-    MapList newMap = targetToFrom.traverse(fromLoci.getMap());
+    MapList newMap = targetToFrom.traverse(fromLoci.getMapping());
 
     if (newMap != null)
     {
@@ -1985,39 +1985,61 @@ public class AlignmentUtils
   static SequenceI makeCdsSequence(SequenceI seq, Mapping mapping,
           AlignmentI dataset)
   {
-    char[] seqChars = seq.getSequence();
-    List<int[]> fromRanges = mapping.getMap().getFromRanges();
-    int cdsWidth = MappingUtils.getLength(fromRanges);
-    char[] newSeqChars = new char[cdsWidth];
+    /*
+     * construct CDS sequence name as "CDS|" with 'from id' held in the mapping
+     * if set (e.g. EMBL protein_id), else sequence name appended
+     */
+    String mapFromId = mapping.getMappedFromId();
+    final String seqId = "CDS|"
+            + (mapFromId != null ? mapFromId : seq.getName());
+
+    SequenceI newSeq = null;
 
-    int newPos = 0;
-    for (int[] range : fromRanges)
+    final MapList maplist = mapping.getMap();
+    if (maplist.isContiguous() && maplist.isFromForwardStrand())
     {
-      if (range[0] <= range[1])
-      {
-        // forward strand mapping - just copy the range
-        int length = range[1] - range[0] + 1;
-        System.arraycopy(seqChars, range[0] - 1, newSeqChars, newPos,
-                length);
-        newPos += length;
-      }
-      else
+      /*
+       * just a subsequence, keep same dataset sequence
+       */
+      int start = maplist.getFromLowest();
+      int end = maplist.getFromHighest();
+      newSeq = seq.getSubSequence(start - 1, end);
+      newSeq.setName(seqId);
+    }
+    else
+    {
+      /*
+       * construct by splicing mapped from ranges
+       */
+      char[] seqChars = seq.getSequence();
+      List<int[]> fromRanges = maplist.getFromRanges();
+      int cdsWidth = MappingUtils.getLength(fromRanges);
+      char[] newSeqChars = new char[cdsWidth];
+
+      int newPos = 0;
+      for (int[] range : fromRanges)
       {
-        // reverse strand mapping - copy and complement one by one
-        for (int i = range[0]; i >= range[1]; i--)
+        if (range[0] <= range[1])
         {
-          newSeqChars[newPos++] = Dna.getComplement(seqChars[i - 1]);
+          // forward strand mapping - just copy the range
+          int length = range[1] - range[0] + 1;
+          System.arraycopy(seqChars, range[0] - 1, newSeqChars, newPos,
+                  length);
+          newPos += length;
+        }
+        else
+        {
+          // reverse strand mapping - copy and complement one by one
+          for (int i = range[0]; i >= range[1]; i--)
+          {
+            newSeqChars[newPos++] = Dna.getComplement(seqChars[i - 1]);
+          }
         }
       }
+
+      newSeq = new Sequence(seqId, newSeqChars, 1, newPos);
     }
 
-    /*
-     * assign 'from id' held in the mapping if set (e.g. EMBL protein_id),
-     * else generate a sequence name
-     */
-    String mapFromId = mapping.getMappedFromId();
-    String seqId = "CDS|" + (mapFromId != null ? mapFromId : seq.getName());
-    SequenceI newSeq = new Sequence(seqId, newSeqChars, 1, newPos);
     if (dataset != null)
     {
       SequenceI[] matches = dataset.findSequenceMatch(newSeq.getName());
@@ -2080,8 +2102,8 @@ public class AlignmentUtils
     {
       for (int ib = 0, nb = refs.size(); ib < nb; ib++)
       {
-         DBRefEntry dbr = refs.get(ib);
-         MapList map;
+        DBRefEntry dbr = refs.get(ib);
+        MapList map;
         if (dbr.hasMap() && (map = dbr.getMap().getMap()).isTripletMap())
         {
           // check if map is the CDS mapping
@@ -2101,14 +2123,14 @@ public class AlignmentUtils
     // and generate appropriate mappings
     for (int ic = 0, nc = direct.size(); ic < nc; ic++)
     {
-       DBRefEntry cdsref = direct.get(ic);
-       Mapping m = cdsref.getMap();
+      DBRefEntry cdsref = direct.get(ic);
+      Mapping m = cdsref.getMap();
       // clone maplist and mapping
       MapList cdsposmap = new MapList(
               Arrays.asList(new int[][]
               { new int[] { cdsSeq.getStart(), cdsSeq.getEnd() } }),
               m.getMap().getToRanges(), 3, 1);
-      Mapping cdsmap = new Mapping(m.getTo(),m.getMap());
+      Mapping cdsmap = new Mapping(m.getTo(), m.getMap());
 
       // create dbref
       DBRefEntry newref = new DBRefEntry(cdsref.getSource(),
@@ -2163,6 +2185,10 @@ public class AlignmentUtils
     {
       copyTo = copyTo.getDatasetSequence();
     }
+    if (fromSeq == copyTo || fromSeq.getDatasetSequence() == copyTo)
+    {
+      return 0; // shared dataset sequence
+    }
 
     /*
      * get features, optionally restricted by an ontology term
@@ -2334,7 +2360,7 @@ public class AlignmentUtils
        }
       } catch (NumberFormatException e)
       {
-        // SwingJS -- need to avoid these.
+        // leave as zero
       }
       /*
        * phase > 0 on first codon means 5' incomplete - skip to the start
@@ -2368,393 +2394,6 @@ public class AlignmentUtils
   }
 
   /**
-   * Maps exon features from dna to protein, and computes variants in peptide
-   * product generated by variants in dna, and adds them as sequence_variant
-   * features on the protein sequence. Returns the number of variant features
-   * added.
-   * 
-   * @param dnaSeq
-   * @param peptide
-   * @param dnaToProtein
-   */
-  public static int computeProteinFeatures(SequenceI dnaSeq,
-          SequenceI peptide, MapList dnaToProtein)
-  {
-    while (dnaSeq.getDatasetSequence() != null)
-    {
-      dnaSeq = dnaSeq.getDatasetSequence();
-    }
-    while (peptide.getDatasetSequence() != null)
-    {
-      peptide = peptide.getDatasetSequence();
-    }
-
-    transferFeatures(dnaSeq, peptide, dnaToProtein, SequenceOntologyI.EXON);
-
-    /*
-     * compute protein variants from dna variants and codon mappings;
-     * NB - alternatively we could retrieve this using the REST service e.g.
-     * http://rest.ensembl.org/overlap/translation
-     * /ENSP00000288602?feature=transcript_variation;content-type=text/xml
-     * which would be a bit slower but possibly more reliable
-     */
-
-    /*
-     * build a map with codon variations for each potentially varying peptide
-     */
-    LinkedHashMap<Integer, List<DnaVariant>[]> variants = buildDnaVariantsMap(
-            dnaSeq, dnaToProtein);
-
-    /*
-     * scan codon variations, compute peptide variants and add to peptide sequence
-     */
-    int count = 0;
-    for (Entry<Integer, List<DnaVariant>[]> variant : variants.entrySet())
-    {
-      int peptidePos = variant.getKey();
-      List<DnaVariant>[] codonVariants = variant.getValue();
-      count += computePeptideVariants(peptide, peptidePos, codonVariants);
-    }
-
-    return count;
-  }
-
-  /**
-   * Computes non-synonymous peptide variants from codon variants and adds them
-   * as sequence_variant features on the protein sequence (one feature per
-   * allele variant). Selected attributes (variant id, clinical significance)
-   * are copied over to the new features.
-   * 
-   * @param peptide
-   *          the protein sequence
-   * @param peptidePos
-   *          the position to compute peptide variants for
-   * @param codonVariants
-   *          a list of dna variants per codon position
-   * @return the number of features added
-   */
-  static int computePeptideVariants(SequenceI peptide, int peptidePos,
-          List<DnaVariant>[] codonVariants)
-  {
-    String residue = String.valueOf(peptide.getCharAt(peptidePos - 1));
-    int count = 0;
-    String base1 = codonVariants[0].get(0).base;
-    String base2 = codonVariants[1].get(0).base;
-    String base3 = codonVariants[2].get(0).base;
-
-    /*
-     * variants in first codon base
-     */
-    for (DnaVariant dnavar : codonVariants[0])
-    {
-      if (dnavar.variant != null)
-      {
-        String alleles = (String) dnavar.variant.getValue(Gff3Helper.ALLELES);
-        if (alleles != null)
-        {
-          for (String base : alleles.split(","))
-          {
-            if (!base1.equalsIgnoreCase(base))
-            {
-              String codon = base.toUpperCase() + base2.toLowerCase()
-                      + base3.toLowerCase();
-              String canonical = base1.toUpperCase() + base2.toLowerCase()
-                      + base3.toLowerCase();
-              if (addPeptideVariant(peptide, peptidePos, residue, dnavar,
-                      codon, canonical))
-              {
-                count++;
-              }
-            }
-          }
-        }
-      }
-    }
-
-    /*
-     * variants in second codon base
-     */
-    for (DnaVariant var : codonVariants[1])
-    {
-      if (var.variant != null)
-      {
-        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
-        if (alleles != null)
-        {
-          for (String base : alleles.split(","))
-          {
-            if (!base2.equalsIgnoreCase(base))
-            {
-              String codon = base1.toLowerCase() + base.toUpperCase()
-                      + base3.toLowerCase();
-              String canonical = base1.toLowerCase() + base2.toUpperCase()
-                      + base3.toLowerCase();
-              if (addPeptideVariant(peptide, peptidePos, residue, var,
-                      codon, canonical))
-              {
-                count++;
-              }
-            }
-          }
-        }
-      }
-    }
-
-    /*
-     * variants in third codon base
-     */
-    for (DnaVariant var : codonVariants[2])
-    {
-      if (var.variant != null)
-      {
-        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
-        if (alleles != null)
-        {
-          for (String base : alleles.split(","))
-          {
-            if (!base3.equalsIgnoreCase(base))
-            {
-              String codon = base1.toLowerCase() + base2.toLowerCase()
-                      + base.toUpperCase();
-              String canonical = base1.toLowerCase() + base2.toLowerCase()
-                      + base3.toUpperCase();
-              if (addPeptideVariant(peptide, peptidePos, residue, var,
-                      codon, canonical))
-              {
-                count++;
-              }
-            }
-          }
-        }
-      }
-    }
-
-    return count;
-  }
-
-  /**
-   * Helper method that adds a peptide variant feature. ID and
-   * clinical_significance attributes of the dna variant (if present) are copied
-   * to the new feature.
-   * 
-   * @param peptide
-   * @param peptidePos
-   * @param residue
-   * @param var
-   * @param codon
-   *          the variant codon e.g. aCg
-   * @param canonical
-   *          the 'normal' codon e.g. aTg
-   * @return true if a feature was added, else false
-   */
-  static boolean addPeptideVariant(SequenceI peptide, int peptidePos,
-          String residue, DnaVariant var, String codon, String canonical)
-  {
-    /*
-     * get peptide translation of codon e.g. GAT -> D
-     * note that variants which are not single alleles,
-     * e.g. multibase variants or HGMD_MUTATION etc
-     * are currently ignored here
-     */
-    String trans = codon.contains("-") ? null
-            : (codon.length() > CODON_LENGTH ? null
-                    : ResidueProperties.codonTranslate(codon));
-    if (trans == null)
-    {
-      return false;
-    }
-    String desc = canonical + "/" + codon;
-    String featureType = "";
-    if (trans.equals(residue))
-    {
-      featureType = SequenceOntologyI.SYNONYMOUS_VARIANT;
-    }
-    else if (ResidueProperties.STOP.equals(trans))
-    {
-      featureType = SequenceOntologyI.STOP_GAINED;
-    }
-    else
-    {
-      String residue3Char = StringUtils
-              .toSentenceCase(ResidueProperties.aa2Triplet.get(residue));
-      String trans3Char = StringUtils
-              .toSentenceCase(ResidueProperties.aa2Triplet.get(trans));
-      desc = "p." + residue3Char + peptidePos + trans3Char;
-      featureType = SequenceOntologyI.NONSYNONYMOUS_VARIANT;
-    }
-    SequenceFeature sf = new SequenceFeature(featureType, desc, peptidePos,
-            peptidePos, var.getSource());
-
-    StringBuilder attributes = new StringBuilder(32);
-    String id = (String) var.variant.getValue(VARIANT_ID);
-    if (id != null)
-    {
-      if (id.startsWith(SEQUENCE_VARIANT))
-      {
-        id = id.substring(SEQUENCE_VARIANT.length());
-      }
-      sf.setValue(VARIANT_ID, id);
-      attributes.append(VARIANT_ID).append("=").append(id);
-      // TODO handle other species variants JAL-2064
-      StringBuilder link = new StringBuilder(32);
-      try
-      {
-        link.append(desc).append(" ").append(id).append(
-                "|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=")
-                .append(URLEncoder.encode(id, "UTF-8"));
-        sf.addLink(link.toString());
-      } catch (UnsupportedEncodingException e)
-      {
-        // as if
-      }
-    }
-    String clinSig = (String) var.variant.getValue(CLINICAL_SIGNIFICANCE);
-    if (clinSig != null)
-    {
-      sf.setValue(CLINICAL_SIGNIFICANCE, clinSig);
-      attributes.append(";").append(CLINICAL_SIGNIFICANCE).append("=")
-              .append(clinSig);
-    }
-    peptide.addSequenceFeature(sf);
-    if (attributes.length() > 0)
-    {
-      sf.setAttributes(attributes.toString());
-    }
-    return true;
-  }
-
-  /**
-   * Builds a map whose key is position in the protein sequence, and value is a
-   * list of the base and all variants for each corresponding codon position.
-   * <p>
-   * This depends on dna variants being held as a comma-separated list as
-   * property "alleles" on variant features.
-   * 
-   * @param dnaSeq
-   * @param dnaToProtein
-   * @return
-   */
-  @SuppressWarnings("unchecked")
-  static LinkedHashMap<Integer, List<DnaVariant>[]> buildDnaVariantsMap(
-          SequenceI dnaSeq, MapList dnaToProtein)
-  {
-    /*
-     * map from peptide position to all variants of the codon which codes for it
-     * LinkedHashMap ensures we keep the peptide features in sequence order
-     */
-    LinkedHashMap<Integer, List<DnaVariant>[]> variants = new LinkedHashMap<>();
-
-    List<SequenceFeature> dnaFeatures = dnaSeq.getFeatures()
-            .getFeaturesByOntology(SequenceOntologyI.SEQUENCE_VARIANT);
-    if (dnaFeatures.isEmpty())
-    {
-      return variants;
-    }
-
-    int dnaStart = dnaSeq.getStart();
-    int[] lastCodon = null;
-    int lastPeptidePostion = 0;
-
-    /*
-     * build a map of codon variations for peptides
-     */
-    for (SequenceFeature sf : dnaFeatures)
-    {
-      int dnaCol = sf.getBegin();
-      if (dnaCol != sf.getEnd())
-      {
-        // not handling multi-locus variant features
-        continue;
-      }
-
-      /*
-       * ignore variant if not a SNP
-       */
-      String alls = (String) sf.getValue(Gff3Helper.ALLELES);
-      if (alls == null)
-      {
-        continue; // non-SNP VCF variant perhaps - can't process this
-      }
-
-      String[] alleles = alls.toUpperCase().split(",");
-      boolean isSnp = true;
-      for (String allele : alleles)
-      {
-        if (allele.trim().length() > 1)
-        {
-          isSnp = false;
-        }
-      }
-      if (!isSnp)
-      {
-        continue;
-      }
-
-      int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
-      if (mapsTo == null)
-      {
-        // feature doesn't lie within coding region
-        continue;
-      }
-      int peptidePosition = mapsTo[0];
-      List<DnaVariant>[] codonVariants = variants.get(peptidePosition);
-      if (codonVariants == null)
-      {
-        codonVariants = new ArrayList[CODON_LENGTH];
-        codonVariants[0] = new ArrayList<>();
-        codonVariants[1] = new ArrayList<>();
-        codonVariants[2] = new ArrayList<>();
-        variants.put(peptidePosition, codonVariants);
-      }
-
-      /*
-       * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
-       */
-      int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
-              : MappingUtils.flattenRanges(dnaToProtein.locateInFrom(
-                      peptidePosition, peptidePosition));
-      lastPeptidePostion = peptidePosition;
-      lastCodon = codon;
-
-      /*
-       * save nucleotide (and any variant) for each codon position
-       */
-      for (int codonPos = 0; codonPos < CODON_LENGTH; codonPos++)
-      {
-        String nucleotide = String.valueOf(
-                dnaSeq.getCharAt(codon[codonPos] - dnaStart)).toUpperCase();
-        List<DnaVariant> codonVariant = codonVariants[codonPos];
-        if (codon[codonPos] == dnaCol)
-        {
-          if (!codonVariant.isEmpty()
-                  && codonVariant.get(0).variant == null)
-          {
-            /*
-             * already recorded base value, add this variant
-             */
-            codonVariant.get(0).variant = sf;
-          }
-          else
-          {
-            /*
-             * add variant with base value
-             */
-            codonVariant.add(new DnaVariant(nucleotide, sf));
-          }
-        }
-        else if (codonVariant.isEmpty())
-        {
-          /*
-           * record (possibly non-varying) base value
-           */
-          codonVariant.add(new DnaVariant(nucleotide));
-        }
-      }
-    }
-    return variants;
-  }
-
-  /**
    * Makes an alignment with a copy of the given sequences, adding in any
    * non-redundant sequences which are mapped to by the cross-referenced
    * sequences.
@@ -2774,7 +2413,7 @@ public class AlignmentUtils
     SequenceIdMatcher matcher = new SequenceIdMatcher(seqs);
     if (xrefs != null)
     {
-       // BH 2019.01.25 streamlined this triply nested loop to remove all iterators
+       // BH 2019.01.25 recoded to remove iterators
        
       for (int ix = 0, nx = xrefs.length; ix < nx; ix++)
       {
@@ -2784,9 +2423,9 @@ public class AlignmentUtils
         {
           for (int ir = 0, nir = dbrefs.size(); ir < nir; ir++)
           {
-                 DBRefEntry dbref = dbrefs.get(ir);
-                 Mapping map = dbref.getMap();
-                 SequenceI mto;
+            DBRefEntry dbref = dbrefs.get(ir);
+            Mapping map = dbref.getMap();
+            SequenceI mto;
             if (map == null || (mto = map.getTo()) == null
                     || mto.isProtein() != isProtein)
             {
@@ -2893,10 +2532,10 @@ public class AlignmentUtils
    * true; else returns false
    * 
    * @param unaligned
-   *          - sequences to be aligned based on aligned
+   *                    - sequences to be aligned based on aligned
    * @param aligned
-   *          - 'guide' alignment containing sequences derived from same dataset
-   *          as unaligned
+   *                    - 'guide' alignment containing sequences derived from same
+   *                    dataset as unaligned
    * @return
    */
   static boolean alignAsSameSequences(AlignmentI unaligned,
@@ -2920,15 +2559,22 @@ public class AlignmentUtils
     }
 
     /*
-     * first pass - check whether all sequences to be aligned share a dataset
-     * sequence with an aligned sequence
+     * first pass - check whether all sequences to be aligned share a 
+     * dataset sequence with an aligned sequence; also note the leftmost
+     * ungapped column from which to copy
      */
+    int leftmost = Integer.MAX_VALUE;
     for (SequenceI seq : unaligned.getSequences())
     {
-      if (!alignedDatasets.containsKey(seq.getDatasetSequence()))
+      final SequenceI ds = seq.getDatasetSequence();
+      if (!alignedDatasets.containsKey(ds))
       {
         return false;
       }
+      SequenceI alignedSeq = alignedDatasets.get(ds)
+              .get(0);
+      int startCol = alignedSeq.findIndex(seq.getStart()); // 1..
+      leftmost = Math.min(leftmost, startCol);
     }
 
     /*
@@ -2936,13 +2582,32 @@ public class AlignmentUtils
      * heuristic rule: pair off sequences in order for the case where 
      * more than one shares the same dataset sequence 
      */
+    final char gapCharacter = aligned.getGapCharacter();
     for (SequenceI seq : unaligned.getSequences())
     {
       List<SequenceI> alignedSequences = alignedDatasets
               .get(seq.getDatasetSequence());
-      // TODO: getSequenceAsString() will be deprecated in the future
-      // TODO: need to leave to SequenceI implementor to update gaps
-      seq.setSequence(alignedSequences.get(0).getSequenceAsString());
+      if (alignedSequences.isEmpty())
+      {
+        /*
+         * defensive check - shouldn't happen! (JAL-3536)
+         */
+        continue;
+      }
+      SequenceI alignedSeq = alignedSequences.get(0);
+
+      /*
+       * gap fill for leading (5') UTR if any
+       */
+      // TODO this copies intron columns - wrong!
+      int startCol = alignedSeq.findIndex(seq.getStart()); // 1..
+      int endCol = alignedSeq.findIndex(seq.getEnd());
+      char[] seqchars = new char[endCol - leftmost + 1];
+      Arrays.fill(seqchars, gapCharacter);
+      char[] toCopy = alignedSeq.getSequence(startCol - 1, endCol);
+      System.arraycopy(toCopy, 0, seqchars, startCol - leftmost,
+              toCopy.length);
+      seq.setSequence(String.valueOf(seqchars));
       if (alignedSequences.size() > 0)
       {
         // pop off aligned sequences (except the last one)
@@ -2950,6 +2615,12 @@ public class AlignmentUtils
       }
     }
 
+    /*
+     * finally remove gapped columns (e.g. introns)
+     */
+    new RemoveGapColCommand("", unaligned.getSequencesArray(), 0,
+            unaligned.getWidth() - 1, unaligned);
+
     return true;
   }
 
index ecdd11e..61add1e 100644 (file)
@@ -32,6 +32,7 @@ import jalview.datamodel.SequenceI;
 import jalview.util.DBRefUtils;
 import jalview.util.MapList;
 import jalview.ws.SequenceFetcher;
+import jalview.ws.seqfetcher.ASequenceFetcher;
 
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -401,6 +402,7 @@ public class CrossRef
   private void retrieveCrossRef(List<DBRefEntry> sourceRefs, SequenceI seq,
           List<DBRefEntry> xrfs, boolean fromDna, AlignedCodonFrame cf)
   {
+    ASequenceFetcher sftch = SequenceFetcher.getInstance();// Factory.getSequenceFetcher();
     SequenceI[] retrieved = null;
     SequenceI dss = seq.getDatasetSequence() == null ? seq
             : seq.getDatasetSequence();
@@ -416,8 +418,7 @@ public class CrossRef
     }
     try
     {
-      retrieved = SequenceFetcher.getInstance()
-              .getSequences(sourceRefs, !fromDna);
+      retrieved = sftch.getSequences(sourceRefs, !fromDna);
     } catch (Exception e)
     {
       System.err.println(
@@ -640,7 +641,7 @@ public class CrossRef
                     @Override
                     public boolean equals(Object o)
                     {
-                      return equals((SequenceFeature) o, true);
+                      return super.equals(o, true);
                     }
                   };
                   matched.addSequenceFeature(newFeature);
@@ -928,7 +929,7 @@ public class CrossRef
 
     if (fromDna)
     {
-      AlignmentUtils.computeProteinFeatures(mapFrom, mapTo, mapping);
+      // AlignmentUtils.computeProteinFeatures(mapFrom, mapTo, mapping);
       mappings.addMap(mapFrom, mapTo, mapping);
     }
     else
index 38217e9..95ca1ba 100644 (file)
@@ -188,7 +188,7 @@ public class FeatureDistanceModel extends DistanceScoreModel
   protected Map<SeqCigar, Set<String>> findFeatureTypesAtColumn(
           SeqCigar[] seqs, int columnPosition)
   {
-    Map<SeqCigar, Set<String>> sfap = new HashMap<SeqCigar, Set<String>>();
+    Map<SeqCigar, Set<String>> sfap = new HashMap<>();
     for (SeqCigar seq : seqs)
     {
       int spos = seq.findPosition(columnPosition);
@@ -197,9 +197,9 @@ public class FeatureDistanceModel extends DistanceScoreModel
         /*
          * position is not a gap
          */
-        Set<String> types = new HashSet<String>();
+        Set<String> types = new HashSet<>();
         List<SequenceFeature> sfs = fr.findFeaturesAtResidue(
-                seq.getRefSeq(), spos);
+                seq.getRefSeq(), spos, spos);
         for (SequenceFeature sf : sfs)
         {
           types.add(sf.getType());
index 86e61e7..6549d64 100644 (file)
@@ -23,6 +23,8 @@ package jalview.api;
 import jalview.commands.CommandI;
 import jalview.schemes.ColourSchemeI;
 
+import java.awt.Rectangle;
+
 /**
  * Interface implemented by gui implementations managing a Jalview Alignment
  * View
@@ -62,4 +64,25 @@ public interface AlignViewControllerGuiI
    * @return
    */
   FeatureSettingsControllerI getFeatureSettingsUI();
+
+  /**
+   * displays the Feature Settigns control panel for the alignment view - if one
+   * exists it is closed and re-opened.
+   * 
+   * @return the current feature settings controller
+   */
+  FeatureSettingsControllerI showFeatureSettingsUI();
+
+  /**
+   * record the last position of a feature settings dialog before it was closed
+   * 
+   * @param bounds
+   */
+  void setFeatureSettingsGeometry(Rectangle bounds);
+
+  /**
+   * 
+   * @return last position of feature settings for this alignment view GUI
+   */
+  Rectangle getFeatureSettingsGeometry();
 }
index 4016c75..065be75 100644 (file)
@@ -199,7 +199,7 @@ public interface AlignViewportI extends ViewStyleI
    * Sets the colour scheme for the background alignment (as distinct from
    * sub-groups, which may have their own colour schemes). A null value is used
    * for no residue colour (white).
-   *
+   * 
    * @param cs
    */
   void setGlobalColourScheme(ColourSchemeI cs);
@@ -433,9 +433,19 @@ public interface AlignViewportI extends ViewStyleI
    */
   void setFollowHighlight(boolean b);
 
+  /**
+   * configure the feature renderer with predefined feature settings
+   * 
+   * @param featureSettings
+   */
   public void applyFeaturesStyle(FeatureSettingsModelI featureSettings);
 
   /**
+   * Apply the given feature settings on top of existing feature settings.
+   */
+  public void mergeFeaturesStyle(FeatureSettingsModelI featureSettings);
+
+  /**
    * check if current selection group is defined on the view, or is simply a
    * temporary group.
    * 
index 404c497..8aa2858 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.api;
 
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.FeatureMatcherSetI;
@@ -161,15 +162,18 @@ public interface FeatureRenderer
   List<SequenceFeature> findFeaturesAtColumn(SequenceI sequence, int column);
 
   /**
-   * Returns features at the specified residue position on the given sequence.
-   * Non-positional features are not included.
+   * Returns features at the specified residue positions on the given sequence.
+   * Non-positional features are not included. Features are returned in render
+   * order of their feature type (last is on top). Within feature type, ordering
+   * is undefined.
    * 
    * @param sequence
-   * @param resNo
-   *          residue position (start..)
+   * @param fromResNo
+   * @param toResNo
    * @return
    */
-  List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence, int resNo);
+  List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence,
+          int fromResNo, int toResNo);
 
   /**
    * get current displayed types, in ordering of rendering (on top last)
@@ -280,4 +284,17 @@ public interface FeatureRenderer
    * @return
    */
   boolean isVisible(SequenceFeature feature);
+
+  /**
+   * Answers a bean containing a mapping, and a list of visible features in this
+   * alignment at a position (or range) which is mappable from the given sequence
+   * residue position in a mapped alignment. Features are returned in render order
+   * of feature type (on top last), with order within feature type undefined. If
+   * no features or mapping are found, answers null.
+   * 
+   * @param sequence
+   * @param pos
+   * @return
+   */
+  MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence, int pos);
 }
index 46f5f44..6b037f5 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.api;
 
+import jalview.controller.FeatureSettingsControllerGuiI;
 import jalview.datamodel.AlignmentI;
 
 /**
@@ -55,4 +56,41 @@ public interface SplitContainerI
    */
   String getComplementTitle(Object af);
 
+  /**
+   * get the 'other' alignFrame in the SplitFrame
+   * 
+   * @param alignFrame
+   * @return the complement alignFrame - or null if alignFrame wasn't held by this
+   *         frame
+   */
+  AlignViewControllerGuiI getComplementAlignFrame(
+          AlignViewControllerGuiI alignFrame);
+
+  /**
+   * add the given UI to the splitframe's feature settings UI holder
+   * 
+   * @param featureSettings
+   * @return
+   */
+  void addFeatureSettingsUI(
+          FeatureSettingsControllerGuiI featureSettings);
+
+  /**
+   * Request to close all feature settings originating from a particular panel.
+   * 
+   * @param featureSettings
+   * @param closeContainingFrame
+   *                               - if false then the tab containing the feature
+   *                               settings will be 'reset' ready for a new
+   *                               feature settings
+   */
+  void closeFeatureSettings(FeatureSettingsControllerI featureSettings,
+          boolean closeContainingFrame);
+
+  /**
+   * 
+   * @return true if a feature settings panel is currently open
+   */
+  boolean isFeatureSettingsOpen();
+
 }
index 2b554ea..a348300 100644 (file)
@@ -24,6 +24,13 @@ import java.awt.Color;
 
 public interface ViewStyleI
 {
+  void setShowComplementFeatures(boolean b);
+
+  boolean isShowComplementFeatures();
+
+  void setShowComplementFeaturesOnTop(boolean b);
+
+  boolean isShowComplementFeaturesOnTop();
 
   void setColourAppliesToAllGroups(boolean b);
 
index 062dbf8..997f185 100644 (file)
@@ -96,6 +96,7 @@ import java.awt.Menu;
 import java.awt.MenuBar;
 import java.awt.MenuItem;
 import java.awt.Panel;
+import java.awt.Rectangle;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.FocusEvent;
@@ -107,6 +108,7 @@ import java.awt.event.KeyListener;
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
 import java.net.URL;
 import java.net.URLEncoder;
 import java.util.Arrays;
@@ -119,9 +121,8 @@ import java.util.Vector;
 
 import org.jmol.viewer.Viewer;
 
-public class AlignFrame extends EmbmenuFrame implements AlignFrameI,
-        ActionListener,
-        ItemListener, KeyListener, AlignViewControllerGuiI
+public class AlignFrame extends EmbmenuFrame implements ActionListener,
+        ItemListener, KeyListener, AlignViewControllerGuiI, AlignFrameI
 {
   public AlignViewControllerI avc;
 
@@ -1204,7 +1205,7 @@ public class AlignFrame extends EmbmenuFrame implements AlignFrameI,
     }
     else if (source == featureSettings)
     {
-      new FeatureSettings(alignPanel);
+      showFeatureSettingsUI();
     }
     else if (source == alProperties)
     {
@@ -1449,12 +1450,13 @@ public class AlignFrame extends EmbmenuFrame implements AlignFrameI,
     {
       features = formatter.printJalviewFormat(
               viewport.getAlignment().getSequencesArray(),
-              alignPanel.getFeatureRenderer(), true);
+              alignPanel.getFeatureRenderer(), true, false);
     }
     else
     {
       features = formatter.printGffFormat(viewport.getAlignment()
-              .getSequencesArray(), alignPanel.getFeatureRenderer(), true);
+              .getSequencesArray(), alignPanel.getFeatureRenderer(), true,
+              false);
     }
 
     if (displayTextbox)
@@ -1571,7 +1573,7 @@ public class AlignFrame extends EmbmenuFrame implements AlignFrameI,
     try
     {
       new URL(url);
-      url = URLEncoder.encode(url);
+      url = URLEncoder.encode(url, "UTF-8");
     }
     /*
      * When we finally deprecate 1.1 compatibility, we can start to use
@@ -1584,6 +1586,13 @@ public class AlignFrame extends EmbmenuFrame implements AlignFrameI,
     {
       url = viewport.applet.getCodeBase() + url;
     }
+    catch (UnsupportedEncodingException ex)
+    {
+      System.err.println(
+              "WARNING = IMPLEMENTATION ERROR - UNSUPPORTED ENCODING EXCEPTION FOR "
+                      + url);
+      ex.printStackTrace();
+    }
     return url;
   }
 
@@ -4332,4 +4341,24 @@ public class AlignFrame extends EmbmenuFrame implements AlignFrameI,
     return alignPanel.av.featureSettings;
   }
 
+  @Override
+  public FeatureSettingsControllerI showFeatureSettingsUI()
+  {
+    return new FeatureSettings(alignPanel);
+  }
+
+  private Rectangle fs_bounds = null;
+
+  @Override
+  public void setFeatureSettingsGeometry(Rectangle bounds)
+  {
+    fs_bounds = bounds;
+  }
+
+  @Override
+  public Rectangle getFeatureSettingsGeometry()
+  {
+    return fs_bounds;
+  }
+
 }
index 2bead46..0324d8c 100644 (file)
@@ -71,7 +71,7 @@ public class AlignViewport extends AlignmentViewport
       {
         try
         {
-          widthScale = new Float(param).floatValue();
+          widthScale = Float.valueOf(param).floatValue();
         } catch (Exception e)
         {
         }
@@ -94,7 +94,7 @@ public class AlignViewport extends AlignmentViewport
       {
         try
         {
-          heightScale = new Float(param).floatValue();
+          heightScale = Float.valueOf(param).floatValue();
         } catch (Exception e)
         {
         }
@@ -392,10 +392,10 @@ public class AlignViewport extends AlignmentViewport
   }
 
   /**
-   * Applies the supplied feature settings descriptor to currently known
-   * features. This supports an 'initial configuration' of feature colouring
-   * based on a preset or user favourite. This may then be modified in the usual
-   * way using the Feature Settings dialogue.
+   * Applies the supplied feature settings descriptor to currently known features.
+   * This supports an 'initial configuration' of feature colouring based on a
+   * preset or user favourite. This may then be modified in the usual way using
+   * the Feature Settings dialogue. NOT IMPLEMENTED FOR APPLET
    * 
    * @param featureSettings
    */
@@ -405,4 +405,18 @@ public class AlignViewport extends AlignmentViewport
     // TODO implement for applet
   }
 
+  /**
+   * Merges the supplied feature settings descriptor with existing feature styles.
+   * This supports an 'initial configuration' of feature colouring based on a
+   * preset or user favourite. This may then be modified in the usual way using
+   * the Feature Settings dialogue. NOT IMPLEMENTED FOR APPLET
+   * 
+   * @param featureSettings
+   */
+  @Override
+  public void mergeFeaturesStyle(FeatureSettingsModelI featureSettings)
+  {
+    // TODO Auto-generated method stub
+
+  }
 }
index ed3d8fa..776e9ad 100644 (file)
@@ -500,7 +500,6 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
     // For now, ignore the mouseWheel font resizing on Macs
     // As the Button2_mask always seems to be true
-    
     if (Platform.isWinMiddleButton(evt))
     {
       mouseWheelPressed = true;
@@ -744,7 +743,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   }
 
   @Override
-  public void highlightSequence(SearchResultsI results)
+  public String highlightSequence(SearchResultsI results)
   {
     if (av.isFollowHighlight())
     {
@@ -761,7 +760,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
     setStatusMessage(results);
     seqCanvas.highlightSearchResults(results);
-
+    return null;
   }
 
   @Override
@@ -820,8 +819,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     {
       if (av.getAlignment().isNucleotide())
       {
-        String base = ResidueProperties.nucleotideName
-                .get(String.valueOf(ch)); // BH 2020.04.07 just Eclipse warning
+        String base = ResidueProperties.nucleotideName.get(ch);
         text.append(" Nucleotide: ").append(base == null ? ch : base);
       }
       else
@@ -1433,8 +1431,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
 
     // DETECT RIGHT MOUSE BUTTON IN AWT
-    if ((evt.getModifiers()
-            & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
+    if ((evt.getModifiersEx()
+            & InputEvent.BUTTON3_DOWN_MASK) == InputEvent.BUTTON3_DOWN_MASK)
     {
       List<SequenceFeature> allFeatures = findFeaturesAtColumn(sequence,
               sequence.findPosition(column + 1));
index 5d82015..0ec9ee0 100755 (executable)
@@ -598,7 +598,7 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi
       System.out.println("No files to open!");
       System.exit(1);
     }
-    boolean haveImport = checkStartVamas(aparser);
+    boolean haveImport = false;// checkStartVamas(aparser);
     // Finally, deal with the remaining input data.
     long progress = -1;
     if (file == null && isJavaAppletTag)
@@ -692,7 +692,7 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi
         System.out.println("CMD [nocalculation] executed successfully!");
       }
 
-      AlignFrame af = new FileLoader(!headless).loadFileWaitTillLoaded(file,
+      AlignFrame af = new FileLoader(!headless).LoadFileWaitTillLoaded(file,
               protocol, format);
       if (af == null)
       {
@@ -713,7 +713,7 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi
             // TODO ?
           }
           AlignFrame af2 = new FileLoader(!headless)
-                  .loadFileWaitTillLoaded(file2, protocol, format);
+                  .LoadFileWaitTillLoaded(file2, protocol, format);
           if (af2 == null)
           {
             System.out.println("error");
@@ -933,7 +933,7 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi
       }
 
       startUpAlframe = new FileLoader(!headless)
-              .loadFileWaitTillLoaded(file, protocol, format);
+              .LoadFileWaitTillLoaded(file, protocol, format);
       // extract groovy arguments before anything else.
     }
 
@@ -965,99 +965,6 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi
     return null;
   }
 
-  private boolean checkStartVamas(ArgsParser aparser)
-  {
-    String vamsasImport = aparser.getValue(ArgsParser.VDOC);
-    String vamsasSession = aparser.getValue(ArgsParser.VSESS);
-    if (vamsasImport == null && vamsasSession == null)
-    {
-      return false;
-    }
-    if (desktop == null || headless)
-    {
-      System.out.println(
-              "Headless vamsas sessions not yet supported. Sorry.");
-      System.exit(1);
-    }
-    boolean haveImport = (vamsasImport != null);
-    if (haveImport)
-    {
-      // if we have a file, start a new session and import it.
-      boolean inSession = false;
-      try
-      {
-        DataSourceType viprotocol = AppletFormatAdapter
-                .checkProtocol(vamsasImport);
-        if (viprotocol == DataSourceType.FILE)
-        {
-          inSession = desktop.vamsasImport(new File(vamsasImport));
-        }
-        else if (viprotocol == DataSourceType.URL)
-        {
-          inSession = desktop.vamsasImport(new URL(vamsasImport));
-        }
-
-      } catch (Exception e)
-      {
-        System.err.println("Exeption when importing " + vamsasImport
-                + " as a vamsas document.");
-        e.printStackTrace();
-      }
-      if (!inSession)
-      {
-        System.err.println("Failed to import " + vamsasImport
-                + " as a vamsas document.");
-      }
-      else
-      {
-        System.out.println("Imported Successfully into new session "
-                + desktop.getVamsasApplication().getCurrentSession());
-      }
-    }
-    if (vamsasSession != null)
-    {
-      if (vamsasImport != null)
-      {
-        // close the newly imported session and import the Jalview specific
-        // remnants into the new session later on.
-        desktop.vamsasStop_actionPerformed(null);
-      }
-      // now join the new session
-      try
-      {
-        if (desktop.joinVamsasSession(vamsasSession))
-        {
-          System.out.println(
-                  "Successfully joined vamsas session " + vamsasSession);
-        }
-        else
-        {
-          System.err.println("WARNING: Failed to join vamsas session "
-                  + vamsasSession);
-        }
-      } catch (Exception e)
-      {
-        System.err.println(
-                "ERROR: Failed to join vamsas session " + vamsasSession);
-        e.printStackTrace();
-      }
-      if (vamsasImport != null)
-      {
-        // the Jalview specific remnants can now be imported into the new
-        // session at the user's leisure.
-        Cache.log.info(
-                "Skipping Push for import of data into existing vamsas session.");
-        // TODO:
-        // enable
-        // this
-        // when
-        // debugged
-        // desktop.getVamsasApplication().push_update();
-      }
-    }
-    return haveImport;
-  }
-
   /**
    * Writes an output file for each format (if any) specified in the
    * command-line arguments. Supported formats are currently
@@ -1387,23 +1294,6 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi
   }
 
   /**
-   * Get the SwingJS applet ID and combine that with the frameType
-   * 
-   * @param frameType
-   *          "alignment", "desktop", etc., or null
-   * @return
-   */
-  public static String getAppID(String frameType)
-  {
-    String id = Cache.getProperty("Info.j2sAppletID");
-    if (id == null)
-    {
-      id = "jalview";
-    }
-    return id + (frameType == null ? "" : "-" + frameType);
-  }
-
-  /**
    * Handle all JalviewLite applet parameters
    * 
    * @param aparser
@@ -1886,7 +1776,7 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi
     {
       alf = getCurrentAlignFrame();
     }
-    return appLoader.getFeaturesFrom(alf, format);
+    return appLoader.getFeaturesFrom(alf, format, true, false);
   }
 
   @Override
@@ -2266,4 +2156,21 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi
     }
   }
 
+  /**
+   * Get the SwingJS applet ID and combine that with the frameType
+   * 
+   * @param frameType
+   *          "alignment", "desktop", etc., or null
+   * @return
+   */
+  public static String getAppID(String frameType)
+  {
+    String id = Cache.getProperty("Info.j2sAppletID");
+    if (id == null)
+    {
+      id = "jalview";
+    }
+    return id + (frameType == null ? "" : "-" + frameType);
+  }
+
 }
\ No newline at end of file
index 8fe2cdc..834a30f 100644 (file)
@@ -1370,7 +1370,8 @@ public class JalviewAppLoader
     return true;
   }
 
-  public String getFeaturesFrom(AlignFrameI alf, String format)
+  public String getFeaturesFrom(AlignFrameI alf, String format,
+          boolean includeNonpositionsFeatures, boolean includeComplement)
   {
     AlignFrame f = ((AlignFrame) alf);
 
@@ -1380,13 +1381,15 @@ public class JalviewAppLoader
     {
       features = formatter.printJalviewFormat(
               f.getViewport().getAlignment().getSequencesArray(),
-              f.alignPanel.getFeatureRenderer(), true);
+              f.alignPanel.getFeatureRenderer(),
+              includeNonpositionsFeatures, includeComplement);
     }
     else
     {
       features = formatter.printGffFormat(
               f.getViewport().getAlignment().getSequencesArray(),
-              f.alignPanel.getFeatureRenderer(), true);
+              f.alignPanel.getFeatureRenderer(),
+              includeNonpositionsFeatures, includeComplement);
     }
 
     if (features == null)
index 33699cc..ee46a89 100644 (file)
@@ -166,9 +166,10 @@ public class AlignViewController implements AlignViewControllerI
     // JBPNote this routine could also mark rows, not just columns.
     // need a decent query structure to allow all types of feature searches
     BitSet bs = new BitSet();
-    SequenceCollectionI sqcol = (viewport.getSelectionGroup() == null
-            || extendCurrent) ? viewport.getAlignment()
-                    : viewport.getSelectionGroup();
+    boolean searchSelection = viewport.getSelectionGroup() != null
+            && !extendCurrent;
+    SequenceCollectionI sqcol = searchSelection ? viewport
+            .getSelectionGroup() : viewport.getAlignment();
 
     int nseq = findColumnsWithFeature(featureType, sqcol, bs);
 
@@ -204,9 +205,10 @@ public class AlignViewController implements AlignViewControllerI
     }
     else
     {
-      avcg.setStatus(MessageManager
-              .formatMessage("label.no_feature_of_type_found", new String[]
-              { featureType }));
+      String key = searchSelection ? "label.no_feature_found_selection"
+              : "label.no_feature_of_type_found";
+      avcg.setStatus(MessageManager.formatMessage(key,
+              new String[] { featureType }));
       if (!extendCurrent)
       {
         cs.clear();
@@ -308,16 +310,34 @@ public class AlignViewController implements AlignViewControllerI
   @Override
   public void sortAlignmentByFeatureDensity(List<String> typ)
   {
-    sortBy(typ, "Sort by Density", AlignmentSorter.FEATURE_DENSITY);
+    String methodText = MessageManager.getString("label.sort_by_density");
+    sortByFeatures(typ, methodText, AlignmentSorter.FEATURE_DENSITY);
   }
 
-  protected void sortBy(List<String> typ, String methodText,
+  /**
+   * Sorts the alignment (or current selection) by either average score or density
+   * of the specified feature types, and adds to the command history. If
+   * {@code types} is null, all visible feature types are used for the sort. If no
+   * feature types apply, does nothing.
+   * 
+   * @param types
+   * @param methodText
+   *                     - text shown in Undo/Redo command
+   * @param method
+   *                     - passed to
+   *                     jalview.analysis.AlignmentSorter.sortByFeatures()
+   */
+  protected void sortByFeatures(List<String> types, String methodText,
           final String method)
   {
     FeatureRenderer fr = alignPanel.getFeatureRenderer();
-    if (typ == null && fr != null)
+    if (types == null && fr != null)
+    {
+      types = fr.getDisplayedFeatureTypes();
+    }
+    if (types.isEmpty())
     {
-      typ = fr.getDisplayedFeatureTypes();
+      return; // nothing to do
     }
     List<String> gps = null;
     if (fr != null)
@@ -339,7 +359,7 @@ public class AlignViewController implements AlignViewControllerI
       stop = al.getWidth();
     }
     SequenceI[] oldOrder = al.getSequencesArray();
-    AlignmentSorter.sortByFeature(typ, gps, start, stop, al, method);
+    AlignmentSorter.sortByFeature(types, gps, start, stop, al, method);
     avcg.addHistoryItem(new OrderCommand(methodText, oldOrder,
             viewport.getAlignment()));
     alignPanel.paintAlignment(true, false);
@@ -349,7 +369,8 @@ public class AlignViewController implements AlignViewControllerI
   @Override
   public void sortAlignmentByFeatureScore(List<String> typ)
   {
-    sortBy(typ, "Sort by Feature Score", AlignmentSorter.FEATURE_SCORE);
+    String methodText = MessageManager.getString("label.sort_by_score");
+    sortByFeatures(typ, methodText, AlignmentSorter.FEATURE_SCORE);
   }
 
   @Override
index 728612e..92571bc 100644 (file)
  */
 package jalview.controller;
 
+import jalview.api.AlignViewControllerGuiI;
+
 public interface FeatureSettingsControllerGuiI
 {
 
+  AlignViewControllerGuiI getAlignframe();
+
+  void featureSettings_isClosed();
+
+  /**
+   * undo any changes made to feature settings whilst the dialog has been visible,
+   * since the last 'apply'
+   */
+  void revert();
+
 }
index 0004abe..b4c7ea4 100755 (executable)
@@ -103,7 +103,8 @@ public class DBRefEntry implements DBRefEntryI
                     : new String(entry.getVersion())),
             (entry.getAccessionId() == null ? ""
                     : new String(entry.getAccessionId())),
-            (entry.getMap() == null ? null : new Mapping(entry.getMap())));
+            (entry.getMap() == null ? null
+                    : new Mapping(entry.getMap())));
   }
 
   @Override
index f8c7ec5..09db9d7 100644 (file)
@@ -34,5 +34,5 @@ public interface GeneLociI
    * 
    * @return
    */
-  MapList getMap();
+  MapList getMapping();
 }
diff --git a/src/jalview/datamodel/GeneLocus.java b/src/jalview/datamodel/GeneLocus.java
new file mode 100644 (file)
index 0000000..f81348f
--- /dev/null
@@ -0,0 +1,91 @@
+package jalview.datamodel;
+
+import jalview.util.MapList;
+
+/**
+ * A specialisation of DBRefEntry used to hold the chromosomal coordinates for a
+ * (typically gene) sequence
+ * <ul>
+ * <li>field <code>source</code> is used to hold a species id e.g. human</li>
+ * <li>field <code>version</code> is used to hold assembly id e.g GRCh38</li>
+ * <li>field <code>accession</code> is used to hold the chromosome id</li>
+ * <li>field <code>map</code> is used to hold the mapping from sequence to
+ * chromosome coordinates</li>
+ * </ul>
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class GeneLocus extends DBRefEntry implements GeneLociI
+{
+  /**
+   * Constructor adapts species, assembly, chromosome to DBRefEntry source,
+   * version, accession, respectively, and saves the mapping of sequence to
+   * chromosomal coordinates
+   * 
+   * @param speciesId
+   * @param assemblyId
+   * @param chromosomeId
+   * @param mapping
+   */
+  public GeneLocus(String speciesId, String assemblyId, String chromosomeId,
+          Mapping mapping)
+  {
+    super(speciesId, assemblyId, chromosomeId, mapping);
+  }
+
+  /**
+   * Constructor
+   * 
+   * @param speciesId
+   * @param assemblyId
+   * @param chromosomeId
+   */
+  public GeneLocus(String speciesId, String assemblyId, String chromosomeId)
+  {
+    this(speciesId, assemblyId, chromosomeId, null);
+  }
+
+  @Override
+  public boolean equals(Object o)
+  {
+    return o instanceof GeneLocus && super.equals(o);
+  }
+
+  @Override
+  public MapList getMapping()
+  {
+    return map == null ? null : map.getMap();
+  }
+
+  /**
+   * Answers the species identifier e.g. "human", stored as field <code>source</code> of
+   * DBRefEntry
+   */
+  @Override
+  public String getSpeciesId()
+  {
+    return getSource();
+  }
+
+  /**
+   * Answers the genome assembly id e.g. "GRCh38", stored as field
+   * <code>version</code> of DBRefEntry
+   */
+  @Override
+  public String getAssemblyId()
+  {
+    return getVersion();
+  }
+
+  /**
+   * Answers the chromosome identifier e.g. "X", stored as field
+   * <code>accession</code> of DBRefEntry
+   */
+  @Override
+  public String getChromosomeId()
+  {
+    return getAccessionId();
+  }
+
+}
diff --git a/src/jalview/datamodel/MappedFeatures.java b/src/jalview/datamodel/MappedFeatures.java
new file mode 100644 (file)
index 0000000..0fa03cf
--- /dev/null
@@ -0,0 +1,236 @@
+package jalview.datamodel;
+
+import jalview.io.gff.Gff3Helper;
+import jalview.schemes.ResidueProperties;
+import jalview.util.MappingUtils;
+import jalview.util.StringUtils;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A data bean to hold a list of mapped sequence features (e.g. CDS features
+ * mapped from protein), and the mapping between the sequences. It also provides
+ * a method to derive peptide variants from codon variants.
+ * 
+ * @author gmcarstairs
+ */
+public class MappedFeatures
+{
+  private static final String HGV_SP = "HGVSp";
+
+  private static final String CSQ = "CSQ";
+
+  /*
+   * the mapping from one sequence to another
+   */
+  public final Mapping mapping;
+
+  /**
+   * the sequence mapped from
+   */
+  public final SequenceI fromSeq;
+
+  /*
+   * features on the sequence mapped to that overlap the mapped positions
+   */
+  public final List<SequenceFeature> features;
+
+  /*
+   * the residue position in the sequence mapped to
+   */
+  private final int toPosition;
+
+  /*
+   * the residue at toPosition 
+   */
+  private final char toResidue;
+
+  /*
+   * if the mapping is 3:1 or 1:3 (peptide to CDS), this holds the
+   * mapped positions i.e. codon base positions in CDS; to
+   * support calculation of peptide variants from alleles
+   */
+  private final int[] codonPos;
+
+  private final char[] baseCodon;
+
+  /**
+   * Constructor
+   * 
+   * @param theMapping
+   * @param from
+   *                      the sequence mapped from (e.g. CDS)
+   * @param pos
+   *                      the residue position in the sequence mapped to
+   * @param res
+   *                      the residue character at position pos
+   * @param theFeatures
+   *                      list of mapped features found in the 'from' sequence at
+   *                      the mapped position(s)
+   */
+  public MappedFeatures(Mapping theMapping, SequenceI from, int pos,
+          char res, List<SequenceFeature> theFeatures)
+  {
+    mapping = theMapping;
+    fromSeq = from;
+    toPosition = pos;
+    toResidue = res;
+    features = theFeatures;
+
+    /*
+     * determine codon positions and canonical codon
+     * for a peptide-to-CDS mapping
+     */
+    int[] codonIntervals = mapping.getMap().locateInFrom(toPosition, toPosition);
+    int[] codonPositions = codonIntervals == null ? null
+            : MappingUtils.flattenRanges(codonIntervals);
+    if (codonPositions != null && codonPositions.length == 3)
+    {
+      codonPos = codonPositions;
+      baseCodon = new char[3];
+      int cdsStart = fromSeq.getStart();
+      baseCodon[0] = Character
+              .toUpperCase(fromSeq.getCharAt(codonPos[0] - cdsStart));
+      baseCodon[1] = Character
+              .toUpperCase(fromSeq.getCharAt(codonPos[1] - cdsStart));
+      baseCodon[2] = Character
+              .toUpperCase(fromSeq.getCharAt(codonPos[2] - cdsStart));
+    }
+    else
+    {
+      codonPos = null;
+      baseCodon = null;
+    }
+  }
+
+  /**
+   * Computes and returns comma-delimited HGVS notation peptide variants derived
+   * from codon allele variants. If no variants are found, answers an empty
+   * string.
+   * 
+   * @param sf
+   *             a sequence feature (which must be one of those held in this
+   *             object)
+   * @return
+   */
+  public String findProteinVariants(SequenceFeature sf)
+  {
+    if (!features.contains(sf) || baseCodon == null)
+    {
+      return "";
+    }
+
+    /*
+     * VCF data may already contain the protein consequence
+     */
+    String hgvsp = sf.getValueAsString(CSQ, HGV_SP);
+    if (hgvsp != null)
+    {
+      int colonPos = hgvsp.lastIndexOf(':');
+      if (colonPos >= 0)
+      {
+        String var = hgvsp.substring(colonPos + 1);
+        if (var.contains("p.")) // sanity check
+        {
+          return var;
+        }
+      }
+    }
+
+    /*
+     * otherwise, compute codon and peptide variant
+     */
+    int cdsPos = sf.getBegin();
+    if (cdsPos != sf.getEnd())
+    {
+      // not handling multi-locus variant features
+      return "";
+    }
+    if (cdsPos != codonPos[0] && cdsPos != codonPos[1]
+            && cdsPos != codonPos[2])
+    {
+      // e.g. feature on intron within spliced codon!
+      return "";
+    }
+
+    String alls = (String) sf.getValue(Gff3Helper.ALLELES);
+    if (alls == null)
+    {
+      return "";
+    }
+
+    String from3 = StringUtils.toSentenceCase(
+            ResidueProperties.aa2Triplet.get(String.valueOf(toResidue)));
+
+    /*
+     * make a peptide variant for each SNP allele 
+     * e.g. C,G,T gives variants G and T for base C
+     */
+    Set<String> variantPeptides = new HashSet<>();
+    String[] alleles = alls.toUpperCase().split(",");
+    StringBuilder vars = new StringBuilder();
+
+    for (String allele : alleles)
+    {
+      allele = allele.trim().toUpperCase();
+      if (allele.length() > 1 || "-".equals(allele))
+      {
+        continue; // multi-locus variant
+      }
+      char[] variantCodon = new char[3];
+      variantCodon[0] = baseCodon[0];
+      variantCodon[1] = baseCodon[1];
+      variantCodon[2] = baseCodon[2];
+
+      /*
+       * poke variant base into canonical codon;
+       * ignore first 'allele' (canonical base)
+       */
+      final int i = cdsPos == codonPos[0] ? 0
+              : (cdsPos == codonPos[1] ? 1 : 2);
+      variantCodon[i] = allele.toUpperCase().charAt(0);
+      if (variantCodon[i] == baseCodon[i])
+      {
+        continue;
+      }
+      String codon = new String(variantCodon);
+      String peptide = ResidueProperties.codonTranslate(codon);
+      boolean synonymous = toResidue == peptide.charAt(0);
+      StringBuilder var = new StringBuilder();
+      if (synonymous)
+      {
+        /*
+         * synonymous variant notation e.g. c.1062C>A(p.=)
+         */
+        var.append("c.").append(String.valueOf(cdsPos))
+                .append(String.valueOf(baseCodon[i])).append(">")
+                .append(String.valueOf(variantCodon[i]))
+                .append("(p.=)");
+      }
+      else
+      {
+        /*
+         * missense variant notation e.g. p.Arg355Met
+         */
+        String to3 = ResidueProperties.STOP.equals(peptide) ? "Ter"
+                : StringUtils.toSentenceCase(
+                        ResidueProperties.aa2Triplet.get(peptide));
+        var.append("p.").append(from3).append(String.valueOf(toPosition))
+                .append(to3);
+      }
+      if (!variantPeptides.contains(peptide)) // duplicate consequence
+      {
+        variantPeptides.add(peptide);
+        if (vars.length() > 0)
+        {
+          vars.append(",");
+        }
+        vars.append(var);
+      }
+    }
+
+    return vars.toString();
+  }
+}
index 6c0e528..f894223 100755 (executable)
@@ -743,7 +743,7 @@ public class Sequence extends ASequence implements SequenceI
             }
 
             @Override
-            public MapList getMap()
+            public MapList getMapping()
             {
               return ref.getMap().getMap();
             }
index 1f2e639..2dd9cf0 100755 (executable)
@@ -28,7 +28,7 @@ import jalview.datamodel.features.FeatureSources;
 import jalview.util.StringUtils;
 
 import java.util.Comparator;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.SortedMap;
@@ -50,10 +50,10 @@ public class SequenceFeature implements FeatureLocationI
 
   private static final String STATUS = "status";
 
-  private static final String STRAND = "STRAND";
+  public static final String STRAND = "STRAND";
 
-  // private key for Phase designed not to conflict with real GFF data
-  private static final String PHASE = "!Phase";
+  // key for Phase designed not to conflict with real GFF data
+  public static final String PHASE = "!Phase";
 
   // private key for ENA location designed not to conflict with real GFF data
   private static final String LOCATION = "!Location";
@@ -61,12 +61,6 @@ public class SequenceFeature implements FeatureLocationI
   private static final String ROW_DATA = "<tr><td>%s</td><td>%s</td><td>%s</td></tr>";
 
   /*
-   * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
-   * name1=value1;name2=value2,value3;...etc
-   */
-  private static final String ATTRIBUTES = "ATTRIBUTES";
-
-  /*
    * type, begin, end, featureGroup, score and contactFeature are final 
    * to ensure that the integrity of SequenceFeatures data store 
    * can't be broken by direct update of these fields
@@ -174,19 +168,13 @@ public class SequenceFeature implements FeatureLocationI
 
     if (sf.otherDetails != null)
     {
-      otherDetails = new HashMap<>();
-      for (Entry<String, Object> entry : sf.otherDetails.entrySet())
-      {
-        otherDetails.put(entry.getKey(), entry.getValue());
-      }
+      otherDetails = new LinkedHashMap<>();
+      otherDetails.putAll(sf.otherDetails);
     }
     if (sf.links != null && sf.links.size() > 0)
     {
       links = new Vector<>();
-      for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
-      {
-        links.addElement(sf.links.elementAt(i));
-      }
+      links.addAll(sf.links);
     }
   }
 
@@ -217,8 +205,7 @@ public class SequenceFeature implements FeatureLocationI
   @Override
   public boolean equals(Object o)
   {
-    return (o instanceof SequenceFeature
-            && equals((SequenceFeature) o, false));
+    return equals(o, false);
   }
 
   /**
@@ -231,19 +218,47 @@ public class SequenceFeature implements FeatureLocationI
    * @param ignoreParent
    * @return
    */
-  public boolean equals(SequenceFeature sf, boolean ignoreParent)
+  public boolean equals(Object o, boolean ignoreParent)
   {
-    return (begin == sf.begin && end == sf.end
-            && getStrand() == sf.getStrand()
-            && (Float.isNaN(score) ? Float.isNaN(sf.score)
-                    : score == sf.score)
-            && (type + description + featureGroup + getPhase())
-                    .equals(sf.type + sf.description + sf.featureGroup
-                            + sf.getPhase())
-            && equalAttribute(getValue("ID"), sf.getValue("ID"))
-            && equalAttribute(getValue("Name"), sf.getValue("Name"))
-            && (ignoreParent || equalAttribute(getValue("Parent"),
-                    sf.getValue("Parent"))));
+    if (o == null || !(o instanceof SequenceFeature))
+    {
+      return false;
+    }
+
+    SequenceFeature sf = (SequenceFeature) o;
+    boolean sameScore = Float.isNaN(score) ? Float.isNaN(sf.score)
+            : score == sf.score;
+    if (begin != sf.begin || end != sf.end || !sameScore)
+    {
+      return false;
+    }
+
+    if (getStrand() != sf.getStrand())
+    {
+      return false;
+    }
+
+    if (!(type + description + featureGroup + getPhase()).equals(
+            sf.type + sf.description + sf.featureGroup + sf.getPhase()))
+    {
+      return false;
+    }
+    if (!equalAttribute(getValue("ID"), sf.getValue("ID")))
+    {
+      return false;
+    }
+    if (!equalAttribute(getValue("Name"), sf.getValue("Name")))
+    {
+      return false;
+    }
+    if (!ignoreParent)
+    {
+      if (!equalAttribute(getValue("Parent"), sf.getValue("Parent")))
+      {
+        return false;
+      }
+    }
+    return true;
   }
 
   /**
@@ -318,6 +333,11 @@ public class SequenceFeature implements FeatureLocationI
     return featureGroup;
   }
 
+  /**
+   * Adds a hyperlink for the feature. This should have the format label|url.
+   * 
+   * @param labelLink
+   */
   public void addLink(String labelLink)
   {
     if (links == null)
@@ -408,7 +428,10 @@ public class SequenceFeature implements FeatureLocationI
     {
       if (otherDetails == null)
       {
-        otherDetails = new HashMap<>();
+        /*
+         * LinkedHashMap preserves insertion order of attributes
+         */
+        otherDetails = new LinkedHashMap<>();
       }
 
       otherDetails.put(key, value);
@@ -451,16 +474,6 @@ public class SequenceFeature implements FeatureLocationI
     return (String) getValue(STATUS);
   }
 
-  public void setAttributes(String attr)
-  {
-    setValue(ATTRIBUTES, attr);
-  }
-
-  public String getAttributes()
-  {
-    return (String) getValue(ATTRIBUTES);
-  }
-
   /**
    * Return 1 for forward strand ('+' in GFF), -1 for reverse strand ('-' in
    * GFF), and 0 for unknown or not (validly) specified
@@ -575,9 +588,11 @@ public class SequenceFeature implements FeatureLocationI
   /**
    * Answers an html-formatted report of feature details
    * 
+   * @param seqName
+   * 
    * @return
    */
-  public String getDetailsReport()
+  public String getDetailsReport(String seqName)
   {
     FeatureSourceI metadata = FeatureSources.getInstance()
             .getSource(source);
@@ -585,9 +600,10 @@ public class SequenceFeature implements FeatureLocationI
     StringBuilder sb = new StringBuilder(128);
     sb.append("<br>");
     sb.append("<table>");
+    sb.append(String.format(ROW_DATA, "Location", seqName,
+            begin == end ? begin
+                    : begin + (isContactFeature() ? ":" : "-") + end));
     sb.append(String.format(ROW_DATA, "Type", type, ""));
-    sb.append(String.format(ROW_DATA, "Start/end", begin == end ? begin
-            : begin + (isContactFeature() ? ":" : "-") + end, ""));
     String desc = StringUtils.stripHtmlTags(description);
     sb.append(String.format(ROW_DATA, "Description", desc, ""));
     if (!Float.isNaN(score) && score != 0f)
@@ -608,10 +624,6 @@ public class SequenceFeature implements FeatureLocationI
       for (Entry<String, Object> entry : ordered.entrySet())
       {
         String key = entry.getKey();
-        if (ATTRIBUTES.equals(key))
-        {
-          continue; // to avoid double reporting
-        }
 
         Object value = entry.getValue();
         if (value instanceof Map<?, ?>)
@@ -709,8 +721,6 @@ public class SequenceFeature implements FeatureLocationI
   {
     source = theSource;
   }
-
 }
 
 class SFSortByEnd implements Comparator<SequenceFeature>
index bdfd208..e01ad17 100644 (file)
@@ -23,7 +23,6 @@ package jalview.ext.ensembl;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.io.gff.SequenceOntologyI;
-import jalview.util.Platform;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -45,7 +44,9 @@ public class EnsemblCdna extends EnsemblSeqProxy
    * or ENSMUST or similar for other species
    * or CCDSnnnnn.nn with at least 3 digits
    */
-  private static Regex ACCESSION_REGEX;
+  private static final Regex ACCESSION_REGEX = new Regex(
+          "(ENS([A-Z]{3}|)[TG][0-9]{11}$)" + "|" + "(CCDS[0-9.]{3,}$)");
+
   /*
    * fetch exon features on genomic sequence (to identify the cdna regions)
    * and cds and variation features (to retain)
@@ -87,16 +88,6 @@ public class EnsemblCdna extends EnsemblSeqProxy
   @Override
   public Regex getAccessionValidator()
   {
-    if (ACCESSION_REGEX == null)
-    {
-      /*
-       * accepts ENSG/T/E/P with 11 digits
-       * or ENSMUSP or similar for other species
-       * or CCDSnnnnn.nn with at least 3 digits
-       */
-      ACCESSION_REGEX = Platform.newRegex(
-              "(ENS([A-Z]{3}|)[TG][0-9]{11}$)" + "|" + "(CCDS[0-9.]{3,}$)", null);
-    }
     return ACCESSION_REGEX;
   }
 
index e4c4365..e28cc7f 100644 (file)
@@ -27,7 +27,9 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.util.JSONUtils;
+import jalview.util.Platform;
 
+import java.io.BufferedReader;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -93,7 +95,10 @@ class EnsemblFeatures extends EnsemblRestClient
     List<String> queries = new ArrayList<>();
     queries.add(query);
     SequenceI seq = parseFeaturesJson(queries);
+    if (seq == null)
+       return null;
     return new Alignment(new SequenceI[] { seq });
+
   }
 
   /**
@@ -124,6 +129,7 @@ class EnsemblFeatures extends EnsemblRestClient
           int end = Integer.parseInt(obj.get("end").toString());
           String source = obj.get("source").toString();
           String strand = obj.get("strand").toString();
+          Object phase = obj.get("phase");
           String alleles = JSONUtils
                   .arrayToStringList((List<Object>) obj.get("alleles"));
           String clinSig = JSONUtils
@@ -149,6 +155,10 @@ class EnsemblFeatures extends EnsemblRestClient
           SequenceFeature sf = new SequenceFeature(type, desc, start, end,
                   source);
           sf.setStrand("1".equals(strand) ? "+" : "-");
+          if (phase != null)
+          {
+            sf.setPhase(phase.toString());
+          }
           setFeatureAttribute(sf, obj, "id");
           setFeatureAttribute(sf, obj, "Parent");
           setFeatureAttribute(sf, obj, "consequence_type");
index 63a6a6c..2d39fd5 100644 (file)
@@ -23,7 +23,6 @@ package jalview.ext.ensembl;
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureSettingsModelI;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.GeneLociI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
@@ -63,6 +62,8 @@ public class EnsemblGene extends EnsemblSeqProxy
       EnsemblFeatureType.exon, EnsemblFeatureType.cds,
       EnsemblFeatureType.variation };
 
+  private static final String CHROMOSOME = "chromosome";
+
   /**
    * Default constructor (to use rest.ensembl.org)
    */
@@ -185,7 +186,7 @@ public class EnsemblGene extends EnsemblSeqProxy
     if (geneLoci != null)
     {
       seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
-              geneLoci.getChromosomeId(), geneLoci.getMap());
+              geneLoci.getChromosomeId(), geneLoci.getMapping());
     }
     else
     {
@@ -207,7 +208,7 @@ public class EnsemblGene extends EnsemblSeqProxy
       return false;
     }
     String[] tokens = description.split(":");
-    if (tokens.length == 6 && tokens[0].startsWith(DBRefEntry.CHROMOSOME))
+    if (tokens.length == 6 && tokens[0].startsWith(CHROMOSOME))
     {
       String ref = tokens[1];
       String chrom = tokens[2];
@@ -458,7 +459,7 @@ public class EnsemblGene extends EnsemblSeqProxy
       return;
     }
 
-    MapList geneMapping = loci.getMap();
+    MapList geneMapping = loci.getMapping();
 
     List<int[]> exons = mapping.getFromRanges();
     List<int[]> transcriptLoci = new ArrayList<>();
index 71730de..97a8e74 100644 (file)
@@ -2,7 +2,9 @@ package jalview.ext.ensembl;
 
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefSource;
+import jalview.util.JSONUtils;
 
+import java.io.BufferedReader;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -17,20 +19,18 @@ import org.json.simple.parser.ParseException;
 public class EnsemblInfo extends EnsemblRestClient
 {
 
-  /**
+  /*
    * cached results of REST /info/divisions service, currently
-   * 
    * <pre>
    * { 
-   *  "ENSEMBLFUNGI", "http://rest.ensemblgenomes.org"},
-   *  "ENSEMBLBACTERIA", "http://rest.ensemblgenomes.org"},
-   *  "ENSEMBLPROTISTS", "http://rest.ensemblgenomes.org"},
-   *  "ENSEMBLMETAZOA", "http://rest.ensemblgenomes.org"},
-   *  "ENSEMBLPLANTS",  "http://rest.ensemblgenomes.org"},
-   *  "ENSEMBL", "http://rest.ensembl.org"
+   *  { "ENSEMBLFUNGI", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLBACTERIA", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLPROTISTS", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLMETAZOA", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLPLANTS",  "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBL", "http://rest.ensembl.org" }
    *  }
    * </pre>
-   * 
    * The values for EnsemblGenomes are retrieved by a REST call, that for
    * Ensembl is added programmatically for convenience of lookup
    */
@@ -92,13 +92,9 @@ public class EnsemblInfo extends EnsemblRestClient
     try
     {
       @SuppressWarnings("unchecked")
-      Iterator<Object> rvals = (Iterator<Object>) getJSON(
-              getDivisionsUrl(ensemblGenomesDomain), null, -1,
-              MODE_ITERATOR, null);
+         Iterator<Object> rvals = (Iterator<Object>) getJSON(getDivisionsUrl(ensemblGenomesDomain), null, -1, MODE_ITERATOR, null);
       if (rvals == null)
-      {
-        return;
-      }
+         return;
       while (rvals.hasNext())
       {
         String division = rvals.next().toString();
@@ -128,8 +124,7 @@ public class EnsemblInfo extends EnsemblRestClient
    * 
    * @return
    */
-  public Set<String> getDivisions()
-  {
+  public Set<String> getDivisions() {
     if (divisions == null)
     {
       fetchDivisions();
index fc37b8a..9b56d6b 100644 (file)
@@ -23,6 +23,8 @@ package jalview.ext.ensembl;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.GeneLociI;
+import jalview.datamodel.GeneLocus;
+import jalview.datamodel.Mapping;
 import jalview.util.MapList;
 
 import java.io.IOException;
@@ -268,34 +270,10 @@ public class EnsemblLookup extends EnsemblRestClient
           fromEnd });
       List<int[]> toRange = Collections.singletonList(new int[] { toStart,
           toEnd });
-      final MapList map = new MapList(fromRange, toRange, 1, 1);
-      return new GeneLociI()
-      {
-
-        @Override
-        public String getSpeciesId()
-        {
-          return species == null ? "" : species;
-        }
-
-        @Override
-        public String getAssemblyId()
-        {
-          return assembly;
-        }
-
-        @Override
-        public String getChromosomeId()
-        {
-          return chromosome;
-        }
-
-        @Override
-        public MapList getMap()
-        {
-          return map;
-        }
-      };
+      final Mapping map = new Mapping(
+              new MapList(fromRange, toRange, 1, 1));
+      return new GeneLocus(species == null ? "" : species, assembly,
+              chromosome, map);
     } catch (NullPointerException | NumberFormatException e)
     {
       Cache.log.error("Error looking up gene loci: " + e.getMessage());
index ae36749..e9cc9e1 100644 (file)
@@ -3,6 +3,8 @@ package jalview.ext.ensembl;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefSource;
 import jalview.datamodel.GeneLociI;
+import jalview.datamodel.GeneLocus;
+import jalview.datamodel.Mapping;
 import jalview.util.MapList;
 
 import java.io.IOException;
@@ -16,6 +18,18 @@ import java.util.Map;
 
 import org.json.simple.parser.ParseException;
 
+/**
+ * A client for the Ensembl REST service /map endpoint, to convert from
+ * coordinates of one genome assembly to another.
+ * <p>
+ * Note that species and assembly identifiers passed to this class must be valid
+ * in Ensembl. They are not case sensitive.
+ * 
+ * @author gmcarstairs
+ * @see https://rest.ensembl.org/documentation/info/assembly_map
+ * @see https://rest.ensembl.org/info/assembly/human?content-type=text/xml
+ * @see https://rest.ensembl.org/info/species?content-type=text/xml
+ */
 public class EnsemblMap extends EnsemblRestClient
 {
   private static final String MAPPED = "mapped";
@@ -340,34 +354,9 @@ GeneLociI parseIdMappingResponse(URL url, String accession,
       final String chr = chromosome;
       List<int[]> fromRange = Collections.singletonList(new int[] { 1,
           fromEnd });
-      final MapList map = new MapList(fromRange, regions, 1, 1);
-      return new GeneLociI()
-      {
-
-        @Override
-        public String getSpeciesId()
-        {
-          return species == null ? "" : species;
-        }
-
-        @Override
-        public String getAssemblyId()
-        {
-          return as;
-        }
-
-        @Override
-        public String getChromosomeId()
-        {
-          return chr;
-        }
-
-        @Override
-        public MapList getMap()
-        {
-          return map;
-        }
-      };
+      Mapping mapping = new Mapping(new MapList(fromRange, regions, 1, 1));
+      return new GeneLocus(species == null ? "" : species, as, chr,
+              mapping);
     } catch (IOException | ParseException | NumberFormatException e)
     {
       // ignore
index f586ed6..0280f16 100644 (file)
@@ -23,7 +23,6 @@ package jalview.ext.ensembl;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.util.Platform;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -43,7 +42,8 @@ public class EnsemblProtein extends EnsemblSeqProxy
    * or ENSMUSP or similar for other species
    * or CCDSnnnnn.nn with at least 3 digits
    */
-  private static Regex ACCESSION_REGEX;
+  private static final Regex ACCESSION_REGEX = new Regex(
+          "(ENS([A-Z]{3}|)P[0-9]{11}$)" + "|" + "(CCDS[0-9.]{3,}$)");
 
   /**
    * Default constructor (to use rest.ensembl.org)
@@ -119,11 +119,6 @@ public class EnsemblProtein extends EnsemblSeqProxy
   @Override
   public Regex getAccessionValidator()
   {
-    if (ACCESSION_REGEX == null)
-    {
-      ACCESSION_REGEX = Platform.newRegex(
-              "(ENS([A-Z]{3}|)P[0-9]{11}$)" + "|" + "(CCDS[0-9.]{3,}$)", null);
-    }
     return ACCESSION_REGEX;
   }
 
index 6d4bc79..771980c 100644 (file)
@@ -20,9 +20,6 @@
  */
 package jalview.ext.ensembl;
 
-import jalview.util.Platform;
-import jalview.util.StringUtils;
-
 import java.io.BufferedReader;
 import java.io.DataOutputStream;
 import java.io.IOException;
@@ -39,6 +36,9 @@ import javax.ws.rs.HttpMethod;
 
 import org.json.simple.parser.ParseException;
 
+import jalview.util.Platform;
+import jalview.util.StringUtils;
+
 /**
  * Base class for Ensembl REST service clients
  * 
@@ -75,7 +75,7 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
 
   private static final String REST_CHANGE_LOG = "https://github.com/Ensembl/ensembl-rest/wiki/Change-log";
 
-  private static final Map<String, EnsemblData> domainData;
+  private static Map<String, EnsemblData> domainData;
 
   private final static long AVAILABILITY_RETEST_INTERVAL = 10000L; // 10 seconds
 
@@ -319,10 +319,10 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
 
     InputStream response = connection.getInputStream();
 
-    Platform.timeCheck(null, Platform.TIME_MARK);
+    // Platform.timeCheck(null, Platform.TIME_MARK);
     Object ret = Platform.parseJSON(response);
-    Platform.timeCheck("EnsemblRestClient.getJSON " + url,
-            Platform.TIME_MARK);
+    // Platform.timeCheck("EnsemblRestClient.getJSON " + url,
+    // Platform.TIME_MARK);
 
     return ret;
   }
index bdaef0b..11f8b7b 100644 (file)
  */
 package jalview.ext.ensembl;
 
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.json.simple.parser.ParseException;
+
 import jalview.analysis.AlignmentUtils;
 import jalview.analysis.Dna;
 import jalview.bin.Cache;
@@ -40,18 +51,6 @@ import jalview.util.Comparison;
 import jalview.util.DBRefUtils;
 import jalview.util.IntRangeComparator;
 import jalview.util.MapList;
-import jalview.util.Platform;
-
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import org.json.simple.parser.ParseException;
 
 /**
  * Base class for Ensembl sequence fetchers
@@ -211,8 +210,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       EnsemblFeatures gffFetcher = new EnsemblFeatures(getDomain());
       EnsemblFeatureType[] features = getFeaturesToFetch();      
       
-      Platform.timeCheck("ESP.getsequencerec1", Platform.TIME_MARK);     
-
+      // Platform.timeCheck("ESP.getsequencerec1", Platform.TIME_MARK);
       
       AlignmentI geneFeatures = gffFetcher.getSequenceRecords(accId,
               features);
@@ -221,7 +219,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
         genomicSequence = geneFeatures.getSequenceAt(0);
       }
       
-      Platform.timeCheck("ESP.getsequencerec2", Platform.TIME_MARK);     
+      // Platform.timeCheck("ESP.getsequencerec2", Platform.TIME_MARK);
       
       if (genomicSequence != null)
       {
@@ -236,7 +234,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
            * fetch and map protein product, and add it as a cross-reference
            * of the retrieved sequence
            */
-            Platform.timeCheck("ESP.transferFeatures", Platform.TIME_MARK);      
+          // Platform.timeCheck("ESP.transferFeatures", Platform.TIME_MARK);
           addProteinProduct(querySeq);
         }
       }
@@ -245,7 +243,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       System.err.println(
               "Error transferring Ensembl features: " + e.getMessage());
     }
-    Platform.timeCheck("ESP.addfeat done", Platform.TIME_MARK);          
+    // Platform.timeCheck("ESP.addfeat done", Platform.TIME_MARK);
   }
 
   /**
@@ -340,8 +338,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
          * copy exon features to protein, compute peptide variants from dna 
          * variants and add as features on the protein sequence ta-da
          */
-        AlignmentUtils.computeProteinFeatures(querySeq, proteinSeq,
-                mapList);
+        // JAL-3187 render on the fly instead
+        // AlignmentUtils.computeProteinFeatures(querySeq, proteinSeq, mapList);
       }
     } catch (Exception e)
     {
@@ -359,7 +357,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   protected void getCrossReferences(SequenceI seq)
   {
          
-      Platform.timeCheck("ESP. getdataseq ", Platform.TIME_MARK);        
+    // Platform.timeCheck("ESP. getdataseq ", Platform.TIME_MARK);
 
          
     while (seq.getDatasetSequence() != null)
@@ -367,7 +365,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       seq = seq.getDatasetSequence();
     }
 
-    Platform.timeCheck("ESP. getxref ", Platform.TIME_MARK);     
+    // Platform.timeCheck("ESP. getxref ", Platform.TIME_MARK);
 
     EnsemblXref xrefFetcher = new EnsemblXref(getDomain(), getDbSource(),
             getEnsemblDataVersion());
@@ -397,8 +395,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
 
     seq.addDBRef(self);
     
-    Platform.timeCheck("ESP. seqprox done ", Platform.TIME_MARK);        
-
+    // Platform.timeCheck("ESP. seqprox done ", Platform.TIME_MARK);
   }
 
   /**
@@ -419,13 +416,11 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       inProgress = false;
       throw new JalviewException("ENSEMBL Rest API not available.");
     }
-       Platform.timeCheck("EnsemblSeqProx.fetchSeq ", Platform.TIME_MARK);
+    // Platform.timeCheck("EnsemblSeqProx.fetchSeq ", Platform.TIME_MARK);
 
     List<SequenceI> seqs = parseSequenceJson(ids);
     if (seqs == null)
-    {
-      return alignment;
-    }
+       return alignment;
 
     if (seqs.isEmpty())
     {
@@ -488,13 +483,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
        * for now, assumes only one sequence returned; refactor if needed
        * in future to handle a JSONArray with more than one
        */
-       
-       Platform.timeCheck("ENS seqproxy", Platform.TIME_MARK);
+      // Platform.timeCheck("ENS seqproxy", Platform.TIME_MARK);
       Map<String, Object> val = (Map<String, Object>) getJSON(null, ids, -1, MODE_MAP, null);
       if (val == null)
-      {
-        return null;
-      }
+         return null;
       Object s = val.get("desc");
       String desc = s == null ? null : s.toString();
       s = val.get("id");
@@ -517,7 +509,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       System.err.println("Error processing JSON response: " + e.toString());
       // ignore
     }
-       Platform.timeCheck("ENS seqproxy2", Platform.TIME_MARK);
+    // Platform.timeCheck("ENS seqproxy2", Platform.TIME_MARK);
     return result;
   }
 
@@ -769,18 +761,6 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
     String comp = complement.toString();
     sf.setValue(Gff3Helper.ALLELES, comp);
     sf.setDescription(comp);
-
-    /*
-     * replace value of "alleles=" in sf.ATTRIBUTES as well
-     * so 'output as GFF' shows reverse complement alleles
-     */
-    String atts = sf.getAttributes();
-    if (atts != null)
-    {
-      atts = atts.replace(Gff3Helper.ALLELES + "=" + alleles,
-              Gff3Helper.ALLELES + "=" + comp);
-      sf.setAttributes(atts);
-    }
   }
 
   /**
@@ -842,9 +822,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       return false;
     }
 
-
-    Platform.timeCheck("ESP. xfer " + sfs.size(), Platform.TIME_MARK);   
-
+    // Platform.timeCheck("ESP. xfer " + sfs.size(), Platform.TIME_MARK);
 
     boolean result = transferFeatures(sfs, targetSequence, mapping,
             accessionId);
index 21f9f7e..7454eb6 100644 (file)
@@ -23,7 +23,6 @@ package jalview.ext.ensembl;
 import jalview.analysis.AlignmentUtils;
 import jalview.bin.Cache;
 import jalview.datamodel.DBRefSource;
-import jalview.util.Platform;
 import jalview.ws.seqfetcher.DbSourceProxyImpl;
 
 import com.stevesoft.pat.Regex;
@@ -46,7 +45,14 @@ abstract class EnsemblSequenceFetcher extends DbSourceProxyImpl
   // ensemblgenomes REST service merged to ensembl 9th April 2019
   protected static final String DEFAULT_ENSEMBL_GENOMES_BASEURL = DEFAULT_ENSEMBL_BASEURL;
 
-  private static Regex ACCESSION_REGEX;
+  /*
+   * accepts ENSG/T/E/P with 11 digits
+   * or ENSMUSP or similar for other species
+   * or CCDSnnnnn.nn with at least 3 digits
+   */
+  private static final Regex ACCESSION_REGEX = new Regex(
+          "(ENS([A-Z]{3}|)[GTEP]{1}[0-9]{11}$)" + "|"
+                  + "(CCDS[0-9.]{3,}$)");
 
   protected final String ensemblGenomesDomain;
 
@@ -115,17 +121,6 @@ abstract class EnsemblSequenceFetcher extends DbSourceProxyImpl
   @Override
   public Regex getAccessionValidator()
   {
-    if (ACCESSION_REGEX == null)
-    {
-      /*
-       * accepts ENSG/T/E/P with 11 digits
-       * or ENSMUSP or similar for other species
-       * or CCDSnnnnn.nn with at least 3 digits
-       */
-      ACCESSION_REGEX = Platform
-              .newRegex("(ENS([A-Z]{3}|)[GTEP]{1}[0-9]{11}$)" + "|"
-                      + "(CCDS[0-9.]{3,}$)", null);
-    }
     return ACCESSION_REGEX;
   }
 
index 14c057f..2859e0f 100644 (file)
@@ -1,14 +1,16 @@
 package jalview.ext.htsjdk;
 
-import htsjdk.samtools.util.CloseableIterator;
-import htsjdk.variant.variantcontext.VariantContext;
-import htsjdk.variant.vcf.VCFFileReader;
-import htsjdk.variant.vcf.VCFHeader;
+import jalview.bin.Cache;
 
 import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
 
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.VariantContext;
+import htsjdk.variant.vcf.VCFFileReader;
+import htsjdk.variant.vcf.VCFHeader;
+
 /**
  * A thin wrapper for htsjdk classes to read either plain, or compressed, or
  * compressed and indexed VCF files
@@ -19,36 +21,52 @@ public class VCFReader implements Closeable, Iterable<VariantContext>
 
   private static final String TBI_EXTENSION = ".tbi";
 
+  private static final String CSI_EXTENSION = ".csi";
+
   private boolean indexed;
 
   private VCFFileReader reader;
 
   /**
-   * Constructor given a raw or compressed VCF file or a (tabix) index file
+   * Constructor given a raw or compressed VCF file or a (csi or tabix) index file
    * <p>
-   * For now, file type is inferred from its suffix: .gz or .bgz for compressed
-   * data, .tbi for an index file, anything else is assumed to be plain text
-   * VCF.
+   * If the file path ends in ".tbi" or ".csi", <em>or</em> appending one of these
+   * extensions gives a valid file path, open as indexed, else as unindexed.
    * 
    * @param f
    * @throws IOException
    */
   public VCFReader(String filePath) throws IOException
   {
-    if (filePath.endsWith(GZ))
+    indexed = false;
+    if (filePath.endsWith(TBI_EXTENSION)
+            || filePath.endsWith(CSI_EXTENSION))
     {
-      if (new File(filePath + TBI_EXTENSION).exists())
-      {
-        indexed = true;
-      }
+      indexed = true;
+      filePath = filePath.substring(0, filePath.length() - 4);
     }
-    else if (filePath.endsWith(TBI_EXTENSION))
+    else if (new File(filePath + TBI_EXTENSION).exists())
+    {
+      indexed = true;
+    }
+    else if (new File(filePath + CSI_EXTENSION).exists())
     {
       indexed = true;
-      filePath = filePath.substring(0, filePath.length() - 4);
     }
 
-    reader = new VCFFileReader(new File(filePath), indexed);
+    /*
+     * we pass the name of the unindexed file to htsjdk,
+     * with a flag to assert whether it is indexed
+     */
+    File file = new File(filePath);
+    if (file.exists())
+    {
+      reader = new VCFFileReader(file, indexed);
+    }
+    else
+    {
+      Cache.log.error("File not found: " + filePath);
+    }
   }
 
   @Override
@@ -88,9 +106,10 @@ public class VCFReader implements Closeable, Iterable<VariantContext>
   public CloseableIterator<VariantContext> query(final String chrom,
           final int start, final int end)
   {
-   if (reader == null) {
-     return null;
-   }
+    if (reader == null)
+    {
+      return null;
+    }
     if (indexed)
     {
       return reader.query(chrom, start, end);
@@ -145,7 +164,7 @@ public class VCFReader implements Closeable, Iterable<VariantContext>
           int vend = variant.getEnd();
           // todo what is the undeprecated way to get
           // the chromosome for the variant?
-          if (chrom.equals(variant.getChr()) && (vstart <= end)
+          if (chrom.equals(variant.getContig()) && (vstart <= end)
                   && (vend >= start))
           {
             return variant;
index fa31fd9..453152e 100644 (file)
@@ -44,12 +44,12 @@ import java.awt.event.ComponentEvent;
 import java.awt.event.ComponentListener;
 import java.io.File;
 import java.net.URL;
-import java.security.AccessControlException;
 import java.util.ArrayList;
 import java.util.BitSet;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
+import java.util.StringTokenizer;
 import java.util.Vector;
 
 import org.jmol.adapter.smarter.SmarterJmolAdapter;
@@ -65,6 +65,8 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
         implements JmolStatusListener, JmolSelectionListener,
         ComponentListener
 {
+  private String lastMessage;
+
   boolean allChainsSelected = false;
 
   /*
@@ -89,8 +91,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
 
   String lastCommand;
 
-  String lastMessage;
-
   boolean loadedInline;
 
   StringBuffer resetLastRes = new StringBuffer();
@@ -619,74 +619,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
    */
   private int _modelFileNameMap[];
 
-  // ////////////////////////////////
-  // /StructureListener
-  // @Override
-  public synchronized String[] getPdbFilex()
-  {
-    if (viewer == null)
-    {
-      return new String[0];
-    }
-    if (modelFileNames == null)
-    {
-      List<String> mset = new ArrayList<>();
-      _modelFileNameMap = new int[viewer.ms.mc];
-      String m = viewer.ms.getModelFileName(0);
-      if (m != null)
-      {
-        String filePath = m;
-        try
-        {
-          filePath = new File(m).getAbsolutePath();
-        } catch (AccessControlException x)
-        {
-          // usually not allowed to do this in applet
-          System.err.println(
-                  "jmolBinding: Using local file string from Jmol: " + m);
-        }
-        if (filePath.indexOf("/file:") != -1)
-        {
-          // applet path with docroot - discard as format won't match pdbfile
-          filePath = m;
-        }
-        mset.add(filePath);
-        _modelFileNameMap[0] = 0; // filename index for first model is always 0.
-      }
-      int j = 1;
-      for (int i = 1; i < viewer.ms.mc; i++)
-      {
-        m = viewer.ms.getModelFileName(i);
-        String filePath = m;
-        if (m != null)
-        {
-          try
-          {
-            filePath = new File(m).getAbsolutePath();
-          } catch (AccessControlException x)
-          {
-            // usually not allowed to do this in applet, so keep raw handle
-            // System.err.println("jmolBinding: Using local file string from
-            // Jmol: "+m);
-          }
-        }
-
-        /*
-         * add this model unless it is read from a structure file we have
-         * already seen (example: 2MJW is an NMR structure with 10 models)
-         */
-        if (!mset.contains(filePath))
-        {
-          mset.add(filePath);
-          _modelFileNameMap[j] = i; // record the model index for the filename
-          j++;
-        }
-      }
-      modelFileNames = mset.toArray(new String[mset.size()]);
-    }
-    return modelFileNames;
-  }
-
   @Override
   public synchronized String[] getStructureFiles()
   {
@@ -827,7 +759,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     viewer.openStringInline(string);
   }
 
-  public void mouseOverStructure(int atomIndex, String strInfo)
+  protected void mouseOverStructure(int atomIndex, final String strInfo)
   {
     int pdbResNum;
     int alocsep = strInfo.indexOf("^");
@@ -881,7 +813,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
       try
       {
         // recover PDB filename for the model hovered over.
-        int mnumber = new Integer(mdlId).intValue() - 1;
+        int mnumber = Integer.valueOf(mdlId).intValue() - 1;
         if (_modelFileNameMap != null)
         {
           int _mp = _modelFileNameMap.length - 1;
@@ -908,18 +840,36 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
       } catch (Exception e)
       {
       }
-      ;
     }
-    if (lastMessage == null || !lastMessage.equals(strInfo))
+
+    /*
+     * highlight position on alignment(s); if some text is returned, 
+     * show this as a second line on the structure hover tooltip
+     */
+    String label = getSsm().mouseOverStructure(pdbResNum, chainId,
+            pdbfilename);
+    if (label != null)
     {
-      getSsm().mouseOverStructure(pdbResNum, chainId, pdbfilename);
+      // change comma to pipe separator (newline token for Jmol)
+      label = label.replace(',', '|');
+      StringTokenizer toks = new StringTokenizer(strInfo, " ");
+      StringBuilder sb = new StringBuilder();
+      sb.append("select ").append(String.valueOf(pdbResNum)).append(":")
+              .append(chainId).append("/1");
+      sb.append(";set hoverLabel \"").append(toks.nextToken()).append(" ")
+              .append(toks.nextToken());
+      sb.append("|").append(label).append("\"");
+      evalStateCommand(sb.toString());
     }
-
-    lastMessage = strInfo;
   }
 
   public void notifyAtomHovered(int atomIndex, String strInfo, String data)
   {
+    if (strInfo.equals(lastMessage))
+    {
+      return;
+    }
+    lastMessage = strInfo;
     if (data != null)
     {
       System.err.println("Ignoring additional hover info: " + data
index 3b7044b..de1d90c 100644 (file)
  */
 package jalview.ext.paradise;
 
-import jalview.util.JSONUtils;
-import jalview.util.MessageManager;
-import jalview.ws.HttpClientUtils;
-
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -40,6 +36,10 @@ import org.apache.http.message.BasicNameValuePair;
 import org.json.simple.parser.ContentHandler;
 import org.json.simple.parser.ParseException;
 
+import jalview.util.JSONUtils;
+import jalview.util.MessageManager;
+import jalview.ws.HttpClientUtils;
+
 /**
  * simple methods for calling the various paradise RNA tools
  * 
@@ -137,13 +137,13 @@ public class Annotate3D
   public static Iterator<Reader> getRNAMLForPDBFileAsString(String pdbfile)
           throws Exception
   {
-    List<NameValuePair> vals = new ArrayList<NameValuePair>();
+    List<NameValuePair> vals = new ArrayList<>();
     vals.add(new BasicNameValuePair("tool", "rnaview"));
     vals.add(new BasicNameValuePair("data", pdbfile));
     vals.add(new BasicNameValuePair("output", "rnaml"));
     // return processJsonResponseFor(HttpClientUtils.doHttpUrlPost(twoDtoolsURL,
     // vals));
-    ArrayList<Reader> readers = new ArrayList<Reader>();
+    ArrayList<Reader> readers = new ArrayList<>();
     final BufferedReader postResponse = HttpClientUtils
             .doHttpUrlPost(twoDtoolsURL, vals, 0, 0);
     readers.add(postResponse);
@@ -151,85 +151,14 @@ public class Annotate3D
 
   }
 
-  /**
-   * @param respons
-   * @return
-   * @throws Exception
-   */
   public static Iterator<Reader> processJsonResponseFor(Reader respons)
           throws Exception
   {
-         // BH 2019 never called?
+    org.json.simple.parser.JSONParser jp = new org.json.simple.parser.JSONParser();
     try
     {
-      @SuppressWarnings("unchecked")
-       final Iterator<Object> rvals = ((List<Object>) JSONUtils.parse(respons)).iterator();
-      return new Iterator<Reader>()
-      {
-        @Override
-        public boolean hasNext()
-        {
-          return rvals.hasNext();
-        }
-
-        @SuppressWarnings("unchecked")
-               @Override
-        public Reader next()
-        {
-          Map<String, Object> val = (Map<String, Object>) rvals.next();
-
-          Object sval = null;
-          try
-          {
-            sval = val.get("2D");
-          } catch (Exception x)
-          {
-            x.printStackTrace();
-          }
-          ;
-          if (sval == null)
-          {
-            System.err.println(
-                    "DEVELOPER WARNING: Annotate3d didn't return a '2D' tag in its response. Consider checking output of server. Response was :"
-                            + val.toString());
-
-            sval = "";
-          }
-          return new StringReader(sval.toString());
-
-        }
-
-        @Override
-        public void remove()
-        {
-          throw new Error(
-                  MessageManager.getString("error.not_implemented_remove"));
-
-        }
-
-        @Override
-        protected Object clone() throws CloneNotSupportedException
-        {
-          throw new CloneNotSupportedException(
-                  MessageManager.getString("error.not_implemented_clone"));
-        }
-
-        @Override
-        public boolean equals(Object obj)
-        {
-          return super.equals(obj);
-        }
-
-        @Override
-        protected void finalize() throws Throwable
-        {
-          while (rvals.hasNext())
-          {
-            rvals.next();
-          }
-          super.finalize();
-        }
-      };
+      final RvalsIterator rvals = new RvalsIterator(respons);
+      return rvals;
     } catch (Exception foo)
     {
       throw new Exception(MessageManager.getString(
@@ -242,7 +171,7 @@ public class Annotate3D
   public static Iterator<Reader> getRNAMLForPDBId(String pdbid)
           throws Exception
   {
-    List<NameValuePair> vals = new ArrayList<NameValuePair>();
+    List<NameValuePair> vals = new ArrayList<>();
     vals.add(new BasicNameValuePair("tool", "rnaview"));
     vals.add(new BasicNameValuePair("pdbid", pdbid));
     vals.add(new BasicNameValuePair("output", "rnaml"));
@@ -250,9 +179,97 @@ public class Annotate3D
             + pdbid + "&output=rnaml");
     // return processJsonResponseFor(new
     // InputStreamReader(geturl.openStream()));
-    ArrayList<Reader> readers = new ArrayList<Reader>();
+    ArrayList<Reader> readers = new ArrayList<>();
     readers.add(new InputStreamReader(geturl.openStream()));
     return readers.iterator();
   }
 
 }
+
+class RvalsIterator implements Iterator, AutoCloseable
+{
+  private Iterator<Object> rvals;
+
+  @SuppressWarnings("unchecked")
+  protected RvalsIterator(Reader respons) throws IOException, ParseException
+  {
+    /*
+     * as in 2.11.1 (pre-JalviewJS)
+     */
+    // final JSONArray responses = (JSONArray) jp.parse(respons);
+    // this.rvals = responses.iterator();
+
+    /*
+     * as in JalviewJS (with comment that this code is never called)
+     */
+    this.rvals = ((List<Object>) JSONUtils.parse(respons)).iterator();
+  }
+
+  @Override
+  public boolean hasNext()
+  {
+    return rvals.hasNext();
+  }
+
+  @Override
+  public Reader next()
+  {
+    // JSONObject val = (JSONObject) rvals.next(); // 2.11.1
+    @SuppressWarnings("unchecked")
+    Map<String, Object> val = (Map<String, Object>) rvals.next();
+
+    Object sval = null;
+    try
+    {
+      sval = val.get("2D");
+    } catch (Exception x)
+    {
+      x.printStackTrace();
+    }
+
+    if (sval == null)
+    {
+      System.err.println(
+              "DEVELOPER WARNING: Annotate3d didn't return a '2D' tag in its response. Consider checking output of server. Response was :"
+                      + val.toString());
+
+      sval = "";
+    }
+    // 2.11.1:
+    // return new StringReader(
+    // (sval instanceof JSONObject) ? ((JSONObject) sval).toString()
+    // : sval.toString());
+    return new StringReader(sval.toString());
+
+  }
+
+  @Override
+  public void remove()
+  {
+    throw new Error(
+            MessageManager.getString("error.not_implemented_remove"));
+
+  }
+
+  @Override
+  protected Object clone() throws CloneNotSupportedException
+  {
+    throw new CloneNotSupportedException(
+            MessageManager.getString("error.not_implemented_clone"));
+  }
+
+  @Override
+  public boolean equals(Object obj)
+  {
+    return super.equals(obj);
+  }
+
+  @Override
+  public void close()
+  {
+    while (rvals.hasNext())
+    {
+      rvals.next();
+    }
+  }
+}
index dad8511..3caaac3 100644 (file)
@@ -26,8 +26,10 @@ import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.gui.Desktop;
 import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureMappingcommandSet;
@@ -104,7 +106,7 @@ public class ChimeraCommands
      * delimited). If length limit issues arise, refactor to return one color
      * command per colour.
      */
-    List<String> commands = new ArrayList<String>();
+    List<String> commands = new ArrayList<>();
     StringBuilder sb = new StringBuilder(256);
     boolean firstColour = true;
     for (Object key : colourMap.keySet())
@@ -196,7 +198,7 @@ public class ChimeraCommands
     AlignViewportI viewport = viewPanel.getAlignViewport();
     HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
     AlignmentI al = viewport.getAlignment();
-    Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<Object, AtomSpecModel>();
+    Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
     Color lastColour = null;
 
     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
@@ -257,7 +259,7 @@ public class ChimeraCommands
               {
                 if (startPos != -1)
                 {
-                  addColourRange(colourMap, lastColour, pdbfnum, startPos,
+                  addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
                           lastPos, lastChain);
                 }
                 startPos = pos;
@@ -269,7 +271,7 @@ public class ChimeraCommands
             // final colour range
             if (lastColour != null)
             {
-              addColourRange(colourMap, lastColour, pdbfnum, startPos,
+              addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
                       lastPos, lastChain);
             }
             // break;
@@ -281,26 +283,32 @@ public class ChimeraCommands
   }
 
   /**
-   * Helper method to add one contiguous colour range to the colour map.
+   * Helper method to add one contiguous range to the AtomSpec model for the given
+   * value (creating the model if necessary). As used by Jalview, {@code value} is
+   * <ul>
+   * <li>a colour, when building a 'colour structure by sequence' command</li>
+   * <li>a feature value, when building a 'set Chimera attributes from features'
+   * command</li>
+   * </ul>
    * 
    * @param map
-   * @param key
+   * @param value
    * @param model
    * @param startPos
    * @param endPos
    * @param chain
    */
-  protected static void addColourRange(Map<Object, AtomSpecModel> map,
-          Object key, int model, int startPos, int endPos, String chain)
+  protected static void addAtomSpecRange(Map<Object, AtomSpecModel> map,
+          Object value, int model, int startPos, int endPos, String chain)
   {
     /*
      * Get/initialize map of data for the colour
      */
-    AtomSpecModel atomSpec = map.get(key);
+    AtomSpecModel atomSpec = map.get(value);
     if (atomSpec == null)
     {
       atomSpec = new AtomSpecModel();
-      map.put(key, atomSpec);
+      map.put(value, atomSpec);
     }
 
     atomSpec.addRange(model, startPos, endPos, chain);
@@ -349,7 +357,7 @@ public class ChimeraCommands
           StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
           AlignmentViewPanel viewPanel)
   {
-    Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<String, Map<Object, AtomSpecModel>>();
+    Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
 
     FeatureRenderer fr = viewPanel.getFeatureRenderer();
     if (fr == null)
@@ -357,8 +365,27 @@ public class ChimeraCommands
       return theMap;
     }
 
+    AlignViewportI viewport = viewPanel.getAlignViewport();
     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
-    if (visibleFeatures.isEmpty())
+
+    /*
+     * if alignment is showing features from complement, we also transfer
+     * these features to the corresponding mapped structure residues
+     */
+    boolean showLinkedFeatures = viewport.isShowComplementFeatures();
+    List<String> complementFeatures = new ArrayList<>();
+    FeatureRenderer complementRenderer = null;
+    if (showLinkedFeatures)
+    {
+      AlignViewportI comp = fr.getViewport().getCodingComplement();
+      if (comp != null)
+      {
+        complementRenderer = Desktop.getAlignFrameFor(comp)
+                .getFeatureRenderer();
+        complementFeatures = complementRenderer.getDisplayedFeatureTypes();
+      }
+    }
+    if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
     {
       return theMap;
     }
@@ -379,16 +406,23 @@ public class ChimeraCommands
         {
           final SequenceI seq = seqs[pdbfnum][seqNo];
           int sp = alignment.findIndex(seq);
-          if (mapping[m].getSequence() == seq && sp > -1)
+          StructureMapping structureMapping = mapping[m];
+          if (structureMapping.getSequence() == seq && sp > -1)
           {
             /*
              * found a sequence with a mapping to a structure;
              * now scan its features
              */
-            SequenceI asp = alignment.getSequenceAt(sp);
-
-            scanSequenceFeatures(visibleFeatures, mapping[m], asp, theMap,
-                    pdbfnum);
+            if (!visibleFeatures.isEmpty())
+            {
+              scanSequenceFeatures(visibleFeatures, structureMapping, seq,
+                      theMap, pdbfnum);
+            }
+            if (showLinkedFeatures)
+            {
+              scanComplementFeatures(complementRenderer, structureMapping,
+                      seq, theMap, pdbfnum);
+            }
           }
         }
       }
@@ -397,9 +431,85 @@ public class ChimeraCommands
   }
 
   /**
-   * Inspect features on the sequence; for each feature that is visible,
-   * determine its mapped ranges in the structure (if any) according to the
-   * given mapping, and add them to the map
+   * Scans visible features in mapped positions of the CDS/peptide complement, and
+   * adds any found to the map of attribute values/structure positions
+   * 
+   * @param complementRenderer
+   * @param structureMapping
+   * @param seq
+   * @param theMap
+   * @param modelNumber
+   */
+  protected static void scanComplementFeatures(
+          FeatureRenderer complementRenderer,
+          StructureMapping structureMapping, SequenceI seq,
+          Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
+  {
+    /*
+     * for each sequence residue mapped to a structure position...
+     */
+    for (int seqPos : structureMapping.getMapping().keySet())
+    {
+      /*
+       * find visible complementary features at mapped position(s)
+       */
+      MappedFeatures mf = complementRenderer
+              .findComplementFeaturesAtResidue(seq, seqPos);
+      if (mf != null)
+      {
+        for (SequenceFeature sf : mf.features)
+        {
+          String type = sf.getType();
+
+          /*
+           * Don't copy features which originated from Chimera
+           */
+          if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
+                  .equals(sf.getFeatureGroup()))
+          {
+            continue;
+          }
+
+          /*
+           * record feature 'value' (score/description/type) as at the
+           * corresponding structure position
+           */
+          List<int[]> mappedRanges = structureMapping
+                  .getPDBResNumRanges(seqPos, seqPos);
+
+          if (!mappedRanges.isEmpty())
+          {
+            String value = sf.getDescription();
+            if (value == null || value.length() == 0)
+            {
+              value = type;
+            }
+            float score = sf.getScore();
+            if (score != 0f && !Float.isNaN(score))
+            {
+              value = Float.toString(score);
+            }
+            Map<Object, AtomSpecModel> featureValues = theMap.get(type);
+            if (featureValues == null)
+            {
+              featureValues = new HashMap<>();
+              theMap.put(type, featureValues);
+            }
+            for (int[] range : mappedRanges)
+            {
+              addAtomSpecRange(featureValues, value, modelNumber, range[0],
+                      range[1], structureMapping.getChain());
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Inspect features on the sequence; for each feature that is visible, determine
+   * its mapped ranges in the structure (if any) according to the given mapping,
+   * and add them to the map.
    * 
    * @param visibleFeatures
    * @param mapping
@@ -418,15 +528,14 @@ public class ChimeraCommands
       String type = sf.getType();
 
       /*
-       * Only copy visible features, don't copy any which originated
-       * from Chimera, and suppress uninteresting ones (e.g. RESNUM)
+       * Don't copy features which originated from Chimera
        */
-      boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
-              .equals(sf.getFeatureGroup());
-      if (isFromViewer)
+      if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
+              .equals(sf.getFeatureGroup()))
       {
         continue;
       }
+
       List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
               sf.getEnd());
 
@@ -445,12 +554,12 @@ public class ChimeraCommands
         Map<Object, AtomSpecModel> featureValues = theMap.get(type);
         if (featureValues == null)
         {
-          featureValues = new HashMap<Object, AtomSpecModel>();
+          featureValues = new HashMap<>();
           theMap.put(type, featureValues);
         }
         for (int[] range : mappedRanges)
         {
-          addColourRange(featureValues, value, modelNumber, range[0],
+          addAtomSpecRange(featureValues, value, modelNumber, range[0],
                   range[1], mapping.getChain());
         }
       }
@@ -475,7 +584,7 @@ public class ChimeraCommands
   protected static List<String> buildSetAttributeCommands(
           Map<String, Map<Object, AtomSpecModel>> featureMap)
   {
-    List<String> commands = new ArrayList<String>();
+    List<String> commands = new ArrayList<>();
     for (String featureType : featureMap.keySet())
     {
       String attributeName = makeAttributeName(featureType);
index 5a86fa1..0d631e6 100644 (file)
@@ -82,10 +82,10 @@ public class SequenceOntology implements SequenceOntologyI
    */
   public SequenceOntology()
   {
-    termsFound = new ArrayList<>();
-    termsNotFound = new ArrayList<>();
-    termsByDescription = new HashMap<>();
-    termIsA = new HashMap<>();
+    termsFound = new ArrayList<String>();
+    termsNotFound = new ArrayList<String>();
+    termsByDescription = new HashMap<String, Term>();
+    termIsA = new HashMap<Term, List<Term>>();
 
     loadOntologyZipFile("so-xp-simple.obo");
   }
@@ -404,7 +404,7 @@ public class SequenceOntology implements SequenceOntologyI
    */
   protected synchronized void findParents(Term childTerm)
   {
-    List<Term> result = new ArrayList<>();
+    List<Term> result = new ArrayList<Term>();
     for (Triple triple : ontology.getTriples(childTerm, null, isA))
     {
       Term parent = triple.getObject();
index e44ccdc..b5c257b 100644 (file)
@@ -33,8 +33,8 @@ import jalview.api.AlignViewControllerGuiI;
 import jalview.api.AlignViewControllerI;
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
-//from JalviewLite imports import jalview.api.FeatureRenderer;
 import jalview.api.FeatureSettingsControllerI;
+import jalview.api.FeatureSettingsModelI;
 import jalview.api.SplitContainerI;
 import jalview.api.ViewStyleI;
 import jalview.api.analysis.SimilarityParamsI;
@@ -300,6 +300,9 @@ public class AlignFrame extends GAlignFrame
     }
 
     viewport = new AlignViewport(al, hiddenColumns, sequenceSetId, viewId);
+    // BH! new alignPanel must come later
+    // alignPanel = new AlignmentPanel(this, viewport);
+    // addAlignmentPanel(alignPanel, true);
 
     init();
   }
@@ -320,7 +323,10 @@ public class AlignFrame extends GAlignFrame
     {
       viewport.hideSequence(hiddenSeqs);
     }
-    init();
+ // BH! new alignPanel must come later
+    //alignPanel = new AlignmentPanel(this, viewport);
+    //addAlignmentPanel(alignPanel, true);
+init();
   }
 
   /**
@@ -335,6 +341,8 @@ public class AlignFrame extends GAlignFrame
   {
     viewport = ap.av;
     alignPanel = ap;
+    // BH! adding must come later
+    // addAlignmentPanel(ap, false);
     init();
   }
 
@@ -344,6 +352,7 @@ public class AlignFrame extends GAlignFrame
    */
   void init()
   {
+          // BH! Here is where we create the panel; note the sequence
     boolean newPanel = (alignPanel == null);
     viewport.setShowAutocalculatedAbove(isShowAutoCalculatedAbove());
     if (newPanel)
@@ -366,7 +375,8 @@ public class AlignFrame extends GAlignFrame
     addAlignmentPanel(alignPanel, newPanel);
 
     // setBackground(Color.white); // BH 2019
-                 
+         
+    // BH meaning "without display, one way or another"
     if (!Jalview.isHeadlessMode())
     {
       progressBar = new ProgressBar(this.statusPanel, this.statusBar);
@@ -386,7 +396,7 @@ public class AlignFrame extends GAlignFrame
       // modifyPID.setEnabled(false);
     }
 
-    String sortby = jalview.bin.Cache.getDefault(Preferences.SORT_ALIGNMENT,
+    String sortby = Cache.getDefault(Preferences.SORT_ALIGNMENT,
             "No sort");
 
     if (sortby.equals("Id"))
@@ -398,6 +408,9 @@ public class AlignFrame extends GAlignFrame
       sortPairwiseMenuItem_actionPerformed(null);
     }
 
+    alignPanel.av
+            .setShowAutocalculatedAbove(isShowAutoCalculatedAbove());
+
     setMenusFromViewport(viewport);
     buildSortByAnnotationScoresMenu();
     calculateTree.addActionListener(new ActionListener()
@@ -414,24 +427,10 @@ public class AlignFrame extends GAlignFrame
     if (Desktop.getDesktopPane() != null)
     {
       this.setDropTarget(new java.awt.dnd.DropTarget(this, this));
-      PropertyChangeListener serviceListener = (Platform.isJS() ? null
-              : addServiceListeners());
-      addInternalFrameListener(new javax.swing.event.InternalFrameAdapter()
+      if (!Platform.isJS())
       {
-        @Override
-        public void internalFrameClosed(
-                javax.swing.event.InternalFrameEvent evt)
-        {
-          // System.out.println("deregistering discoverer listener");
-          if (serviceListener != null)
-          {
-            Desktop.getInstance().removeJalviewPropertyChangeListener(
-                    "services", serviceListener);
-          }
-          closeMenuItem_actionPerformed(true);
-        }
-      });
-
+        addServiceListeners();
+      }
       setGUINucleotide();
     }
 
@@ -823,6 +822,7 @@ public class AlignFrame extends GAlignFrame
       {
         ap.av.getAlignment().padGaps();
       }
+      // BH! important option for JalviewJS
       if (Jalview.getInstance().getStartCalculations())
       {
         ap.av.updateConservation(ap);
@@ -848,9 +848,11 @@ public class AlignFrame extends GAlignFrame
   }
 
   /* Set up intrinsic listeners for dynamically generated GUI bits. */
-  private PropertyChangeListener addServiceListeners()
+  private void addServiceListeners()
   {
-    PropertyChangeListener serviceListener = new PropertyChangeListener()
+    PropertyChangeListener thisListener;
+    Desktop.getInstance().addJalviewPropertyChangeListener("services",
+            thisListener = new PropertyChangeListener()
     {
       @Override
       public void propertyChange(PropertyChangeEvent evt)
@@ -862,16 +864,26 @@ public class AlignFrame extends GAlignFrame
             @Override
             public void run()
             {
-              System.err.println("Rebuild WS Menu for service change");
+                      System.err.println(
+                              "Rebuild WS Menu for service change");
               BuildWebServiceMenu();
             }
           });
         }
       }
-    };
-
-    Desktop.getInstance().addJalviewPropertyChangeListener("services",
-            serviceListener);
+    });
+    addInternalFrameListener(new javax.swing.event.InternalFrameAdapter()
+    {
+      @Override
+      public void internalFrameClosed(
+              javax.swing.event.InternalFrameEvent evt)
+      {
+        // System.out.println("deregistering discoverer listener");
+        Desktop.getInstance().removeJalviewPropertyChangeListener("services",
+                thisListener);
+        closeMenuItem_actionPerformed(true);
+      }
+    });
     // Finally, build the menu once to get current service state
     new Thread(new Runnable()
     {
@@ -881,7 +893,6 @@ public class AlignFrame extends GAlignFrame
         BuildWebServiceMenu();
       }
     }).start();
-    return serviceListener;
   }
 
   /**
@@ -942,9 +953,8 @@ public class AlignFrame extends GAlignFrame
     scaleLeft.setVisible(av.getWrapAlignment());
     scaleRight.setVisible(av.getWrapAlignment());
     annotationPanelMenuItem.setState(av.isShowAnnotation());
-    /*
-     * Show/hide annotations only enabled if annotation panel is shown
-     */
+    //   Show/hide annotations only enabled if annotation panel is shown
+     
     syncAnnotationMenuItems();
 
     viewBoxesMenuItem.setSelected(av.getShowBoxes());
@@ -1086,9 +1096,14 @@ public class AlignFrame extends GAlignFrame
       DataSourceType protocol = fileName.startsWith("http:")
               ? DataSourceType.URL
               : DataSourceType.FILE;
-      loader.loadFile(viewport,
-              (fileObject == null ? fileName : fileObject), protocol,
-              currentFileFormat);
+      if (fileObject == null)
+      {
+        loader.LoadFile(fileName, protocol, currentFileFormat);
+      }
+      else
+      {
+        loader.loadFile(null, fileObject, protocol, currentFileFormat);
+      }
     }
     else
     {
@@ -1100,11 +1115,11 @@ public class AlignFrame extends GAlignFrame
 
       if (fileObject == null)
       {
-
+// BH! Q: What about https?
         DataSourceType protocol = (fileName.startsWith("http:")
                 ? DataSourceType.URL
                 : DataSourceType.FILE);
-        newframe = loader.loadFileWaitTillLoaded(fileName, protocol,
+        newframe = loader.LoadFileWaitTillLoaded(fileName, protocol,
                 currentFileFormat);
       }
       else
@@ -1310,7 +1325,7 @@ public class AlignFrame extends GAlignFrame
           try
           {
             String tempFilePath = doBackup ? backupfiles.getTempFilePath() : file;
-                       PrintWriter out = new PrintWriter(
+                        PrintWriter out = new PrintWriter(
                     new FileWriter(tempFilePath));
 
             out.print(output);
@@ -1569,6 +1584,11 @@ public class AlignFrame extends GAlignFrame
 
       if (closeAllTabs)
       {
+        if (featureSettings != null && featureSettings.isOpen())
+        {
+          featureSettings.close();
+          featureSettings = null;
+        }
         /*
          * this will raise an INTERNAL_FRAME_CLOSED event and this method will
          * be called recursively, with the frame now in 'closed' state
@@ -2184,7 +2204,7 @@ public class AlignFrame extends GAlignFrame
                     newGraphGroups.add(q, null);
                   }
                   newGraphGroups.set(newann.graphGroup,
-                          new Integer(++fgroup));
+                          Integer.valueOf(++fgroup));
                 }
                 newann.graphGroup = newGraphGroups.get(newann.graphGroup)
                         .intValue();
@@ -2231,7 +2251,7 @@ public class AlignFrame extends GAlignFrame
                     newGraphGroups.add(q, null);
                   }
                   newGraphGroups.set(newann.graphGroup,
-                          new Integer(++fgroup));
+                          Integer.valueOf(++fgroup));
                 }
                 newann.graphGroup = newGraphGroups.get(newann.graphGroup)
                         .intValue();
@@ -2429,33 +2449,33 @@ public class AlignFrame extends GAlignFrame
 
     Runnable okAction = new Runnable() 
     {
-               @Override
-               public void run() 
-               {
-                   SequenceI[] cut = sg.getSequences()
-                           .toArray(new SequenceI[sg.getSize()]);
-
-                   addHistoryItem(new EditCommand(
-                           MessageManager.getString("label.cut_sequences"), Action.CUT,
-                           cut, sg.getStartRes(), sg.getEndRes() - sg.getStartRes() + 1,
-                           viewport.getAlignment()));
-
-                   viewport.setSelectionGroup(null);
-                   viewport.sendSelection();
-                   viewport.getAlignment().deleteGroup(sg);
-
-                   viewport.firePropertyChange("alignment", null,
-                           viewport.getAlignment().getSequences());
-                   if (viewport.getAlignment().getHeight() < 1)
-                   {
-                     try
-                     {
-                       AlignFrame.this.setClosed(true);
-                     } catch (Exception ex)
-                     {
-                     }
-                   }
-               }};
+                @Override
+                public void run() 
+                {
+                    SequenceI[] cut = sg.getSequences()
+                            .toArray(new SequenceI[sg.getSize()]);
+
+                    addHistoryItem(new EditCommand(
+                            MessageManager.getString("label.cut_sequences"), Action.CUT,
+                            cut, sg.getStartRes(), sg.getEndRes() - sg.getStartRes() + 1,
+                            viewport.getAlignment()));
+
+                    viewport.setSelectionGroup(null);
+                    viewport.sendSelection();
+                    viewport.getAlignment().deleteGroup(sg);
+
+                    viewport.firePropertyChange("alignment", null,
+                            viewport.getAlignment().getSequences());
+                    if (viewport.getAlignment().getHeight() < 1)
+                    {
+                      try
+                      {
+                        AlignFrame.this.setClosed(true);
+                      } catch (Exception ex)
+                      {
+                      }
+                    }
+                }};
 
     /*
      * If the cut affects all sequences, prompt for confirmation
@@ -2463,20 +2483,20 @@ public class AlignFrame extends GAlignFrame
     boolean wholeHeight = sg.getSize() == viewport.getAlignment().getHeight();
     boolean wholeWidth = (((sg.getEndRes() - sg.getStartRes())
             + 1) == viewport.getAlignment().getWidth()) ? true : false;
-       if (wholeHeight && wholeWidth)
-       {
-           JvOptionPane dialog = JvOptionPane.newOptionDialog(Desktop.getDesktopPane());
-               dialog.setResponseHandler(0, okAction); // 0 = OK_OPTION
-           Object[] options = new Object[] { MessageManager.getString("action.ok"),
-                   MessageManager.getString("action.cancel") };
-               dialog.showDialog(MessageManager.getString("warn.delete_all"),
-                   MessageManager.getString("label.delete_all"),
-                   JvOptionPane.DEFAULT_OPTION, JvOptionPane.PLAIN_MESSAGE, null,
-                   options, options[0]);
-       } else 
-       {
-               okAction.run();
-       }
+        if (wholeHeight && wholeWidth)
+        {
+            JvOptionPane dialog = JvOptionPane.newOptionDialog(Desktop.getDesktopPane());
+                dialog.setResponseHandler(0, okAction); // 0 = OK_OPTION
+            Object[] options = new Object[] { MessageManager.getString("action.ok"),
+                    MessageManager.getString("action.cancel") };
+                dialog.showDialog(MessageManager.getString("warn.delete_all"),
+                    MessageManager.getString("label.delete_all"),
+                    JvOptionPane.DEFAULT_OPTION, JvOptionPane.PLAIN_MESSAGE, null,
+                    options, options[0]);
+        } else 
+        {
+                okAction.run();
+        }
   }
 
   /**
@@ -3276,9 +3296,15 @@ public class AlignFrame extends GAlignFrame
   @Override
   public void featureSettings_actionPerformed(ActionEvent e)
   {
+    showFeatureSettingsUI();
+  }
+
+  @Override
+  public FeatureSettingsControllerI showFeatureSettingsUI()
+  {
     if (featureSettings != null)
     {
-      featureSettings.close();
+      featureSettings.closeOldSettings();
       featureSettings = null;
     }
     if (!showSeqFeatures.isSelected())
@@ -3288,6 +3314,7 @@ public class AlignFrame extends GAlignFrame
       showSeqFeatures_actionPerformed(null);
     }
     featureSettings = new FeatureSettings(this);
+    return featureSettings;
   }
 
   /**
@@ -4695,6 +4722,7 @@ public class AlignFrame extends GAlignFrame
                   // associating PDB files which have no IDs.
                   for (SequenceI toassoc : (SequenceI[]) fm[2])
                   {
+                         // BH! check
                     PDBEntry pe = AssociatePdbFileWithSeq
                             .associatePdbWithSeq(fm[0].toString(),
                                     (DataSourceType) fm[1], toassoc, false);
@@ -4862,12 +4890,24 @@ public class AlignFrame extends GAlignFrame
           {
             if (parseFeaturesFile(file, sourceType))
             {
+              SplitFrame splitFrame = (SplitFrame) getSplitViewContainer();
+              if (splitFrame != null)
+              {
+                splitFrame.repaint();
+              }
+              else
+              {
               alignPanel.paintAlignment(true, true);
             }
           }
+          }
           else
           {
-            new FileLoader().loadFile(viewport, file, sourceType, format);
+                 if (file instanceof File) {
+                  new FileLoader().loadFile(viewport, (File) file, sourceType, format);
+                 } else {
+                  new FileLoader().LoadFile(viewport, (String) file, sourceType, format);
+                 }
           }
         }
       }
@@ -4926,6 +4966,21 @@ public class AlignFrame extends GAlignFrame
       viewport = alignPanel.av;
       avc.setViewportAndAlignmentPanel(viewport, alignPanel);
       setMenusFromViewport(viewport);
+      if (featureSettings != null && featureSettings.isOpen()
+              && featureSettings.fr.getViewport() != viewport)
+      {
+        if (viewport.isShowSequenceFeatures())
+        {
+          // refresh the featureSettings to reflect UI change
+          showFeatureSettingsUI();
+    }
+        else
+        {
+          // close feature settings for this view.
+          featureSettings.close();
+        }
+      }
+
     }
 
     /*
@@ -5149,6 +5204,13 @@ public class AlignFrame extends GAlignFrame
               @Override
               public void finished()
               {
+
+                for (FeatureSettingsModelI srcSettings : dbRefFetcher
+                        .getFeatureSettingsModels())
+                {
+
+                  alignPanel.av.mergeFeaturesStyle(srcSettings);
+                }
                 AlignFrame.this.setMenusForViewport();
               }
             });
@@ -5195,6 +5257,7 @@ public class AlignFrame extends GAlignFrame
               }
               if (otherdb.size() == 1)
               {
+                 // BH cleaner code
                 DbSourceProxy src = otherdb.get(0);
                 DbSourceProxy[] dassource = new DbSourceProxy[] {
                     src };
@@ -5225,6 +5288,10 @@ public class AlignFrame extends GAlignFrame
                                   @Override
                                   public void finished()
                                   {
+                                    FeatureSettingsModelI srcSettings = dassource[0]
+                                            .getFeatureColourScheme();
+                                    alignPanel.av.mergeFeaturesStyle(
+                                            srcSettings);
                                     AlignFrame.this.setMenusForViewport();
                                   }
                                 });
@@ -5855,6 +5922,20 @@ public class AlignFrame extends GAlignFrame
 
   }
 
+
+  private Rectangle lastFeatureSettingsBounds = null;
+  @Override
+  public void setFeatureSettingsGeometry(Rectangle bounds)
+  {
+    lastFeatureSettingsBounds = bounds;
+  }
+
+  @Override
+  public Rectangle getFeatureSettingsGeometry()
+  {
+    return lastFeatureSettingsBounds;
+  }
+  
   /**
    * BH 2019 from JalviewLite
    * 
index fb3ec3a..954cfcc 100644 (file)
@@ -47,6 +47,7 @@ import jalview.schemes.UserColourScheme;
 import jalview.structure.SelectionSource;
 import jalview.structure.StructureSelectionManager;
 import jalview.structure.VamsasSource;
+import jalview.util.ColorUtils;
 import jalview.util.MessageManager;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.ws.params.AutoCalcSetting;
@@ -56,6 +57,7 @@ import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.FontMetrics;
 import java.awt.Rectangle;
+import java.util.ArrayList;
 import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
@@ -77,9 +79,9 @@ public class AlignViewport extends AlignmentViewport
 
   boolean antiAlias = false;
 
-  private Rectangle explodedGeometry;
+  private Rectangle explodedGeometry = null;
 
-  private String viewName;
+  private String viewName = null;
 
   /*
    * Flag set true on the view that should 'gather' multiple views of the same
@@ -206,7 +208,10 @@ public class AlignViewport extends AlignmentViewport
    */
   private void applyViewProperties()
   {
-    antiAlias = Cache.getDefault(Preferences.ANTI_ALIAS, false);
+         // BH! using final static strings here because we also use these in 
+         // JS version startup api
+         // BH was false
+    antiAlias = Cache.getDefault(Preferences.ANTI_ALIAS, true);
 
     viewStyle.setShowJVSuffix(
             Cache.getDefault(Preferences.SHOW_JVSUFFIX, true));
@@ -297,8 +302,8 @@ public class AlignViewport extends AlignmentViewport
       schemeName = Cache.getDefault(Preferences.DEFAULT_COLOUR,
               ResidueColourScheme.NONE);
     }
-    ColourSchemeI colourScheme = ColourSchemeProperty.getColourScheme(this,
-            alignment, schemeName);
+    ColourSchemeI colourScheme = ColourSchemeProperty
+            .getColourScheme(this, alignment, schemeName);
     residueShading = new ResidueShader(colourScheme);
 
     if (colourScheme instanceof UserColourScheme)
@@ -823,10 +828,10 @@ public class AlignViewport extends AlignmentViewport
   public static void openLinkedAlignmentAs(AlignFrame thisFrame,
           AlignmentI thisAlignment, AlignmentI al, String title, int mode)
   {
-    /*
-     * Identify protein and dna alignments. Make a copy of this one if opening
-     * in a new split pane.
-     */
+     // BH: thisAlignment is already a copy if mode == SPLIT_FRAME
+     // Identify protein and dna alignments. Make a copy of this one if opening
+     // in a new split pane.
+     
     AlignmentI protein = al.isNucleotide() ? thisAlignment : al;
     AlignmentI cdna = al.isNucleotide() ? al : thisAlignment;
 
@@ -865,8 +870,7 @@ public class AlignViewport extends AlignmentViewport
 
     try
     {
-      newAlignFrame.setMaximum(jalview.bin.Cache
-              .getDefault(Preferences.SHOW_FULLSCREEN, false));
+      newAlignFrame.setMaximum(Cache.getDefault(Preferences.SHOW_FULLSCREEN, false));
     } catch (java.beans.PropertyVetoException ex)
     {
     }
@@ -937,10 +941,9 @@ public class AlignViewport extends AlignmentViewport
     if (ap != null)
     {
       // modify GUI elements to reflect geometry change
-      Dimension idw = getAlignPanel().getIdPanel().getIdCanvas()
-              .getPreferredSize();
+      Dimension idw = ap.getIdPanel().getIdCanvas().getPreferredSize();
       idw.width = i;
-      getAlignPanel().getIdPanel().getIdCanvas().setPreferredSize(idw);
+      ap.getIdPanel().getIdCanvas().setPreferredSize(idw);
     }
   }
 
@@ -1029,6 +1032,35 @@ public class AlignViewport extends AlignmentViewport
   @Override
   public void applyFeaturesStyle(FeatureSettingsModelI featureSettings)
   {
+    transferFeaturesStyles(featureSettings, false);
+  }
+
+  /**
+   * Applies the supplied feature settings descriptor to currently known features.
+   * This supports an 'initial configuration' of feature colouring based on a
+   * preset or user favourite. This may then be modified in the usual way using
+   * the Feature Settings dialogue.
+   * 
+   * @param featureSettings
+   */
+  @Override
+  public void mergeFeaturesStyle(FeatureSettingsModelI featureSettings)
+  {
+    transferFeaturesStyles(featureSettings, true);
+  }
+
+  /**
+   * when mergeOnly is set, then group and feature visibility or feature colours
+   * are not modified for features and groups already known to the feature
+   * renderer. Feature ordering is always adjusted, and transparency is always set
+   * regardless.
+   * 
+   * @param featureSettings
+   * @param mergeOnly
+   */
+  private void transferFeaturesStyles(FeatureSettingsModelI featureSettings,
+          boolean mergeOnly)
+  {
     if (featureSettings == null)
     {
       return;
@@ -1036,12 +1068,24 @@ public class AlignViewport extends AlignmentViewport
 
     FeatureRenderer fr = getAlignPanel().getSeqPanel().seqCanvas
             .getFeatureRenderer();
+    List<String> origRenderOrder = new ArrayList<>(),
+            origGroups = new ArrayList<>();
+    // preserve original render order - allows differentiation between user configured colours and autogenerated ones
+    origRenderOrder.addAll(fr.getRenderOrder());
+    origGroups.addAll(fr.getFeatureGroups());
+
     fr.findAllFeatures(true);
     List<String> renderOrder = fr.getRenderOrder();
     FeaturesDisplayedI displayed = fr.getFeaturesDisplayed();
+    if (!mergeOnly)
+    {
+      // only clear displayed features if we are merging
     displayed.clear();
+    }
     // TODO this clears displayed.featuresRegistered - do we care?
-
+    //
+    // JAL-3330 - JBP - yes we do - calling applyFeatureStyle to a view where
+    // feature visibility has already been configured is not very friendly
     /*
      * set feature colour if specified by feature settings
      * set visibility of all features
@@ -1050,6 +1094,16 @@ public class AlignViewport extends AlignmentViewport
     {
       FeatureColourI preferredColour = featureSettings
               .getFeatureColour(type);
+      FeatureColourI origColour = fr.getFeatureStyle(type);
+      if (!mergeOnly || (!origRenderOrder.contains(type)
+              || origColour == null
+              || (!origColour.isGraduatedColour()
+                      && origColour.getColour() != null
+                      && origColour.getColour().equals(
+                              ColorUtils.createColourFromName(type)))))
+      {
+        // if we are merging, only update if there wasn't already a colour defined for
+        // this type
       if (preferredColour != null)
       {
         fr.setColour(type, preferredColour);
@@ -1059,13 +1113,19 @@ public class AlignViewport extends AlignmentViewport
         displayed.setVisible(type);
       }
     }
+    }
 
     /*
      * set visibility of feature groups
      */
     for (String group : fr.getFeatureGroups())
     {
-      fr.setGroupVisibility(group, featureSettings.isGroupDisplayed(group));
+      if (!mergeOnly || !origGroups.contains(group))
+      {
+        // when merging, display groups only if the aren't already marked as not visible
+        fr.setGroupVisibility(group,
+                featureSettings.isGroupDisplayed(group));
+      }
     }
 
     /*
index 81960e0..d84287f 100644 (file)
@@ -30,17 +30,17 @@ import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
 import jalview.util.MessageManager;
 
-import java.awt.BorderLayout;
 import java.awt.Color;
-import java.awt.FlowLayout;
+import java.awt.Dimension;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.io.FileWriter;
 import java.io.PrintWriter;
 
-import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
 import javax.swing.ButtonGroup;
 import javax.swing.JButton;
+import javax.swing.JCheckBox;
 import javax.swing.JInternalFrame;
 import javax.swing.JLabel;
 import javax.swing.JLayeredPane;
@@ -71,6 +71,29 @@ public class AnnotationExporter extends JPanel
 
   private boolean wholeView;
 
+  /*
+   * option to export linked (CDS/peptide) features when shown 
+   * on the alignment, converted to this alignment's coordinates
+   */
+  private JCheckBox includeLinkedFeatures;
+
+  /*
+   * output format option shown for feature export
+   */
+  JRadioButton GFFFormat = new JRadioButton();
+
+  /*
+   * output format option shown for annotation export
+   */
+  JRadioButton CSVFormat = new JRadioButton();
+
+  private JPanel linkedFeaturesPanel;
+
+  /**
+   * Constructor
+   * 
+   * @param panel
+   */
   public AnnotationExporter(AlignmentPanel panel)
   {
     this.ap = panel;
@@ -85,17 +108,25 @@ public class AnnotationExporter extends JPanel
     frame = new JInternalFrame();
     frame.setContentPane(this);
     frame.setLayer(JLayeredPane.PALETTE_LAYER);
-    Desktop.addInternalFrame(frame, "", frame.getPreferredSize().width,
-            frame.getPreferredSize().height);
+    Dimension preferredSize = frame.getPreferredSize();
+    Desktop.addInternalFrame(frame, "", true, preferredSize.width,
+            preferredSize.height, true, true);
   }
 
   /**
-   * Configures the diglog for options to export visible features
+   * Configures the dialog for options to export visible features. If from a split
+   * frame panel showing linked features, make the option to include these in the
+   * export visible.
    */
   public void exportFeatures()
   {
     exportFeatures = true;
     CSVFormat.setVisible(false);
+    if (ap.av.isShowComplementFeatures())
+    {
+      linkedFeaturesPanel.setVisible(true);
+      frame.pack();
+    }
     frame.setTitle(MessageManager.getString("label.export_features"));
   }
 
@@ -216,14 +247,17 @@ public class AnnotationExporter extends JPanel
 
     FeaturesFile formatter = new FeaturesFile();
     final FeatureRenderer fr = ap.getFeatureRenderer();
+    boolean includeComplement = includeLinkedFeatures.isSelected();
+
     if (GFFFormat.isSelected())
     {
-      text = formatter.printGffFormat(sequences, fr, includeNonPositional);
+      text = formatter.printGffFormat(sequences, fr, includeNonPositional,
+              includeComplement);
     }
     else
     {
       text = formatter.printJalviewFormat(sequences, fr,
-              includeNonPositional);
+              includeNonPositional, includeComplement);
     }
     return text;
   }
@@ -269,11 +303,70 @@ public class AnnotationExporter extends JPanel
     }
   }
 
+  /**
+   * Adds widgets to the panel
+   * 
+   * @throws Exception
+   */
   private void jbInit() throws Exception
   {
-    this.setLayout(new BorderLayout());
+    this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+    this.setBackground(Color.white);
+
+    JPanel formatPanel = buildFormatOptionsPanel();
+    JPanel linkedFeatures = buildLinkedFeaturesPanel();
+    JPanel actionsPanel = buildActionsPanel();
+
+    this.add(formatPanel);
+    this.add(linkedFeatures);
+    this.add(actionsPanel);
+  }
+
+  /**
+   * Builds a panel with a checkbox for the option to export linked (CDS/peptide)
+   * features. This is hidden by default, and only made visible if exporting
+   * features from a split frame panel which is configured to show linked
+   * features.
+   * 
+   * @return
+   */
+  private JPanel buildLinkedFeaturesPanel()
+  {
+    linkedFeaturesPanel = new JPanel();
+    linkedFeaturesPanel.setOpaque(false);
+
+    boolean nucleotide = ap.av.isNucleotide();
+    String complement = nucleotide
+            ? MessageManager.getString("label.protein").toLowerCase()
+            : "CDS";
+    JLabel label = new JLabel(
+            MessageManager.formatMessage("label.include_linked_features",
+                    complement));
+    label.setHorizontalAlignment(SwingConstants.TRAILING);
+    String tooltip = MessageManager
+            .formatMessage("label.include_linked_tooltip", complement);
+    label.setToolTipText(
+            JvSwingUtils.wrapTooltip(true, tooltip));
+
+    includeLinkedFeatures = new JCheckBox();
+    linkedFeaturesPanel.add(label);
+    linkedFeaturesPanel.add(includeLinkedFeatures);
+    linkedFeaturesPanel.setVisible(false);
+
+    return linkedFeaturesPanel;
+  }
+
+  /**
+   * Builds the panel with to File or Textbox or Close actions
+   * 
+   * @return
+   */
+  JPanel buildActionsPanel()
+  {
+    JPanel actionsPanel = new JPanel();
+    actionsPanel.setOpaque(false);
 
-    toFile.setText(MessageManager.getString("label.to_file"));
+    JButton toFile = new JButton(MessageManager.getString("label.to_file"));
     toFile.addActionListener(new ActionListener()
     {
       @Override
@@ -282,7 +375,8 @@ public class AnnotationExporter extends JPanel
         toFile_actionPerformed();
       }
     });
-    toTextbox.setText(MessageManager.getString("label.to_textbox"));
+    JButton toTextbox = new JButton(
+            MessageManager.getString("label.to_textbox"));
     toTextbox.addActionListener(new ActionListener()
     {
       @Override
@@ -291,7 +385,7 @@ public class AnnotationExporter extends JPanel
         toTextbox_actionPerformed();
       }
     });
-    close.setText(MessageManager.getString("action.close"));
+    JButton close = new JButton(MessageManager.getString("action.close"));
     close.addActionListener(new ActionListener()
     {
       @Override
@@ -300,52 +394,49 @@ public class AnnotationExporter extends JPanel
         close_actionPerformed();
       }
     });
+
+    actionsPanel.add(toFile);
+    actionsPanel.add(toTextbox);
+    actionsPanel.add(close);
+
+    return actionsPanel;
+  }
+
+  /**
+   * Builds the panel with options to output in Jalview, GFF or CSV format. GFF is
+   * only made visible when exporting features, CSV only when exporting
+   * annotation.
+   * 
+   * @return
+   */
+  JPanel buildFormatOptionsPanel()
+  {
+    JPanel formatPanel = new JPanel();
+    // formatPanel.setBorder(BorderFactory.createEtchedBorder());
+    formatPanel.setOpaque(false);
+
+    JRadioButton jalviewFormat = new JRadioButton("Jalview");
     jalviewFormat.setOpaque(false);
     jalviewFormat.setSelected(true);
-    jalviewFormat.setText("Jalview");
     GFFFormat.setOpaque(false);
     GFFFormat.setText("GFF");
     CSVFormat.setOpaque(false);
     CSVFormat.setText(MessageManager.getString("label.csv_spreadsheet"));
-    jLabel1.setHorizontalAlignment(SwingConstants.TRAILING);
-    jLabel1.setText(MessageManager.getString("action.format") + " ");
-    this.setBackground(Color.white);
-    jPanel3.setBorder(BorderFactory.createEtchedBorder());
-    jPanel3.setOpaque(false);
-    jPanel1.setOpaque(false);
-    jPanel1.add(toFile);
-    jPanel1.add(toTextbox);
-    jPanel1.add(close);
-    jPanel3.add(jLabel1);
-    jPanel3.add(jalviewFormat);
-    jPanel3.add(GFFFormat);
-    jPanel3.add(CSVFormat);
+
+    ButtonGroup buttonGroup = new ButtonGroup();
     buttonGroup.add(jalviewFormat);
     buttonGroup.add(GFFFormat);
     buttonGroup.add(CSVFormat);
-    this.add(jPanel3, BorderLayout.CENTER);
-    this.add(jPanel1, BorderLayout.SOUTH);
-  }
-
-  JPanel jPanel1 = new JPanel();
-
-  JButton toFile = new JButton();
-
-  JButton toTextbox = new JButton();
-
-  JButton close = new JButton();
 
-  ButtonGroup buttonGroup = new ButtonGroup();
+    JLabel format = new JLabel(
+            MessageManager.getString("action.format") + " ");
+    format.setHorizontalAlignment(SwingConstants.TRAILING);
 
-  JRadioButton jalviewFormat = new JRadioButton();
+    formatPanel.add(format);
+    formatPanel.add(jalviewFormat);
+    formatPanel.add(GFFFormat);
+    formatPanel.add(CSVFormat);
 
-  JRadioButton GFFFormat = new JRadioButton();
-
-  JRadioButton CSVFormat = new JRadioButton();
-
-  JLabel jLabel1 = new JLabel();
-
-  JPanel jPanel3 = new JPanel();
-
-  FlowLayout flowLayout1 = new FlowLayout();
+    return formatPanel;
+  }
 }
index d37cb29..62fecab 100644 (file)
@@ -356,12 +356,12 @@ public class CrossRefAction implements Runnable
             seq.getLength());
     if (geneLoci != null)
     {
-      MapList map = geneLoci.getMap();
+      MapList map = geneLoci.getMapping();
       int mappedFromLength = MappingUtils.getLength(map.getFromRanges());
       if (mappedFromLength == seq.getLength())
       {
         seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
-                geneLoci.getChromosomeId(), geneLoci.getMap());
+                geneLoci.getChromosomeId(), geneLoci.getMapping());
         retrievedLoci.put(dbref, geneLoci);
         return true;
       }
@@ -374,12 +374,12 @@ public class CrossRefAction implements Runnable
             seq.getLength());
     if (geneLoci != null)
     {
-      MapList map = geneLoci.getMap();
+      MapList map = geneLoci.getMapping();
       int mappedFromLength = MappingUtils.getLength(map.getFromRanges());
       if (mappedFromLength == seq.getLength())
       {
         seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
-                geneLoci.getChromosomeId(), geneLoci.getMap());
+                geneLoci.getChromosomeId(), geneLoci.getMapping());
         retrievedLoci.put(dbref, geneLoci);
         return true;
       }
index e13a63e..717fb1a 100644 (file)
@@ -39,7 +39,6 @@ import jalview.io.FormatAdapter;
 import jalview.io.IdentifyFile;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
-import jalview.jbgui.GDesktop;
 import jalview.jbgui.GSplitFrame;
 import jalview.jbgui.GStructureViewer;
 import jalview.project.Jalview2XML;
@@ -49,9 +48,9 @@ import jalview.util.BrowserLauncher;
 import jalview.util.ImageMaker.TYPE;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
+import jalview.util.ShortcutKeyMaskExWrapper;
 import jalview.util.UrlConstants;
 import jalview.viewmodel.AlignmentViewport;
-import jalview.ws.jws1.Discoverer;
 import jalview.ws.params.ParamManager;
 import jalview.ws.utils.UrlDownloadClient;
 
@@ -84,12 +83,13 @@ import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
-import java.io.BufferedInputStream;
 import java.io.File;
-import java.io.FileOutputStream;
+import java.io.FileWriter;
 import java.io.IOException;
+import java.lang.reflect.Method;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.ListIterator;
@@ -111,7 +111,6 @@ import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JDesktopPane;
-import javax.swing.JFrame;
 import javax.swing.JInternalFrame;
 import javax.swing.JLabel;
 import javax.swing.JMenuItem;
@@ -125,8 +124,6 @@ import javax.swing.event.HyperlinkEvent;
 import javax.swing.event.HyperlinkEvent.EventType;
 import javax.swing.event.InternalFrameAdapter;
 import javax.swing.event.InternalFrameEvent;
-import javax.swing.event.MenuEvent;
-import javax.swing.event.MenuListener;
 
 import org.stackoverflowusers.file.WindowsShortcut;
 
@@ -137,11 +134,18 @@ import org.stackoverflowusers.file.WindowsShortcut;
  * @author $author$
  * @version $Revision: 1.155 $
  */
-@SuppressWarnings("serial")
-public class Desktop extends GDesktop
+public class Desktop extends jalview.jbgui.GDesktop
         implements DropTargetListener, ClipboardOwner, IProgressIndicator,
         StructureSelectionManagerProvider, ApplicationSingletonI
 {
+  private static final String CITATION = "<br><br>Development managed by The Barton Group, University of Dundee, Scotland, UK.<br>"
+          + "<br><br>For help, see the FAQ at <a href=\"http://www.jalview.org/faq\">www.jalview.org/faq</a> and/or join the jalview-discuss@jalview.org mailing list"
+          + "<br><br>If  you use Jalview, please cite:"
+          + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
+          + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
+          + "<br>Bioinformatics doi: 10.1093/bioinformatics/btp033";
+
+  private static final String DEFAULT_AUTHORS = "The Jalview Authors (See AUTHORS file for current list)";
 
   private final static int DEFAULT_MIN_WIDTH = 300;
 
@@ -153,12 +157,16 @@ public class Desktop extends GDesktop
 
   private final static String EXPERIMENTAL_FEATURES = "EXPERIMENTAL_FEATURES";
 
+  protected static final String CONFIRM_KEYBOARD_QUIT = "CONFIRM_KEYBOARD_QUIT";
+
+  public static HashMap<String, FileWriter> savingFiles = new HashMap<>();
+
   private JalviewChangeSupport changeSupport = new JalviewChangeSupport();
 
   /**
    * news reader - null if it was never started.
    */
-  BlogReader jvnews = null;
+  private BlogReader jvnews = null;
 
   private File projectFile;
 
@@ -197,12 +205,6 @@ public class Desktop extends GDesktop
             listener);
   }
 
-  public static MyDesktopPane getDesktopPane()
-  {
-    Desktop desktop = Desktop.getInstance();
-    return desktop == null ? null : desktop.desktopPane;
-  }
-
   public static StructureSelectionManager getStructureSelectionManager()
   {
     return StructureSelectionManager
@@ -215,10 +217,13 @@ public class Desktop extends GDesktop
 
   static final int yOffset = 30;
 
-  public Discoverer discoverer;
+  // BH was static
+  public jalview.ws.jws1.Discoverer discoverer;
 
+  //BH was static
   public Object[] jalviewClipboard;
 
+//BH was static
   public boolean internalCopy = false;
 
   private static int fileLoadingCount = 0;
@@ -349,23 +354,6 @@ public class Desktop extends GDesktop
     // All other methods, simply delegate
 
   }
-
-  public MyDesktopPane desktopPane;
-
-  /**
-   * Answers an 'application scope' singleton instance of this class. Separate
-   * SwingJS 'applets' running in the same browser page will each have a
-   * distinct instance of Desktop.
-   * 
-   * @return
-   */
-  public static Desktop getInstance()
-  {
-    return Jalview.isHeadlessMode() ? null
-            : (Desktop) ApplicationSingletonProvider
-                    .getInstance(Desktop.class);
-  }
-  
   /**
    * Private constructor enforces singleton pattern. It is called by reflection
    * from ApplicationSingletonProvider.getInstance().
@@ -380,22 +368,49 @@ public class Desktop extends GDesktop
        * block are spawned off as threads rather than waited for during this
        * constructor.
        */
-      if (!Platform.isJS())
+
+      doConfigureStructurePrefs();
+      setTitle("Jalview " + Cache.getProperty("VERSION"));
+      
+      try
+      {
+        if (Platform.getJavaVersion() >= 11)
+        {
+          // BH use reflection so that this code can be in both the Java8 and
+          // Java11 versions
+          Class<?> j11APQHandlers = Class
+                  .forName("jalview.gui.APQHandlers");
+          Method meth = j11APQHandlers.getMethod("setAPQHandlers",
+                  new Class<?>[]
+                  { Desktop.class });
+          meth.invoke(j11APQHandlers.newInstance(), this);
+        }
+      } catch (Throwable t)
       {
-        doVamsasClientCheck();
+        System.out.println(
+                "Desktop Error setting APQHandlers: " + t.toString());
       }
 
-      doConfigureStructurePrefs();
-      setTitle("Jalview " + jalview.bin.Cache.getProperty("VERSION"));
-      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
-      boolean selmemusage = jalview.bin.Cache.getDefault("SHOW_MEMUSAGE",
+      addWindowListener(new WindowAdapter()
+      {
+
+        @Override
+        public void windowClosing(WindowEvent ev)
+        {
+          quit();
+        }
+      });
+      
+      boolean selmemusage = Cache.getDefault("SHOW_MEMUSAGE",
               false);
-      boolean showjconsole = jalview.bin.Cache
-              .getDefault("SHOW_JAVA_CONSOLE", false);
-      desktopPane = new MyDesktopPane(selmemusage);
 
-      showMemusage.setSelected(selmemusage);
-      desktopPane.setBackground(Color.white);
+    boolean showjconsole = Cache.getDefault("SHOW_JAVA_CONSOLE",
+            false);
+    desktopPane = new MyDesktopPane(selmemusage);
+
+    showMemusage.setSelected(selmemusage);
+    desktopPane.setBackground(Color.white);
+
       getContentPane().setLayout(new BorderLayout());
       // alternate config - have scrollbars - see notes in JAL-153
       // JScrollPane sp = new JScrollPane();
@@ -450,19 +465,7 @@ public class Desktop extends GDesktop
       {
 
         jconsole = new Console(this, showjconsole);
-        // add essential build information
-        jconsole.setHeader("Jalview Version: "
-                + jalview.bin.Cache.getProperty("VERSION") + "\n"
-                + "Jalview Installation: "
-                + jalview.bin.Cache.getDefault("INSTALLATION", "unknown")
-                + "\n" + "Build Date: "
-                + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown")
-                + "\n" + "Java version: "
-                + System.getProperty("java.version") + "\n"
-                + System.getProperty("os.arch") + " "
-                + System.getProperty("os.name") + " "
-                + System.getProperty("os.version"));
-
+        jconsole.setHeader(Cache.getVersionDetailsForConsole());
         showConsole(showjconsole);
 
         showNews.setVisible(false);
@@ -480,7 +483,7 @@ public class Desktop extends GDesktop
           @Override
           public void run()
           {
-            new SplashScreen();
+            new SplashScreen(true);
           }
         });
 
@@ -576,14 +579,14 @@ public class Desktop extends GDesktop
     // configure services
     StructureSelectionManager ssm = StructureSelectionManager
             .getStructureSelectionManager(this);
-    if (jalview.bin.Cache.getDefault(Preferences.ADD_SS_ANN, true))
+    if (Cache.getDefault(Preferences.ADD_SS_ANN, true))
     {
-      ssm.setAddTempFacAnnot(jalview.bin.Cache
+      ssm.setAddTempFacAnnot(Cache
               .getDefault(Preferences.ADD_TEMPFACT_ANN, true));
-      ssm.setProcessSecondaryStructure(jalview.bin.Cache
+      ssm.setProcessSecondaryStructure(Cache
               .getDefault(Preferences.STRUCT_FROM_PDB, true));
       ssm.setSecStructServices(
-              jalview.bin.Cache.getDefault(Preferences.USE_RNAVIEW, true));
+              Cache.getDefault(Preferences.USE_RNAVIEW, true));
     }
     else
     {
@@ -619,7 +622,6 @@ public class Desktop extends GDesktop
       public void run()
       {
         Cache.log.debug("Downloading data from identifiers.org");
-        // UrlDownloadClient client = new UrlDownloadClient();
         try
         {
           UrlDownloadClient.download(IdOrgSettings.getUrl(),
@@ -674,25 +676,25 @@ public class Desktop extends GDesktop
   {
     // TODO: lock aspect ratio for scaling desktop Bug #0058199
     Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
-    String x = jalview.bin.Cache.getProperty(windowName + "SCREEN_X");
-    String y = jalview.bin.Cache.getProperty(windowName + "SCREEN_Y");
-    String width = jalview.bin.Cache
+    String x = Cache.getProperty(windowName + "SCREEN_X");
+    String y = Cache.getProperty(windowName + "SCREEN_Y");
+    String width = Cache
             .getProperty(windowName + "SCREEN_WIDTH");
-    String height = jalview.bin.Cache
+    String height = Cache
             .getProperty(windowName + "SCREEN_HEIGHT");
     if ((x != null) && (y != null) && (width != null) && (height != null))
     {
       int ix = Integer.parseInt(x), iy = Integer.parseInt(y),
               iw = Integer.parseInt(width), ih = Integer.parseInt(height);
-      if (jalview.bin.Cache.getProperty("SCREENGEOMETRY_WIDTH") != null)
+      if (Cache.getProperty("SCREENGEOMETRY_WIDTH") != null)
       {
         // attempt #1 - try to cope with change in screen geometry - this
         // version doesn't preserve original jv aspect ratio.
         // take ratio of current screen size vs original screen size.
         double sw = ((1f * screenSize.width) / (1f * Integer.parseInt(
-                jalview.bin.Cache.getProperty("SCREENGEOMETRY_WIDTH"))));
+                Cache.getProperty("SCREENGEOMETRY_WIDTH"))));
         double sh = ((1f * screenSize.height) / (1f * Integer.parseInt(
-                jalview.bin.Cache.getProperty("SCREENGEOMETRY_HEIGHT"))));
+                Cache.getProperty("SCREENGEOMETRY_HEIGHT"))));
         // rescale the bounds depending upon the current screen geometry.
         ix = (int) (ix * sw);
         iw = (int) (iw * sw);
@@ -700,17 +702,17 @@ public class Desktop extends GDesktop
         ih = (int) (ih * sh);
         while (ix >= screenSize.width)
         {
-          jalview.bin.Cache.log.debug(
+          Cache.log.debug(
                   "Window geometry location recall error: shifting horizontal to within screenbounds.");
           ix -= screenSize.width;
         }
         while (iy >= screenSize.height)
         {
-          jalview.bin.Cache.log.debug(
+          Cache.log.debug(
                   "Window geometry location recall error: shifting vertical to within screenbounds.");
           iy -= screenSize.height;
         }
-        jalview.bin.Cache.log.debug(
+        Cache.log.debug(
                 "Got last known dimensions for " + windowName + ": x:" + ix
                         + " y:" + iy + " width:" + iw + " height:" + ih);
       }
@@ -720,46 +722,6 @@ public class Desktop extends GDesktop
     return null;
   }
 
-  private void doVamsasClientCheck()
-  {
-    if (Cache.vamsasJarsPresent())
-    {
-      setupVamsasDisconnectedGui();
-      VamsasMenu.setVisible(true);
-      final Desktop us = this;
-      VamsasMenu.addMenuListener(new MenuListener()
-      {
-        // this listener remembers when the menu was first selected, and
-        // doesn't rebuild the session list until it has been cleared and
-        // reselected again.
-        boolean refresh = true;
-
-        @Override
-        public void menuCanceled(MenuEvent e)
-        {
-          refresh = true;
-        }
-
-        @Override
-        public void menuDeselected(MenuEvent e)
-        {
-          refresh = true;
-        }
-
-        @Override
-        public void menuSelected(MenuEvent e)
-        {
-          if (refresh)
-          {
-            us.buildVamsasStMenu();
-            refresh = false;
-          }
-        }
-      });
-      vamsasStart.setVisible(true);
-    }
-  }
-
   protected void showPasteMenu(int x, int y)
   {
     JPopupMenu popup = new JPopupMenu();
@@ -793,7 +755,7 @@ public class Desktop extends GDesktop
         FileFormatI format = new IdentifyFile().identify(file,
                 DataSourceType.PASTE);
 
-        new FileLoader().loadFile(file, DataSourceType.PASTE, format);
+        new FileLoader().LoadFile(file, DataSourceType.PASTE, format);
 
       }
     } catch (Exception ex)
@@ -895,6 +857,7 @@ public class Desktop extends GDesktop
     // the current window title
 
     frame.setTitle(title);
+    // BH fix
     if (w > 0 && (frame.getWidth() < 1 || frame.getHeight() < 1))
     {
       frame.setSize(w, h);
@@ -1031,6 +994,7 @@ public class Desktop extends GDesktop
    */
   private static void setKeyBindings(JInternalFrame frame)
   {
+    @SuppressWarnings("serial")
     final Action closeAction = new AbstractAction()
     {
       @Override
@@ -1046,7 +1010,7 @@ public class Desktop extends GDesktop
     KeyStroke ctrlWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
             InputEvent.CTRL_DOWN_MASK);
     KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
-            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
+            ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx());
 
     InputMap inputMap = frame
             .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
@@ -1141,9 +1105,13 @@ public class Desktop extends GDesktop
           if (file instanceof File)
           {
             Platform.cacheFileData((File) file);
+            new FileLoader().loadFile(null, (File) file, protocol, format);
           }
-          new FileLoader().loadFile(null, file, protocol, format);
+          else
+          {
+            new FileLoader().LoadFile((String) file, protocol, format);
 
+          }
         }
       } catch (Exception ex)
       {
@@ -1165,7 +1133,7 @@ public class Desktop extends GDesktop
   {
     String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
     JalviewFileChooser chooser = JalviewFileChooser
-            .forRead(Cache.getProperty("LAST_DIRECTORY"), fileFormat, true);
+            .forRead(Cache.getProperty("LAST_DIRECTORY"), fileFormat, BackupFiles.getEnabled());
 
     chooser.setFileView(new JalviewFileView());
     chooser.setDialogTitle(
@@ -1199,7 +1167,7 @@ public class Desktop extends GDesktop
           }
         }
 
-        new FileLoader().loadFile(viewport, selectedFile,
+        new FileLoader().LoadFile(viewport, selectedFile,
                 DataSourceType.FILE, format);
       }
     });
@@ -1273,12 +1241,12 @@ public class Desktop extends GDesktop
         {
           if (viewport != null)
           {
-            new FileLoader().loadFile(viewport, url, DataSourceType.URL,
+            new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
                     FileFormat.Jalview);
           }
           else
           {
-            new FileLoader().loadFile(url, DataSourceType.URL,
+            new FileLoader().LoadFile(url, DataSourceType.URL,
                     FileFormat.Jalview);
           }
         }
@@ -1308,12 +1276,12 @@ public class Desktop extends GDesktop
 
           if (viewport != null)
           {
-            new FileLoader().loadFile(viewport, url, DataSourceType.URL,
+            new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
                     format);
           }
           else
           {
-            new FileLoader().loadFile(url, DataSourceType.URL, format);
+            new FileLoader().LoadFile(url, DataSourceType.URL, format);
           }
         }
       }
@@ -1353,9 +1321,9 @@ public class Desktop extends GDesktop
   public void quit()
   {
     Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
-    jalview.bin.Cache.setProperty("SCREENGEOMETRY_WIDTH",
+    Cache.setProperty("SCREENGEOMETRY_WIDTH",
             screen.width + "");
-    jalview.bin.Cache.setProperty("SCREENGEOMETRY_HEIGHT",
+    Cache.setProperty("SCREENGEOMETRY_HEIGHT",
             screen.height + "");
     storeLastKnownDimensions("", new Rectangle(getBounds().x, getBounds().y,
             getWidth(), getHeight()));
@@ -1387,14 +1355,14 @@ public class Desktop extends GDesktop
 
   private void storeLastKnownDimensions(String string, Rectangle jc)
   {
-    jalview.bin.Cache.log.debug("Storing last known dimensions for "
+    Cache.log.debug("Storing last known dimensions for "
             + string + ": x:" + jc.x + " y:" + jc.y + " width:" + jc.width
             + " height:" + jc.height);
 
-    jalview.bin.Cache.setProperty(string + "SCREEN_X", jc.x + "");
-    jalview.bin.Cache.setProperty(string + "SCREEN_Y", jc.y + "");
-    jalview.bin.Cache.setProperty(string + "SCREEN_WIDTH", jc.width + "");
-    jalview.bin.Cache.setProperty(string + "SCREEN_HEIGHT", jc.height + "");
+    Cache.setProperty(string + "SCREEN_X", jc.x + "");
+    Cache.setProperty(string + "SCREEN_Y", jc.y + "");
+    Cache.setProperty(string + "SCREEN_WIDTH", jc.width + "");
+    Cache.setProperty(string + "SCREEN_HEIGHT", jc.height + "");
   }
 
   /**
@@ -1406,53 +1374,45 @@ public class Desktop extends GDesktop
   @Override
   public void aboutMenuItem_actionPerformed(ActionEvent e)
   {
-    // StringBuffer message = getAboutMessage(false);
-    // JvOptionPane.showInternalMessageDialog(Desktop.getDesktop(),
-    //
-    // message.toString(), "About Jalview", JvOptionPane.INFORMATION_MESSAGE);
     new Thread(new Runnable()
     {
       @Override
       public void run()
       {
+        // BH! true meaning "interactive" here (applet branch); was false in
+        // develop version??
         new SplashScreen(true);
       }
     }).start();
   }
 
-  public StringBuffer getAboutMessage(boolean shortv)
+  /**
+   * Returns the html text for the About screen, including any available version
+   * number, build details, author details and citation reference, but without
+   * the enclosing {@code html} tags
+   * 
+   * @return
+   */
+  public String getAboutMessage()
   {
-    StringBuffer message = new StringBuffer();
-    message.append("<html>");
-    if (shortv)
-    {
-      message.append("<h1><strong>Version: "
-              + jalview.bin.Cache.getProperty("VERSION")
-              + "</strong></h1>");
-      message.append("<strong>Last Updated: <em>"
-              + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown")
-              + "</em></strong>");
+    StringBuilder message = new StringBuilder(1024);
+    message.append("<h1><strong>Version: ")
+            .append(Cache.getProperty("VERSION")).append("</strong></h1>")
+            .append("<strong>Built: <em>")
+            .append(Cache.getDefault("BUILD_DATE", "unknown"))
+            .append("</em> from ").append(Cache.getBuildDetailsForSplash())
+            .append("</strong>");
 
-    }
-    else
+    String latestVersion = Cache.getDefault("LATEST_VERSION", "Checking");
+    if (latestVersion.equals("Checking"))
     {
-
-      message.append("<strong>Version "
-              + jalview.bin.Cache.getProperty("VERSION")
-              + "; last updated: "
-              + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown"));
+      // JBP removed this message for 2.11: May be reinstated in future version
+      // message.append("<br>...Checking latest version...</br>");
     }
-
-    if (jalview.bin.Cache.getDefault("LATEST_VERSION", "Checking")
-            .equals("Checking"))
-    {
-      message.append("<br>...Checking latest version...</br>");
-    }
-    else if (!jalview.bin.Cache.getDefault("LATEST_VERSION", "Checking")
-            .equals(jalview.bin.Cache.getProperty("VERSION")))
+    else if (!latestVersion.equals(Cache.getProperty("VERSION")))
     {
       boolean red = false;
-      if (jalview.bin.Cache.getProperty("VERSION").toLowerCase()
+      if (Cache.getProperty("VERSION").toLowerCase()
               .indexOf("automated build") == -1)
       {
         red = true;
@@ -1461,29 +1421,22 @@ public class Desktop extends GDesktop
         message.append("<div style=\"color: #FF0000;font-style: bold;\">");
       }
 
-      message.append("<br>!! Version "
-              + jalview.bin.Cache.getDefault("LATEST_VERSION",
-                      "..Checking..")
-              + " is available for download from "
-              + jalview.bin.Cache.getDefault("www.jalview.org",
-                      "http://www.jalview.org")
-              + " !!");
+      message.append("<br>!! Version ")
+              .append(Cache.getDefault("LATEST_VERSION", "..Checking.."))
+              .append(" is available for download from ")
+              .append(Cache.getDefault("www.jalview.org",
+                      "http://www.jalview.org"))
+              .append(" !!");
       if (red)
       {
         message.append("</div>");
       }
     }
-    message.append("<br>Authors:  " + jalview.bin.Cache.getDefault(
-            "AUTHORFNAMES",
-            "The Jalview Authors (See AUTHORS file for current list)")
-            + "<br><br>Development managed by The Barton Group, University of Dundee, Scotland, UK.<br>"
-            + "<br><br>For help, see the FAQ at <a href=\"http://www.jalview.org/faq\">www.jalview.org/faq</a> and/or join the jalview-discuss@jalview.org mailing list"
-            + "<br><br>If  you use Jalview, please cite:"
-            + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
-            + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
-            + "<br>Bioinformatics doi: 10.1093/bioinformatics/btp033"
-            + "</html>");
-    return message;
+    message.append("<br>Authors:  ");
+    message.append(Cache.getDefault("AUTHORFNAMES", DEFAULT_AUTHORS));
+    message.append(CITATION);
+
+    return message.toString();
   }
 
   /**
@@ -1533,10 +1486,6 @@ public class Desktop extends GDesktop
     }
     Jalview.setCurrentAlignFrame(null);
     System.out.println("ALL CLOSED");
-    if (v_client != null)
-    {
-      // TODO clear binding to vamsas document objects on close_all
-    }
 
     /*
      * reset state of singleton objects as appropriate (clear down session state
@@ -1572,9 +1521,9 @@ public class Desktop extends GDesktop
   protected void garbageCollect_actionPerformed(ActionEvent e)
   {
     // We simply collect the garbage
-    jalview.bin.Cache.log.debug("Collecting garbage...");
+    Cache.log.debug("Collecting garbage...");
     System.gc();
-    jalview.bin.Cache.log.debug("Finished garbage collection.");
+    Cache.log.debug("Finished garbage collection.");
   }
 
   /*
@@ -1764,13 +1713,13 @@ public class Desktop extends GDesktop
           setProgressBar(MessageManager.formatMessage(
                   "label.saving_jalview_project", new Object[]
                   { chosenFile.getName() }), chosenFile.hashCode());
-          jalview.bin.Cache.setProperty("LAST_DIRECTORY",
+          Cache.setProperty("LAST_DIRECTORY",
                   chosenFile.getParent());
           // TODO catch and handle errors for savestate
           // TODO prevent user from messing with the Desktop whilst we're saving
           try
           {
-               boolean doBackup = BackupFiles.getEnabled();
+            boolean doBackup = BackupFiles.getEnabled();
             BackupFiles backupfiles = doBackup ? new BackupFiles(chosenFile) : null;
 
             new Jalview2XML().saveState(doBackup ? backupfiles.getTempFile() : chosenFile);
@@ -1830,7 +1779,7 @@ public class Desktop extends GDesktop
         "Jalview Project (old)" };
     JalviewFileChooser chooser = new JalviewFileChooser(
             Cache.getProperty("LAST_DIRECTORY"), suffix, desc,
-            "Jalview Project", true, true); // last two booleans: allFiles,
+            "Jalview Project", true, BackupFiles.getEnabled()); // last two booleans: allFiles,
                                             // allowBackupFiles
     chooser.setFileView(new JalviewFileView());
     chooser.setDialogTitle(MessageManager.getString("label.restore_state"));
@@ -1848,24 +1797,26 @@ public class Desktop extends GDesktop
           @Override
           public void run()
           {
-               try 
+            try
             {
+              // BH was String "choice" here but needs to be File object
               new Jalview2XML().loadJalviewAlign(selectedFile);
             } catch (OutOfMemoryError oom)
-               {
-                 new OOMWarning("Whilst loading project from " + choice, oom);
-               } catch (Exception ex)
-               {
-                 Cache.log.error(
-                         "Problems whilst loading project from " + choice, ex);
+            {
+              new OOMWarning("Whilst loading project from " + choice, oom);
+            } catch (Exception ex)
+            {
+              Cache.log.error(
+                      "Problems whilst loading project from " + choice, ex);
               JvOptionPane.showMessageDialog(Desktop.getDesktopPane(),
-                         MessageManager.formatMessage(
-                                 "label.error_whilst_loading_project_from",
-                               new Object[]
-                                   { choice }),
-                         MessageManager.getString("label.couldnt_load_project"),
-                         JvOptionPane.WARNING_MESSAGE);
-               }
+                      MessageManager.formatMessage(
+                              "label.error_whilst_loading_project_from",
+                              new Object[]
+                              { choice }),
+                      MessageManager
+                              .getString("label.couldnt_load_project"),
+                      JvOptionPane.WARNING_MESSAGE);
+            }
           }
         }).start();
       }
@@ -2057,11 +2008,29 @@ public class Desktop extends GDesktop
       return;
     }
 
+    // BH! not in applet branch
+    // FIXME: ideally should use UI interface API
+    FeatureSettings viewFeatureSettings = (af.featureSettings != null
+            && af.featureSettings.isOpen())
+            ? af.featureSettings
+            : null;
+    Rectangle fsBounds = af.getFeatureSettingsGeometry();
     for (int i = 0; i < size; i++)
     {
       AlignmentPanel ap = af.alignPanels.get(i);
       AlignFrame newaf = new AlignFrame(ap);
 
+      // BH! not in applet branch
+      // transfer reference for existing feature settings to new alignFrame
+      if (ap == af.alignPanel)
+      {
+        if (viewFeatureSettings != null && viewFeatureSettings.fr.ap == ap)
+        {
+          newaf.featureSettings = viewFeatureSettings;
+        }
+        newaf.setFeatureSettingsGeometry(fsBounds);
+      }
+
       /*
        * Restore the view's last exploded frame geometry if known. Multiple
        * views from one exploded frame share and restore the same (frame)
@@ -2077,8 +2046,19 @@ public class Desktop extends GDesktop
 
       addInternalFrame(newaf, af.getTitle(), AlignFrame.DEFAULT_WIDTH,
               AlignFrame.DEFAULT_HEIGHT);
+      // BH! not in applet branch
+      // and materialise a new feature settings dialog instance for the new alignframe
+      // (closes the old as if 'OK' was pressed)
+      if (ap == af.alignPanel && newaf.featureSettings != null
+              && newaf.featureSettings.isOpen()
+              && af.alignPanel.getAlignViewport().isShowSequenceFeatures())
+      {
+        newaf.showFeatureSettingsUI();
+      }
     }
 
+    // BH! not in applet branch
+    af.featureSettings = null;
     af.alignPanels.clear();
     af.closeMenuItem_actionPerformed(true);
 
@@ -2119,374 +2099,32 @@ public class Desktop extends GDesktop
 
         if (gatherThis)
         {
-          af.alignPanels.clear();
-          af.closeMenuItem_actionPerformed(true);
-        }
-      }
-    }
-
-  }
-
-  jalview.gui.VamsasApplication v_client = null;
-
-  @Override
-  public void vamsasImport_actionPerformed(ActionEvent e)
-  {
-    // TODO: JAL-3048 not needed for Jalview-JS
-
-    if (v_client == null)
-    {
-      // Load and try to start a session.
-      JalviewFileChooser chooser = new JalviewFileChooser(
-              jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
-
-      chooser.setFileView(new JalviewFileView());
-      chooser.setDialogTitle(
-              MessageManager.getString("label.open_saved_vamsas_session"));
-      chooser.setToolTipText(MessageManager.getString(
-              "label.select_vamsas_session_opened_as_new_vamsas_session"));
-
-      int value = chooser.showOpenDialog(this);
-
-      if (value == JalviewFileChooser.APPROVE_OPTION)
-      {
-        String fle = chooser.getSelectedFile().toString();
-        if (!vamsasImport(chooser.getSelectedFile()))
-        {
-          JvOptionPane.showInternalMessageDialog(Desktop.getDesktopPane(),
-                  MessageManager.formatMessage(
-                          "label.couldnt_import_as_vamsas_session",
-                          new Object[]
-                          { fle }),
-                  MessageManager
-                          .getString("label.vamsas_document_import_failed"),
-                  JvOptionPane.ERROR_MESSAGE);
-        }
-      }
-    }
-    else
-    {
-      jalview.bin.Cache.log.error(
-              "Implementation error - load session from a running session is not supported.");
-    }
-  }
-
-  /**
-   * import file into a new vamsas session (uses jalview.gui.VamsasApplication)
-   * 
-   * @param file
-   * @return true if import was a success and a session was started.
-   */
-  public boolean vamsasImport(URL url)
-  {
-    // TODO: create progress bar
-    if (v_client != null)
-    {
-
-      jalview.bin.Cache.log.error(
-              "Implementation error - load session from a running session is not supported.");
-      return false;
-    }
-
-    try
-    {
-      // copy the URL content to a temporary local file
-      // TODO: be a bit cleverer here with nio (?!)
-      File file = File.createTempFile("vdocfromurl", ".vdj");
-      FileOutputStream fos = new FileOutputStream(file);
-      BufferedInputStream bis = new BufferedInputStream(url.openStream());
-      byte[] buffer = new byte[2048];
-      int ln;
-      while ((ln = bis.read(buffer)) > -1)
-      {
-        fos.write(buffer, 0, ln);
-      }
-      bis.close();
-      fos.close();
-      v_client = new jalview.gui.VamsasApplication(this, file,
-              url.toExternalForm());
-    } catch (Exception ex)
-    {
-      jalview.bin.Cache.log.error(
-              "Failed to create new vamsas session from contents of URL "
-                      + url,
-              ex);
-      return false;
-    }
-    setupVamsasConnectedGui();
-    v_client.initial_update(); // TODO: thread ?
-    return v_client.inSession();
-  }
-
-  /**
-   * import file into a new vamsas session (uses jalview.gui.VamsasApplication)
-   * 
-   * @param file
-   * @return true if import was a success and a session was started.
-   */
-  public boolean vamsasImport(File file)
-  {
-    if (v_client != null)
-    {
-
-      jalview.bin.Cache.log.error(
-              "Implementation error - load session from a running session is not supported.");
-      return false;
-    }
-
-    setProgressBar(MessageManager.formatMessage(
-            "status.importing_vamsas_session_from", new Object[]
-            { file.getName() }), file.hashCode());
-    try
-    {
-      v_client = new jalview.gui.VamsasApplication(this, file, null);
-    } catch (Exception ex)
-    {
-      setProgressBar(MessageManager.formatMessage(
-              "status.importing_vamsas_session_from", new Object[]
-              { file.getName() }), file.hashCode());
-      jalview.bin.Cache.log.error(
-              "New vamsas session from existing session file failed:", ex);
-      return false;
-    }
-    setupVamsasConnectedGui();
-    v_client.initial_update(); // TODO: thread ?
-    setProgressBar(MessageManager.formatMessage(
-            "status.importing_vamsas_session_from", new Object[]
-            { file.getName() }), file.hashCode());
-    return v_client.inSession();
-  }
-
-  public boolean joinVamsasSession(String mysesid)
-  {
-    if (v_client != null)
-    {
-      throw new Error(MessageManager
-              .getString("error.try_join_vamsas_session_another"));
-    }
-    if (mysesid == null)
-    {
-      throw new Error(
-              MessageManager.getString("error.invalid_vamsas_session_id"));
-    }
-    v_client = new VamsasApplication(this, mysesid);
-    setupVamsasConnectedGui();
-    v_client.initial_update();
-    return (v_client.inSession());
-  }
-
-  @Override
-  public void vamsasStart_actionPerformed(ActionEvent e)
-  {
-    if (v_client == null)
-    {
-      // Start a session.
-      // we just start a default session for moment.
-      /*
-       * JalviewFileChooser chooser = new JalviewFileChooser(jalview.bin.Cache.
-       * getProperty("LAST_DIRECTORY"));
-       * 
-       * chooser.setFileView(new JalviewFileView());
-       * chooser.setDialogTitle("Load Vamsas file");
-       * chooser.setToolTipText("Import");
-       * 
-       * int value = chooser.showOpenDialog(this);
-       * 
-       * if (value == JalviewFileChooser.APPROVE_OPTION) { v_client = new
-       * jalview.gui.VamsasApplication(this, chooser.getSelectedFile());
-       */
-      v_client = new VamsasApplication(this);
-      setupVamsasConnectedGui();
-      v_client.initial_update(); // TODO: thread ?
-    }
-    else
-    {
-      // store current data in session.
-      v_client.push_update(); // TODO: thread
-    }
-  }
-
-  protected void setupVamsasConnectedGui()
-  {
-    vamsasStart.setText(MessageManager.getString("label.session_update"));
-    vamsasSave.setVisible(true);
-    vamsasStop.setVisible(true);
-    vamsasImport.setVisible(false); // Document import to existing session is
-    // not possible for vamsas-client-1.0.
-  }
-
-  protected void setupVamsasDisconnectedGui()
-  {
-    vamsasSave.setVisible(false);
-    vamsasStop.setVisible(false);
-    vamsasImport.setVisible(true);
-    vamsasStart
-            .setText(MessageManager.getString("label.new_vamsas_session"));
-  }
-
-  @Override
-  public void vamsasStop_actionPerformed(ActionEvent e)
-  {
-    if (v_client != null)
-    {
-      v_client.end_session();
-      v_client = null;
-      setupVamsasDisconnectedGui();
-    }
-  }
-
-  protected void buildVamsasStMenu()
-  {
-    if (v_client == null)
-    {
-      String[] sess = null;
-      try
-      {
-        sess = VamsasApplication.getSessionList();
-      } catch (Exception e)
-      {
-        jalview.bin.Cache.log.warn("Problem getting current sessions list.",
-                e);
-        sess = null;
-      }
-      if (sess != null)
-      {
-        jalview.bin.Cache.log.debug(
-                "Got current sessions list: " + sess.length + " entries.");
-        VamsasStMenu.removeAll();
-        for (int i = 0; i < sess.length; i++)
-        {
-          JMenuItem sessit = new JMenuItem();
-          sessit.setText(sess[i]);
-          sessit.setToolTipText(MessageManager
-                  .formatMessage("label.connect_to_session", new Object[]
-                  { sess[i] }));
-          final Desktop dsktp = this;
-          final String mysesid = sess[i];
-          sessit.addActionListener(new ActionListener()
-          {
-
-            @Override
-            public void actionPerformed(ActionEvent e)
+            if (af.featureSettings != null && af.featureSettings.isOpen())
             {
-              if (dsktp.v_client == null)
+              if (source.featureSettings == null)
               {
-                Thread rthr = new Thread(new Runnable()
-                {
-
-                  @Override
-                  public void run()
-                  {
-                    dsktp.v_client = new VamsasApplication(dsktp, mysesid);
-                    dsktp.setupVamsasConnectedGui();
-                    dsktp.v_client.initial_update();
-                  }
-
-                });
-                rthr.start();
+                // preserve the feature settings geometry for this frame
+                source.featureSettings = af.featureSettings;
+                source.setFeatureSettingsGeometry(
+                        af.getFeatureSettingsGeometry());
+              }
+              else
+              {
+                // close it and forget
+                af.featureSettings.close();
               }
             }
-          });
-          VamsasStMenu.add(sessit);
-        }
-        // don't show an empty menu.
-        VamsasStMenu.setVisible(sess.length > 0);
-
-      }
-      else
-      {
-        jalview.bin.Cache.log.debug("No current vamsas sessions.");
-        VamsasStMenu.removeAll();
-        VamsasStMenu.setVisible(false);
-      }
-    }
-    else
-    {
-      // Not interested in the content. Just hide ourselves.
-      VamsasStMenu.setVisible(false);
-    }
-  }
-
-  @Override
-  public void vamsasSave_actionPerformed(ActionEvent e)
-  {
-    // TODO: JAL-3048 not needed for Jalview-JS
-
-    if (v_client != null)
-    {
-      // TODO: VAMSAS DOCUMENT EXTENSION is VDJ
-      JalviewFileChooser chooser = new JalviewFileChooser("vdj",
-              "Vamsas Document");
-
-      chooser.setFileView(new JalviewFileView());
-      chooser.setDialogTitle(MessageManager
-              .getString("label.save_vamsas_document_archive"));
-
-      int value = chooser.showSaveDialog(this);
-
-      if (value == JalviewFileChooser.APPROVE_OPTION)
-      {
-        java.io.File choice = chooser.getSelectedFile();
-        JPanel progpanel = addProgressPanel(MessageManager
-                .formatMessage("label.saving_vamsas_doc", new Object[]
-                { choice.getName() }));
-        Cache.setProperty("LAST_DIRECTORY", choice.getParent());
-        String warnmsg = null;
-        String warnttl = null;
-        try
-        {
-          v_client.vclient.storeDocument(choice);
-        } catch (Error ex)
-        {
-          warnttl = "Serious Problem saving Vamsas Document";
-          warnmsg = ex.toString();
-          jalview.bin.Cache.log
-                  .error("Error Whilst saving document to " + choice, ex);
-
-        } catch (Exception ex)
-        {
-          warnttl = "Problem saving Vamsas Document.";
-          warnmsg = ex.toString();
-          jalview.bin.Cache.log.warn(
-                  "Exception Whilst saving document to " + choice, ex);
-
-        }
-        removeProgressPanel(progpanel);
-        if (warnmsg != null)
-        {
-          JvOptionPane.showInternalMessageDialog(Desktop.getDesktopPane(),
-
-                  warnmsg, warnttl, JvOptionPane.ERROR_MESSAGE);
+            af.alignPanels.clear();
+            af.closeMenuItem_actionPerformed(true);
+          }
         }
       }
-    }
-  }
-
-  JPanel vamUpdate = null;
-
-  /**
-   * hide vamsas user gui bits when a vamsas document event is being handled.
-   * 
-   * @param b
-   *          true to hide gui, false to reveal gui
-   */
-  public void setVamsasUpdate(boolean b)
-  {
-    Cache.log.debug("Setting gui for Vamsas update "
-            + (b ? "in progress" : "finished"));
-
-    if (vamUpdate != null)
+    // refresh the feature setting UI for the source frame if it exists
+    if (source.featureSettings != null
+            && source.featureSettings.isOpen())
     {
-      this.removeProgressPanel(vamUpdate);
+      source.showFeatureSettingsUI();
     }
-    if (b)
-    {
-      vamUpdate = this.addProgressPanel(
-              MessageManager.getString("label.updating_vamsas_session"));
-    }
-    vamsasStart.setVisible(!b);
-    vamsasStop.setVisible(!b);
-    vamsasSave.setVisible(!b);
   }
 
   public JInternalFrame[] getAllFrames()
@@ -2692,7 +2330,7 @@ public class Desktop extends GDesktop
   {
     if (Jalview.isHeadlessMode())
     {
-      // Desktop.getDesktop() is null in headless mode
+      // Desktop.getDesktopPane() is null in headless mode
       return new AlignFrame[] { Jalview.getCurrentAlignFrame() };
     }
 
@@ -2776,7 +2414,7 @@ public class Desktop extends GDesktop
       openGroovyConsole();
     } catch (Exception ex)
     {
-      jalview.bin.Cache.log.error("Groovy Shell Creation failed.", ex);
+      Cache.log.error("Groovy Shell Creation failed.", ex);
       JvOptionPane.showInternalMessageDialog(desktopPane,
 
               MessageManager.getString("label.couldnt_create_groovy_shell"),
@@ -2837,7 +2475,7 @@ public class Desktop extends GDesktop
   {
     getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
             .put(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
-                    Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()),
+                    ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx()),
                     "Quit");
     getRootPane().getActionMap().put("Quit", new AbstractAction()
     {
@@ -2888,7 +2526,7 @@ public class Desktop extends GDesktop
   @Override
   public void setProgressBar(String message, long id)
   {
-           Platform.timeCheck("Desktop " + message, Platform.TIME_MARK);     
+    // Platform.timeCheck("Desktop " + message, Platform.TIME_MARK);
 
     if (progressBars == null)
     {
@@ -2896,18 +2534,18 @@ public class Desktop extends GDesktop
       progressBarHandlers = new Hashtable<>();
     }
 
-    if (progressBars.get(new Long(id)) != null)
+    if (progressBars.get(Long.valueOf(id)) != null)
     {
-      JPanel panel = progressBars.remove(new Long(id));
-      if (progressBarHandlers.contains(new Long(id)))
+      JPanel panel = progressBars.remove(Long.valueOf(id));
+      if (progressBarHandlers.contains(Long.valueOf(id)))
       {
-        progressBarHandlers.remove(new Long(id));
+        progressBarHandlers.remove(Long.valueOf(id));
       }
       removeProgressPanel(panel);
     }
     else
     {
-      progressBars.put(new Long(id), addProgressPanel(message));
+      progressBars.put(Long.valueOf(id), addProgressPanel(message));
     }
   }
 
@@ -2922,13 +2560,13 @@ public class Desktop extends GDesktop
           final IProgressIndicatorHandler handler)
   {
     if (progressBarHandlers == null
-            || !progressBars.containsKey(new Long(id)))
+            || !progressBars.containsKey(Long.valueOf(id)))
     {
       throw new Error(MessageManager.getString(
               "error.call_setprogressbar_before_registering_handler"));
     }
-    progressBarHandlers.put(new Long(id), handler);
-    final JPanel progressPanel = progressBars.get(new Long(id));
+    progressBarHandlers.put(Long.valueOf(id), handler);
+    final JPanel progressPanel = progressBars.get(Long.valueOf(id));
     if (handler.canCancel())
     {
       JButton cancel = new JButton(
@@ -2989,12 +2627,6 @@ public class Desktop extends GDesktop
     return null;
   }
 
-  public VamsasApplication getVamsasApplication()
-  {
-    return v_client;
-
-  }
-
   /**
    * flag set if jalview GUI is being operated programmatically
    */
@@ -3035,8 +2667,8 @@ public class Desktop extends GDesktop
       // todo: changesupport handlers need to be transferred
       if (discoverer == null)
       {
-        discoverer = Discoverer.getInstance();
-        // register PCS handler for getDesktop().
+        discoverer = jalview.ws.jws1.Discoverer.getInstance();
+        // register PCS handler for getDesktopPane().
         discoverer.addPropertyChangeListener(changeSupport);
       }
       // JAL-940 - disabled JWS1 service configuration - always start discoverer
@@ -3175,13 +2807,13 @@ public class Desktop extends GDesktop
       {
         try
         {
-          if (progress != null)
+          if (progress != null && !Platform.isJS())
           {
             progress.setProgressBar(MessageManager
                     .formatMessage("status.opening_params", new Object[]
                     { url }), this.hashCode());
           }
-          BrowserLauncher.openURL(url);
+          Platform.openURL(url);
         } catch (Exception ex)
         {
           JvOptionPane.showInternalMessageDialog(Desktop.getDesktopPane(),
@@ -3192,7 +2824,7 @@ public class Desktop extends GDesktop
 
           ex.printStackTrace();
         }
-        if (progress != null)
+        if (progress != null && !Platform.isJS())
         {
           progress.setProgressBar(null, this.hashCode());
         }
@@ -3254,13 +2886,14 @@ public class Desktop extends GDesktop
   /**
    * flag indicating if dialogExecutor should try to acquire a permit
    */
-  volatile boolean dialogPause = true;
+  private volatile boolean dialogPause = true;
 
   /**
    * pause the queue
    */
-  java.util.concurrent.Semaphore block = new Semaphore(0);
+  private java.util.concurrent.Semaphore block = new Semaphore(0);
 
+  // BH was static
   private groovy.ui.Console groovyConsole;
 
   public StructureViewer lastTargetedView;
@@ -3286,6 +2919,7 @@ public class Desktop extends GDesktop
           {
           }
         }
+        // BH! Q: do we mean System.headless ? or "nogui/nodisplay" headless?
         if (Jalview.isHeadlessMode())
         {
           return;
@@ -3768,4 +3402,39 @@ public class Desktop extends GDesktop
     return result;
   }
 
+  
+
+  public MyDesktopPane desktopPane;
+
+  /**
+   * Get the instance of the JDesktopPane from the application-local Desktop
+   * (JFrame) instance
+   * 
+   * The key here is that the Java application can have multiple static
+   * instances of the desktop JFrame because those instances are sandboxed, but
+   * the SwingJS JFrames will be in the same VM-like space. So we need
+   * application singletons, at least for JavaScript.
+   * 
+   * @return
+   */
+  public static MyDesktopPane getDesktopPane()
+  {
+    Desktop desktop = Desktop.getInstance();
+    return desktop == null ? null : desktop.desktopPane;
+  }
+
+  /**
+   * Answers an 'application scope' singleton instance of this class. Separate
+   * SwingJS 'applets' running in the same browser page will each have a
+   * distinct instance of Desktop.
+   * 
+   * @return
+   */
+  public static Desktop getInstance()
+  {
+    return Jalview.isHeadlessMode() ? null
+            : (Desktop) ApplicationSingletonProvider
+                    .getInstance(Desktop.class);
+  }
+  
 }
index ddadd6d..9ffcaee 100644 (file)
  */
 package jalview.gui;
 
-import jalview.api.FeatureColourI;
-import jalview.api.FeatureSettingsControllerI;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.SequenceI;
-import jalview.datamodel.features.FeatureMatcher;
-import jalview.datamodel.features.FeatureMatcherI;
-import jalview.datamodel.features.FeatureMatcherSet;
-import jalview.datamodel.features.FeatureMatcherSetI;
-import jalview.gui.Help.HelpId;
-import jalview.gui.JalviewColourChooser.ColourChooserListener;
-import jalview.io.JalviewFileChooser;
-import jalview.io.JalviewFileView;
-import jalview.schemes.FeatureColour;
-import jalview.util.MessageManager;
-import jalview.util.Platform;
-import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
-import jalview.xml.binding.jalview.JalviewUserColours;
-import jalview.xml.binding.jalview.JalviewUserColours.Colour;
-import jalview.xml.binding.jalview.JalviewUserColours.Filter;
-import jalview.xml.binding.jalview.ObjectFactory;
-
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Component;
 import java.awt.Dimension;
+import java.awt.FlowLayout;
 import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.GridLayout;
@@ -108,8 +88,35 @@ import javax.xml.bind.Marshaller;
 import javax.xml.stream.XMLInputFactory;
 import javax.xml.stream.XMLStreamReader;
 
+import jalview.api.AlignViewControllerGuiI;
+import jalview.api.AlignViewportI;
+import jalview.api.FeatureColourI;
+import jalview.api.FeatureSettingsControllerI;
+import jalview.api.SplitContainerI;
+import jalview.api.ViewStyleI;
+import jalview.controller.FeatureSettingsControllerGuiI;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.gui.Help.HelpId;
+import jalview.gui.JalviewColourChooser.ColourChooserListener;
+import jalview.io.JalviewFileChooser;
+import jalview.io.JalviewFileView;
+import jalview.schemes.FeatureColour;
+import jalview.util.MessageManager;
+import jalview.util.Platform;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
+import jalview.viewmodel.styles.ViewStyle;
+import jalview.xml.binding.jalview.JalviewUserColours;
+import jalview.xml.binding.jalview.JalviewUserColours.Colour;
+import jalview.xml.binding.jalview.JalviewUserColours.Filter;
+import jalview.xml.binding.jalview.ObjectFactory;
+
 public class FeatureSettings extends JPanel
-        implements FeatureSettingsControllerI
+        implements FeatureSettingsControllerI, FeatureSettingsControllerGuiI
 {
   private static final String SEQUENCE_FEATURE_COLOURS = MessageManager
           .getString("label.sequence_feature_colours");
@@ -145,7 +152,9 @@ public class FeatureSettings extends JPanel
 
   float originalTransparency;
 
-  Map<String, FeatureMatcherSetI> originalFilters;
+  private ViewStyleI originalViewStyle;
+
+  private Map<String, FeatureMatcherSetI> originalFilters;
 
   final JInternalFrame frame;
 
@@ -157,6 +166,10 @@ public class FeatureSettings extends JPanel
 
   JSlider transparency = new JSlider();
 
+  private JCheckBox showComplementOnTop;
+
+  private JCheckBox showComplement;
+
   /*
    * when true, constructor is still executing - so ignore UI events
    */
@@ -172,10 +185,36 @@ public class FeatureSettings extends JPanel
   boolean handlingUpdate = false;
 
   /*
+   * a change listener to ensure the dialog is updated if
+   * FeatureRenderer discovers new features
+   */
+  private PropertyChangeListener change;
+
+  /*
    * holds {featureCount, totalExtent} for each feature type
    */
   Map<String, float[]> typeWidth = null;
 
+  private void storeOriginalSettings()
+  {
+    // save transparency for restore on Cancel
+    originalTransparency = fr.getTransparency();
+
+    updateTransparencySliderFromFR();
+
+    originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy
+    originalViewStyle = new ViewStyle(af.viewport.getViewStyle());
+  }
+
+  private void updateTransparencySliderFromFR()
+  {
+    boolean incon = inConstruction;
+    inConstruction = true;
+
+    int transparencyAsPercent = (int) (fr.getTransparency() * 100);
+    transparency.setValue(100 - transparencyAsPercent);
+    inConstruction = incon;
+  }
   /**
    * Constructor
    * 
@@ -186,12 +225,7 @@ public class FeatureSettings extends JPanel
     this.af = alignFrame;
     fr = af.getFeatureRenderer();
 
-    // save transparency for restore on Cancel
-    originalTransparency = fr.getTransparency();
-    int originalTransparencyAsPercent = (int) (originalTransparency * 100);
-    transparency.setMaximum(100 - originalTransparencyAsPercent);
-
-    originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy
+    storeOriginalSettings();
 
     try
     {
@@ -277,20 +311,23 @@ public class FeatureSettings extends JPanel
       @Override
       public void mousePressed(MouseEvent evt)
       {
-        selectedRow = table.rowAtPoint(evt.getPoint());
+        Point pt = evt.getPoint();
+        selectedRow = table.rowAtPoint(pt);
         String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
         if (evt.isPopupTrigger())
         {
           Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
           showPopupMenu(selectedRow, type, colour, evt.getPoint());
         }
-        else if (evt.getClickCount() == 2)
+        else if (evt.getClickCount() == 2
+                && table.columnAtPoint(pt) == TYPE_COLUMN)
         {
           boolean invertSelection = evt.isAltDown();
           boolean toggleSelection = Platform.isControlDown(evt);
           boolean extendSelection = evt.isShiftDown();
           fr.ap.alignFrame.avc.markColumnsContainingFeatures(
                   invertSelection, extendSelection, toggleSelection, type);
+          fr.ap.av.sendSelection();
         }
       }
 
@@ -346,7 +383,6 @@ public class FeatureSettings extends JPanel
     }
 
     discoverAllFeatureData();
-    final PropertyChangeListener change;
     final FeatureSettings fs = this;
     fr.addPropertyChangeListener(change = new PropertyChangeListener()
     {
@@ -364,11 +400,47 @@ public class FeatureSettings extends JPanel
 
     });
 
+    SplitContainerI splitframe = af.getSplitViewContainer();
+    if (splitframe != null)
+    {
+      frame = null; // keeps eclipse happy
+      splitframe.addFeatureSettingsUI(this);
+    }
+    else
+    {
     frame = new JInternalFrame();
     frame.setContentPane(this);
-    Desktop.addInternalFrame(frame,
-            MessageManager.getString("label.sequence_feature_settings"),
-            600, Platform.isAMacAndNotJS() ? 480 : 450);
+      Rectangle bounds = af.getFeatureSettingsGeometry();
+      String title;
+      if (af.getAlignPanels().size() > 1 || Desktop.getAlignmentPanels(
+              af.alignPanel.av.getSequenceSetId()).length > 1)
+      {
+        title = MessageManager.formatMessage(
+                "label.sequence_feature_settings_for_view",
+                af.alignPanel.getViewName());
+      }
+      else
+      {
+        title = MessageManager.getString("label.sequence_feature_settings");
+      }
+      if (bounds == null)
+      {
+        if (Platform.isAMacAndNotJS())
+        {
+          Desktop.addInternalFrame(frame, title, 600, 480);
+        }
+        else
+        {
+          Desktop.addInternalFrame(frame, title, 600, 450);
+        }
+      }
+      else
+      {
+        Desktop.addInternalFrame(frame, title, false, bounds.width,
+                bounds.height);
+        frame.setBounds(bounds);
+        frame.setVisible(true);
+      }
     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
 
     frame.addInternalFrameListener(
@@ -378,24 +450,48 @@ public class FeatureSettings extends JPanel
               public void internalFrameClosed(
                       javax.swing.event.InternalFrameEvent evt)
               {
-                fr.removePropertyChangeListener(change);
-              }
+                  featureSettings_isClosed();
+                };
             });
     frame.setLayer(JLayeredPane.PALETTE_LAYER);
+    }
     inConstruction = false;
   }
 
   /**
-   * Constructs and shows a popup menu of possible actions on the selected row
-   * and feature type
+   * Sets the state of buttons to show complement features from viewport
+   * settings
+   */
+  private void updateComplementButtons()
+  {
+    showComplement.setSelected(af.getViewport().isShowComplementFeatures());
+    showComplementOnTop
+            .setSelected(af.getViewport().isShowComplementFeaturesOnTop());
+  }
+
+  @Override
+  public AlignViewControllerGuiI getAlignframe()
+  {
+    return af;
+  }
+
+  @Override
+  public void featureSettings_isClosed()
+  {
+    fr.removePropertyChangeListener(change);
+    change = null;
+  }
+
+       /**
+        * Constructs and shows a popup menu of possible actions on the selected row and
+        * feature type
    * 
    * @param rowSelected
    * @param type
    * @param typeCol
    * @param pt
    */
-  protected void showPopupMenu(final int rowSelected, final String type,
-          final Object typeCol, final Point pt)
+       protected void showPopupMenu(final int rowSelected, final String type, final Object typeCol, final Point pt)
   {
     JPopupMenu men = new JPopupMenu(MessageManager
             .formatMessage("label.settings_for_param", new String[]
@@ -421,10 +517,9 @@ public class FeatureSettings extends JPanel
       {
         if (e.getSource() == variableColourCB)
         {
-          men.setVisible(true); // BH 2018 for JavaScript because this is a
-                                // checkbox
-          men.setVisible(false); // BH 2018 for JavaScript because this is a
-                                 // checkbox
+                 // BH 2018 for JavaScript because this is a checkbox
+                 men.setVisible(true);
+          men.setVisible(false);
           if (featureColour.isSimpleColour())
           {
             /*
@@ -486,8 +581,7 @@ public class FeatureSettings extends JPanel
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        af.avc.sortAlignmentByFeatureScore(Arrays.asList(new String[]
-                { type }));
+        sortByScore(Arrays.asList(new String[] { type }));
       }
     });
     JMenuItem dens = new JMenuItem(
@@ -498,8 +592,7 @@ public class FeatureSettings extends JPanel
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        af.avc.sortAlignmentByFeatureDensity(Arrays.asList(new String[]
-                { type }));
+        sortByDensity(Arrays.asList(new String[] { type }));
       }
     });
     men.add(dens);
@@ -513,6 +606,7 @@ public class FeatureSettings extends JPanel
       {
         fr.ap.alignFrame.avc.markColumnsContainingFeatures(false, false,
                 false, type);
+        fr.ap.av.sendSelection();
       }
     });
     JMenuItem clearCols = new JMenuItem(MessageManager
@@ -524,6 +618,7 @@ public class FeatureSettings extends JPanel
       {
         fr.ap.alignFrame.avc.markColumnsContainingFeatures(true, false,
                 false, type);
+        fr.ap.av.sendSelection();
       }
     });
     JMenuItem hideCols = new JMenuItem(
@@ -534,6 +629,7 @@ public class FeatureSettings extends JPanel
       public void actionPerformed(ActionEvent arg0)
       {
         fr.ap.alignFrame.hideFeatureColumns(type, true);
+        fr.ap.av.sendSelection();
       }
     });
     JMenuItem hideOtherCols = new JMenuItem(
@@ -544,6 +640,7 @@ public class FeatureSettings extends JPanel
       public void actionPerformed(ActionEvent arg0)
       {
         fr.ap.alignFrame.hideFeatureColumns(type, false);
+        fr.ap.av.sendSelection();
       }
     });
     men.add(selCols);
@@ -553,6 +650,47 @@ public class FeatureSettings extends JPanel
     men.show(table, pt.x, pt.y);
   }
 
+  /**
+   * Sort the sequences in the alignment by the number of features for the given
+   * feature types (or all features if null)
+   * 
+   * @param featureTypes
+   */
+  protected void sortByDensity(List<String> featureTypes)
+  {
+    af.avc.sortAlignmentByFeatureDensity(featureTypes);
+  }
+
+  /**
+   * Sort the sequences in the alignment by average score for the given feature
+   * types (or all features if null)
+   * 
+   * @param featureTypes
+   */
+  protected void sortByScore(List<String> featureTypes)
+  {
+    af.avc.sortAlignmentByFeatureScore(featureTypes);
+  }
+
+  /**
+   * Returns true if at least one feature type is visible. Else shows a warning
+   * dialog and returns false.
+   * 
+   * @param title
+   * @return
+   */
+  private boolean canSortBy(String title)
+  {
+    if (fr.getDisplayedFeatureTypes().isEmpty())
+    {
+      JvOptionPane.showMessageDialog(this,
+              MessageManager.getString("label.no_features_to_sort_by"),
+              title, JvOptionPane.OK_OPTION);
+      return false;
+    }
+    return true;
+  }
+
   @Override
   synchronized public void discoverAllFeatureData()
   {
@@ -607,7 +745,7 @@ public class FeatureSettings extends JPanel
       {
         fr.setGroupVisibility(check.getText(), check.isSelected());
         resetTable(new String[] { grp });
-        af.alignPanel.paintAlignment(true, true);
+        refreshDisplay();
       }
     });
     groupPanel.add(check);
@@ -705,7 +843,7 @@ public class FeatureSettings extends JPanel
         data[dataIndex][FILTER_COLUMN] = featureFilter == null
                 ? new FeatureMatcherSet()
                 : featureFilter;
-        data[dataIndex][SHOW_COLUMN] = new Boolean(
+        data[dataIndex][SHOW_COLUMN] = Boolean.valueOf(
                 af.getViewport().getFeaturesDisplayed().isVisible(type));
         dataIndex++;
         displayableTypes.remove(type);
@@ -732,7 +870,7 @@ public class FeatureSettings extends JPanel
       data[dataIndex][FILTER_COLUMN] = featureFilter == null
               ? new FeatureMatcherSet()
               : featureFilter;
-      data[dataIndex][SHOW_COLUMN] = new Boolean(true);
+      data[dataIndex][SHOW_COLUMN] = Boolean.valueOf(true);
       dataIndex++;
       displayableTypes.remove(type);
     }
@@ -1137,49 +1275,64 @@ public class FeatureSettings extends JPanel
     table.repaint();
   }
 
+  /**
+   * close ourselves but leave any existing UI handlers (e.g a CDS/Protein tabbed
+   * feature settings dialog) intact
+   */
+  public void closeOldSettings()
+  {
+    closeDialog(false);
+  }
+
+  /**
+   * close the feature settings dialog (and any containing frame)
+   */
   public void close()
   {
+    closeDialog(true);
+  }
+
+  private void closeDialog(boolean closeContainingFrame)
+  {
     try
     {
+      if (frame != null)
+      {
+        af.setFeatureSettingsGeometry(frame.getBounds());
       frame.setClosed(true);
+      }
+      else
+      {
+        SplitContainerI sc = af.getSplitViewContainer();
+        sc.closeFeatureSettings(this, closeContainingFrame);
+        af.featureSettings = null;
+      }
     } catch (Exception exe)
     {
     }
 
   }
 
-  /**
-   * Update the priority order of features; only repaint if this changed the
-   * order of visible features. Any newly discovered feature types are set to
-   * visible. Returns true if repaint was requested, false if not.
-   * 
-   * @param data
-   * @return
-   */
-  public boolean updateFeatureRenderer(Object[][] data)
+  public void updateFeatureRenderer(Object[][] data)
   {
-    return updateFeatureRenderer(data, true);
+    updateFeatureRenderer(data, true);
   }
 
   /**
    * Update the priority order of features; only repaint if this changed the
-   * order of visible features. Returns true if repaint was requested, false if
-   * not.
+   * order of visible features
    * 
    * @param data
    * @param visibleNew
-   * @return
    */
-  boolean updateFeatureRenderer(Object[][] data, boolean visibleNew)
+  void updateFeatureRenderer(Object[][] data, boolean visibleNew)
   {
     FeatureSettingsBean[] rowData = getTableAsBeans(data);
 
     if (fr.setFeaturePriority(rowData, visibleNew))
     {
-      af.alignPanel.paintAlignment(true, true);
-      return true;
+      refreshDisplay();
     }
-    return false;
   }
 
   /**
@@ -1204,6 +1357,9 @@ public class FeatureSettings extends JPanel
   {
     this.setLayout(new BorderLayout());
 
+    final boolean hasComplement = af.getViewport()
+            .getCodingComplement() != null;
+
     JPanel settingsPane = new JPanel();
     settingsPane.setLayout(new BorderLayout());
 
@@ -1237,26 +1393,32 @@ public class FeatureSettings extends JPanel
       }
     });
 
-    JButton sortByScore = new JButton(
-            MessageManager.getString("label.seq_sort_by_score"));
+    final String byScoreLabel = MessageManager.getString("label.seq_sort_by_score");
+    JButton sortByScore = new JButton(byScoreLabel);
     sortByScore.setFont(JvSwingUtils.getLabelFont());
     sortByScore.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        af.avc.sortAlignmentByFeatureScore(null);
+        if (canSortBy(byScoreLabel))
+        {
+          sortByScore(null);
+        }
       }
     });
-    JButton sortByDens = new JButton(
-            MessageManager.getString("label.sequence_sort_by_density"));
+    final String byDensityLabel = MessageManager.getString("label.sequence_sort_by_density");
+    JButton sortByDens = new JButton(byDensityLabel);
     sortByDens.setFont(JvSwingUtils.getLabelFont());
     sortByDens.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        af.avc.sortAlignmentByFeatureDensity(null);
+        if (canSortBy(byDensityLabel))
+        {
+          sortByDensity(null);
+        }
       }
     });
 
@@ -1276,27 +1438,49 @@ public class FeatureSettings extends JPanel
         }
       }
     });
-
-    JButton cancel = new JButton(MessageManager.getString("action.cancel"));
+    // Cancel for a SplitFrame should just revert changes to the currently displayed
+    // settings. May want to do this for either or both - so need a splitview
+    // feature settings cancel/OK.
+    JButton cancel = new JButton(MessageManager
+            .getString(hasComplement ? "action.revert" : "action.cancel"));
+    cancel.setToolTipText(MessageManager.getString(hasComplement
+            ? "action.undo_changes_to_feature_settings"
+            : "action.undo_changes_to_feature_settings_and_close_the_dialog"));
     cancel.setFont(JvSwingUtils.getLabelFont());
+    // TODO: disable cancel (and apply!) until current settings are different
     cancel.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        cancel();
+        revert();
+        refreshDisplay();
+        if (!hasComplement)
+        {
+          close();
+        }
       }
     });
-
-    JButton ok = new JButton(MessageManager.getString("action.ok"));
+    // Cancel for the whole dialog should cancel both CDS and Protein.
+    // OK for an individual feature settings just applies changes, but dialog
+    // remains open
+    JButton ok = new JButton(MessageManager
+            .getString(hasComplement ? "action.apply" : "action.ok"));
     ok.setFont(JvSwingUtils.getLabelFont());
     ok.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
+        if (!hasComplement)
+        {
         close();
       }
+        else
+        {
+          storeOriginalSettings();
+        }
+      }
     });
 
     JButton loadColours = new JButton(
@@ -1334,7 +1518,7 @@ public class FeatureSettings extends JPanel
         if (!inConstruction)
         {
           fr.setTransparency((100 - transparency.getValue()) / 100f);
-          af.alignPanel.paintAlignment(true, true);
+          refreshDisplay();
         }
       }
     });
@@ -1343,8 +1527,41 @@ public class FeatureSettings extends JPanel
     transparency.setToolTipText(
             MessageManager.getString("label.transparency_tip"));
 
-    JPanel transPanel = new JPanel(new GridLayout(1, 2));
-    bigPanel.add(transPanel, BorderLayout.SOUTH);
+    boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
+    String text = MessageManager.formatMessage("label.show_linked_features",
+            nucleotide
+                    ? MessageManager.getString("label.protein")
+                            .toLowerCase()
+                    : "CDS");
+    showComplement = new JCheckBox(text);
+    showComplement.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        af.getViewport()
+                .setShowComplementFeatures(showComplement.isSelected());
+        refreshDisplay();
+      }
+    });
+
+    showComplementOnTop = new JCheckBox(
+            MessageManager.getString("label.on_top"));
+    showComplementOnTop.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        af.getViewport().setShowComplementFeaturesOnTop(
+                showComplementOnTop.isSelected());
+        refreshDisplay();
+      }
+    });
+
+    updateComplementButtons();
+
+    JPanel lowerPanel = new JPanel(new GridLayout(1, 2));
+    bigPanel.add(lowerPanel, BorderLayout.SOUTH);
 
     JPanel transbuttons = new JPanel(new GridLayout(5, 1));
     transbuttons.add(optimizeOrder);
@@ -1352,8 +1569,20 @@ public class FeatureSettings extends JPanel
     transbuttons.add(sortByScore);
     transbuttons.add(sortByDens);
     transbuttons.add(help);
-    transPanel.add(transparency);
-    transPanel.add(transbuttons);
+
+    JPanel transPanelLeft = new JPanel(
+            new GridLayout(hasComplement ? 4 : 2, 1));
+    transPanelLeft.add(new JLabel(" Colour transparency" + ":"));
+    transPanelLeft.add(transparency);
+    if (hasComplement)
+    {
+      JPanel cp = new JPanel(new FlowLayout(FlowLayout.LEFT));
+      cp.add(showComplement);
+      cp.add(showComplementOnTop);
+      transPanelLeft.add(cp);
+    }
+    lowerPanel.add(transPanelLeft);
+    lowerPanel.add(transbuttons);
 
     JPanel buttonPanel = new JPanel();
     buttonPanel.add(ok);
@@ -1367,24 +1596,19 @@ public class FeatureSettings extends JPanel
   }
 
   /**
-   * On Cancel, restore settings as they were when the dialog was opened (or
-   * possibly with any new features added while the dialog was open)
+   * Repaints alignment, structure and overview (if shown). If there is a
+   * complementary view which is showing this view's features, then also
+   * repaints that.
    */
-  void cancel()
+  void refreshDisplay()
   {
-    fr.setTransparency(originalTransparency);
-    fr.setFeatureFilters(originalFilters);
-    boolean repainted = updateFeatureRenderer(originalData);
-
-    /*
-     * ensure alignment (and Overview if visible) are redrawn
-     */
-    if (!repainted)
+    af.alignPanel.paintAlignment(true, true);
+    AlignViewportI complement = af.getViewport().getCodingComplement();
+    if (complement != null && complement.isShowComplementFeatures())
     {
-      af.alignPanel.paintAlignment(true, true);
+      AlignFrame af2 = Desktop.getAlignFrameFor(complement);
+      af2.alignPanel.paintAlignment(true, true);
     }
-
-    close();
   }
 
   /**
@@ -1529,13 +1753,22 @@ public class FeatureSettings extends JPanel
     }
 
     /**
-     * Answers the class of the object in column c of the first row of the table
+     * Answers the class of column c of the table
      */
     @Override
     public Class<?> getColumnClass(int c)
     {
-      Object v = getValueAt(0, c);
-      return v == null ? null : v.getClass();
+      switch (c)
+      {
+      case TYPE_COLUMN:
+        return String.class;
+      case COLOUR_COLUMN:
+        return FeatureColour.class;
+      case FILTER_COLUMN:
+        return FeatureMatcherSet.class;
+      default:
+        return Boolean.class;
+      }
     }
 
     @Override
@@ -1935,6 +2168,27 @@ public class FeatureSettings extends JPanel
       return button;
     }
   }
+
+  public boolean isOpen()
+  {
+    if (af.getSplitViewContainer() != null)
+    {
+      return af.getSplitViewContainer().isFeatureSettingsOpen();
+    }
+    return frame != null && !frame.isClosed();
+  }
+
+  @Override
+  public void revert()
+  {
+    fr.setTransparency(originalTransparency);
+    fr.setFeatureFilters(originalFilters);
+    updateFeatureRenderer(originalData);
+    af.getViewport().setViewStyle(originalViewStyle);
+    updateTransparencySliderFromFR();
+    updateComplementButtons();
+    refreshDisplay();
+  }
 }
 
 class FeatureIcon implements Icon
index dadf25a..10641eb 100755 (executable)
@@ -22,7 +22,6 @@ package jalview.gui;
 
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Sequence;
-import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.gui.SeqPanel.MousePos;
@@ -436,30 +435,13 @@ public class IdPanel extends JPanel
     }
 
     Sequence sq = (Sequence) av.getAlignment().getSequenceAt(pos.seqIndex);
-
-    /*
-     *  build a new links menu based on the current links
-     *  and any non-positional features
-     */
-    List<SequenceFeature> features = null;
     if (sq != null)
     {
-    List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
-      features = sq.getFeatures().getNonPositionalFeatures();
-    for (SequenceFeature sf : features)
-    {
-      if (sf.links != null)
-      {
-        nlinks.addAll(sf.links);
+      PopupMenu pop = new PopupMenu(alignPanel, sq,
+              Preferences.getGroupURLLinks());
+      pop.show(this, e.getX(), e.getY());
       }
     }
-    }
-
-    PopupMenu pop = new PopupMenu(alignPanel, sq, features,
-            Preferences.getGroupURLLinks() // empty list; not implemented
-    );
-    pop.show(this, e.getX(), e.getY());
-  }
 
   /**
    * On right mouse click on a Consensus annotation label, shows a limited popup
index 3145f7c..2e6e1b8 100644 (file)
@@ -24,15 +24,16 @@ import jalview.analysis.AAFrequency;
 import jalview.analysis.AlignmentAnnotationUtils;
 import jalview.analysis.AlignmentUtils;
 import jalview.analysis.Conservation;
+import jalview.api.AlignViewportI;
 import jalview.bin.Cache;
 import jalview.commands.ChangeCaseCommand;
 import jalview.commands.EditCommand;
 import jalview.commands.EditCommand.Action;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.Annotation;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
@@ -48,12 +49,14 @@ import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemes;
 import jalview.schemes.PIDColourScheme;
 import jalview.schemes.ResidueColourScheme;
+import jalview.util.Comparison;
 import jalview.util.GroupUrlLink;
 import jalview.util.GroupUrlLink.UrlStringTooLongException;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.util.StringUtils;
 import jalview.util.UrlLink;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -68,6 +71,7 @@ import java.util.Hashtable;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.Vector;
@@ -89,6 +93,24 @@ import javax.swing.JScrollPane;
  */
 public class PopupMenu extends JPopupMenu implements ColourChangeListener
 {
+  /*
+   * maximum length of feature description to include in popup menu item text
+   */
+  private static final int FEATURE_DESC_MAX = 40;
+
+  /*
+   * true for ID Panel menu, false for alignment panel menu
+   */
+  private final boolean forIdPanel;
+
+  private final AlignmentPanel ap;
+
+  /*
+   * the sequence under the cursor when clicked
+   * (additional sequences may be selected)
+   */
+  private final SequenceI sequence;
+
   JMenu groupMenu = new JMenu();
 
   JMenuItem groupName = new JMenuItem();
@@ -103,28 +125,14 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
 
   protected JMenuItem modifyConservation = new JMenuItem();
 
-  AlignmentPanel ap;
-
   JMenu sequenceMenu = new JMenu();
 
-  JMenuItem sequenceName = new JMenuItem();
-
-  JMenuItem sequenceDetails = new JMenuItem();
-
-  JMenuItem sequenceSelDetails = new JMenuItem();
-
   JMenuItem makeReferenceSeq = new JMenuItem();
 
-  JMenuItem chooseAnnotations = new JMenuItem();
-
-  SequenceI sequence;
-
   JMenuItem createGroupMenuItem = new JMenuItem();
 
   JMenuItem unGroupMenuItem = new JMenuItem();
 
-  JMenuItem outline = new JMenuItem();
-
   JMenu colourMenu = new JMenu();
 
   JCheckBoxMenuItem showBoxes = new JCheckBoxMenuItem();
@@ -137,18 +145,12 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
 
   JMenu editMenu = new JMenu();
 
-  JMenuItem cut = new JMenuItem();
-
-  JMenuItem copy = new JMenuItem();
-
   JMenuItem upperCase = new JMenuItem();
 
   JMenuItem lowerCase = new JMenuItem();
 
   JMenuItem toggle = new JMenuItem();
 
-  JMenu pdbMenu = new JMenu();
-
   JMenu outputMenu = new JMenu();
 
   JMenu seqShowAnnotationsMenu = new JMenu();
@@ -165,22 +167,14 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   JMenuItem groupAddReferenceAnnotations = new JMenuItem(
           MessageManager.getString("label.add_reference_annotations"));
 
-  JMenuItem sequenceFeature = new JMenuItem();
-
   JMenuItem textColour = new JMenuItem();
 
-  JMenu jMenu1 = new JMenu();
+  JMenu editGroupMenu = new JMenu();
 
-  JMenuItem pdbStructureDialog = new JMenuItem();
+  JMenuItem chooseStructure = new JMenuItem();
 
   JMenu rnaStructureMenu = new JMenu();
 
-  JMenuItem editSequence = new JMenuItem();
-
-  JMenu groupLinksMenu;
-
-  JMenuItem hideInsertions = new JMenuItem();
-
   /**
    * Constructs a menu with sub-menu items for any hyperlinks for the sequence
    * and/or features provided. Hyperlinks may include a lookup by sequence id,
@@ -191,7 +185,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
    * @param features
    * @return
    */
-  static JMenu buildLinkMenu(final SequenceI seq,
+  protected static JMenu buildLinkMenu(final SequenceI seq,
           List<SequenceFeature> features)
   {
     JMenu linkMenu = new JMenu(MessageManager.getString("action.link"));
@@ -317,9 +311,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   }
 
   /**
-   * For the popup menu on the idPanel.
-   * 
-   * Add a late bound groupURL item to the given linkMenu
+   * add a late bound groupURL item to the given linkMenu
    * 
    * @param linkMenu
    * @param label
@@ -351,15 +343,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
           {
             try
             {
-              // Object[] { int[] { number of matches seqs },
-              // boolean[] { which matched },
-              // StringBuffer[] { segment generated from inputs },
-              // String[] { url }
-              // }
-
-              // TODO bug: urlstub is { int[], boolean[] } but constructFrom
-              // requires something else.
-
               showLink(urlgenerator.constructFrom(urlstub));
             } catch (UrlStringTooLongException e2)
             {
@@ -374,42 +357,55 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   }
 
   /**
-   * Creates a new PopupMenu object.
+   * Constructor for a PopupMenu for a click in the alignment panel (on a residue)
    * 
    * @param ap
+   *              the panel in which the mouse is clicked
    * @param seq
-   * @param features
-   *          non-positional features (for seq not null), or positional features
-   *          at residue (for seq equal to null)
+   *              the sequence under the mouse
+   * @throws NullPointerException
+   *                                if seq is null
    */
-  public PopupMenu(final AlignmentPanel ap, SequenceI seq,
-          List<SequenceFeature> features)
+  public PopupMenu(final AlignmentPanel ap, SequenceI seq, int column)
   {
-    this(ap, seq, features, null);
+    this(false, ap, seq, column, null);
   }
 
   /**
-   * Constructor
+   * Constructor for a PopupMenu for a click in the sequence id panel
    * 
    * @param alignPanel
+   *                     the panel in which the mouse is clicked
    * @param seq
-   *          the sequence under the cursor if in the Id panel, null if in the
-   *          sequence panel
-   * @param features
-   *          non-positional features if in the Id panel, features at the
-   *          clicked residue if in the sequence panel
+   *                     the sequence under the mouse click
    * @param groupLinks
-   *          not implemented -- empty list
+   *                     templates for sequence external links
+   * @throws NullPointerException
+   *                                if seq is null
    */
   public PopupMenu(final AlignmentPanel alignPanel, final SequenceI seq,
-          List<SequenceFeature> features, List<String> groupLinks)
+          List<String> groupLinks)
   {
-    // /////////////////////////////////////////////////////////
-    // If this is activated from the sequence panel, the user may want to
-    // edit or annotate a particular residue. Therefore display the residue menu
-    //
-    // If from the IDPanel, we must display the sequence menu
-    // ////////////////////////////////////////////////////////
+    this(true, alignPanel, seq, -1, groupLinks);
+  }
+
+  /**
+   * Private constructor that constructs a popup menu for either sequence ID
+   * Panel, or alignment context
+   * 
+   * @param fromIdPanel
+   * @param alignPanel
+   * @param seq
+   * @param column
+   *                      aligned column position (0...)
+   * @param groupLinks
+   */
+  private PopupMenu(boolean fromIdPanel,
+          final AlignmentPanel alignPanel,
+          final SequenceI seq, final int column, List<String> groupLinks)
+  {
+    Objects.requireNonNull(seq);
+    this.forIdPanel = fromIdPanel;
     this.ap = alignPanel;
     sequence = seq;
 
@@ -434,9 +430,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
      * 'reference annotations' that may be added to the alignment. First for the
      * currently selected sequence (if there is one):
      */
-    final List<SequenceI> selectedSequence = (seq == null
-            ? Collections.<SequenceI> emptyList()
-            : Arrays.asList(seq));
+    final List<SequenceI> selectedSequence = (forIdPanel && seq != null
+            ? Arrays.asList(seq)
+            : Collections.<SequenceI> emptyList());
     buildAnnotationTypesMenus(seqShowAnnotationsMenu,
             seqHideAnnotationsMenu, selectedSequence);
     configureReferenceAnnotationsMenu(seqAddReferenceAnnotations,
@@ -461,9 +457,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       e.printStackTrace();
     }
 
-    JMenuItem menuItem;
-    if (seq != null)
+    if (forIdPanel)
     {
+    JMenuItem menuItem;
       sequenceMenu.setText(sequence.getName());
       if (seq == alignPanel.av.getAlignment().getSeqrep())
       {
@@ -618,7 +614,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
       if (addOption)
       {
-        menuItem = new JMenuItem(
+        JMenuItem menuItem = new JMenuItem(
                 MessageManager.getString("action.reveal_all"));
         menuItem.addActionListener(new ActionListener()
         {
@@ -670,7 +666,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       // add any groupURLs to the groupURL submenu and make it visible
       if (groupLinks != null && groupLinks.size() > 0)
       {
-        // not implemented -- empty list
         buildGroupURLMenu(sg, groupLinks);
       }
       // Add a 'show all structures' for the current selection
@@ -711,27 +706,57 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     {
       createGroupMenuItem.setVisible(true);
       unGroupMenuItem.setVisible(false);
-      jMenu1.setText(MessageManager.getString("action.edit_new_group"));
+      editGroupMenu.setText(MessageManager.getString("action.edit_new_group"));
     }
     else
     {
       createGroupMenuItem.setVisible(false);
       unGroupMenuItem.setVisible(true);
-      jMenu1.setText(MessageManager.getString("action.edit_group"));
+      editGroupMenu.setText(MessageManager.getString("action.edit_group"));
     }
 
-    if (seq == null)
+    if (!forIdPanel)
     {
       sequenceMenu.setVisible(false);
-      pdbStructureDialog.setVisible(false);
+      chooseStructure.setVisible(false);
       rnaStructureMenu.setVisible(false);
     }
 
+    addLinksAndFeatures(seq, column);
+  }
+
+  /**
+   * Adds
+   * <ul>
+   * <li>configured sequence database links (ID panel popup menu)</li>
+   * <li>non-positional feature links (ID panel popup menu)</li>
+   * <li>positional feature links (alignment panel popup menu)</li>
+   * <li>feature details links (alignment panel popup menu)</li>
+   * </ul>
+   * If this panel is also showed complementary (CDS/protein) features, then links
+   * to their feature details are also added.
+   * 
+   * @param seq
+   * @param column
+   */
+  void addLinksAndFeatures(final SequenceI seq, final int column)
+  {
+    List<SequenceFeature> features = null;
+    if (forIdPanel)
+    {
+      features = sequence.getFeatures().getNonPositionalFeatures();
+    }
+    else
+    {
+      features = ap.getFeatureRenderer().findFeaturesAtColumn(sequence,
+              column + 1);
+    }
+
     addLinks(seq, features);
 
-    if (seq == null)
+    if (!forIdPanel)
     {
-      addFeatureDetails(features);
+      addFeatureDetails(features, seq, column);
     }
   }
 
@@ -739,69 +764,121 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
    * Add a link to show feature details for each sequence feature
    * 
    * @param features
+   * @param column
+   * @param seq
    */
-  protected void addFeatureDetails(List<SequenceFeature> features)
+  protected void addFeatureDetails(List<SequenceFeature> features,
+          SequenceI seq, int column)
   {
-    if (features == null || features.isEmpty())
+    /*
+     * add features in CDS/protein complement at the corresponding
+     * position if configured to do so
+   */
+    MappedFeatures mf = null;
+    if (ap.av.isShowComplementFeatures())
     {
+      if (!Comparison.isGap(sequence.getCharAt(column)))
+  {
+        AlignViewportI complement = ap.getAlignViewport()
+                .getCodingComplement();
+        AlignFrame af = Desktop.getAlignFrameFor(complement);
+        FeatureRendererModel fr2 = af.getFeatureRenderer();
+        int seqPos = sequence.findPosition(column);
+        mf = fr2.findComplementFeaturesAtResidue(sequence, seqPos);
+      }
+    }
+
+    if (features.isEmpty() && mf == null)
+    {
+      /*
+       * no features to show at this position
+       */
       return;
     }
     JMenu details = new JMenu(
             MessageManager.getString("label.feature_details"));
     add(details);
 
+    String name = seq.getName();
     for (final SequenceFeature sf : features)
     {
-      int start = sf.getBegin();
-      int end = sf.getEnd();
-      String desc = null;
-      if (start == end)
+      addFeatureDetailsMenuItem(details, name, sf);
+    }
+
+    if (mf != null)
+    {
+      name = mf.fromSeq == seq ? mf.mapping.getTo().getName()
+              : mf.fromSeq.getName();
+      for (final SequenceFeature sf : mf.features)
       {
-        desc = String.format("%s %d", sf.getType(), start);
+        addFeatureDetailsMenuItem(details, name, sf);
       }
-      else
+    }
+  }
+
+  /**
+   * A helper method to add one menu item whose action is to show details for one
+   * feature. The menu text includes feature description, but this may be
+   * truncated.
+   * 
+   * @param details
+   * @param seqName
+   * @param sf
+   */
+  void addFeatureDetailsMenuItem(JMenu details, final String seqName,
+          final SequenceFeature sf)
+  {
+      int start = sf.getBegin();
+      int end = sf.getEnd();
+    StringBuilder desc = new StringBuilder();
+    desc.append(sf.getType()).append(" ").append(String.valueOf(start));
+    if (start != end)
       {
-        desc = String.format("%s %d-%d", sf.getType(), start, end);
+      desc.append("-").append(String.valueOf(end));
       }
-      String tooltip = desc;
       String description = sf.getDescription();
       if (description != null)
       {
+      desc.append(" ");
         description = StringUtils.stripHtmlTags(description);
-        if (description.length() > 12)
+
+      /*
+       * truncate overlong descriptions unless they contain an href
+       * (as truncation could leave corrupted html)
+       */
+      boolean hasLink = description.indexOf("a href") > -1;
+      if (description.length() > FEATURE_DESC_MAX && !hasLink)
         {
-          desc = desc + " " + description.substring(0, 12) + "..";
+        description = description.substring(0, FEATURE_DESC_MAX) + "...";
         }
-        else
-        {
-          desc = desc + " " + description;
+      desc.append(description);
         }
-        tooltip = tooltip + " " + description;
-      }
-      if (sf.getFeatureGroup() != null)
+    String featureGroup = sf.getFeatureGroup();
+    if (featureGroup != null)
       {
-        tooltip = tooltip + (" (" + sf.getFeatureGroup() + ")");
+      desc.append(" (").append(featureGroup).append(")");
       }
-      JMenuItem item = new JMenuItem(desc);
-      item.setToolTipText(tooltip);
+    String htmlText = JvSwingUtils.wrapTooltip(true, desc.toString());
+    JMenuItem item = new JMenuItem(htmlText);
       item.addActionListener(new ActionListener()
       {
         @Override
         public void actionPerformed(ActionEvent e)
         {
-          showFeatureDetails(sf);
+        showFeatureDetails(seqName, sf);
         }
       });
       details.add(item);
     }
-  }
 
   /**
    * Opens a panel showing a text report of feature dteails
    * 
+   * @param seqName
+   * 
    * @param sf
    */
-  protected void showFeatureDetails(SequenceFeature sf)
+  protected void showFeatureDetails(String seqName, SequenceFeature sf)
   {
     JInternalFrame details;
     if (Platform.isJS())
@@ -813,7 +890,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       // TODO JAL-3026 set style of table correctly for feature details
       JLabel reprt = new JLabel(MessageManager
               .formatMessage("label.html_content", new Object[]
-              { sf.getDetailsReport() }));
+              { sf.getDetailsReport(seqName) }));
       reprt.setBackground(Color.WHITE);
       reprt.setOpaque(true);
       panel.add(reprt, BorderLayout.CENTER);
@@ -831,7 +908,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       // it appears Java's CSS does not support border-collaps :-(
       cap.addStylesheetRule("table { border-collapse: collapse;}");
       cap.addStylesheetRule("table, td, th {border: 1px solid black;}");
-      cap.setText(sf.getDetailsReport());
+      cap.setText(sf.getDetailsReport(seqName));
       details = cap;
     }
     Desktop.addInternalFrame(details,
@@ -850,12 +927,12 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
    */
   void addLinks(final SequenceI seq, List<SequenceFeature> features)
   {
-    JMenu linkMenu = buildLinkMenu(seq, features);
+    JMenu linkMenu = buildLinkMenu(forIdPanel ? seq : null, features);
 
     // only add link menu if it has entries
     if (linkMenu.getItemCount() > 0)
     {
-      if (sequence != null)
+      if (forIdPanel)
       {
         sequenceMenu.add(linkMenu);
       }
@@ -996,12 +1073,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     showOrHideMenu.add(item);
   }
 
-  /**
-   *
-   * @param sg
-   * @param groupLinks
-   *          not implemented -- empty list
-   */
   private void buildGroupURLMenu(SequenceGroup sg, List<String> groupLinks)
   {
 
@@ -1009,7 +1080,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     // menu appears asap
     // sequence only URLs
     // ID/regex match URLs
-    groupLinksMenu = new JMenu(
+    JMenu groupLinksMenu = new JMenu(
             MessageManager.getString("action.group_link"));
     // three types of url that might be created.
     JMenu[] linkMenus = new JMenu[] { null,
@@ -1063,15 +1134,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         }
       }
     }
-    if (groupLinks.size() == 0)
-    {
-      return;
-    }
     // now create group links for all distinct ID/sequence sets.
     boolean addMenu = false; // indicates if there are any group links to give
                              // to user
-
-    // not implmeented -- empty list
     for (String link : groupLinks)
     {
       GroupUrlLink urlLink = null;
@@ -1083,7 +1148,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         Cache.log.error("Exception for GroupURLLink '" + link + "'", foo);
         continue;
       }
-      ;
       if (!urlLink.isValid())
       {
         Cache.log.error(urlLink.getInvalidMessage());
@@ -1124,7 +1188,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       {
         urlset = urlLink.makeUrlStubs(ids, seqstr,
                 "FromJalview" + System.currentTimeMillis(), false);
-        // { int[], boolean[] } only here
       } catch (UrlStringTooLongException e)
       {
       }
@@ -1176,7 +1239,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
     });
     sequenceMenu.setText(MessageManager.getString("label.sequence"));
-    sequenceName.setText(
+
+    JMenuItem sequenceName = new JMenuItem(
             MessageManager.getString("label.edit_name_description"));
     sequenceName.addActionListener(new ActionListener()
     {
@@ -1186,8 +1250,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         sequenceName_actionPerformed();
       }
     });
-    chooseAnnotations
-            .setText(MessageManager.getString("action.choose_annotations"));
+    JMenuItem chooseAnnotations = new JMenuItem(
+            MessageManager.getString("action.choose_annotations"));
     chooseAnnotations.addActionListener(new ActionListener()
     {
       @Override
@@ -1196,24 +1260,24 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         chooseAnnotations_actionPerformed(e);
       }
     });
-    sequenceDetails
-            .setText(MessageManager.getString("label.sequence_details"));
+    JMenuItem sequenceDetails = new JMenuItem(
+            MessageManager.getString("label.sequence_details"));
     sequenceDetails.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        sequenceDetails_actionPerformed();
+        createSequenceDetailsReport(new SequenceI[] { sequence });
       }
     });
-    sequenceSelDetails
-            .setText(MessageManager.getString("label.sequence_details"));
+    JMenuItem sequenceSelDetails = new JMenuItem(
+            MessageManager.getString("label.sequence_details"));
     sequenceSelDetails.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        sequenceSelectionDetails_actionPerformed();
+        createSequenceDetailsReport(ap.av.getSequenceSelection());
       }
     });
 
@@ -1238,7 +1302,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
     });
 
-    outline.setText(MessageManager.getString("action.border_colour"));
+    JMenuItem outline = new JMenuItem(
+            MessageManager.getString("action.border_colour"));
     outline.addActionListener(new ActionListener()
     {
       @Override
@@ -1288,7 +1353,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
     });
     editMenu.setText(MessageManager.getString("action.edit"));
-    cut.setText(MessageManager.getString("action.cut"));
+    JMenuItem cut = new JMenuItem(MessageManager.getString("action.cut"));
     cut.addActionListener(new ActionListener()
     {
       @Override
@@ -1306,7 +1371,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         changeCase(e);
       }
     });
-    copy.setText(MessageManager.getString("action.copy"));
+    JMenuItem copy = new JMenuItem(MessageManager.getString("action.copy"));
     copy.addActionListener(new ActionListener()
     {
       @Override
@@ -1343,7 +1408,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
             .setText(MessageManager.getString("label.show_annotations"));
     groupHideAnnotationsMenu
             .setText(MessageManager.getString("label.hide_annotations"));
-    sequenceFeature.setText(
+    JMenuItem sequenceFeature = new JMenuItem(
             MessageManager.getString("label.create_sequence_feature"));
     sequenceFeature.addActionListener(new ActionListener()
     {
@@ -1353,10 +1418,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         sequenceFeature_actionPerformed();
       }
     });
-    jMenu1.setText(MessageManager.getString("label.group"));
-    pdbStructureDialog.setText(
+    editGroupMenu.setText(MessageManager.getString("label.group"));
+    chooseStructure.setText(
             MessageManager.getString("label.show_pdbstruct_dialog"));
-    pdbStructureDialog.addActionListener(new ActionListener()
+    chooseStructure.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent actionEvent)
@@ -1374,7 +1439,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
             .setText(MessageManager.getString("label.view_rna_structure"));
 
     // colStructureMenu.setText("Colour By Structure");
-    editSequence.setText(
+    JMenuItem editSequence = new JMenuItem(
             MessageManager.getString("label.edit_sequence") + "...");
     editSequence.addActionListener(new ActionListener()
     {
@@ -1396,8 +1461,16 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
 
       }
     });
-    hideInsertions
-            .setText(MessageManager.getString("label.hide_insertions"));
+
+    groupMenu.add(sequenceSelDetails);
+    add(groupMenu);
+    add(sequenceMenu);
+    add(rnaStructureMenu);
+    add(chooseStructure);
+    if (forIdPanel)
+    {
+      JMenuItem hideInsertions = new JMenuItem(
+              MessageManager.getString("label.hide_insertions"));
     hideInsertions.addActionListener(new ActionListener()
     {
 
@@ -1407,14 +1480,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         hideInsertions_actionPerformed(e);
       }
     });
-
-    groupMenu.add(sequenceSelDetails);
-    add(groupMenu);
-    add(sequenceMenu);
-    add(rnaStructureMenu);
-    add(pdbStructureDialog);
-    if (sequence != null)
-    {
       add(hideInsertions);
     }
     // annotations configuration panel suppressed for now
@@ -1435,7 +1500,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     groupMenu.add(sequenceFeature);
     groupMenu.add(createGroupMenuItem);
     groupMenu.add(unGroupMenuItem);
-    groupMenu.add(jMenu1);
+    groupMenu.add(editGroupMenu);
     sequenceMenu.add(sequenceName);
     sequenceMenu.add(sequenceDetails);
     sequenceMenu.add(makeReferenceSeq);
@@ -1449,17 +1514,13 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     editMenu.add(upperCase);
     editMenu.add(lowerCase);
     editMenu.add(toggle);
-    // JBPNote: These shouldn't be added here - should appear in a generic
-    // 'apply web service to this sequence menu'
-    // pdbMenu.add(RNAFold);
-    // pdbMenu.add(ContraFold);
-    jMenu1.add(groupName);
-    jMenu1.add(colourMenu);
-    jMenu1.add(showBoxes);
-    jMenu1.add(showText);
-    jMenu1.add(showColourText);
-    jMenu1.add(outline);
-    jMenu1.add(displayNonconserved);
+    editGroupMenu.add(groupName);
+    editGroupMenu.add(colourMenu);
+    editGroupMenu.add(showBoxes);
+    editGroupMenu.add(showText);
+    editGroupMenu.add(showColourText);
+    editGroupMenu.add(outline);
+    editGroupMenu.add(displayNonconserved);
   }
 
   /**
@@ -1722,11 +1783,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     createSequenceDetailsReport(ap.av.getSequenceSelection());
   }
 
-  protected void sequenceDetails_actionPerformed()
-  {
-    createSequenceDetailsReport(new SequenceI[] { sequence });
-  }
-
   public void createSequenceDetailsReport(SequenceI[] sequences)
   {
     StringBuilder contents = new StringBuilder(128);
@@ -1936,7 +1992,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   }
 
   /**
-   * Shows a dialog where sequence name and description may be edited
+   * Shows a dialog where the sequence name and description may be edited. If a
+   * name containing spaces is entered, these are converted to underscores, with a
+   * warning message.
    */
   void sequenceName_actionPerformed()
   {
@@ -2008,8 +2066,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       {
         getGroup().setOutlineColour(c);
         refresh();
+      }
       };
-    };
     JalviewColourChooser.showColourChooser(Desktop.getDesktopPane(),
             title, Color.BLUE, listener);
   }
@@ -2171,25 +2229,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     }
   }
 
-  public void colourByStructure(String pdbid)
-  {
-    Annotation[] anots = ap.av.getStructureSelectionManager()
-            .colourSequenceFromStructure(sequence, pdbid);
-
-    AlignmentAnnotation an = new AlignmentAnnotation("Structure",
-            "Coloured by " + pdbid, anots);
-
-    ap.av.getAlignment().addAnnotation(an);
-    an.createSequenceMapping(sequence, 0, true);
-    // an.adjustForAlignment();
-    ap.av.getAlignment().setAnnotationIndex(an, 0);
-
-    ap.adjustAnnotationHeight();
-
-    sequence.addAlignmentAnnotation(an);
-
-  }
-
   /**
    * Shows a dialog where sequence characters may be edited. Any changes are
    * applied, and added as an available 'Undo' item in the edit commands
@@ -2199,16 +2238,16 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   {
     SequenceGroup sg = ap.av.getSelectionGroup();
 
+    SequenceI seq = sequence;
     if (sg != null)
     {
-      if (sequence == null)
+      if (seq == null)
       {
-        sequence = sg.getSequenceAt(0);
+        seq = sg.getSequenceAt(0);
       }
 
       EditNameDialog dialog = new EditNameDialog(
-              sequence.getSequenceAsString(sg.getStartRes(),
-                      sg.getEndRes() + 1),
+              seq.getSequenceAsString(sg.getStartRes(), sg.getEndRes() + 1),
               null, MessageManager.getString("label.edit_sequence"), null);
       dialog.showDialog(ap.alignFrame,
               MessageManager.getString("label.edit_sequence"),
index b188888..85d6025 100644 (file)
@@ -29,6 +29,7 @@ import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
@@ -50,6 +51,7 @@ import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.viewmodel.ViewportRanges;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -63,6 +65,7 @@ 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;
 
@@ -224,7 +227,7 @@ public class SeqPanel extends JPanel
   SearchResultsI lastSearchResults;
 
   /**
-   * Constructor
+   * Creates a new SeqPanel object
    * 
    * @param viewport
    * @param alignPanel
@@ -841,11 +844,11 @@ public class SeqPanel extends JPanel
    * the start of the highlighted region.
    */
   @Override
-  public void highlightSequence(SearchResultsI results)
+  public String highlightSequence(SearchResultsI results)
   {
     if (results == null || results.equals(lastSearchResults))
     {
-      return;
+      return null;
     }
     lastSearchResults = results;
 
@@ -871,6 +874,77 @@ public class SeqPanel extends JPanel
     {
       setStatusMessage(results);
     }
+    return results.isEmpty() ? null : getHighlightInfo(results);
+  }
+
+  /**
+   * temporary hack: answers a message suitable to show on structure hover
+   * label. This is normally null. It is a peptide variation description if
+   * <ul>
+   * <li>results are a single residue in a protein alignment</li>
+   * <li>there is a mapping to a coding sequence (codon)</li>
+   * <li>there are one or more SNP variant features on the codon</li>
+   * </ul>
+   * in which case the answer is of the format (e.g.) "p.Glu388Asp"
+   * 
+   * @param results
+   * @return
+   */
+  private String getHighlightInfo(SearchResultsI results)
+  {
+    /*
+     * ideally, just find mapped CDS (as we don't care about render style here);
+     * for now, go via split frame complement's FeatureRenderer
+     */
+    AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
+    if (complement == null)
+    {
+      return null;
+    }
+    AlignFrame af = Desktop.getAlignFrameFor(complement);
+    FeatureRendererModel fr2 = af.getFeatureRenderer();
+
+    int j = results.getSize();
+    List<String> infos = new ArrayList<>();
+    for (int i = 0; i < j; i++)
+    {
+      SearchResultMatchI match = results.getResults().get(i);
+      int pos = match.getStart();
+      if (pos == match.getEnd())
+      {
+        SequenceI seq = match.getSequence();
+        SequenceI ds = seq.getDatasetSequence() == null ? seq
+                : seq.getDatasetSequence();
+        MappedFeatures mf = fr2
+                .findComplementFeaturesAtResidue(ds, pos);
+        if (mf != null)
+        {
+          for (SequenceFeature sf : mf.features)
+          {
+            String pv = mf.findProteinVariants(sf);
+            if (pv.length() > 0 && !infos.contains(pv))
+            {
+              infos.add(pv);
+            }
+          }
+        }
+      }
+    }
+
+    if (infos.isEmpty())
+    {
+      return null;
+    }
+    StringBuilder sb = new StringBuilder();
+    for (String info : infos)
+    {
+      if (sb.length() > 0)
+      {
+        sb.append("|");
+      }
+      sb.append(info);
+    }
+    return sb.toString();
   }
 
   @Override
@@ -977,25 +1051,57 @@ public class SeqPanel extends JPanel
      * add features that straddle the gap (pos may be the residue before or
      * after the gap)
      */
+    int unshownFeatures = 0;
     if (av.isShowSequenceFeatures())
     {
       List<SequenceFeature> features = ap.getFeatureRenderer()
               .findFeaturesAtColumn(sequence, column + 1);
-      seqARep.appendFeatures(tooltipText, pos, features,
-              this.ap.getSeqPanel().seqCanvas.fr);
+      unshownFeatures = seqARep.appendFeaturesLengthLimit(tooltipText, pos,
+              features,
+              this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
+
+      /*
+       * add features in CDS/protein complement at the corresponding
+       * position if configured to do so
+       */
+      if (av.isShowComplementFeatures())
+      {
+        if (!Comparison.isGap(sequence.getCharAt(column)))
+        {
+          AlignViewportI complement = ap.getAlignViewport()
+                  .getCodingComplement();
+          AlignFrame af = Desktop.getAlignFrameFor(complement);
+          FeatureRendererModel fr2 = af.getFeatureRenderer();
+          MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
+                  pos);
+          if (mf != null)
+          {
+            unshownFeatures = seqARep.appendFeaturesLengthLimit(
+                    tooltipText, pos, mf, fr2,
+                    MAX_TOOLTIP_LENGTH);
+          }
+        }
+      }
     }
-    if (tooltipText.length() == 6) // <html>
+    if (tooltipText.length() == 6) // "<html>"
     {
       setToolTipText(null);
       lastTooltip = null;
     }
     else
     {
-      if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
+      if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
       {
         tooltipText.setLength(MAX_TOOLTIP_LENGTH);
         tooltipText.append("...");
       }
+      if (unshownFeatures > 0)
+      {
+        tooltipText.append("<br/>").append("... ").append("<i>")
+                .append(MessageManager.formatMessage(
+                        "label.features_not_shown", unshownFeatures))
+                .append("</i>");
+      }
       String textString = tooltipText.toString();
       if (lastTooltip == null || !lastTooltip.equals(textString))
       {
@@ -1949,13 +2055,19 @@ public class SeqPanel extends JPanel
       return;
     }
 
-    /*
-     * start scrolling if mouse dragging, whether the drag started
-     * in the scale panel or this panel
-     */
-    if (mouseDragging || ap.getScalePanel().isMouseDragging())
+    // BH check was:
+//    /*
+//     * start scrolling if mouse dragging, whether the drag started
+//     * in the scale panel or this panel
+//     */
+//    if (mouseDragging || ap.getScalePanel().isMouseDragging())
+//    {
+//      startScrolling(new Point(e.getX(), 0));
+//    }
+
+    if (mouseDragging && scrollThread == null)
     {
-      startScrolling(new Point(e.getX(), 0));
+      startScrolling(e.getPoint());
     }
   }
 
@@ -1974,7 +2086,7 @@ public class SeqPanel extends JPanel
       return;
     }
 
-    if (evt.getClickCount() > 1)
+    if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
     {
       sg = av.getSelectionGroup();
       if (sg != null && sg.getSize() == 1
@@ -2175,12 +2287,12 @@ public class SeqPanel extends JPanel
     final int column = pos.column;
     final int seq = pos.seqIndex;
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-    List<SequenceFeature> features = ap.getFeatureRenderer()
-            .findFeaturesAtColumn(sequence, column + 1);
-
-    PopupMenu pop = new PopupMenu(ap, null, features);
+    if (sequence != null)
+    {
+      PopupMenu pop = new PopupMenu(ap, sequence, column);
     pop.show(this, evt.getX(), evt.getY());
   }
+  }
 
   /**
    * Update the display after mouse up on a selection or group
@@ -2428,10 +2540,17 @@ public class SeqPanel extends JPanel
               // if (!scrollOnce() {t.stop();}) gives compiler error :-(
               scrollThread.scrollOnce();
             }
+          }
+        });
+        t.addActionListener(new ActionListener()
+        {
+          @Override
+          public void actionPerformed(ActionEvent e)
+          {
             if (scrollThread == null)
             {
               // SeqPanel.stopScrolling called
-              ((Timer) e.getSource()).stop();
+              t.stop();
             }
           }
         });
index 4fb9136..782c842 100755 (executable)
@@ -29,218 +29,220 @@ import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.Image;
-import java.awt.Insets;
 import java.awt.MediaTracker;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
+import java.awt.Toolkit;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
-import java.beans.PropertyVetoException;
+import java.net.URL;
 
 import javax.swing.JInternalFrame;
 import javax.swing.JLabel;
 import javax.swing.JLayeredPane;
 import javax.swing.JPanel;
 import javax.swing.JTextPane;
-import javax.swing.Timer;
-import javax.swing.border.EmptyBorder;
 import javax.swing.event.HyperlinkEvent;
 import javax.swing.event.HyperlinkListener;
 
 /**
- * A class that serves both as an initial 5-second splash screen (interactive
- * false) as well as for the Help menu item action (interactive true).
- * 
- * As a splash screen, the frame closes if clicked by the user.
- * 
- * Closure loop converted from a while/sleep loop to a JavaScript-compatible
- * state machine by Bob Hanson 2019.11.26.
- * 
- * TODO: get JTextPane working for read-only HTML.
+ * DOCUMENT ME!
  * 
+ * @author $author$
+ * @version $Revision$
  */
-@SuppressWarnings("serial")
 public class SplashScreen extends JPanel
         implements Runnable, HyperlinkListener
 {
-  private static final int STATE_INIT = 0;
-
-  private static final int STATE_LOOP = 1;
+  private static final int SHOW_FOR_SECS = 5;
 
-  private static final int STATE_DONE = 2;
+  private static final int FONT_SIZE = 11;
 
-  // boolean visible = true;
+  private boolean visible = true;
 
   private JPanel iconimg = new JPanel(new BorderLayout());
 
-  /**
-   * Temporary SwingJS Hack: Either a JLabel in JavaScript or a JTextPane in
-   * Java
+  /*
+   * as JTextPane in Java, JLabel in javascript
    */
-  protected Component htmlPane;
+  private Component splashText;
 
   private JInternalFrame iframe;
 
   private Image image;
 
-  private final static int fontSize = 11;
-
-  protected final static Font largeFont = new Font("Verdana", Font.BOLD,
-          fontSize + 6);
+  private boolean transientDialog = false;
 
-  int yoffset = 30;
+  private long oldTextLength = -1;
 
-  /**
-   * Creates a new SplashScreen object.
-   */
-  public SplashScreen()
-  {
-    this(false);
-  }
-
-  protected boolean isInteractive = false;
-
-  /**
-   * 
-   * @param interactive
-   *          if true - an internal dialog is opened rather than a free-floating
-   *          splash screen
+  /*
+   * allow click in the initial splash screen to dismiss it
+   * immediately (not if opened from About menu)
    */
-  public SplashScreen(boolean isInteractive)
-  {
-    this.isInteractive = isInteractive;
-    Thread t = new Thread(this, "SplashScreen");
-    t.start();
-  }
-
-  MouseAdapter closer = new MouseAdapter()
+  private MouseAdapter closer = new MouseAdapter()
   {
     @Override
     public void mousePressed(MouseEvent evt)
     {
-      try
+      if (transientDialog)
       {
-        if (!isInteractive)
+        try
         {
-          setVisible(false);
+          visible = false;
           closeSplash();
+        } catch (Exception ex)
+        {
         }
-      } catch (Exception ex)
-      {
       }
     }
   };
 
   /**
+   * Constructor that displays the splash screen
+   * 
+   * @param isTransient
+   *          if true the panel removes itself on click or after a few seconds;
+   *          if false it stays up until closed by the user
+   */
+  public SplashScreen(boolean isTransient)
+  {
+    this.transientDialog = isTransient;
+
+    if (Platform.isJS()) // BH 2019
+    {
+      splashText = new JLabel("");
+      run();
+    }
+    else
+    {
+      /**
+       * Java only
+       *
+       * @j2sIgnore
+       */
+      {
+        splashText = new JTextPane();
+        Thread t = new Thread(this);
+        t.start();
+      }
+    }
+  }
+
+  /**
    * ping the jalview version page then create and display the jalview
    * splashscreen window.
    */
-  protected void initSplashScreenWindow()
+  void initSplashScreenWindow()
   {
     addMouseListener(closer);
+
     try
     {
-      java.net.URL url = getClass().getResource("/images/Jalview_Logo.png");
-      image = java.awt.Toolkit.getDefaultToolkit().createImage(url);
-      MediaTracker mt = new MediaTracker(this);
-      mt.addImage(image, 0);
-      Image logo = (Platform.isJS() ? null
-              : java.awt.Toolkit.getDefaultToolkit().createImage(getClass()
-                      .getResource("/images/Jalview_Logo_small.png")));
-      if (logo != null)
+      URL url = getClass().getResource("/images/Jalview_Logo.png");
+      URL urllogo = getClass()
+              .getResource("/images/Jalview_Logo_small.png");
+
+      if (!Platform.isJS() && url != null)
       {
+        image = Toolkit.getDefaultToolkit().createImage(url);
+        Image logo = Toolkit.getDefaultToolkit().createImage(urllogo);
+        MediaTracker mt = new MediaTracker(this);
+        mt.addImage(image, 0);
         mt.addImage(logo, 1);
-      }
-      do
-      {
-        try
-        {
-          mt.waitForAll();
-        } catch (InterruptedException x)
+        do
         {
-        }
-        if (mt.isErrorAny())
-        {
-          System.err.println("Error when loading images!");
-        }
-      } while (!mt.checkAll());
-      if (url != null)
-      {
+          try
+          {
+            mt.waitForAll();
+          } catch (InterruptedException x)
+          {
+          }
+          if (mt.isErrorAny())
+          {
+            System.err.println("Error when loading images!");
+          }
+        } while (!mt.checkAll());
         Desktop.getInstance().setIconImage(logo);
       }
     } catch (Exception ex)
     {
     }
+
     iframe = new JInternalFrame();
     iframe.setFrameIcon(null);
-    iframe.setClosable(isInteractive);
+    iframe.setClosable(true);
     this.setLayout(new BorderLayout());
     iframe.setContentPane(this);
     iframe.setLayer(JLayeredPane.PALETTE_LAYER);
-    SplashImage splashimg = new SplashImage(image);
-    iconimg.add(splashimg, BorderLayout.CENTER);
-    add(iconimg, BorderLayout.NORTH);
+    if (Platform.isJS())
+    {
+      // ignore in JavaScript
+    }
+    else
+    /**
+     * Java only
+     * 
+     * @j2sIgnore
+     */
+    {
+      ((JTextPane) splashText).setEditable(false);
+
+      SplashImage splashimg = new SplashImage(image);
+      iconimg.add(splashimg, BorderLayout.CENTER);
+      add(iconimg, BorderLayout.NORTH);
+    }
+    add(splashText, BorderLayout.CENTER);
+    splashText.addMouseListener(closer);
     Desktop.getDesktopPane().add(iframe);
     refreshText();
   }
 
-  String oldtext;
-
-  private int mainState;
-
   /**
    * update text in author text panel reflecting current version information
    */
   protected boolean refreshText()
   {
-    Desktop desktop = Desktop.getInstance();
-    String newtext = desktop.getAboutMessage(true).toString();
+    String newtext = Desktop.getInstance().getAboutMessage();
     // System.err.println("Text found: \n"+newtext+"\nEnd of newtext.");
-    if (!newtext.equals(oldtext))
+    if (oldTextLength != newtext.length())
     {
       iframe.setVisible(false);
-      oldtext = newtext;
+      oldTextLength = newtext.length();
       if (Platform.isJS()) // BH 2019
       {
-        // BH TODO SwingJS does not implement HTML style. Could rethink this.
-
-        if (htmlPane == null)
-        {
-          htmlPane = new JLabel();
-        }
-        JLabel l = (JLabel)htmlPane;
-        l.setText(newtext);
-        Font f = htmlPane.getFont();
-        l.setFont(new Font(f.getFamily(), Font.PLAIN, f.getSize()));
-        l.setBorder(new EmptyBorder(new Insets(5, 5, 5, 5)));
-        l.setOpaque(true);
-        l.setBackground(Color.white);
-        htmlPane = l;
+        /*
+         * SwingJS doesn't have HTMLEditorKit, required for a JTextPane
+         * to display formatted html, so we use a simple alternative
+         */
+        String text = "<html><br><br><img src=\"swingjs/j2s/images/Jalview_Logo.png\"/><br>"
+                + newtext + "</html>";
+        JLabel ta = new JLabel(text);
+        ta.setOpaque(true);
+        ta.setBackground(Color.white);
+        splashText = ta;
       }
       else
       /**
        * Java only
-       * 
+       *
        * @j2sIgnore
        */
       {
-        if (htmlPane == null)
-        {
-          htmlPane = new JTextPane();
-        }
-        JTextPane pane = (JTextPane)htmlPane;
-        pane.setEditable(false);
-        pane.setContentType("text/html");
-        pane.setText(newtext);
-        pane.addHyperlinkListener(this);
-        htmlPane = pane;
+        JTextPane jtp = new JTextPane();
+        jtp.setEditable(false);
+        jtp.setContentType("text/html");
+        jtp.setText("<html>" + newtext + "</html>");
+        jtp.addHyperlinkListener(this);
+        splashText = jtp;
       }
-      htmlPane.addMouseListener(closer);
-      htmlPane.setSize(new Dimension(750, 375));
-      add(htmlPane, BorderLayout.CENTER);
-      int h = htmlPane.getHeight() + iconimg.getHeight();
-      iframe.setBounds(Math.max(0, (desktop.getWidth() - 750) / 2),
-              Math.max(0, (desktop.getHeight() - h) / 2), 750, h);
+      splashText.addMouseListener(closer);
+
+      splashText.setVisible(true);
+      splashText.setSize(new Dimension(750, 375));
+      add(splashText, BorderLayout.CENTER);
+      revalidate();
+      iframe.setBounds((Desktop.getInstance().getWidth() - 750) / 2,
+              (Desktop.getInstance().getHeight() - 375) / 2, 750,
+              splashText.getHeight() + iconimg.getHeight());
+      iframe.validate();
       iframe.setVisible(true);
       return true;
     }
@@ -253,88 +255,57 @@ public class SplashScreen extends JPanel
   @Override
   public void run()
   {
-    mainState = STATE_INIT;
-    mainLoop();
-  }
+    initSplashScreenWindow();
 
-  protected long startTime;
+    long startTime = System.currentTimeMillis() / 1000;
 
-  /**
-   * A simple state machine with just three states: init, loop, and done. Ideal
-   * for a simple while/sleep loop that works in Java and JavaScript
-   * identically.
-   * 
-   */
-  protected void mainLoop()
-  {
-    while (true)
+    while (visible)
     {
-      switch (mainState)
+      iframe.repaint();
+      try
       {
-      case STATE_INIT:
-        initSplashScreenWindow();
-        startTime = System.currentTimeMillis() / 1000;
-        mainState = STATE_LOOP;
-        continue;
-      case STATE_LOOP:
-        if (!isVisible())
-        {
-          mainState = STATE_DONE;
-          continue;
-        }
-        if (!isInteractive
-                && ((System.currentTimeMillis() / 1000) - startTime) > 5)
-        {
-          setVisible(false);
-          continue;
-        }
-        if (isVisible() && refreshText())
-        {
-          iframe.repaint();
-        }
-        if (isInteractive)
-        {
-          return;
-        }
-        Timer timer = new Timer(500, new ActionListener()
-        {
-          @Override
-          public void actionPerformed(ActionEvent e)
-          {
-            mainLoop();
-          }
+        Thread.sleep(500);
+      } catch (Exception ex)
+      {
+      }
 
-        });
-        timer.setRepeats(false);
-        timer.start();
-        return;
-      case STATE_DONE:
-        closeSplash();
-        Desktop.getInstance().startDialogQueue();
+      if (transientDialog && ((System.currentTimeMillis() / 1000)
+              - startTime) > SHOW_FOR_SECS)
+      {
+        visible = false;
+      }
+
+      if (visible && refreshText())
+      {
+        iframe.repaint();
+      }
+      if (!transientDialog)
+      {
         return;
       }
     }
 
+    closeSplash();
+    Desktop.getInstance().startDialogQueue();
   }
 
   /**
-   * Close the internal frame, either from the timer expiring or from the mouse
-   * action.
+   * DOCUMENT ME!
    */
   public void closeSplash()
   {
-    setVisible(false);
     try
     {
+
       iframe.setClosed(true);
-    } catch (PropertyVetoException e)
+    } catch (Exception ex)
     {
     }
   }
 
-  private class SplashImage extends JPanel
+  public class SplashImage extends JPanel
   {
-    private Image image;
+    Image image;
 
     public SplashImage(Image todisplay)
     {
@@ -358,7 +329,7 @@ public class SplashScreen extends JPanel
       g.setColor(Color.white);
       g.fillRect(0, 0, getWidth(), getHeight());
       g.setColor(Color.black);
-      g.setFont(largeFont);
+      g.setFont(new Font("Verdana", Font.BOLD, FONT_SIZE + 6));
 
       if (image != null)
       {
@@ -366,46 +337,6 @@ public class SplashScreen extends JPanel
                 (getHeight() - image.getHeight(this)) / 2, this);
       }
     }
-    /*
-     * int y = yoffset;
-     * 
-     * g.drawString("Jalview " + jalview.bin.Cache.getProperty("VERSION"), 50,
-     * y);
-     * 
-     * FontMetrics fm = g.getFontMetrics(); int vwidth =
-     * fm.stringWidth("Jalview " + jalview.bin.Cache.getProperty("VERSION"));
-     * g.setFont(new Font("Verdana", Font.BOLD, fontSize + 2)); g.drawString(
-     * "Last updated: " + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown"),
-     * 50 + vwidth + 5, y); if (jalview.bin.Cache.getDefault("LATEST_VERSION",
-     * "Checking").equals( "Checking")) { // Displayed when code version and
-     * jnlp version do not match g.drawString("...Checking latest version...",
-     * 50, y += fontSize + 10); y += 5; g.setColor(Color.black); } else if
-     * (!jalview.bin.Cache.getDefault("LATEST_VERSION", "Checking")
-     * .equals(jalview.bin.Cache.getProperty("VERSION"))) { if
-     * (jalview.bin.Cache.getProperty("VERSION").toLowerCase()
-     * .indexOf("automated build") == -1) { // Displayed when code version and
-     * jnlp version do not match and code // version is not a development build
-     * g.setColor(Color.red); } g.drawString( "!! Jalview version " +
-     * jalview.bin.Cache.getDefault("LATEST_VERSION", "..Checking..") +
-     * " is available for download from "
-     * +jalview.bin.Cache.getDefault("www.jalview.org"
-     * ,"http://www.jalview.org")+" !!", 50, y += fontSize + 10); y += 5;
-     * g.setColor(Color.black); }
-     * 
-     * g.setFont(new Font("Verdana", Font.BOLD, fontSize)); g.drawString(
-     * "Authors: Jim Procter, Andrew Waterhouse, Michele Clamp, James Cuff, Steve Searle,"
-     * , 50, y += fontSize + 4); g.drawString("David Martin & Geoff Barton.",
-     * 60, y += fontSize + 4); g.drawString(
-     * "Development managed by The Barton Group, University of Dundee.", 50, y
-     * += fontSize + 4); g.drawString("If  you use Jalview, please cite: ", 50,
-     * y += fontSize + 4); g.drawString(
-     * "Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
-     * , 50, y += fontSize + 4); g.drawString(
-     * "Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
-     * , 50, y += fontSize + 4);
-     * g.drawString("Bioinformatics doi: 10.1093/bioinformatics/btp033", 50, y
-     * += fontSize + 4); }
-     */
   }
 
   @Override
@@ -414,5 +345,4 @@ public class SplashScreen extends JPanel
     Desktop.hyperlinkUpdate(e);
 
   }
-
 }
index 253b02c..d13be17 100644 (file)
  */
 package jalview.gui;
 
-import jalview.api.SplitContainerI;
-import jalview.datamodel.AlignmentI;
-import jalview.jbgui.GAlignFrame;
-import jalview.jbgui.GSplitFrame;
-import jalview.structure.StructureSelectionManager;
-import jalview.util.Platform;
-import jalview.viewmodel.AlignmentViewport;
-
+import java.awt.BorderLayout;
 import java.awt.Component;
-import java.awt.Toolkit;
+import java.awt.Dimension;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.KeyAdapter;
@@ -42,12 +35,32 @@ import java.util.Map.Entry;
 
 import javax.swing.AbstractAction;
 import javax.swing.InputMap;
+import javax.swing.JButton;
 import javax.swing.JComponent;
+import javax.swing.JDesktopPane;
+import javax.swing.JInternalFrame;
+import javax.swing.JLayeredPane;
 import javax.swing.JMenuItem;
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
 import javax.swing.KeyStroke;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
 import javax.swing.event.InternalFrameAdapter;
 import javax.swing.event.InternalFrameEvent;
 
+import jalview.api.AlignViewControllerGuiI;
+import jalview.api.FeatureSettingsControllerI;
+import jalview.api.SplitContainerI;
+import jalview.controller.FeatureSettingsControllerGuiI;
+import jalview.datamodel.AlignmentI;
+import jalview.jbgui.GAlignFrame;
+import jalview.jbgui.GSplitFrame;
+import jalview.structure.StructureSelectionManager;
+import jalview.util.MessageManager;
+import jalview.util.Platform;
+import jalview.viewmodel.AlignmentViewport;
+
 /**
  * An internal frame on the desktop that hosts a horizontally split view of
  * linked DNA and Protein alignments. Additional views can be created in linked
@@ -74,6 +87,13 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI
 
   private static final long serialVersionUID = 1L;
 
+  /**
+   * geometry for Feature Settings Holder
+   */
+  private static final int FS_MIN_WIDTH = 400;
+
+  private static final int FS_MIN_HEIGHT = 400;
+
   public SplitFrame(GAlignFrame top, GAlignFrame bottom)
   {
     super(top, bottom);
@@ -160,26 +180,21 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI
    */
   public void adjustLayout()
   {
+    final AlignViewport topViewport = ((AlignFrame) getTopFrame()).viewport;
+    final AlignViewport bottomViewport = ((AlignFrame) getBottomFrame()).viewport;
+
     /*
      * Ensure sequence ids are the same width so sequences line up
      */
-    int w1 = ((AlignFrame) getTopFrame()).getViewport().getIdWidth();
-    int w2 = ((AlignFrame) getBottomFrame()).getViewport().getIdWidth();
+    int w1 = topViewport.getIdWidth();
+    int w2 = bottomViewport.getIdWidth();
     int w3 = Math.max(w1, w2);
-    if (w1 != w3)
-    {
-      ((AlignFrame) getTopFrame()).getViewport().setIdWidth(w3);
-    }
-    if (w2 != w3)
-    {
-      ((AlignFrame) getBottomFrame()).getViewport().setIdWidth(w3);
-    }
+    topViewport.setIdWidth(w3);
+    bottomViewport.setIdWidth(w3);
 
     /*
      * Scale protein to either 1 or 3 times character width of dna
      */
-    final AlignViewport topViewport = ((AlignFrame) getTopFrame()).viewport;
-    final AlignViewport bottomViewport = ((AlignFrame) getBottomFrame()).viewport;
     final AlignmentI topAlignment = topViewport.getAlignment();
     final AlignmentI bottomAlignment = bottomViewport.getAlignment();
     AlignmentViewport cdna = topAlignment.isNucleotide() ? topViewport
@@ -412,7 +427,7 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI
      * Ctrl-W / Cmd-W - close view or window
      */
     KeyStroke key_cmdW = KeyStroke.getKeyStroke(KeyEvent.VK_W,
-            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
+            jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false);
     action = new AbstractAction()
     {
       @Override
@@ -433,7 +448,7 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI
      * Ctrl-T / Cmd-T open new view
      */
     KeyStroke key_cmdT = KeyStroke.getKeyStroke(KeyEvent.VK_T,
-            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
+            jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false);
     AbstractAction action = new AbstractAction()
     {
       @Override
@@ -762,6 +777,22 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI
             { (AlignFrame) getTopFrame(), (AlignFrame) getBottomFrame() });
   }
 
+  @Override
+  public AlignFrame getComplementAlignFrame(
+          AlignViewControllerGuiI alignFrame)
+  {
+    if (getTopFrame() == alignFrame)
+    {
+      return (AlignFrame) getBottomFrame();
+    }
+    if (getBottomFrame() == alignFrame)
+    {
+      return (AlignFrame) getTopFrame();
+    }
+    // we didn't know anything about this frame...
+    return null;
+  }
+
   /**
    * Replace Cmd-F Find action with our version. This is necessary because the
    * 'default' Finder searches in the first AlignFrame it finds. We need it to
@@ -773,7 +804,7 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI
      * Ctrl-F / Cmd-F open Finder dialog, 'focused' on the right alignment
      */
     KeyStroke key_cmdF = KeyStroke.getKeyStroke(KeyEvent.VK_F,
-            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
+            jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false);
     AbstractAction action = new AbstractAction()
     {
       @Override
@@ -789,4 +820,283 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI
     };
     overrideKeyBinding(key_cmdF, action);
   }
+
+  /**
+   * Override to do nothing if triggered from one of the child frames
+   */
+  @Override
+  public void setSelected(boolean selected) throws PropertyVetoException
+  {
+    JDesktopPane desktopPane = getDesktopPane();
+    JInternalFrame fr = desktopPane == null ? null
+            : desktopPane.getSelectedFrame();
+    if (fr == getTopFrame() || fr == getBottomFrame())
+    {
+      /* 
+       * patch for JAL-3288 (deselecting top/bottom frame closes popup menu); 
+       * it may be possible to remove this method in future
+       * if the underlying Java behaviour changes
+       */
+      if (selected)
+      {
+        moveToFront();
+      }
+      return;
+    }
+    super.setSelected(selected);
+  }
+
+  /**
+   * holds the frame for feature settings, so Protein and DNA tabs can be managed
+   */
+  JInternalFrame featureSettingsUI;
+
+  JTabbedPane featureSettingsPanels;
+
+  @Override
+  public void addFeatureSettingsUI(
+          FeatureSettingsControllerGuiI featureSettings)
+  {
+    boolean showInternalFrame = false;
+    if (featureSettingsUI == null || featureSettingsPanels == null)
+    {
+      showInternalFrame = true;
+      featureSettingsPanels = new JTabbedPane();
+      featureSettingsPanels.addChangeListener(new ChangeListener()
+      {
+
+        @Override
+        public void stateChanged(ChangeEvent e)
+        {
+          if (e.getSource() != featureSettingsPanels
+                  || featureSettingsUI == null
+                  || featureSettingsUI.isClosed()
+                  || !featureSettingsUI.isVisible())
+          {
+            // not our tabbed pane
+            return;
+          }
+          int tab = featureSettingsPanels.getSelectedIndex();
+          if (tab < 0 || featureSettingsPanels
+                  .getSelectedComponent() instanceof FeatureSettingsControllerGuiI)
+          {
+            // no tab selected or already showing a feature settings GUI
+            return;
+          }
+          getAlignFrames().get(tab).showFeatureSettingsUI();
 }
+      });
+      featureSettingsUI = new JInternalFrame(MessageManager.getString(
+              "label.sequence_feature_settings_for_CDS_and_Protein"));
+      featureSettingsPanels.setOpaque(true);
+
+      JPanel dialog = new JPanel();
+      dialog.setOpaque(true);
+      dialog.setLayout(new BorderLayout());
+      dialog.add(featureSettingsPanels, BorderLayout.CENTER);
+      JPanel buttons = new JPanel();
+      JButton ok = new JButton(MessageManager.getString("action.ok"));
+      ok.addActionListener(new ActionListener()
+      {
+
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          try
+          {
+            featureSettingsUI.setClosed(true);
+          } catch (PropertyVetoException pv)
+          {
+            pv.printStackTrace();
+          }
+        }
+      });
+      JButton cancel = new JButton(
+              MessageManager.getString("action.cancel"));
+      cancel.addActionListener(new ActionListener()
+      {
+
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          try
+          {
+            for (Component fspanel : featureSettingsPanels.getComponents())
+            {
+              if (fspanel instanceof FeatureSettingsControllerGuiI)
+              {
+                ((FeatureSettingsControllerGuiI) fspanel).revert();
+              }
+            }
+            featureSettingsUI.setClosed(true);
+          } catch (Exception pv)
+          {
+            pv.printStackTrace();
+          }
+        }
+      });
+      buttons.add(ok);
+      buttons.add(cancel);
+      dialog.add(buttons, BorderLayout.SOUTH);
+      featureSettingsUI.setContentPane(dialog);
+      createDummyTabs();
+    }
+    if (featureSettingsPanels
+            .indexOfTabComponent((Component) featureSettings) > -1)
+    {
+      // just show the feature settings !
+      featureSettingsPanels
+              .setSelectedComponent((Component) featureSettings);
+      return;
+    }
+    // otherwise replace the dummy tab with the given feature settings
+    int pos = getAlignFrames().indexOf(featureSettings.getAlignframe());
+    // if pos==-1 then alignFrame isn't managed by this splitframe
+    if (pos == 0)
+    {
+      featureSettingsPanels.removeTabAt(0);
+      featureSettingsPanels.insertTab(tabName[0], null,
+              (Component) featureSettings,
+              MessageManager.formatMessage(
+                      "label.sequence_feature_settings_for", tabName[0]),
+              0);
+    }
+    if (pos == 1)
+    {
+      featureSettingsPanels.removeTabAt(1);
+      featureSettingsPanels.insertTab(tabName[1], null,
+              (Component) featureSettings,
+              MessageManager.formatMessage(
+                      "label.sequence_feature_settings_for", tabName[1]),
+              1);
+    }
+    featureSettingsPanels.setSelectedComponent((Component) featureSettings);
+    // TODO: JAL-3535 - construct a feature settings title including names of
+    // currently selected CDS and Protein names
+
+    if (showInternalFrame)
+    {
+      if (Platform.isAMacAndNotJS())
+      {
+        Desktop.addInternalFrame(featureSettingsUI,
+                MessageManager.getString(
+                        "label.sequence_feature_settings_for_CDS_and_Protein"),
+                600, 480);
+      }
+      else
+      {
+        Desktop.addInternalFrame(featureSettingsUI,
+                MessageManager.getString(
+                        "label.sequence_feature_settings_for_CDS_and_Protein"),
+                600, 450);
+      }
+      featureSettingsUI
+              .setMinimumSize(new Dimension(FS_MIN_WIDTH, FS_MIN_HEIGHT));
+
+      featureSettingsUI.addInternalFrameListener(
+              new javax.swing.event.InternalFrameAdapter()
+              {
+                @Override
+                public void internalFrameClosed(
+                        javax.swing.event.InternalFrameEvent evt)
+                {
+                  for (int tab = 0; tab < featureSettingsPanels
+                          .getTabCount();)
+                  {
+                    FeatureSettingsControllerGuiI fsettings = (FeatureSettingsControllerGuiI) featureSettingsPanels
+                            .getTabComponentAt(tab);
+                    if (fsettings != null)
+                    {
+                      featureSettingsPanels.removeTabAt(tab);
+                      fsettings.featureSettings_isClosed();
+                    }
+                    else
+                    {
+                      tab++;
+                    }
+                  }
+                  featureSettingsPanels = null;
+                  featureSettingsUI = null;
+                };
+              });
+      featureSettingsUI.setLayer(JLayeredPane.PALETTE_LAYER);
+    }
+  }
+
+  /**
+   * tab names for feature settings
+   */
+  private String[] tabName = new String[] {
+      MessageManager.getString("label.CDS"),
+      MessageManager.getString("label.protein") };
+
+  /**
+   * create placeholder tabs which materialise the feature settings for a given
+   * view. Also reinitialises any tabs containing stale feature settings
+   */
+  private void createDummyTabs()
+  {
+    for (int tabIndex = 0; tabIndex < 2; tabIndex++)
+    {
+      JPanel dummyTab = new JPanel();
+      featureSettingsPanels.addTab(tabName[tabIndex], dummyTab);
+    }
+  }
+
+  private void replaceWithDummyTab(FeatureSettingsControllerI toClose)
+  {
+    Component dummyTab = null;
+    for (int tabIndex = 0; tabIndex < 2; tabIndex++)
+    {
+      if (featureSettingsPanels.getTabCount() > tabIndex)
+      {
+        dummyTab = featureSettingsPanels.getTabComponentAt(tabIndex);
+        if (dummyTab instanceof FeatureSettingsControllerGuiI
+                && !dummyTab.isVisible())
+        {
+          featureSettingsPanels.removeTabAt(tabIndex);
+          // close the feature Settings tab
+          ((FeatureSettingsControllerGuiI) dummyTab)
+                  .featureSettings_isClosed();
+          // create a dummy tab in its place
+          dummyTab = new JPanel();
+          featureSettingsPanels.insertTab(tabName[tabIndex], null, dummyTab,
+                  MessageManager.formatMessage(
+                          "label.sequence_feature_settings_for",
+                          tabName[tabIndex]),
+                  tabIndex);
+        }
+      }
+    }
+  }
+
+  @Override
+  public void closeFeatureSettings(
+          FeatureSettingsControllerI featureSettings,
+          boolean closeContainingFrame)
+  {
+    if (featureSettingsUI != null)
+    {
+      if (closeContainingFrame)
+      {
+        try
+        {
+          featureSettingsUI.setClosed(true);
+        } catch (Exception x)
+        {
+        }
+        featureSettingsUI = null;
+      }
+      else
+      {
+        replaceWithDummyTab(featureSettings);
+      }
+    }
+  }
+
+  @Override
+  public boolean isFeatureSettingsOpen()
+  {
+    return featureSettingsUI != null && !featureSettingsUI.isClosed();
+  }
+}
\ No newline at end of file
index ef814d3..1320c0f 100644 (file)
@@ -688,7 +688,8 @@ public class VamsasApplication implements SelectionSource, VamsasSource
 
   public void disableGui(boolean b)
   {
-    Desktop.getInstance().setVamsasUpdate(b);
+    // JAL-3311 TODO: remove this class!
+    // Desktop.instance.setVamsasUpdate(b);
   }
 
   Hashtable _backup_vobj2jv;
index aa21b0f..821384a 100755 (executable)
@@ -29,12 +29,13 @@ import jalview.api.FeaturesSourceI;
 import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.FeatureMatcherSet;
 import jalview.datamodel.features.FeatureMatcherSetI;
-import jalview.io.gff.GffHelperBase;
+import jalview.gui.Desktop;
 import jalview.io.gff.GffHelperFactory;
 import jalview.io.gff.GffHelperI;
 import jalview.schemes.FeatureColour;
@@ -49,9 +50,11 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.TreeMap;
 
 /**
  * Parses and writes features files, which may be in Jalview, GFF2 or GFF3
@@ -71,6 +74,8 @@ import java.util.Map.Entry;
  */
 public class FeaturesFile extends AlignFile implements FeaturesSourceI
 {
+  private static final String EQUALS = "=";
+
   private static final String TAB_REGEX = "\\t";
 
   private static final String STARTGROUP = "STARTGROUP";
@@ -441,7 +446,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       float score = Float.NaN;
       try
       {
-        score = new Float(gffColumns[6]).floatValue();
+        score = Float.valueOf(gffColumns[6]).floatValue();
       } catch (NumberFormatException ex)
       {
         sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup);
@@ -563,31 +568,38 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
-   * Returns contents of a Jalview format features file, for visible features,
-   * as filtered by type and group. Features with a null group are displayed if
-   * their feature type is visible. Non-positional features may optionally be
-   * included (with no check on type or group).
+   * Returns contents of a Jalview format features file, for visible features, as
+   * filtered by type and group. Features with a null group are displayed if their
+   * feature type is visible. Non-positional features may optionally be included
+   * (with no check on type or group).
    * 
    * @param sequences
    * @param fr
    * @param includeNonPositional
-   *          if true, include non-positional features (regardless of group or
-   *          type)
+   *                               if true, include non-positional features
+   *                               (regardless of group or type)
+   * @param includeComplement
+   *                               if true, include visible complementary
+   *                               (CDS/protein) positional features, with
+   *                               locations converted to local sequence
+   *                               coordinates
    * @return
    */
   public String printJalviewFormat(SequenceI[] sequences,
-          FeatureRenderer fr, boolean includeNonPositional)
+          FeatureRenderer fr, boolean includeNonPositional,
+          boolean includeComplement)
   {
     Map<String, FeatureColourI> visibleColours = fr
             .getDisplayedFeatureCols();
     Map<String, FeatureMatcherSetI> featureFilters = fr.getFeatureFilters();
 
-    if (!includeNonPositional
-            && (visibleColours == null || visibleColours.isEmpty()))
-    {
-      // no point continuing.
-      return "No Features Visible";
-    }
+    // BH check this is out?
+//    if (!includeNonPositional
+//            && (visibleColours == null || visibleColours.isEmpty()))
+//    {
+//      // no point continuing.
+//      return "No Features Visible";
+//    }
 
     /*
      * write out feature colours (if we know them)
@@ -620,10 +632,151 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
     int count = outputFeaturesByGroup(out, fr, types, sequences,
             includeNonPositional);
 
+    if (includeComplement)
+    {
+      count += outputComplementFeatures(out, fr, sequences);
+    }
+
     return count > 0 ? out.toString() : "No Features Visible";
   }
 
   /**
+   * Outputs any visible complementary (CDS/peptide) positional features as
+   * Jalview format, within feature group. The coordinates of the linked features
+   * are converted to the corresponding positions of the local sequences.
+   * 
+   * @param out
+   * @param fr
+   * @param sequences
+   * @return
+   */
+  private int outputComplementFeatures(StringBuilder out,
+          FeatureRenderer fr, SequenceI[] sequences)
+  {
+    AlignViewportI comp = fr.getViewport().getCodingComplement();
+    FeatureRenderer fr2 = Desktop.getAlignFrameFor(comp)
+            .getFeatureRenderer();
+
+    /*
+     * bin features by feature group and sequence
+     */
+    Map<String, Map<String, List<SequenceFeature>>> map = new TreeMap<>(
+            String.CASE_INSENSITIVE_ORDER);
+    int count = 0;
+
+    for (SequenceI seq : sequences)
+    {
+      /*
+       * find complementary features
+       */
+      List<SequenceFeature> complementary = findComplementaryFeatures(seq,
+              fr2);
+      String seqName = seq.getName();
+
+      for (SequenceFeature sf : complementary)
+      {
+        String group = sf.getFeatureGroup();
+        if (!map.containsKey(group))
+        {
+          map.put(group, new LinkedHashMap<>()); // preserves sequence order
+        }
+        Map<String, List<SequenceFeature>> groupFeatures = map.get(group);
+        if (!groupFeatures.containsKey(seqName))
+        {
+          groupFeatures.put(seqName, new ArrayList<>());
+        }
+        List<SequenceFeature> foundFeatures = groupFeatures.get(seqName);
+        foundFeatures.add(sf);
+        count++;
+      }
+    }
+
+    /*
+     * output features by group
+     */
+    for (Entry<String, Map<String, List<SequenceFeature>>> groupFeatures : map.entrySet())
+    {
+      out.append(newline);
+      String group = groupFeatures.getKey();
+      if (!"".equals(group))
+      {
+        out.append(STARTGROUP).append(TAB).append(group).append(newline);
+      }
+      Map<String, List<SequenceFeature>> seqFeaturesMap = groupFeatures
+              .getValue();
+      for (Entry<String, List<SequenceFeature>> seqFeatures : seqFeaturesMap
+              .entrySet())
+      {
+        String sequenceName = seqFeatures.getKey();
+        for (SequenceFeature sf : seqFeatures.getValue())
+        {
+          formatJalviewFeature(out, sequenceName, sf);
+        }
+      }
+      if (!"".equals(group))
+      {
+        out.append(ENDGROUP).append(TAB).append(group).append(newline);
+      }
+    }
+
+    return count;
+  }
+
+  /**
+   * Answers a list of mapped features visible in the (CDS/protein) complement,
+   * with feature positions translated to local sequence coordinates
+   * 
+   * @param seq
+   * @param fr2
+   * @return
+   */
+  protected List<SequenceFeature> findComplementaryFeatures(SequenceI seq,
+          FeatureRenderer fr2)
+  {
+    /*
+     * avoid duplication of features (e.g. peptide feature 
+     * at all 3 mapped codon positions)
+     */
+    List<SequenceFeature> found = new ArrayList<>();
+    List<SequenceFeature> complementary = new ArrayList<>();
+
+    for (int pos = seq.getStart(); pos <= seq.getEnd(); pos++)
+    {
+      MappedFeatures mf = fr2.findComplementFeaturesAtResidue(seq, pos);
+
+      if (mf != null)
+      {
+        MapList mapping = mf.mapping.getMap();
+        for (SequenceFeature sf : mf.features)
+        {
+          /*
+           * make a virtual feature with local coordinates
+           */
+          if (!found.contains(sf))
+          {
+            String group = sf.getFeatureGroup();
+            if (group == null)
+            {
+              group = "";
+            }
+            found.add(sf);
+            int begin = sf.getBegin();
+            int end = sf.getEnd();
+            int[] range = mf.mapping.getTo() == seq.getDatasetSequence()
+                    ? mapping.locateInTo(begin, end)
+                    : mapping.locateInFrom(begin, end);
+            SequenceFeature sf2 = new SequenceFeature(sf, range[0],
+                    range[1], group, sf.getScore());
+            complementary.add(sf2);
+          }
+        }
+      }
+    }
+
+    return complementary;
+  }
+
+  /**
    * Outputs any feature filters defined for visible feature types, sandwiched by
    * STARTFILTERS and ENDFILTERS lines
    * 
@@ -745,7 +898,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
               }
             }
             firstInGroup = false;
-            out.append(formatJalviewFeature(sequenceName, sf));
+            formatJalviewFeature(out, sequenceName, sf);
           }
         }
       }
@@ -759,14 +912,16 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
+   * Formats one feature in Jalview format and appends to the string buffer
+   * 
    * @param out
    * @param sequenceName
    * @param sequenceFeature
    */
   protected String formatJalviewFeature(
-          String sequenceName, SequenceFeature sequenceFeature)
+          StringBuilder out, String sequenceName,
+          SequenceFeature sequenceFeature)
   {
-    StringBuilder out = new StringBuilder(64);
     if (sequenceFeature.description == null
             || sequenceFeature.description.equals(""))
     {
@@ -791,7 +946,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
           if (sequenceFeature.description.indexOf(href) == -1)
           {
-            out.append(" <a href=\"" + href + "\">" + label + "</a>");
+            out.append(" <a href=\"").append(href).append("\">")
+                    .append(label).append("</a>");
           }
         }
 
@@ -882,23 +1038,25 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    *          a map whose keys are the type names of visible features
    * @param visibleFeatureGroups
    * @param includeNonPositionalFeatures
+   * @param includeComplement
    * @return
    */
   public String printGffFormat(SequenceI[] sequences,
-          FeatureRenderer fr, boolean includeNonPositionalFeatures)
+          FeatureRenderer fr, boolean includeNonPositionalFeatures,
+          boolean includeComplement)
+  {
+    FeatureRenderer fr2 = null;
+    if (includeComplement)
   {
+      AlignViewportI comp = fr.getViewport().getCodingComplement();
+      fr2 = Desktop.getAlignFrameFor(comp).getFeatureRenderer();
+    }
+
     Map<String, FeatureColourI> visibleColours = fr.getDisplayedFeatureCols();
 
     StringBuilder out = new StringBuilder(256);
 
-    out.append(String.format("%s %d" + newline, GFF_VERSION,
-            gffVersion == 0 ? 2 : gffVersion));
-
-    if (!includeNonPositionalFeatures
-            && (visibleColours == null || visibleColours.isEmpty()))
-    {
-      return out.toString();
-    }
+    out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion));
 
     String[] types = visibleColours == null ? new String[0]
             : visibleColours.keySet()
@@ -906,6 +1064,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
     for (SequenceI seq : sequences)
     {
+      List<SequenceFeature> seqFeatures = new ArrayList<>();
       List<SequenceFeature> features = new ArrayList<>();
       if (includeNonPositionalFeatures)
       {
@@ -918,15 +1077,40 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
       for (SequenceFeature sf : features)
       {
-        if (!sf.isNonPositional() && !fr.isVisible(sf))
+        if (sf.isNonPositional() || fr.isVisible(sf))
         {
           /*
-           * feature hidden by group visibility, colour threshold,
+           * drop features hidden by group visibility, colour threshold,
            * or feature filter condition
            */
-          continue;
+          seqFeatures.add(sf);
         }
+      }
+
+      if (includeComplement)
+      {
+        seqFeatures.addAll(findComplementaryFeatures(seq, fr2));
+      }
+
+      /*
+       * sort features here if wanted
+       */
+      for (SequenceFeature sf : seqFeatures)
+      {
+        formatGffFeature(out, seq, sf);
+        out.append(newline);
+      }
+    }
 
+    return out.toString();
+  }
+
+  /**
+   * Formats one feature as GFF and appends to the string buffer
+   */
+  private void formatGffFeature(StringBuilder out, SequenceI seq,
+          SequenceFeature sf)
+  {
         String source = sf.featureGroup;
         if (source == null)
         {
@@ -953,18 +1137,111 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
         String phase = sf.getPhase();
         out.append(phase == null ? "." : phase);
 
-        // miscellaneous key-values (GFF column 9)
-        String attributes = sf.getAttributes();
-        if (attributes != null)
+    if (sf.otherDetails != null && !sf.otherDetails.isEmpty())
         {
-          out.append(TAB).append(attributes);
-        }
+      Map<String, Object> map = sf.otherDetails;
+      formatAttributes(out, map);
+    }
+  }
 
-        out.append(newline);
+  /**
+   * A helper method that outputs attributes stored in the map as
+   * semicolon-delimited values e.g.
+   * 
+   * <pre>
+   * AC_Male=0;AF_NFE=0.00000e 00;Hom_FIN=0;GQ_MEDIAN=9
+   * </pre>
+   * 
+   * A map-valued attribute is formatted as a comma-delimited list within braces,
+   * for example
+   * 
+   * <pre>
+   * jvmap_CSQ={ALLELE_NUM=1,UNIPARC=UPI0002841053,Feature=ENST00000585561}
+   * </pre>
+   * 
+   * The {@code jvmap_} prefix designates a values map and is removed if the value
+   * is parsed when read in. (The GFF3 specification allows 'semi-structured data'
+   * to be represented provided the attribute name begins with a lower case
+   * letter.)
+   * 
+   * @param sb
+   * @param map
+   * @see http://gmod.org/wiki/GFF3#GFF3_Format
+   */
+  void formatAttributes(StringBuilder sb, Map<String, Object> map)
+  {
+    sb.append(TAB);
+    boolean first = true;
+    for (String key : map.keySet())
+    {
+      if (SequenceFeature.STRAND.equals(key)
+              || SequenceFeature.PHASE.equals(key))
+      {
+        /*
+         * values stashed in map but output to their own columns
+         */
+        continue;
+      }
+      {
+        if (!first)
+        {
+          sb.append(";");
+        }
+      }
+      first = false;
+      Object value = map.get(key);
+      if (value instanceof Map<?, ?>)
+      {
+        formatMapAttribute(sb, key, (Map<?, ?>) value);
+      }
+      else
+      {
+        String formatted = StringUtils.urlEncode(value.toString(),
+                GffHelperI.GFF_ENCODABLE);
+        sb.append(key).append(EQUALS).append(formatted);
       }
     }
+  }
 
-    return out.toString();
+  /**
+   * Formats the map entries as
+   * 
+   * <pre>
+   * key=key1=value1,key2=value2,...
+   * </pre>
+   * 
+   * and appends this to the string buffer
+   * 
+   * @param sb
+   * @param key
+   * @param map
+   */
+  private void formatMapAttribute(StringBuilder sb, String key,
+          Map<?, ?> map)
+  {
+    if (map == null || map.isEmpty())
+    {
+      return;
+    }
+
+    /*
+     * AbstractMap.toString would be a shortcut here, but more reliable
+     * to code the required format in case toString changes in future
+     */
+    sb.append(key).append(EQUALS);
+    boolean first = true;
+    for (Entry<?, ?> entry : map.entrySet())
+    {
+      if (!first)
+      {
+        sb.append(",");
+      }
+      first = false;
+      sb.append(entry.getKey().toString()).append(EQUALS);
+      String formatted = StringUtils.urlEncode(entry.getValue().toString(),
+              GffHelperI.GFF_ENCODABLE);
+      sb.append(formatted);
+    }
   }
 
   /**
@@ -1111,37 +1388,38 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
     return seq;
   }
 
-  /**
-   * Process the 'column 9' data of the GFF file. This is less formally defined,
-   * and its interpretation will vary depending on the tool that has generated
-   * it.
-   * 
-   * @param attributes
-   * @param sf
-   */
-  protected void processGffColumnNine(String attributes, SequenceFeature sf)
-  {
-    sf.setAttributes(attributes);
-
-    /*
-     * Parse attributes in column 9 and add them to the sequence feature's 
-     * 'otherData' table; use Note as a best proxy for description
-     */
-    char nameValueSeparator = gffVersion == 3 ? '=' : ' ';
-    // TODO check we don't break GFF2 values which include commas here
-    Map<String, List<String>> nameValues = GffHelperBase
-            .parseNameValuePairs(attributes, ";", nameValueSeparator, ",");
-    for (Entry<String, List<String>> attr : nameValues.entrySet())
-    {
-      String values = StringUtils.listToDelimitedString(attr.getValue(),
-              "; ");
-      sf.setValue(attr.getKey(), values);
-      if (NOTE.equals(attr.getKey()))
-      {
-        sf.setDescription(values);
-      }
-    }
-  }
+  // BH! check that we did not lose something here. 
+//  /**
+//   * Process the 'column 9' data of the GFF file. This is less formally defined,
+//   * and its interpretation will vary depending on the tool that has generated
+//   * it.
+//   * 
+//   * @param attributes
+//   * @param sf
+//   */
+//  protected void processGffColumnNine(String attributes, SequenceFeature sf)
+//  {
+//    sf.setAttributes(attributes);
+//
+//    /*
+//     * Parse attributes in column 9 and add them to the sequence feature's 
+//     * 'otherData' table; use Note as a best proxy for description
+//     */
+//    char nameValueSeparator = gffVersion == 3 ? '=' : ' ';
+//    // TODO check we don't break GFF2 values which include commas here
+//    Map<String, List<String>> nameValues = GffHelperBase
+//            .parseNameValuePairs(attributes, ";", nameValueSeparator, ",");
+//    for (Entry<String, List<String>> attr : nameValues.entrySet())
+//    {
+//      String values = StringUtils.listToDelimitedString(attr.getValue(),
+//              "; ");
+//      sf.setValue(attr.getKey(), values);
+//      if (NOTE.equals(attr.getKey()))
+//      {
+//        sf.setDescription(values);
+//      }
+//    }
+//  }
 
   /**
    * After encountering ##fasta in a GFF3 file, process the remainder of the
index 0254b5f..a64582d 100755 (executable)
@@ -70,8 +70,6 @@ public class FileLoader implements Runnable
    */
   private static int MAX_HISTORY = 11;
 
-  private File selectedFile;
-
   String file;
 
   DataSourceType protocol;
@@ -81,6 +79,37 @@ public class FileLoader implements Runnable
   AlignmentFileReaderI source; // alternative specification of where data
                                // comes from
 
+  AlignViewport viewport;
+
+  AlignFrame alignFrame;
+
+  long loadtime;
+
+  long memused;
+
+  boolean raiseGUI = true;
+
+  private File selectedFile;
+
+  /**
+   * default constructor always raised errors in GUI dialog boxes
+   */
+  public FileLoader()
+  {
+    this(true);
+  }
+
+  /**
+   * construct a Fileloader that may raise errors non-interactively
+   * 
+   * @param raiseGUI
+   *          true if errors are to be raised as GUI dialog boxes
+   */
+  public FileLoader(boolean raiseGUI)
+  {
+    this.raiseGUI = raiseGUI;
+  }
+
   /**
    * It is critical that all these fields are set, as this instance is reused.
    * 
@@ -100,47 +129,69 @@ public class FileLoader implements Runnable
     this.format = format;
   }
 
-  AlignViewport viewport;
-
-  AlignFrame alignFrame;
-
-  long loadtime;
-
-  long memused;
-
-  boolean raiseGUI = true;
 
   /**
-   * default constructor always raised errors in GUI dialog boxes
+   * Uppercase LoadFile is deprecated because it does not pass byte[] data to
+   * JavaScript
+   * 
+   * @param viewport
+   * @param file
+   * @param protocol
+   * @param format
    */
-  public FileLoader()
+  @Deprecated
+  public void LoadFile(AlignViewport viewport, String file,
+          DataSourceType protocol, FileFormatI format)
   {
-    this(true);
+    if (viewport != null)
+    {
+      this.viewport = viewport;
+    }
+    loadFile(file, protocol, format);
+  }
+
+  public void LoadFile(AlignViewport viewport, File file,
+          DataSourceType protocol, FileFormatI format)
+  {
+    loadFile(viewport, file, protocol, format);
   }
 
   /**
-   * construct a Fileloader that may raise errors non-interactively
+   * Uppercase LoadFile is deprecated because it does not pass byte[] data to
+   * JavaScript
    * 
-   * @param raiseGUI
-   *          true if errors are to be raised as GUI dialog boxes
+   * @param file
+   * @param protocol
+   * @param format
    */
-  public FileLoader(boolean raiseGUI)
+  @Deprecated
+  public void LoadFile(String file, DataSourceType protocol,
+          FileFormatI format)
   {
-    this.raiseGUI = raiseGUI;
+    loadFile(file, protocol, format);
   }
 
-  public void loadFile(AlignViewport viewport, Object file,
+  /**
+   * necessary to use Object here in order to pass the file data
+   * 
+   * @param viewport
+   * @param file
+   *          File preferably to String
+   * @param protocol
+   * @param format
+   */
+  public void loadFile(AlignViewport viewport, File file,
           DataSourceType protocol, FileFormatI format)
   {
-    this.viewport = viewport;
-    if (file instanceof File) {
-      this.selectedFile = (File) file;
-      file = selectedFile.getPath();
+    if (viewport != null)
+    {
+      this.viewport = viewport;
     }
-    loadFile(file.toString(), protocol, format);
+    this.selectedFile = file;
+    loadFile(selectedFile.getPath(), protocol, format);
   }
 
-  public void loadFile(String file, DataSourceType protocol,
+  private void loadFile(String file, DataSourceType protocol,
           FileFormatI format)
   {
     this.file = file;
@@ -166,10 +217,11 @@ public class FileLoader implements Runnable
    * @param sourceType
    * @return alignFrame constructed from file contents
    */
+  @Deprecated
   public AlignFrame LoadFileWaitTillLoaded(String file,
           DataSourceType sourceType)
   {
-    return loadFileWaitTillLoaded(file, sourceType, null);
+    return LoadFileWaitTillLoaded(file, sourceType, null);
   }
 
   /**
@@ -180,7 +232,8 @@ public class FileLoader implements Runnable
    * @param format
    * @return alignFrame constructed from file contents
    */
-  public AlignFrame loadFileWaitTillLoaded(String file,
+  @Deprecated
+  public AlignFrame LoadFileWaitTillLoaded(String file,
           DataSourceType sourceType, FileFormatI format)
   {
     setFileFields(null, null, file, sourceType, format);
@@ -237,6 +290,7 @@ public class FileLoader implements Runnable
       // refer to it as.
       return;
     }
+    // BH logic change here just includes ignoring file==null
     if (file == null
             || file.indexOf(System.getProperty("java.io.tmpdir")) > -1)
     {
@@ -248,6 +302,7 @@ public class FileLoader implements Runnable
 
     String historyItems = Cache.getProperty(type);
 
+    // BH simpler coding
     if (historyItems != null)
     {
       String[] tokens = historyItems.split("\\t");
index 27e4da2..4933cd6 100644 (file)
@@ -23,6 +23,8 @@ package jalview.io;
 import jalview.api.FeatureColourI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
+import jalview.datamodel.GeneLociI;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.util.MessageManager;
@@ -44,6 +46,8 @@ import java.util.Map;
  */
 public class SequenceAnnotationReport
 {
+  private static final int MAX_DESCRIPTION_LENGTH = 40;
+
   private static final String COMMA = ",";
 
   private static final String ELLIPSIS = "...";
@@ -66,11 +70,11 @@ public class SequenceAnnotationReport
     @Override
     public int compare(DBRefEntry ref1, DBRefEntry ref2)
     {
-      if (ref1.isChromosome())
+      if (ref1 instanceof GeneLociI)
       {
         return -1;
       }
-      if (ref2.isChromosome())
+      if (ref2 instanceof GeneLociI)
       {
         return 1;
       }
@@ -120,23 +124,65 @@ public class SequenceAnnotationReport
   }
 
   /**
-   * Append text for the list of features to the tooltip
+   * Append text for the list of features to the tooltip Returns number of
+   * features left if maxlength limit is (or would have been) reached
    * 
    * @param sb
-   * @param rpos
+   * @param residuePos
    * @param features
    * @param minmax
+   * @param maxlength
    */
-  public void appendFeatures(final StringBuilder sb, int rpos,
+  public int appendFeaturesLengthLimit(final StringBuilder sb,
+          int residuePos, List<SequenceFeature> features,
+          FeatureRendererModel fr, int maxlength)
+  {
+    for (int i = 0; i < features.size(); i++)
+    {
+      SequenceFeature feature = features.get(i);
+      if (appendFeature(sb, residuePos, fr, feature, null, maxlength))
+      {
+        return features.size() - i;
+      }
+    }
+    return 0;
+  }
+
+  public void appendFeatures(final StringBuilder sb, int residuePos,
           List<SequenceFeature> features, FeatureRendererModel fr)
   {
-    if (features != null)
+    appendFeaturesLengthLimit(sb, residuePos, features, fr, 0);
+  }
+
+  /**
+   * Appends text for mapped features (e.g. CDS feature for peptide or vice versa)
+   * Returns number of features left if maxlength limit is (or would have been)
+   * reached
+   * 
+   * @param sb
+   * @param residuePos
+   * @param mf
+   * @param fr
+   * @param maxlength
+   */
+  public int appendFeaturesLengthLimit(StringBuilder sb, int residuePos,
+          MappedFeatures mf, FeatureRendererModel fr, int maxlength)
+  {
+    for (int i = 0; i < mf.features.size(); i++)
     {
-      for (SequenceFeature feature : features)
+      SequenceFeature feature = mf.features.get(i);
+      if (appendFeature(sb, residuePos, fr, feature, mf, maxlength))
       {
-        appendFeature(sb, rpos, fr, feature);
+        return mf.features.size() - i;
       }
     }
+    return 0;
+  }
+
+  public void appendFeatures(StringBuilder sb, int residuePos,
+          MappedFeatures mf, FeatureRendererModel fr)
+  {
+    appendFeaturesLengthLimit(sb, residuePos, mf, fr, 0);
   }
 
   /**
@@ -147,26 +193,28 @@ public class SequenceAnnotationReport
    * @param minmax
    * @param feature
    */
-  void appendFeature(final StringBuilder sb, int rpos,
-          FeatureRendererModel fr, SequenceFeature feature)
+  boolean appendFeature(final StringBuilder sb0, int rpos,
+          FeatureRendererModel fr, SequenceFeature feature,
+          MappedFeatures mf, int maxlength)
   {
+    StringBuilder sb = new StringBuilder();
     if (feature.isContactFeature())
     {
       if (feature.getBegin() == rpos || feature.getEnd() == rpos)
       {
-        if (sb.length() > 6)
+        if (sb0.length() > 6)
         {
-          sb.append("<br>");
+          sb.append("<br/>");
         }
         sb.append(feature.getType()).append(" ").append(feature.getBegin())
                 .append(":").append(feature.getEnd());
       }
-      return;
+      return appendTextMaxLengthReached(sb0, sb, maxlength);
     }
 
-    if (sb.length() > 6)
+    if (sb0.length() > 6)
     {
-      sb.append("<br>");
+      sb.append("<br/>");
     }
     // TODO: remove this hack to display link only features
     boolean linkOnly = feature.getValue("linkonly") != null;
@@ -187,6 +235,20 @@ public class SequenceAnnotationReport
       if (description != null && !description.equals(feature.getType()))
       {
         description = StringUtils.stripHtmlTags(description);
+
+        /*
+         * truncate overlong descriptions unless they contain an href
+         * before the truncation point (as truncation could leave corrupted html)
+         */
+        int linkindex = description.toLowerCase().indexOf("<a ");
+        boolean hasLink = linkindex > -1
+                && linkindex < MAX_DESCRIPTION_LENGTH;
+        if (description.length() > MAX_DESCRIPTION_LENGTH && !hasLink)
+        {
+          description = description.substring(0, MAX_DESCRIPTION_LENGTH)
+                  + ELLIPSIS;
+        }
+
         sb.append("; ").append(description);
       }
 
@@ -217,6 +279,36 @@ public class SequenceAnnotationReport
           }
         }
       }
+
+      if (mf != null)
+      {
+        String variants = mf.findProteinVariants(feature);
+        if (!variants.isEmpty())
+        {
+          sb.append(" ").append(variants);
+        }
+      }
+    }
+    return appendTextMaxLengthReached(sb0, sb, maxlength);
+  }
+
+  void appendFeature(final StringBuilder sb, int rpos,
+          FeatureRendererModel fr, SequenceFeature feature,
+          MappedFeatures mf)
+  {
+    appendFeature(sb, rpos, fr, feature, mf, 0);
+  }
+
+  private static boolean appendTextMaxLengthReached(StringBuilder sb0,
+          StringBuilder sb, int maxlength)
+  {
+    boolean ret = false;
+    if (maxlength == 0 || sb0.length() + sb.length() < maxlength)
+    {
+      sb0.append(sb);
+      return false;
+    } else {
+      return true;
     }
   }
 
@@ -277,7 +369,8 @@ public class SequenceAnnotationReport
                       + (urllink.get(0).toLowerCase()
                               .equals(urllink.get(1).toLowerCase()) ? urllink
                               .get(0) : (urllink.get(0) + ":" + urllink
-                              .get(1))) + "</a></br>");
+                                              .get(1)))
+                      + "</a><br/>");
             }
           } catch (Exception x)
           {
@@ -348,7 +441,7 @@ public class SequenceAnnotationReport
     if (sequence.getDescription() != null)
     {
       tmp = sequence.getDescription();
-      sb.append("<br>").append(tmp);
+      sb.append(tmp);
       maxWidth = Math.max(maxWidth, tmp.length());
     }
     SequenceI ds = sequence;
@@ -371,7 +464,7 @@ public class SequenceAnnotationReport
               .getNonPositionalFeatures())
       {
         int sz = -sb.length();
-        appendFeature(sb, 0, fr, sf);
+        appendFeature(sb, 0, fr, sf, null);
         sz += sb.length();
         maxWidth = Math.max(maxWidth, sz);
       }
@@ -434,7 +527,7 @@ public class SequenceAnnotationReport
       countForSource++;
       if (countForSource == 1 || !summary)
       {
-        sb.append("<br>");
+        sb.append("<br/>");
       }
       if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
       {
@@ -460,11 +553,11 @@ public class SequenceAnnotationReport
     }
     if (moreSources)
     {
-      sb.append("<br>").append(source).append(COMMA).append(ELLIPSIS);
+      sb.append("<br/>").append(source).append(COMMA).append(ELLIPSIS);
     }
     if (ellipsis)
     {
-      sb.append("<br>(");
+      sb.append("<br/>(");
       sb.append(MessageManager.getString("label.output_seq_details"));
       sb.append(")");
     }
index 19045d5..a15a116 100644 (file)
@@ -38,20 +38,10 @@ public class Gff2Helper extends GffHelperBase
    */
   public static Map<String, List<String>> parseNameValuePairs(String text)
   {
-    // TODO: can a value include a comma? if so it will be broken by this
     return parseNameValuePairs(text, ";", ' ', ",");
   }
 
   /**
-   * Return ' ' as the name-value separator used in column 9 attributes.
-   */
-  @Override
-  protected char getNameValueSeparator()
-  {
-    return ' ';
-  }
-
-  /**
    * Default processing if not overridden is just to construct a sequence
    * feature
    */
index 28bcf44..e422ed4 100644 (file)
@@ -350,15 +350,6 @@ public class Gff3Helper extends GffHelperBase
   }
 
   /**
-   * Return '=' as the name-value separator used in column 9 attributes.
-   */
-  @Override
-  protected char getNameValueSeparator()
-  {
-    return '=';
-  }
-
-  /**
    * Modifies the default SequenceFeature in order to set the Target sequence id
    * as the description
    */
@@ -424,6 +415,11 @@ public class Gff3Helper extends GffHelperBase
       desc = (String) sf.getValue(ID);
     }
 
+    /*
+     * and decode comma, equals, semi-colon as required by GFF3 spec
+     */
+    desc = StringUtils.urlDecode(desc, GFF_ENCODABLE);
+
     return desc;
   }
 }
index 1d4d3ac..3db1755 100644 (file)
@@ -43,7 +43,13 @@ import java.util.Map.Entry;
  */
 public abstract class GffHelperBase implements GffHelperI
 {
-  private static final String NOTE = "Note";
+  private static final String INVALID_GFF_ATTRIBUTE_FORMAT = "Invalid GFF attribute format: ";
+
+  protected static final String COMMA = ",";
+
+  protected static final String EQUALS = "=";
+
+  protected static final String NOTE = "Note";
 
   /*
    * GFF columns 1-9 (zero-indexed):
@@ -260,9 +266,12 @@ public abstract class GffHelperBase implements GffHelperI
 
   /**
    * Parses the input line to a map of name / value(s) pairs. For example the
-   * line <br>
+   * line
+   * 
+   * <pre>
    * Notes=Fe-S;Method=manual curation, prediction; source = Pfam; Notes = Metal
-   * <br>
+   * </pre>
+   * 
    * if parsed with delimiter=";" and separators {' ', '='} <br>
    * would return a map with { Notes={Fe=S, Metal}, Method={manual curation,
    * prediction}, source={Pfam}} <br>
@@ -272,57 +281,80 @@ public abstract class GffHelperBase implements GffHelperI
    * name), or GFF3 format (which uses '=' as the name/value delimiter, and
    * strictly does not allow repeat occurrences of the same name - but does
    * allow a comma-separated list of values).
+   * <p>
+   * Returns a (possibly empty) map of lists of values by attribute name.
    * 
    * @param text
    * @param namesDelimiter
    *          the major delimiter between name-value pairs
    * @param nameValueSeparator
-   *          one or more separators used between name and value
+   *          separator used between name and value
    * @param valuesDelimiter
    *          delimits a list of more than one value
-   * @return the name-values map (which may be empty but never null)
+   * @return
    */
   public static Map<String, List<String>> parseNameValuePairs(String text,
           String namesDelimiter, char nameValueSeparator,
           String valuesDelimiter)
   {
-    Map<String, List<String>> map = new HashMap<String, List<String>>();
+    Map<String, List<String>> map = new HashMap<>();
     if (text == null || text.trim().length() == 0)
     {
       return map;
     }
 
-    for (String pair : text.trim().split(namesDelimiter))
+    /*
+     * split by major delimiter (; for GFF3)
+     */
+    for (String nameValuePair : text.trim().split(namesDelimiter))
     {
-      pair = pair.trim();
-      if (pair.length() == 0)
+      nameValuePair = nameValuePair.trim();
+      if (nameValuePair.length() == 0)
       {
         continue;
       }
 
-      int sepPos = pair.indexOf(nameValueSeparator);
+      /*
+       * find name/value separator (= for GFF3)
+       */
+      int sepPos = nameValuePair.indexOf(nameValueSeparator);
       if (sepPos == -1)
       {
-        // no name=value present
+        // no name=value found
         continue;
       }
 
-      String key = pair.substring(0, sepPos).trim();
-      String values = pair.substring(sepPos + 1).trim();
-      if (values.length() > 0)
+      String name = nameValuePair.substring(0, sepPos).trim();
+      String values = nameValuePair.substring(sepPos + 1).trim();
+      if (values.isEmpty())
+      {
+        continue;
+      }
+
+      List<String> vals = map.get(name);
+      if (vals == null)
+      {
+        vals = new ArrayList<>();
+        map.put(name, vals);
+      }
+
+      /*
+       * if 'values' contains more name/value separators, parse as a map
+       * (nested sub-attribute values)
+       */
+      if (values.indexOf(nameValueSeparator) != -1)
+      {
+        vals.add(values);
+      }
+      else
       {
-        List<String> vals = map.get(key);
-        if (vals == null)
-        {
-          vals = new ArrayList<String>();
-          map.put(key, vals);
-        }
         for (String val : values.split(valuesDelimiter))
         {
           vals.add(val);
         }
       }
     }
+
     return map;
   }
 
@@ -357,8 +389,7 @@ public abstract class GffHelperBase implements GffHelperI
       int end = Integer.parseInt(gff[END_COL]);
 
       /*
-       * default 'score' is 0 rather than Float.NaN as the latter currently
-       * disables the 'graduated colour => colour by label' option
+       * default 'score' is 0 rather than Float.NaN - see JAL-2554
        */
       float score = 0f;
       try
@@ -379,22 +410,32 @@ public abstract class GffHelperBase implements GffHelperI
       if (attributes != null)
       {
         /*
-         * save 'raw' column 9 to allow roundtrip output as input
-         */
-        sf.setAttributes(gff[ATTRIBUTES_COL]);
-
-        /*
          * Add attributes in column 9 to the sequence feature's 
-         * 'otherData' table; use Note as a best proxy for description
+         * 'otherData' table; use Note as a best proxy for description;
+         * decode any encoded comma, equals, semi-colon as per GFF3 spec
          */
         for (Entry<String, List<String>> attr : attributes.entrySet())
         {
-          String values = StringUtils.listToDelimitedString(attr.getValue(),
-                  ",");
-          sf.setValue(attr.getKey(), values);
-          if (NOTE.equals(attr.getKey()))
+          String key = attr.getKey();
+          List<String> values = attr.getValue();
+          if (values.size() == 1 && values.get(0).contains(EQUALS))
+          {
+            /*
+             * 'value' is actually nested subattributes as x=a,y=b,z=c
+             */
+            Map<String, String> valueMap = parseAttributeMap(values.get(0));
+            sf.setValue(key, valueMap);
+          }
+          else
           {
-            sf.setDescription(values);
+            String csvValues = StringUtils.listToDelimitedString(values,
+                    COMMA);
+            csvValues = StringUtils.urlDecode(csvValues, GFF_ENCODABLE);
+            sf.setValue(key, csvValues);
+            if (NOTE.equals(key))
+            {
+              sf.setDescription(csvValues);
+            }
           }
         }
       }
@@ -408,12 +449,102 @@ public abstract class GffHelperBase implements GffHelperI
   }
 
   /**
-   * Returns the character used to separate attributes names from values in GFF
-   * column 9. This is space for GFF2, '=' for GFF3.
+   * Parses a (GFF3 format) list of comma-separated key=value pairs into a Map
+   * of {@code key,
+   * value} <br>
+   * An input string like {@code a=b,c,d=e,f=g,h} is parsed to
+   * 
+   * <pre>
+   * a = "b,c"
+   * d = "e"
+   * f = "g,h"
+   * </pre>
+   * 
+   * @param s
    * 
    * @return
    */
-  protected abstract char getNameValueSeparator();
+  protected static Map<String, String> parseAttributeMap(String s)
+  {
+    Map<String, String> map = new HashMap<>();
+    String[] fields = s.split(EQUALS);
+
+    /*
+     * format validation
+     */
+    boolean valid = true;
+    if (fields.length < 2)
+    {
+      /*
+       * need at least A=B here
+       */
+      valid = false;
+    }
+    else if (fields[0].isEmpty() || fields[0].contains(COMMA))
+    {
+      /*
+       * A,B=C is not a valid start, nor is =C
+       */
+      valid = false;
+    }
+    else
+    {
+      for (int i = 1; i < fields.length - 1; i++)
+      {
+        if (fields[i].isEmpty() || !fields[i].contains(COMMA))
+        {
+          /*
+           * intermediate tokens must include value,name
+           */
+          valid = false;
+        }
+      }
+    }
+
+    if (!valid)
+    {
+      System.err.println(INVALID_GFF_ATTRIBUTE_FORMAT + s);
+      return map;
+    }
+
+    int i = 0;
+    while (i < fields.length - 1)
+    {
+      boolean lastPair = i == fields.length - 2;
+      String before = fields[i];
+      String after = fields[i + 1];
+
+      /*
+       * if 'key' looks like a,b,c then the last token is the
+       * key
+       */
+      String theKey = before.contains(COMMA)
+              ? before.substring(before.lastIndexOf(COMMA) + 1)
+              : before;
+
+      theKey = theKey.trim();
+      if (theKey.isEmpty())
+      {
+        System.err.println(INVALID_GFF_ATTRIBUTE_FORMAT + s);
+        map.clear();
+        return map;
+      }
+
+      /*
+       * if 'value' looks like a,b,c then all but the last token is the value,
+       * unless this is the last field (no more = to follow), in which case
+       * all of it makes up the value
+       */
+      String theValue = after.contains(COMMA) && !lastPair
+              ? after.substring(0, after.lastIndexOf(COMMA))
+              : after;
+      map.put(StringUtils.urlDecode(theKey, GFF_ENCODABLE),
+              StringUtils.urlDecode(theValue, GFF_ENCODABLE));
+      i += 1;
+    }
+
+    return map;
+  }
 
   /**
    * Returns any existing mapping held on the alignment between the given
index 7fbcf5c..387ee60 100644 (file)
@@ -35,6 +35,12 @@ import java.util.List;
  */
 public interface GffHelperI
 {
+  /*
+   * GFF3 spec requires comma, equals, semi-colon, tab, percent characters to be
+   * encoded as %2C, %3D, %3B, %09, %25 respectively within data values
+   * see https://github.com/The-Sequence-Ontology/Specifications/blob/master/gff3.md
+   */
+  final String GFF_ENCODABLE = ",=;\t%";
 
   final String RENAME_TOKEN = "$RENAME_TO$";
 
index 72e906c..7d354e0 100644 (file)
@@ -70,6 +70,7 @@ public class SequenceOntologyLite implements SequenceOntologyI
     { "snRNA", "transcript" },
     { "miRNA", "transcript" },
     { "lincRNA", "transcript" },
+    { "lnc_RNA", "transcript" },
     { "rRNA", "transcript" },
     { "mRNA", "transcript" },
     // there are many more sub-types of ncRNA...
index bf988da..168f1c6 100644 (file)
@@ -1,6 +1,5 @@
 package jalview.io.vcf;
 
-import jalview.analysis.AlignmentUtils;
 import jalview.analysis.Dna;
 import jalview.api.AlignViewControllerGuiI;
 import jalview.bin.Cache;
@@ -20,14 +19,18 @@ import jalview.io.gff.SequenceOntologyI;
 import jalview.util.MapList;
 import jalview.util.MappingUtils;
 import jalview.util.MessageManager;
+import jalview.util.StringUtils;
 
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 
@@ -35,8 +38,10 @@ import htsjdk.samtools.SAMException;
 import htsjdk.samtools.SAMSequenceDictionary;
 import htsjdk.samtools.SAMSequenceRecord;
 import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.tribble.TribbleException;
 import htsjdk.variant.variantcontext.Allele;
 import htsjdk.variant.variantcontext.VariantContext;
+import htsjdk.variant.vcf.VCFConstants;
 import htsjdk.variant.vcf.VCFHeader;
 import htsjdk.variant.vcf.VCFHeaderLine;
 import htsjdk.variant.vcf.VCFHeaderLineCount;
@@ -51,6 +56,23 @@ import htsjdk.variant.vcf.VCFInfoHeaderLine;
  */
 public class VCFLoader
 {
+  private static final String VCF_ENCODABLE = ":;=%,";
+
+  /*
+   * Jalview feature attributes for VCF fixed column data
+   */
+  private static final String VCF_POS = "POS";
+
+  private static final String VCF_ID = "ID";
+
+  private static final String VCF_QUAL = "QUAL";
+
+  private static final String VCF_FILTER = "FILTER";
+
+  private static final String NO_VALUE = VCFConstants.MISSING_VALUE_v4; // '.'
+
+  private static final String DEFAULT_SPECIES = "homo_sapiens";
+
   /**
    * A class to model the mapping from sequence to VCF coordinates. Cases include
    * <ul>
@@ -82,7 +104,7 @@ public class VCFLoader
 
   /*
    * Lookup keys, and default values, for Preference entries that describe
-   * patterns for VCF and VEP fields to capture 
+   * patterns for VCF and VEP fields to capture
    */
   private static final String VEP_FIELDS_PREF = "VEP_FIELDS";
 
@@ -93,6 +115,18 @@ public class VCFLoader
   private static final String DEFAULT_VEP_FIELDS = ".*";// "Allele,Consequence,IMPACT,SWISSPROT,SIFT,PolyPhen,CLIN_SIG";
 
   /*
+   * Lookup keys, and default values, for Preference entries that give
+   * mappings from tokens in the 'reference' header to species or assembly
+   */
+  private static final String VCF_ASSEMBLY = "VCF_ASSEMBLY";
+
+  private static final String DEFAULT_VCF_ASSEMBLY = "assembly19=GRCh37,hs37=GRCh37,grch37=GRCh37,grch38=GRCh38";
+
+  private static final String VCF_SPECIES = "VCF_SPECIES"; // default is human
+
+  private static final String DEFAULT_REFERENCE = "grch37"; // fallback default is human GRCh37
+
+  /*
    * keys to fields of VEP CSQ consequence data
    * see https://www.ensembl.org/info/docs/tools/vep/vep_formats.html
    */
@@ -114,12 +148,6 @@ public class VCFLoader
   private static final String PIPE_REGEX = "\\|";
 
   /*
-   * key for Allele Frequency output by VEP
-   * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html
-   */
-  private static final String ALLELE_FREQUENCY_KEY = "AF";
-
-  /*
    * delimiter that separates multiple consequence data blocks
    */
   private static final String COMMA = ",";
@@ -155,6 +183,16 @@ public class VCFLoader
   private VCFHeader header;
 
   /*
+   * species (as a valid Ensembl term) the VCF is for 
+   */
+  private String vcfSpecies;
+
+  /*
+   * genome assembly version (as a valid Ensembl identifier) the VCF is for 
+   */
+  private String vcfAssembly;
+
+  /*
    * a Dictionary of contigs (if present) referenced in the VCF file
    */
   private SAMSequenceDictionary dictionary;
@@ -191,6 +229,12 @@ public class VCFLoader
    */
   Map<Integer, String> vepFieldsOfInterest;
 
+  /*
+   * key:value for which rejected data has been seen
+   * (the error is logged only once for each combination)
+   */
+  private Set<String> badData;
+
   /**
    * Constructor given a VCF file
    * 
@@ -246,12 +290,18 @@ public class VCFLoader
    */
   public SequenceI loadVCFContig(String contig)
   {
-    String ref = header.getOtherHeaderLine(VCFHeader.REFERENCE_KEY)
-            .getValue();
+    VCFHeaderLine headerLine = header.getOtherHeaderLine(VCFHeader.REFERENCE_KEY);
+    if (headerLine == null)
+    {
+      Cache.log.error("VCF reference header not found");
+      return null;
+    }
+    String ref = headerLine.getValue();
     if (ref.startsWith("file://"))
     {
       ref = ref.substring(7);
     }
+    setSpeciesAndAssembly(ref);
 
     SequenceI seq = null;
     File dbFile = new File(ref);
@@ -260,12 +310,12 @@ public class VCFLoader
     {
       HtsContigDb db = new HtsContigDb("", dbFile);
       seq = db.getSequenceProxy(contig);
-      loadSequenceVCF(seq, ref);
+      loadSequenceVCF(seq);
       db.close();
     }
     else
     {
-      System.err.println("VCF reference not found: " + ref);
+      Cache.log.error("VCF reference not found: " + ref);
     }
 
     return seq;
@@ -284,7 +334,9 @@ public class VCFLoader
     {
       VCFHeaderLine ref = header
               .getOtherHeaderLine(VCFHeader.REFERENCE_KEY);
-      String vcfAssembly = ref.getValue();
+      String reference = ref == null ? null : ref.getValue();
+
+      setSpeciesAndAssembly(reference);
 
       int varCount = 0;
       int seqCount = 0;
@@ -294,7 +346,7 @@ public class VCFLoader
        */
       for (SequenceI seq : seqs)
       {
-        int added = loadSequenceVCF(seq, vcfAssembly);
+        int added = loadSequenceVCF(seq);
         if (added > 0)
         {
           seqCount++;
@@ -338,6 +390,71 @@ public class VCFLoader
   }
 
   /**
+   * Attempts to determine and save the species and genome assembly version to
+   * which the VCF data applies. This may be done by parsing the {@code reference}
+   * header line, configured in a property file, or (potentially) confirmed
+   * interactively by the user.
+   * <p>
+   * The saved values should be identifiers valid for Ensembl's REST service
+   * {@code map} endpoint, so they can be used (if necessary) to retrieve the
+   * mapping between VCF coordinates and sequence coordinates.
+   * 
+   * @param reference
+   * @see https://rest.ensembl.org/documentation/info/assembly_map
+   * @see https://rest.ensembl.org/info/assembly/human?content-type=text/xml
+   * @see https://rest.ensembl.org/info/species?content-type=text/xml
+   */
+  protected void setSpeciesAndAssembly(String reference)
+  {
+    if (reference == null)
+    {
+      Cache.log.error("No VCF ##reference found, defaulting to "
+              + DEFAULT_REFERENCE + ":" + DEFAULT_SPECIES);
+      reference = DEFAULT_REFERENCE; // default to GRCh37 if not specified
+    }
+    reference = reference.toLowerCase();
+
+    /*
+     * for a non-human species, or other assembly identifier,
+     * specify as a Jalview property file entry e.g.
+     * VCF_ASSEMBLY = hs37=GRCh37,assembly19=GRCh37
+     * VCF_SPECIES = c_elegans=celegans
+     * to map a token in the reference header to a value
+     */
+    String prop = Cache.getDefault(VCF_ASSEMBLY, DEFAULT_VCF_ASSEMBLY);
+    for (String token : prop.split(","))
+    {
+      String[] tokens = token.split("=");
+      if (tokens.length == 2)
+      {
+        if (reference.contains(tokens[0].trim().toLowerCase()))
+        {
+          vcfAssembly = tokens[1].trim();
+          break;
+        }
+      }
+    }
+
+    vcfSpecies = DEFAULT_SPECIES;
+    prop = Cache.getProperty(VCF_SPECIES);
+    if (prop != null)
+    {
+      for (String token : prop.split(","))
+      {
+        String[] tokens = token.split("=");
+        if (tokens.length == 2)
+        {
+          if (reference.contains(tokens[0].trim().toLowerCase()))
+          {
+            vcfSpecies = tokens[1].trim();
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  /**
    * Opens the VCF file and parses header data
    * 
    * @param filePath
@@ -563,7 +680,8 @@ public class VCFLoader
         /*
          * dna-to-peptide product mapping
          */
-        AlignmentUtils.computeProteinFeatures(seq, mapTo, map);
+        // JAL-3187 render on the fly instead
+        // AlignmentUtils.computeProteinFeatures(seq, mapTo, map);
       }
       else
       {
@@ -588,12 +706,11 @@ public class VCFLoader
    * and returns the number of variant features added
    * 
    * @param seq
-   * @param vcfAssembly
    * @return
    */
-  protected int loadSequenceVCF(SequenceI seq, String vcfAssembly)
+  protected int loadSequenceVCF(SequenceI seq)
   {
-    VCFMap vcfMap = getVcfMap(seq, vcfAssembly);
+    VCFMap vcfMap = getVcfMap(seq);
     if (vcfMap == null)
     {
       return 0;
@@ -614,10 +731,9 @@ public class VCFLoader
    * Answers a map from sequence coordinates to VCF chromosome ranges
    * 
    * @param seq
-   * @param vcfAssembly
    * @return
    */
-  private VCFMap getVcfMap(SequenceI seq, String vcfAssembly)
+  private VCFMap getVcfMap(SequenceI seq)
   {
     /*
      * simplest case: sequence has id and length matching a VCF contig
@@ -648,34 +764,28 @@ public class VCFLoader
     String species = seqCoords.getSpeciesId();
     String chromosome = seqCoords.getChromosomeId();
     String seqRef = seqCoords.getAssemblyId();
-    MapList map = seqCoords.getMap();
+    MapList map = seqCoords.getMapping();
 
-    if (!vcfSpeciesMatchesSequence(vcfAssembly, species))
+    // note this requires the configured species to match that
+    // returned with the Ensembl sequence; todo: support aliases?
+    if (!vcfSpecies.equalsIgnoreCase(species))
     {
+      Cache.log.warn("No VCF loaded to " + seq.getName()
+              + " as species not matched");
       return null;
     }
 
-    if (vcfAssemblyMatchesSequence(vcfAssembly, seqRef))
+    if (seqRef.equalsIgnoreCase(vcfAssembly))
     {
       return new VCFMap(chromosome, map);
     }
 
-    if (!"GRCh38".equalsIgnoreCase(seqRef) // Ensembl
-            || !vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD
-    {
-      return null;
-    }
-
     /*
-     * map chromosomal coordinates from sequence to VCF if the VCF
-     * data has a different reference assembly to the sequence
+     * VCF data has a different reference assembly to the sequence:
+     * query Ensembl to map chromosomal coordinates from sequence to VCF
      */
-    // TODO generalise for cases other than GRCh38 -> GRCh37 !
-    // - or get the user to choose in a dialog
-
     List<int[]> toVcfRanges = new ArrayList<>();
     List<int[]> fromSequenceRanges = new ArrayList<>();
-    String toRef = "GRCh37";
 
     for (int[] range : map.getToRanges())
     {
@@ -687,12 +797,13 @@ public class VCFLoader
       }
 
       int[] newRange = mapReferenceRange(range, chromosome, "human", seqRef,
-              toRef);
+              vcfAssembly);
       if (newRange == null)
       {
         Cache.log.error(
                 String.format("Failed to map %s:%s:%s:%d:%d to %s", species,
-                        chromosome, seqRef, range[0], range[1], toRef));
+                        chromosome, seqRef, range[0], range[1],
+                        vcfAssembly));
         continue;
       }
       else
@@ -733,62 +844,6 @@ public class VCFLoader
   }
 
   /**
-   * Answers true if we determine that the VCF data uses the same reference
-   * assembly as the sequence, else false
-   * 
-   * @param vcfAssembly
-   * @param seqRef
-   * @return
-   */
-  private boolean vcfAssemblyMatchesSequence(String vcfAssembly,
-          String seqRef)
-  {
-    // TODO improve on this stub, which handles gnomAD and
-    // hopes for the best for other cases
-
-    if ("GRCh38".equalsIgnoreCase(seqRef) // Ensembl
-            && vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD
-    {
-      return false;
-    }
-    return true;
-  }
-
-  /**
-   * Answers true if the species inferred from the VCF reference identifier
-   * matches that for the sequence
-   * 
-   * @param vcfAssembly
-   * @param speciesId
-   * @return
-   */
-  boolean vcfSpeciesMatchesSequence(String vcfAssembly, String speciesId)
-  {
-    // PROBLEM 1
-    // there are many aliases for species - how to equate one with another?
-    // PROBLEM 2
-    // VCF ##reference header is an unstructured URI - how to extract species?
-    // perhaps check if ref includes any (Ensembl) alias of speciesId??
-    // TODO ask the user to confirm this??
-
-    if (vcfAssembly.contains("Homo_sapiens") // gnomAD exome data example
-            && "HOMO_SAPIENS".equals(speciesId)) // Ensembl species id
-    {
-      return true;
-    }
-
-    if (vcfAssembly.contains("c_elegans") // VEP VCF response example
-            && "CAENORHABDITIS_ELEGANS".equals(speciesId)) // Ensembl
-    {
-      return true;
-    }
-
-    // this is not a sustainable solution...
-
-    return false;
-  }
-
-  /**
    * Queries the VCF reader for any variants that overlap the mapped chromosome
    * ranges of the sequence, and adds as variant features. Returns the number of
    * overlapping variants found.
@@ -811,24 +866,35 @@ public class VCFLoader
     {
       int vcfStart = Math.min(range[0], range[1]);
       int vcfEnd = Math.max(range[0], range[1]);
-      CloseableIterator<VariantContext> variants = reader
-              .query(map.chromosome, vcfStart, vcfEnd);
-      while (variants.hasNext())
+      try
       {
-        VariantContext variant = variants.next();
+        CloseableIterator<VariantContext> variants = reader
+                .query(map.chromosome, vcfStart, vcfEnd);
+        while (variants.hasNext())
+        {
+          VariantContext variant = variants.next();
 
-        int[] featureRange = map.map.locateInFrom(variant.getStart(),
-                variant.getEnd());
+          int[] featureRange = map.map.locateInFrom(variant.getStart(),
+                  variant.getEnd());
 
-        if (featureRange != null)
-        {
-          int featureStart = Math.min(featureRange[0], featureRange[1]);
-          int featureEnd = Math.max(featureRange[0], featureRange[1]);
-          count += addAlleleFeatures(seq, variant, featureStart, featureEnd,
-                  forwardStrand);
+          if (featureRange != null)
+          {
+            int featureStart = Math.min(featureRange[0], featureRange[1]);
+            int featureEnd = Math.max(featureRange[0], featureRange[1]);
+            count += addAlleleFeatures(seq, variant, featureStart,
+                    featureEnd, forwardStrand);
+          }
         }
+        variants.close();
+      } catch (TribbleException e)
+      {
+        /*
+         * RuntimeException throwable by htsjdk
+         */
+        String msg = String.format("Error reading VCF for %s:%d-%d: %s ",
+                map.chromosome, vcfStart, vcfEnd);
+        Cache.log.error(msg);
       }
-      variants.close();
     }
 
     return count;
@@ -958,7 +1024,20 @@ public class VCFLoader
             featureEnd, FEATURE_GROUP_VCF);
     sf.setSource(sourceId);
 
-    sf.setValue(Gff3Helper.ALLELES, alleles);
+    /*
+     * save the derived alleles as a named attribute; this will be
+     * needed when Jalview computes derived peptide variants
+     */
+    addFeatureAttribute(sf, Gff3Helper.ALLELES, alleles);
+
+    /*
+     * add selected VCF fixed column data as feature attributes
+     */
+    addFeatureAttribute(sf, VCF_POS, String.valueOf(variant.getStart()));
+    addFeatureAttribute(sf, VCF_ID, variant.getID());
+    addFeatureAttribute(sf, VCF_QUAL,
+            String.valueOf(variant.getPhredScaledQual()));
+    addFeatureAttribute(sf, VCF_FILTER, getFilter(variant));
 
     addAlleleProperties(variant, sf, altAlleleIndex, consequence);
 
@@ -968,6 +1047,53 @@ public class VCFLoader
   }
 
   /**
+   * Answers the VCF FILTER value for the variant - or an approximation to it.
+   * This field is either PASS, or a semi-colon separated list of filters not
+   * passed. htsjdk saves filters as a HashSet, so the order when reassembled into
+   * a list may be different.
+   * 
+   * @param variant
+   * @return
+   */
+  String getFilter(VariantContext variant)
+  {
+    Set<String> filters = variant.getFilters();
+    if (filters.isEmpty())
+    {
+      return NO_VALUE;
+    }
+    Iterator<String> iterator = filters.iterator();
+    String first = iterator.next();
+    if (filters.size() == 1)
+    {
+      return first;
+    }
+
+    StringBuilder sb = new StringBuilder(first);
+    while (iterator.hasNext())
+    {
+      sb.append(";").append(iterator.next());
+    }
+
+    return sb.toString();
+  }
+
+  /**
+   * Adds one feature attribute unless the value is null, empty or '.'
+   * 
+   * @param sf
+   * @param key
+   * @param value
+   */
+  void addFeatureAttribute(SequenceFeature sf, String key, String value)
+  {
+    if (value != null && !value.isEmpty() && !NO_VALUE.equals(value))
+    {
+      sf.setValue(key, value);
+    }
+  }
+
+  /**
    * Determines the Sequence Ontology term to use for the variant feature type in
    * Jalview. The default is 'sequence_variant', but a more specific term is used
    * if:
@@ -1161,14 +1287,6 @@ public class VCFLoader
       }
 
       /*
-       * filter out fields we don't want to capture
-       */
-      if (!vcfFieldsOfInterest.contains(key))
-      {
-        continue;
-      }
-
-      /*
        * we extract values for other data which are allele-specific; 
        * these may be per alternate allele (INFO[key].Number = 'A') 
        * or per allele including reference (INFO[key].Number = 'R') 
@@ -1205,10 +1323,81 @@ public class VCFLoader
        * take the index'th value
        */
       String value = getAttributeValue(variant, key, index);
-      if (value != null)
+      if (value != null && isValid(variant, key, value))
+      {
+        /*
+         * decode colon, semicolon, equals sign, percent sign, comma (only)
+         * as required by the VCF specification (para 1.2)
+         */
+        value = StringUtils.urlDecode(value, VCF_ENCODABLE);
+        addFeatureAttribute(sf, key, value);
+      }
+    }
+  }
+
+  /**
+   * Answers true for '.', null, or an empty value, or if the INFO type is String.
+   * If the INFO type is Integer or Float, answers false if the value is not in
+   * valid format.
+   * 
+   * @param variant
+   * @param infoId
+   * @param value
+   * @return
+   */
+  protected boolean isValid(VariantContext variant, String infoId,
+          String value)
+  {
+    if (value == null || value.isEmpty() || NO_VALUE.equals(value))
+    {
+      return true;
+    }
+    VCFInfoHeaderLine infoHeader = header.getInfoHeaderLine(infoId);
+    if (infoHeader == null)
+    {
+      Cache.log.error("Field " + infoId + " has no INFO header");
+      return false;
+    }
+    VCFHeaderLineType infoType = infoHeader.getType();
+    try
+    {
+      if (infoType == VCFHeaderLineType.Integer)
       {
-        sf.setValue(key, value);
+        Integer.parseInt(value);
       }
+      else if (infoType == VCFHeaderLineType.Float)
+      {
+        Float.parseFloat(value);
+      }
+    } catch (NumberFormatException e)
+    {
+      logInvalidValue(variant, infoId, value);
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Logs an error message for malformed data; duplicate messages (same id and
+   * value) are not logged
+   * 
+   * @param variant
+   * @param infoId
+   * @param value
+   */
+  private void logInvalidValue(VariantContext variant, String infoId,
+          String value)
+  {
+    if (badData == null)
+    {
+      badData = new HashSet<>();
+    }
+    String token = infoId + ":" + value;
+    if (!badData.contains(token))
+    {
+      badData.add(token);
+      Cache.log.error(String.format("Invalid VCF data at %s:%d %s=%s",
+              variant.getContig(), variant.getStart(), infoId, value));
     }
   }
 
@@ -1260,6 +1449,11 @@ public class VCFLoader
             String id = vepFieldsOfInterest.get(i);
             if (id != null)
             {
+              /*
+               * VCF spec requires encoding of special characters e.g. '='
+               * so decode them here before storing
+               */
+              field = StringUtils.urlDecode(field, VCF_ENCODABLE);
               csqValues.put(id, field);
             }
           }
index 133ab57..d46e9a6 100644 (file)
@@ -6011,7 +6011,7 @@ public class Jalview2XML
   private jalview.datamodel.Mapping addMapping(Mapping m)
   {
     SequenceI dsto = null;
-    // Mapping m = dr.getMapping();
+    // Mapping m = dr.getMap();
     int fr[] = new int[m.getMapListFrom().size() * 2];
     Iterator<MapListFrom> from = m.getMapListFrom().iterator();// enumerateMapListFrom();
     for (int _i = 0; from.hasNext(); _i += 2)
index 81ff739..f8c5bea 100644 (file)
@@ -28,7 +28,15 @@ public interface SequenceListener
   // TODO remove this? never called on SequenceListener type
   public void mouseOverSequence(SequenceI sequence, int index, int pos);
 
-  public void highlightSequence(SearchResultsI results);
+  /**
+   * Highlights any position(s) represented by the search results and
+   * (optionally) returns an informative message about the position(s)
+   * higlighted
+   * 
+   * @param results
+   * @return
+   */
+  public String highlightSequence(SearchResultsI results);
 
   // TODO remove this? never called
   public void updateColours(SequenceI sequence, int index);
index 82d66f2..d6eec47 100644 (file)
@@ -43,6 +43,7 @@ import jalview.io.DataSourceType;
 import jalview.io.StructureFile;
 import jalview.util.MappingUtils;
 import jalview.util.MessageManager;
+import jalview.util.Platform;
 import jalview.ws.sifts.SiftsClient;
 import jalview.ws.sifts.SiftsException;
 import jalview.ws.sifts.SiftsSettings;
@@ -890,13 +891,14 @@ public class StructureSelectionManager implements ApplicationSingletonI
    * @param pdbResNum
    * @param chain
    * @param pdbfile
+   * @return
    */
-  public void mouseOverStructure(int pdbResNum, String chain,
+  public String mouseOverStructure(int pdbResNum, String chain,
           String pdbfile)
   {
     AtomSpec atomSpec = new AtomSpec(pdbfile, chain, pdbResNum, 0);
     List<AtomSpec> atoms = Collections.singletonList(atomSpec);
-    mouseOverStructure(atoms);
+    return mouseOverStructure(atoms);
   }
 
   /**
@@ -904,12 +906,12 @@ public class StructureSelectionManager implements ApplicationSingletonI
    * 
    * @param atoms
    */
-  public void mouseOverStructure(List<AtomSpec> atoms)
+  public String mouseOverStructure(List<AtomSpec> atoms)
   {
     if (listeners == null)
     {
       // old or prematurely sent event
-      return;
+      return null;
     }
     boolean hasSequenceListener = false;
     for (int i = 0; i < listeners.size(); i++)
@@ -921,18 +923,24 @@ public class StructureSelectionManager implements ApplicationSingletonI
     }
     if (!hasSequenceListener)
     {
-      return;
+      return null;
     }
 
     SearchResultsI results = findAlignmentPositionsForStructurePositions(
             atoms);
+    String result = null;
     for (Object li : listeners)
     {
       if (li instanceof SequenceListener)
       {
-        ((SequenceListener) li).highlightSequence(results);
+        String s = ((SequenceListener) li).highlightSequence(results);
+        if (s != null)
+        {
+          result = s;
+        }
       }
     }
+    return result;
   }
 
   /**
@@ -1202,7 +1210,8 @@ public class StructureSelectionManager implements ApplicationSingletonI
     StringBuilder sb = new StringBuilder(64);
     for (StructureMapping sm : mappings)
     {
-      if (sm.pdbfile.equals(pdbfile) && seqs.contains(sm.sequence))
+      if (Platform.pathEquals(sm.pdbfile, pdbfile)
+              && seqs.contains(sm.sequence))
       {
         sb.append(sm.mappingDetails);
         sb.append(NEWLINE);
@@ -1264,22 +1273,43 @@ public class StructureSelectionManager implements ApplicationSingletonI
   }
 
   /**
-   * Reset this object to its initial state by removing all registered
-   * listeners, codon mappings, PDB file mappings.
-   * 
-   * Called only by Desktop and testng.
-   * 
+   * Resets this object to its initial state by removing all registered
+   * listeners, codon mappings, PDB file mappings
    */
   public void resetAll()
   {
-    mappings.clear();
-    seqmappings.clear();
-    sel_listeners.clear();
-    listeners.clear();
-    commandListeners.clear();
-    view_listeners.clear();
-    pdbFileNameId.clear();
-    pdbIdFileName.clear();
+    if (mappings != null)
+    {
+      mappings.clear();
+    }
+    if (seqmappings != null)
+    {
+      seqmappings.clear();
+    }
+    if (sel_listeners != null)
+    {
+      sel_listeners.clear();
+    }
+    if (listeners != null)
+    {
+      listeners.clear();
+    }
+    if (commandListeners != null)
+    {
+      commandListeners.clear();
+    }
+    if (view_listeners != null)
+    {
+      view_listeners.clear();
+    }
+    if (pdbFileNameId != null)
+    {
+      pdbFileNameId.clear();
+    }
+    if (pdbIdFileName != null)
+    {
+      pdbIdFileName.clear();
+    }
   }
 
   public void addSelectionListener(SelectionListener selecter)
index 9f28ee1..731e976 100644 (file)
@@ -1236,4 +1236,14 @@ public class MapList
     return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio);
   }
 
+  /**
+   * Answers true if the mapping is from one contiguous range to another, else
+   * false
+   * 
+   * @return
+   */
+  public boolean isContiguous()
+  {
+    return fromShifts.size() == 1 && toShifts.size() == 1;
+  }
 }
index 99c82a4..d32735b 100644 (file)
@@ -133,6 +133,7 @@ public class Platform
    /**
    *
    * @return true if we are running in non-interactive no UI mode
+   * based on System.getProperty("java.awt.headless")
    */
    public static boolean isHeadless()
    {
@@ -847,4 +848,22 @@ public class Platform
     return f.toString();
   }
 
+  private static float javaVersion;
+  
+  public static float getJavaVersion()
+  {
+    if (javaVersion == 0)
+    {
+      try
+      {
+        return javaVersion = Float.parseFloat(
+                System.getProperty("java.specification.version"));
+      } catch (Exception e)
+      {
+        javaVersion = 1.8f;
+      }
+    }
+    return javaVersion;
+  }
+
 }
index 2cbbfbf..a05dc95 100644 (file)
@@ -20,6 +20,8 @@
  */
 package jalview.util;
 
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.regex.Pattern;
@@ -29,8 +31,16 @@ public class StringUtils
   private static final Pattern DELIMITERS_PATTERN = Pattern
           .compile(".*='[^']*(?!')");
 
+  private static final char PERCENT = '%';
+
   private static final boolean DEBUG = false;
 
+  /*
+   * URL encoded characters, indexed by char value
+   * e.g. urlEncodings['='] = urlEncodings[61] = "%3D"
+   */
+  private static String[] urlEncodings = new String[255];
+
   /**
    * Returns a new character array, after inserting characters into the given
    * character array.
@@ -446,8 +456,66 @@ public class StringUtils
   }
 
   /**
-   * Answers true if the string is not empty and consists only of digits, or
-   * characters 'a'-'f' or 'A'-'F', else false
+   * Answers the input string with any occurrences of the 'encodeable' characters
+   * replaced by their URL encoding
+   * 
+   * @param s
+   * @param encodable
+   * @return
+   */
+  public static String urlEncode(String s, String encodable)
+  {
+    if (s == null || s.isEmpty())
+    {
+      return s;
+    }
+
+    /*
+     * do % encoding first, as otherwise it may double-encode!
+     */
+    if (encodable.indexOf(PERCENT) != -1)
+    {
+      s = urlEncode(s, PERCENT);
+    }
+
+    for (char c : encodable.toCharArray())
+    {
+      if (c != PERCENT)
+      {
+        s = urlEncode(s, c);
+      }
+    }
+    return s;
+  }
+
+  /**
+   * Answers the input string with any occurrences of {@code c} replaced with
+   * their url encoding. Answers the input string if it is unchanged.
+   * 
+   * @param s
+   * @param c
+   * @return
+   */
+  static String urlEncode(String s, char c)
+  {
+    String decoded = String.valueOf(c);
+    if (s.indexOf(decoded) != -1)
+    {
+      String encoded = getUrlEncoding(c);
+      if (!encoded.equals(decoded))
+      {
+        s = s.replace(decoded, encoded);
+      }
+    }
+    return s;
+  }
+
+  /**
+   * Answers the input string with any occurrences of the specified (unencoded)
+   * characters replaced by their URL decoding.
+   * <p>
+   * Example: {@code urlDecode("a%3Db%3Bc", "-;=,")} should answer
+   * {@code "a=b;c"}.
    * 
    * @param s
    * @return
@@ -470,4 +538,64 @@ public class StringUtils
     }
     return true;
   }
+  /**
+   * Answers the input string with any occurrences of the specified (unencoded)
+   * characters replaced by their URL decoding.
+   * <p>
+   * Example: {@code urlDecode("a%3Db%3Bc", "-;=,")} should answer
+   * {@code "a=b;c"}.
+   * 
+   * @param s
+   * @param encodable
+   * @return
+   */
+  public static String urlDecode(String s, String encodable)
+  {
+    if (s == null || s.isEmpty())
+    {
+      return s;
+    }
+
+    for (char c : encodable.toCharArray())
+    {
+      String encoded = getUrlEncoding(c);
+      if (s.indexOf(encoded) != -1)
+      {
+        String decoded = String.valueOf(c);
+        s = s.replace(encoded, decoded);
+      }
+    }
+    return s;
+  }
+
+  /**
+  * Does a lazy lookup of the url encoding of the given character, saving the
+  * value for repeat lookups
+  * 
+  * @param c
+  * @return
+  */
+ private static String getUrlEncoding(char c)
+ {
+   if (c < 0 || c >= urlEncodings.length)
+   {
+     return String.valueOf(c);
+   }
+
+   String enc = urlEncodings[c];
+   if (enc == null)
+   {
+     try
+     {
+       enc = urlEncodings[c] = URLEncoder.encode(String.valueOf(c),
+               "UTF-8");
+     } catch (UnsupportedEncodingException e)
+     {
+       enc = urlEncodings[c] = String.valueOf(c);
+     }
+   }
+   return enc;
+ }
+
+  
 }
index 21f9dae..b1f595f 100644 (file)
@@ -2747,6 +2747,30 @@ public abstract class AlignmentViewport
     viewStyle.setProteinFontAsCdna(b);
   }
 
+  @Override
+  public void setShowComplementFeatures(boolean b)
+  {
+    viewStyle.setShowComplementFeatures(b);
+  }
+
+  @Override
+  public boolean isShowComplementFeatures()
+  {
+    return viewStyle.isShowComplementFeatures();
+  }
+
+  @Override
+  public void setShowComplementFeaturesOnTop(boolean b)
+  {
+    viewStyle.setShowComplementFeaturesOnTop(b);
+  }
+
+  @Override
+  public boolean isShowComplementFeaturesOnTop()
+  {
+    return viewStyle.isShowComplementFeaturesOnTop();
+  }
+
   /**
    * @return true if view should scroll to show the highlighted region of a
    *         sequence
index 8219c6a..5ca3ac5 100644 (file)
  */
 package jalview.viewmodel.seqfeatures;
 
-import jalview.api.AlignViewportI;
-import jalview.api.FeatureColourI;
-import jalview.api.FeaturesDisplayedI;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.SequenceFeature;
-import jalview.datamodel.SequenceI;
-import jalview.datamodel.features.FeatureMatcherSetI;
-import jalview.datamodel.features.SequenceFeatures;
-import jalview.renderer.seqfeatures.FeatureRenderer;
-import jalview.schemes.FeatureColour;
-import jalview.util.ColorUtils;
-
 import java.awt.Color;
 import java.beans.PropertyChangeListener;
 import java.beans.PropertyChangeSupport;
@@ -47,6 +35,25 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
+import jalview.api.AlignViewportI;
+import jalview.api.FeatureColourI;
+import jalview.api.FeaturesDisplayedI;
+import jalview.datamodel.AlignedCodonFrame;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.MappedFeatures;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.SearchResultMatchI;
+import jalview.datamodel.SearchResults;
+import jalview.datamodel.SearchResultsI;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.datamodel.features.SequenceFeatures;
+import jalview.renderer.seqfeatures.FeatureRenderer;
+import jalview.schemes.FeatureColour;
+import jalview.util.ColorUtils;
+import jalview.util.Platform;
+
 public abstract class FeatureRendererModel
         implements jalview.api.FeatureRenderer
 {
@@ -324,6 +331,8 @@ public abstract class FeatureRendererModel
      * include features unless their feature group is not displayed, or
      * they are hidden (have no colour) based on a filter or colour threshold
      */
+    
+    // BH! check -- !featureGroupNotShown(sf) is from applet branch. 
     for (SequenceFeature sf : features)
     {
       if (!featureGroupNotShown(sf) && getColour(sf) != null)
@@ -653,7 +662,7 @@ public abstract class FeatureRendererModel
     {
       featureOrder = new Hashtable<>();
     }
-    featureOrder.put(type, new Float(position));
+    featureOrder.put(type, Float.valueOf(position));
     return position;
   }
 
@@ -853,7 +862,7 @@ public abstract class FeatureRendererModel
     }
     if (newGroupsVisible)
     {
-      featureGroups.put(group, new Boolean(true));
+      featureGroups.put(group, Boolean.valueOf(true));
       return true;
     }
     return false;
@@ -889,7 +898,7 @@ public abstract class FeatureRendererModel
   @Override
   public void setGroupVisibility(String group, boolean visible)
   {
-    featureGroups.put(group, new Boolean(visible));
+    featureGroups.put(group, Boolean.valueOf(visible));
   }
 
   @Override
@@ -901,7 +910,7 @@ public abstract class FeatureRendererModel
       for (String gst : toset)
       {
         Boolean st = featureGroups.get(gst);
-        featureGroups.put(gst, new Boolean(visible));
+        featureGroups.put(gst, Boolean.valueOf(visible));
         if (st != null)
         {
           rdrw = rdrw || (visible != st.booleanValue());
@@ -984,14 +993,14 @@ public abstract class FeatureRendererModel
    * @param sequenceFeature
    * @return
    */
-  protected boolean featureGroupNotShown(final SequenceFeature sequenceFeature)
+  public boolean featureGroupNotShown(final SequenceFeature sequenceFeature)
   {
-    Boolean b;
     return featureGroups != null
             && sequenceFeature.featureGroup != null
-            && sequenceFeature.featureGroup.length() > 0
-            && (b = featureGroups.get(sequenceFeature.featureGroup)) != null
-            && !b.booleanValue();
+            && sequenceFeature.featureGroup.length() != 0
+            && featureGroups.containsKey(sequenceFeature.featureGroup)
+            && !featureGroups.get(sequenceFeature.featureGroup)
+                    .booleanValue();
   }
 
   /**
@@ -999,7 +1008,7 @@ public abstract class FeatureRendererModel
    */
   @Override
   public List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence,
-          int resNo)
+          int fromResNo, int toResNo)
   {
     List<SequenceFeature> result = new ArrayList<>();
     if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
@@ -1012,12 +1021,11 @@ public abstract class FeatureRendererModel
      * displayed, and feature group is null or the empty string
      * or marked for display
      */
-    Set<String> visibleFeatures = getFeaturesDisplayed()
-            .getVisibleFeatures();
+    List<String> visibleFeatures = getDisplayedFeatureTypes();
     String[] visibleTypes = visibleFeatures
             .toArray(new String[visibleFeatures.size()]);
     List<SequenceFeature> features = sequence.getFeatures().findFeatures(
-            resNo, resNo, visibleTypes);
+            fromResNo, toResNo, visibleTypes);
   
     for (SequenceFeature sf : features)
     {
@@ -1040,6 +1048,16 @@ public abstract class FeatureRendererModel
    */
   public void filterFeaturesForDisplay(List<SequenceFeature> features)
   {
+// BH! check -- what was the problem here? How is JalviewJS's IntervalStore different from 
+    // other IntervalStore?
+    /*
+     * fudge: JalviewJS's IntervalStore lacks the sort method called :-(
+     */
+    if (Platform.isJS())
+    {
+      return;
+    }
+
     /*
      * don't remove 'redundant' features if 
      * - transparency is applied (feature count affects depth of feature colour)
@@ -1185,6 +1203,99 @@ public abstract class FeatureRendererModel
   }
 
   @Override
+  public MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence,
+          int pos)
+  {
+    SequenceI ds = sequence.getDatasetSequence();
+    if (ds == null)
+    {
+      ds = sequence;
+    }
+    final char residue = ds.getCharAt(pos - ds.getStart());
+
+    List<SequenceFeature> found = new ArrayList<>();
+    List<AlignedCodonFrame> mappings = this.av.getAlignment()
+            .getCodonFrame(sequence);
+
+    /*
+     * fudge: if no mapping found, check the complementary alignment
+     * todo: only store in one place? StructureSelectionManager?
+     */
+    if (mappings.isEmpty())
+    {
+      mappings = this.av.getCodingComplement().getAlignment()
+              .getCodonFrame(sequence);
+    }
+
+    /*
+     * todo: direct lookup of CDS for peptide and vice-versa; for now,
+     * have to search through an unordered list of mappings for a candidate
+     */
+    Mapping mapping = null;
+    SequenceI mapFrom = null;
+
+    for (AlignedCodonFrame acf : mappings)
+    {
+      mapping = acf.getMappingForSequence(sequence);
+      if (mapping == null || !mapping.getMap().isTripletMap())
+      {
+        continue; // we are only looking for 3:1 or 1:3 mappings
+      }
+      SearchResultsI sr = new SearchResults();
+      acf.markMappedRegion(ds, pos, sr);
+      for (SearchResultMatchI match : sr.getResults())
+      {
+        int fromRes = match.getStart();
+        int toRes = match.getEnd();
+        mapFrom = match.getSequence();
+        List<SequenceFeature> fs = findFeaturesAtResidue(
+                mapFrom, fromRes, toRes);
+        for (SequenceFeature sf : fs)
+        {
+          if (!found.contains(sf))
+          {
+            found.add(sf);
+          }
+        }
+      }
+
+      /*
+       * just take the first mapped features we find
+       */
+      if (!found.isEmpty())
+      {
+        break;
+      }
+    }
+    if (found.isEmpty())
+    {
+      return null;
+    }
+
+    /*
+     * sort by renderorder, inefficiently
+     */
+    List<SequenceFeature> result = new ArrayList<>();
+    for (String type : renderOrder)
+    {
+      for (SequenceFeature sf : found)
+      {
+        if (type.equals(sf.getType()))
+        {
+          result.add(sf);
+          if (result.size() == found.size())
+          {
+            return new MappedFeatures(mapping, mapFrom, pos, residue,
+                    result);
+          }
+        }
+      }
+    }
+    
+    return new MappedFeatures(mapping, mapFrom, pos, residue, result);
+  }
+
+  @Override
   public boolean isVisible(SequenceFeature feature)
   {
     if (feature == null)
index 16aa580..91f2f0c 100644 (file)
@@ -213,6 +213,8 @@ public class ViewStyle implements ViewStyleI
     setShowNPFeats(vs.isShowNPFeats());
     setShowSequenceFeaturesHeight(vs.isShowSequenceFeaturesHeight());
     setShowSequenceFeatures(vs.isShowSequenceFeatures());
+    setShowComplementFeatures(vs.isShowComplementFeatures());
+    setShowComplementFeaturesOnTop(vs.isShowComplementFeaturesOnTop());
     setShowText(vs.getShowText());
     setShowUnconserved(vs.getShowUnconserved());
     setTextColour(vs.getTextColour());
@@ -275,6 +277,9 @@ public class ViewStyle implements ViewStyleI
             && isShowSequenceFeaturesHeight() == vs
                     .isShowSequenceFeaturesHeight()
             && isShowSequenceFeatures() == vs.isShowSequenceFeatures()
+            && isShowComplementFeatures() == vs.isShowComplementFeatures()
+            && isShowComplementFeaturesOnTop() == vs
+                    .isShowComplementFeaturesOnTop()
             && getShowText() == vs.getShowText()
             && getShowUnconserved() == vs.getShowUnconserved()
             && getThreshold() == vs.getThreshold()
@@ -365,6 +370,10 @@ public class ViewStyle implements ViewStyleI
 
   private int fontStyle;
 
+  private boolean showComplementFeatures;
+
+  private boolean showComplementFeaturesOnTop;
+
   /**
    * GUI state
    * 
@@ -1111,4 +1120,28 @@ public class ViewStyle implements ViewStyleI
   {
     proteinFontAsCdna = b;
   }
+
+  @Override
+  public void setShowComplementFeatures(boolean b)
+  {
+    showComplementFeatures = b;
+  }
+
+  @Override
+  public boolean isShowComplementFeatures()
+  {
+    return showComplementFeatures;
+  }
+
+  @Override
+  public void setShowComplementFeaturesOnTop(boolean b)
+  {
+    showComplementFeaturesOnTop = b;
+  }
+
+  @Override
+  public boolean isShowComplementFeaturesOnTop()
+  {
+    return showComplementFeaturesOnTop;
+  }
 }
index 7745b74..736d8cb 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.ws;
 
 import jalview.analysis.AlignSeq;
+import jalview.api.FeatureSettingsModelI;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefEntry;
@@ -40,8 +41,10 @@ import jalview.ws.seqfetcher.DbSourceProxy;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Enumeration;
+import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Map;
 import java.util.StringTokenizer;
 import java.util.Vector;
 
@@ -318,6 +321,9 @@ public class DBRefFetcher implements Runnable
             Arrays.asList(dataset));
     List<String> warningMessages = new ArrayList<>();
 
+    // clear any old feature display settings recorded from past sessions
+    featureDisplaySettings = null;
+
     int db = 0;
     while (sdataset.size() > 0 && db < dbSources.length)
     {
@@ -384,7 +390,7 @@ public class DBRefFetcher implements Runnable
           }
           if (retrieved != null)
           {
-            transferReferences(sdataset, dbsource.getDbSource(), retrieved,
+            transferReferences(sdataset, dbsource, retrieved,
                     trimDsSeqs, warningMessages);
           }
         }
@@ -513,7 +519,8 @@ public class DBRefFetcher implements Runnable
    * @param warningMessages
    *          a list of messages to add to
    */
-  boolean transferReferences(Vector<SequenceI> sdataset, String dbSource,
+  boolean transferReferences(Vector<SequenceI> sdataset,
+          DbSourceProxy dbSourceProxy,
           AlignmentI retrievedAl, boolean trimDatasetSeqs,
           List<String> warningMessages)
   {
@@ -523,6 +530,7 @@ public class DBRefFetcher implements Runnable
       return false;
     }
 
+    String dbSource = dbSourceProxy.getDbName();
     boolean modified = false;
     SequenceI[] retrieved = recoverDbSequences(
             retrievedAl.getSequencesArray());
@@ -594,6 +602,10 @@ public class DBRefFetcher implements Runnable
        * seqs.elementAt(jj); if (!sequenceMatches.contains(sequence)) {
        * sequenceMatches.addElement(sequence); } } } }
        */
+      if (sequenceMatches.size() > 0)
+      {
+        addFeatureSettings(dbSourceProxy);
+      }
       // sequenceMatches now contains the set of all sequences associated with
       // the returned db record
       final String retrievedSeqString = retrievedSeq.getSequenceAsString();
@@ -762,6 +774,33 @@ public class DBRefFetcher implements Runnable
     return modified;
   }
 
+  Map<String, FeatureSettingsModelI> featureDisplaySettings = null;
+
+  private void addFeatureSettings(DbSourceProxy dbSourceProxy)
+  {
+    FeatureSettingsModelI fsettings = dbSourceProxy
+            .getFeatureColourScheme();
+    if (fsettings != null)
+    {
+      if (featureDisplaySettings == null)
+      {
+        featureDisplaySettings = new HashMap<>();
+      }
+      featureDisplaySettings.put(dbSourceProxy.getDbName(), fsettings);
+    }
+  }
+
+  /**
+   * 
+   * @return any feature settings associated with sources that have provided sequences
+   */
+  public List<FeatureSettingsModelI>getFeatureSettingsModels()
+  {
+    return featureDisplaySettings == null
+            ? Arrays.asList(new FeatureSettingsModelI[0])
+            : Arrays.asList(featureDisplaySettings.values()
+                    .toArray(new FeatureSettingsModelI[1]));
+  }
   /**
    * Adds the message to the list unless it already contains it
    * 
@@ -786,9 +825,7 @@ public class DBRefFetcher implements Runnable
   {
        int n;
        if (sequencesArray == null || (n = sequencesArray.length) == 0)
-  {
-    return sequencesArray;
-  }
+         return sequencesArray;
     ArrayList<SequenceI> nseq = new ArrayList<>();
     for (int i = 0;i < n; i++)
     {
index b03fc71..19366e0 100644 (file)
@@ -40,6 +40,7 @@ import jalview.ws.ebi.EBIFetchClient;
 import jalview.xml.binding.embl.EntryType;
 import jalview.xml.binding.embl.EntryType.Feature;
 import jalview.xml.binding.embl.EntryType.Feature.Qualifier;
+import jalview.xml.binding.jalview.JalviewModel;
 import jalview.xml.binding.embl.ROOT;
 import jalview.xml.binding.embl.XrefType;
 
@@ -53,7 +54,6 @@ import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.regex.Pattern;
 
 import javax.xml.bind.JAXBContext;
 import javax.xml.bind.JAXBElement;
@@ -70,8 +70,6 @@ public abstract class EmblXmlSource extends EbiFileRetrievedProxy
    */
   private static final String EMBL_NOT_FOUND_REPLY = "ERROR 12 No entries found.";
 
-  private static final Pattern SPACE_PATTERN = Pattern.compile(" ");
-
   public EmblXmlSource()
   {
     super();
@@ -652,8 +650,6 @@ public abstract class EmblXmlSource extends EbiFileRetrievedProxy
   
     try
     {
-      // BH 2019.10.06 note: exo-location of "X53828.1:60..1058" causes
-      // RemoteFormatTest to fail at this point.
       List<int[]> ranges = DnaUtils.parseLocation(location);
       return listToArray(ranges);
     } catch (ParseException e)
@@ -707,19 +703,10 @@ public abstract class EmblXmlSource extends EbiFileRetrievedProxy
     SequenceFeature sf = new SequenceFeature(type, desc, begin, end, group);
     if (!vals.isEmpty())
     {
-      StringBuilder sb = new StringBuilder();
-      boolean first = true;
       for (Entry<String, String> val : vals.entrySet())
       {
-        if (!first)
-        {
-          sb.append(";");
-        }
-        sb.append(val.getKey()).append("=").append(val.getValue());
-        first = false;
         sf.setValue(val.getKey(), val.getValue());
       }
-      sf.setAttributes(sb.toString());
     }
     return sf;
   }
index 78d190b..4c4f9c3 100644 (file)
Binary files a/swingjs/SwingJS-site.zip and b/swingjs/SwingJS-site.zip differ
index 303fea5..0c81ba1 100644 (file)
Binary files a/swingjs/net.sf.j2s.core-j11.jar and b/swingjs/net.sf.j2s.core-j11.jar differ
index 0d6b0a8..5a41b05 100644 (file)
@@ -1 +1 @@
-20200408084722 
+20200409124432 
index 78d190b..4c4f9c3 100644 (file)
Binary files a/swingjs/ver/3.2.9/SwingJS-site.zip and b/swingjs/ver/3.2.9/SwingJS-site.zip differ
index 303fea5..0c81ba1 100644 (file)
Binary files a/swingjs/ver/3.2.9/net.sf.j2s.core-j11.jar and b/swingjs/ver/3.2.9/net.sf.j2s.core-j11.jar differ
index 0d6b0a8..5a41b05 100644 (file)
@@ -1 +1 @@
-20200408084722 
+20200409124432 
index cc438be..35e4017 100644 (file)
@@ -975,7 +975,7 @@ public class AlignmentUtilsTests
     assertTrue(AlignmentUtils.haveCrossRef(seq2, seq1));
 
     // now the other way round
-       seq1.setDBRefs(null);
+        seq1.setDBRefs(null);
     seq2.addDBRef(new DBRefEntry("EMBL", "1", "A12345"));
     assertTrue(AlignmentUtils.haveCrossRef(seq1, seq2));
     assertTrue(AlignmentUtils.haveCrossRef(seq2, seq1));
@@ -1871,408 +1871,6 @@ public class AlignmentUtilsTests
   }
 
   /**
-   * Test the method that computes a map of codon variants for each protein
-   * position from "sequence_variant" features on dna
-   */
-  @Test(groups = "Functional")
-  public void testBuildDnaVariantsMap()
-  {
-    SequenceI dna = new Sequence("dna", "atgAAATTTGGGCCCtag");
-    MapList map = new MapList(new int[] { 1, 18 }, new int[] { 1, 5 }, 3, 1);
-
-    /*
-     * first with no variants on dna
-     */
-    LinkedHashMap<Integer, List<DnaVariant>[]> variantsMap = AlignmentUtils
-            .buildDnaVariantsMap(dna, map);
-    assertTrue(variantsMap.isEmpty());
-
-    /*
-     * single allele codon 1, on base 1
-     */
-    SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 1, 1,
-            0f, null);
-    sf1.setValue("alleles", "T");
-    sf1.setValue("ID", "sequence_variant:rs758803211");
-    dna.addSequenceFeature(sf1);
-
-    /*
-     * two alleles codon 2, on bases 2 and 3 (distinct variants)
-     */
-    SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 5, 5,
-            0f, null);
-    sf2.setValue("alleles", "T");
-    sf2.setValue("ID", "sequence_variant:rs758803212");
-    dna.addSequenceFeature(sf2);
-    SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 6, 6,
-            0f, null);
-    sf3.setValue("alleles", "G");
-    sf3.setValue("ID", "sequence_variant:rs758803213");
-    dna.addSequenceFeature(sf3);
-
-    /*
-     * two alleles codon 3, both on base 2 (one variant)
-     */
-    SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 8, 8,
-            0f, null);
-    sf4.setValue("alleles", "C, G");
-    sf4.setValue("ID", "sequence_variant:rs758803214");
-    dna.addSequenceFeature(sf4);
-
-    // no alleles on codon 4
-
-    /*
-     * alleles on codon 5 on all 3 bases (distinct variants)
-     */
-    SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 13,
-            13, 0f, null);
-    sf5.setValue("alleles", "C, G"); // (C duplicates given base value)
-    sf5.setValue("ID", "sequence_variant:rs758803215");
-    dna.addSequenceFeature(sf5);
-    SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 14,
-            14, 0f, null);
-    sf6.setValue("alleles", "g, a"); // should force to upper-case
-    sf6.setValue("ID", "sequence_variant:rs758803216");
-    dna.addSequenceFeature(sf6);
-
-    SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 15,
-            15, 0f, null);
-    sf7.setValue("alleles", "A, T");
-    sf7.setValue("ID", "sequence_variant:rs758803217");
-    dna.addSequenceFeature(sf7);
-
-    /*
-     * build map - expect variants on positions 1, 2, 3, 5
-     */
-    variantsMap = AlignmentUtils.buildDnaVariantsMap(dna, map);
-    assertEquals(4, variantsMap.size());
-
-    /*
-     * protein residue 1: variant on codon (ATG) base 1, not on 2 or 3
-     */
-    List<DnaVariant>[] pep1Variants = variantsMap.get(1);
-    assertEquals(3, pep1Variants.length);
-    assertEquals(1, pep1Variants[0].size());
-    assertEquals("A", pep1Variants[0].get(0).base); // codon[1] base
-    assertSame(sf1, pep1Variants[0].get(0).variant); // codon[1] variant
-    assertEquals(1, pep1Variants[1].size());
-    assertEquals("T", pep1Variants[1].get(0).base); // codon[2] base
-    assertNull(pep1Variants[1].get(0).variant); // no variant here
-    assertEquals(1, pep1Variants[2].size());
-    assertEquals("G", pep1Variants[2].get(0).base); // codon[3] base
-    assertNull(pep1Variants[2].get(0).variant); // no variant here
-
-    /*
-     * protein residue 2: variants on codon (AAA) bases 2 and 3
-     */
-    List<DnaVariant>[] pep2Variants = variantsMap.get(2);
-    assertEquals(3, pep2Variants.length);
-    assertEquals(1, pep2Variants[0].size());
-    // codon[1] base recorded while processing variant on codon[2]
-    assertEquals("A", pep2Variants[0].get(0).base);
-    assertNull(pep2Variants[0].get(0).variant); // no variant here
-    // codon[2] base and variant:
-    assertEquals(1, pep2Variants[1].size());
-    assertEquals("A", pep2Variants[1].get(0).base);
-    assertSame(sf2, pep2Variants[1].get(0).variant);
-    // codon[3] base was recorded when processing codon[2] variant
-    // and then the variant for codon[3] added to it
-    assertEquals(1, pep2Variants[2].size());
-    assertEquals("A", pep2Variants[2].get(0).base);
-    assertSame(sf3, pep2Variants[2].get(0).variant);
-
-    /*
-     * protein residue 3: variants on codon (TTT) base 2 only
-     */
-    List<DnaVariant>[] pep3Variants = variantsMap.get(3);
-    assertEquals(3, pep3Variants.length);
-    assertEquals(1, pep3Variants[0].size());
-    assertEquals("T", pep3Variants[0].get(0).base); // codon[1] base
-    assertNull(pep3Variants[0].get(0).variant); // no variant here
-    assertEquals(1, pep3Variants[1].size());
-    assertEquals("T", pep3Variants[1].get(0).base); // codon[2] base
-    assertSame(sf4, pep3Variants[1].get(0).variant); // codon[2] variant
-    assertEquals(1, pep3Variants[2].size());
-    assertEquals("T", pep3Variants[2].get(0).base); // codon[3] base
-    assertNull(pep3Variants[2].get(0).variant); // no variant here
-
-    /*
-     * three variants on protein position 5
-     */
-    List<DnaVariant>[] pep5Variants = variantsMap.get(5);
-    assertEquals(3, pep5Variants.length);
-    assertEquals(1, pep5Variants[0].size());
-    assertEquals("C", pep5Variants[0].get(0).base); // codon[1] base
-    assertSame(sf5, pep5Variants[0].get(0).variant); // codon[1] variant
-    assertEquals(1, pep5Variants[1].size());
-    assertEquals("C", pep5Variants[1].get(0).base); // codon[2] base
-    assertSame(sf6, pep5Variants[1].get(0).variant); // codon[2] variant
-    assertEquals(1, pep5Variants[2].size());
-    assertEquals("C", pep5Variants[2].get(0).base); // codon[3] base
-    assertSame(sf7, pep5Variants[2].get(0).variant); // codon[3] variant
-  }
-
-  /**
-   * Tests for the method that computes all peptide variants given codon
-   * variants
-   */
-  @Test(groups = "Functional")
-  public void testComputePeptideVariants()
-  {
-    /*
-     * scenario: AAATTTCCC codes for KFP
-     * variants:
-     *           GAA -> E             source: Ensembl
-     *           CAA -> Q             source: dbSNP
-     *           TAA -> STOP          source: dnSNP
-     *           AAG synonymous       source: COSMIC
-     *           AAT -> N             source: Ensembl
-     *           ...TTC synonymous    source: dbSNP
-     *           ......CAC,CGC -> H,R source: COSMIC
-     *                 (one variant with two alleles)
-     */
-    SequenceI peptide = new Sequence("pep/10-12", "KFP");
-
-    /*
-     * two distinct variants for codon 1 position 1
-     * second one has clinical significance
-     */
-    String ensembl = "Ensembl";
-    String dbSnp = "dbSNP";
-    String cosmic = "COSMIC";
-
-    /*
-     * NB setting "id" (as returned by Ensembl for features in JSON format);
-     * previously "ID" (as returned for GFF3 format)
-     */
-    SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 1, 1,
-            0f, ensembl);
-    sf1.setValue("alleles", "A,G"); // AAA -> GAA -> K/E
-    sf1.setValue("id", "var1.125A>G");
-
-    SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 1, 1,
-            0f, dbSnp);
-    sf2.setValue("alleles", "A,C"); // AAA -> CAA -> K/Q
-    sf2.setValue("id", "var2");
-    sf2.setValue("clinical_significance", "Dodgy");
-
-    SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 1, 1,
-            0f, dbSnp);
-    sf3.setValue("alleles", "A,T"); // AAA -> TAA -> stop codon
-    sf3.setValue("id", "var3");
-    sf3.setValue("clinical_significance", "Bad");
-
-    SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 3, 3,
-            0f, cosmic);
-    sf4.setValue("alleles", "A,G"); // AAA -> AAG synonymous
-    sf4.setValue("id", "var4");
-    sf4.setValue("clinical_significance", "None");
-
-    SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 3, 3,
-            0f, ensembl);
-    sf5.setValue("alleles", "A,T"); // AAA -> AAT -> K/N
-    sf5.setValue("id", "sequence_variant:var5"); // prefix gets stripped off
-    sf5.setValue("clinical_significance", "Benign");
-
-    SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 6, 6,
-            0f, dbSnp);
-    sf6.setValue("alleles", "T,C"); // TTT -> TTC synonymous
-    sf6.setValue("id", "var6");
-
-    SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 8, 8,
-            0f, cosmic);
-    sf7.setValue("alleles", "C,A,G"); // CCC -> CAC,CGC -> P/H/R
-    sf7.setValue("id", "var7");
-    sf7.setValue("clinical_significance", "Good");
-
-    List<DnaVariant> codon1Variants = new ArrayList<>();
-    List<DnaVariant> codon2Variants = new ArrayList<>();
-    List<DnaVariant> codon3Variants = new ArrayList<>();
-
-    List<DnaVariant> codonVariants[] = new ArrayList[3];
-    codonVariants[0] = codon1Variants;
-    codonVariants[1] = codon2Variants;
-    codonVariants[2] = codon3Variants;
-
-    /*
-     * compute variants for protein position 1
-     */
-    codon1Variants.add(new DnaVariant("A", sf1));
-    codon1Variants.add(new DnaVariant("A", sf2));
-    codon1Variants.add(new DnaVariant("A", sf3));
-    codon2Variants.add(new DnaVariant("A"));
-    // codon2Variants.add(new DnaVariant("A"));
-    codon3Variants.add(new DnaVariant("A", sf4));
-    codon3Variants.add(new DnaVariant("A", sf5));
-    AlignmentUtils.computePeptideVariants(peptide, 1, codonVariants);
-
-    /*
-     * compute variants for protein position 2
-     */
-    codon1Variants.clear();
-    codon2Variants.clear();
-    codon3Variants.clear();
-    codon1Variants.add(new DnaVariant("T"));
-    codon2Variants.add(new DnaVariant("T"));
-    codon3Variants.add(new DnaVariant("T", sf6));
-    AlignmentUtils.computePeptideVariants(peptide, 2, codonVariants);
-
-    /*
-     * compute variants for protein position 3
-     */
-    codon1Variants.clear();
-    codon2Variants.clear();
-    codon3Variants.clear();
-    codon1Variants.add(new DnaVariant("C"));
-    codon2Variants.add(new DnaVariant("C", sf7));
-    codon3Variants.add(new DnaVariant("C"));
-    AlignmentUtils.computePeptideVariants(peptide, 3, codonVariants);
-
-    /*
-     * verify added sequence features for
-     * var1 K -> E Ensembl
-     * var2 K -> Q dbSNP
-     * var3 K -> stop
-     * var4 synonymous
-     * var5 K -> N Ensembl
-     * var6 synonymous
-     * var7 P -> H COSMIC
-     * var8 P -> R COSMIC
-     */
-    List<SequenceFeature> sfs = peptide.getSequenceFeatures();
-    SequenceFeatures.sortFeatures(sfs, true);
-    assertEquals(8, sfs.size());
-
-    /*
-     * features are sorted by start position ascending, but in no
-     * particular order where start positions match; asserts here
-     * simply match the data returned (the order is not important)
-     */
-    // AAA -> AAT -> K/N
-    SequenceFeature sf = sfs.get(0);
-    assertEquals(1, sf.getBegin());
-    assertEquals(1, sf.getEnd());
-    assertEquals("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Lys1Asn", sf.getDescription());
-    assertEquals("var5", sf.getValue("id"));
-    assertEquals("Benign", sf.getValue("clinical_significance"));
-    assertEquals("id=var5;clinical_significance=Benign",
-            sf.getAttributes());
-    assertEquals(1, sf.links.size());
-    assertEquals(
-            "p.Lys1Asn var5|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var5",
-            sf.links.get(0));
-    assertEquals(ensembl, sf.getFeatureGroup());
-
-    // AAA -> CAA -> K/Q
-    sf = sfs.get(1);
-    assertEquals(1, sf.getBegin());
-    assertEquals(1, sf.getEnd());
-    assertEquals("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Lys1Gln", sf.getDescription());
-    assertEquals("var2", sf.getValue("id"));
-    assertEquals("Dodgy", sf.getValue("clinical_significance"));
-    assertEquals("id=var2;clinical_significance=Dodgy", sf.getAttributes());
-    assertEquals(1, sf.links.size());
-    assertEquals(
-            "p.Lys1Gln var2|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var2",
-            sf.links.get(0));
-    assertEquals(dbSnp, sf.getFeatureGroup());
-
-    // AAA -> GAA -> K/E
-    sf = sfs.get(2);
-    assertEquals(1, sf.getBegin());
-    assertEquals(1, sf.getEnd());
-    assertEquals("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Lys1Glu", sf.getDescription());
-    assertEquals("var1.125A>G", sf.getValue("id"));
-    assertNull(sf.getValue("clinical_significance"));
-    assertEquals("id=var1.125A>G", sf.getAttributes());
-    assertEquals(1, sf.links.size());
-    // link to variation is urlencoded
-    assertEquals(
-            "p.Lys1Glu var1.125A>G|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var1.125A%3EG",
-            sf.links.get(0));
-    assertEquals(ensembl, sf.getFeatureGroup());
-
-    // AAA -> TAA -> stop codon
-    sf = sfs.get(3);
-    assertEquals(1, sf.getBegin());
-    assertEquals(1, sf.getEnd());
-    assertEquals("stop_gained", sf.getType());
-    assertEquals("Aaa/Taa", sf.getDescription());
-    assertEquals("var3", sf.getValue("id"));
-    assertEquals("Bad", sf.getValue("clinical_significance"));
-    assertEquals("id=var3;clinical_significance=Bad", sf.getAttributes());
-    assertEquals(1, sf.links.size());
-    assertEquals(
-            "Aaa/Taa var3|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var3",
-            sf.links.get(0));
-    assertEquals(dbSnp, sf.getFeatureGroup());
-
-    // AAA -> AAG synonymous
-    sf = sfs.get(4);
-    assertEquals(1, sf.getBegin());
-    assertEquals(1, sf.getEnd());
-    assertEquals("synonymous_variant", sf.getType());
-    assertEquals("aaA/aaG", sf.getDescription());
-    assertEquals("var4", sf.getValue("id"));
-    assertEquals("None", sf.getValue("clinical_significance"));
-    assertEquals("id=var4;clinical_significance=None", sf.getAttributes());
-    assertEquals(1, sf.links.size());
-    assertEquals(
-            "aaA/aaG var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
-            sf.links.get(0));
-    assertEquals(cosmic, sf.getFeatureGroup());
-
-    // TTT -> TTC synonymous
-    sf = sfs.get(5);
-    assertEquals(2, sf.getBegin());
-    assertEquals(2, sf.getEnd());
-    assertEquals("synonymous_variant", sf.getType());
-    assertEquals("ttT/ttC", sf.getDescription());
-    assertEquals("var6", sf.getValue("id"));
-    assertNull(sf.getValue("clinical_significance"));
-    assertEquals("id=var6", sf.getAttributes());
-    assertEquals(1, sf.links.size());
-    assertEquals(
-            "ttT/ttC var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
-            sf.links.get(0));
-    assertEquals(dbSnp, sf.getFeatureGroup());
-
-    // var7 generates two distinct protein variant features (two alleles)
-    // CCC -> CGC -> P/R
-    sf = sfs.get(6);
-    assertEquals(3, sf.getBegin());
-    assertEquals(3, sf.getEnd());
-    assertEquals("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Pro3Arg", sf.getDescription());
-    assertEquals("var7", sf.getValue("id"));
-    assertEquals("Good", sf.getValue("clinical_significance"));
-    assertEquals("id=var7;clinical_significance=Good", sf.getAttributes());
-    assertEquals(1, sf.links.size());
-    assertEquals(
-            "p.Pro3Arg var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
-            sf.links.get(0));
-    assertEquals(cosmic, sf.getFeatureGroup());
-
-    // CCC -> CAC -> P/H
-    sf = sfs.get(7);
-    assertEquals(3, sf.getBegin());
-    assertEquals(3, sf.getEnd());
-    assertEquals("nonsynonymous_variant", sf.getType());
-    assertEquals("p.Pro3His", sf.getDescription());
-    assertEquals("var7", sf.getValue("id"));
-    assertEquals("Good", sf.getValue("clinical_significance"));
-    assertEquals("id=var7;clinical_significance=Good", sf.getAttributes());
-    assertEquals(1, sf.links.size());
-    assertEquals(
-            "p.Pro3His var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
-            sf.links.get(0));
-    assertEquals(cosmic, sf.getFeatureGroup());
-  }
-
-  /**
    * Tests for the method that maps the subset of a dna sequence that has CDS
    * (or subtype) feature, with CDS strand = '-' (reverse)
    */
@@ -2592,9 +2190,16 @@ public class AlignmentUtilsTests
     AlignmentI al2 = new Alignment(new SequenceI[] { dna3, dna4 });
     ((Alignment) al2).createDatasetAlignment();
 
+    /*
+     * alignment removes gapped columns (two internal, two trailing)
+     */
     assertTrue(AlignmentUtils.alignAsSameSequences(al1, al2));
-    assertEquals(seq1, al1.getSequenceAt(0).getSequenceAsString());
-    assertEquals(seq2, al1.getSequenceAt(1).getSequenceAsString());
+    String aligned1 = "-cc-GG-GTTT-aaa";
+    assertEquals(aligned1,
+            al1.getSequenceAt(0).getSequenceAsString());
+    String aligned2 = "C--C-Cgg-gtttAAA";
+    assertEquals(aligned2,
+            al1.getSequenceAt(1).getSequenceAsString());
 
     /*
      * add another sequence to 'aligned' - should still succeed, since
@@ -2604,8 +2209,8 @@ public class AlignmentUtilsTests
     dna5.createDatasetSequence();
     al2.addSequence(dna5);
     assertTrue(AlignmentUtils.alignAsSameSequences(al1, al2));
-    assertEquals(seq1, al1.getSequenceAt(0).getSequenceAsString());
-    assertEquals(seq2, al1.getSequenceAt(1).getSequenceAsString());
+    assertEquals(aligned1, al1.getSequenceAt(0).getSequenceAsString());
+    assertEquals(aligned2, al1.getSequenceAt(1).getSequenceAsString());
 
     /*
      * add another sequence to 'unaligned' - should fail, since now not
@@ -2623,15 +2228,15 @@ public class AlignmentUtilsTests
   {
     SequenceI dna1 = new Sequence("dna1", "cccGGGTTTaaa");
     SequenceI dna2 = new Sequence("dna2", "CCCgggtttAAA");
-    SequenceI as1 = dna1.deriveSequence();
-    SequenceI as2 = dna1.deriveSequence().getSubSequence(3, 7);
-    SequenceI as3 = dna2.deriveSequence();
+    SequenceI as1 = dna1.deriveSequence(); // cccGGGTTTaaa/1-12
+    SequenceI as2 = dna1.deriveSequence().getSubSequence(3, 7); // GGGT/4-7
+    SequenceI as3 = dna2.deriveSequence(); // CCCgggtttAAA/1-12
     as1.insertCharAt(6, 5, '-');
-    String s_as1 = as1.getSequenceAsString();
+    assertEquals("cccGGG-----TTTaaa", as1.getSequenceAsString());
     as2.insertCharAt(6, 5, '-');
-    String s_as2 = as2.getSequenceAsString();
-    as3.insertCharAt(6, 5, '-');
-    String s_as3 = as3.getSequenceAsString();
+    assertEquals("GGGT-----", as2.getSequenceAsString());
+    as3.insertCharAt(3, 5, '-');
+    assertEquals("CCC-----gggtttAAA", as3.getSequenceAsString());
     AlignmentI aligned = new Alignment(new SequenceI[] { as1, as2, as3 });
 
     // why do we need to cast this still ?
@@ -2643,10 +2248,13 @@ public class AlignmentUtilsTests
         uas3 });
     ((Alignment) tobealigned).createDatasetAlignment();
 
+    /*
+     * alignAs lines up dataset sequences and removes empty columns (two)
+     */
     assertTrue(AlignmentUtils.alignAsSameSequences(tobealigned, aligned));
-    assertEquals(s_as1, uas1.getSequenceAsString());
-    assertEquals(s_as2, uas2.getSequenceAsString());
-    assertEquals(s_as3, uas3.getSequenceAsString());
+    assertEquals("cccGGG---TTTaaa", uas1.getSequenceAsString());
+    assertEquals("GGGT", uas2.getSequenceAsString());
+    assertEquals("CCC---gggtttAAA", uas3.getSequenceAsString());
   }
 
   @Test(groups = { "Functional" })
@@ -2685,7 +2293,7 @@ public class AlignmentUtilsTests
      * transcript 'CDS' is 10-16, 17-21
      * which is 'gene' 158-164, 210-214
      */
-    MapList toMap = toLoci.getMap();
+    MapList toMap = toLoci.getMapping();
     assertEquals(1, toMap.getFromRanges().size());
     assertEquals(2, toMap.getFromRanges().get(0).length);
     assertEquals(1, toMap.getFromRanges().get(0)[0]);
@@ -2708,7 +2316,7 @@ public class AlignmentUtilsTests
     AlignmentUtils.transferGeneLoci(from, map, to);
     assertEquals("GRCh38", toLoci.getAssemblyId());
     assertEquals("7", toLoci.getChromosomeId());
-    toMap = toLoci.getMap();
+    toMap = toLoci.getMapping();
     assertEquals("[ [1, 12] ] 1:1 to [ [158, 164] [210, 214] ]",
             toMap.toString());
   }
index c955979..cd8f9eb 100644 (file)
@@ -48,7 +48,7 @@ public class SequenceFeatureTest
             12.5f, "group");
     sf1.setValue("STRAND", "+");
     sf1.setValue("Note", "Testing");
-    Integer count = new Integer(7);
+    Integer count = Integer.valueOf(7);
     sf1.setValue("Count", count);
 
     SequenceFeature sf2 = new SequenceFeature(sf1);
@@ -106,7 +106,7 @@ public class SequenceFeatureTest
     assertEquals("+", sf1.getValue("STRAND"));
     assertNull(sf1.getValue("strand")); // case-sensitive
     assertEquals(".", sf1.getValue("unknown", "."));
-    Integer i = new Integer(27);
+    Integer i = Integer.valueOf(27);
     assertSame(i, sf1.getValue("Unknown", i));
   }
 
@@ -277,43 +277,46 @@ public class SequenceFeatureTest
   @Test(groups = { "Functional" })
   public void testGetDetailsReport()
   {
+    SequenceI seq = new Sequence("TestSeq", "PLRFQMD");
+    String seqName = seq.getName();
+
     // single locus, no group, no score
     SequenceFeature sf = new SequenceFeature("variant", "G,C", 22, 22, null);
-    String expected = "<br><table><tr><td>Type</td><td>variant</td><td></td></tr>"
-            + "<tr><td>Start/end</td><td>22</td><td></td></tr>"
+    String expected = "<br><table><tr><td>Location</td><td>TestSeq</td><td>22</td></tr>"
+            + "<tr><td>Type</td><td>variant</td><td></td></tr>"
             + "<tr><td>Description</td><td>G,C</td><td></td></tr></table>";
-    assertEquals(expected, sf.getDetailsReport());
+    assertEquals(expected, sf.getDetailsReport(seqName));
 
     // contact feature
     sf = new SequenceFeature("Disulphide Bond", "a description", 28, 31,
             null);
-    expected = "<br><table><tr><td>Type</td><td>Disulphide Bond</td><td></td></tr>"
-            + "<tr><td>Start/end</td><td>28:31</td><td></td></tr>"
+    expected = "<br><table><tr><td>Location</td><td>TestSeq</td><td>28:31</td></tr>"
+            + "<tr><td>Type</td><td>Disulphide Bond</td><td></td></tr>"
             + "<tr><td>Description</td><td>a description</td><td></td></tr></table>";
-    assertEquals(expected, sf.getDetailsReport());
+    assertEquals(expected, sf.getDetailsReport(seqName));
 
     sf = new SequenceFeature("variant", "G,C", 22, 33,
             12.5f, "group");
     sf.setValue("Parent", "ENSG001");
     sf.setValue("Child", "ENSP002");
-    expected = "<br><table><tr><td>Type</td><td>variant</td><td></td></tr>"
-            + "<tr><td>Start/end</td><td>22-33</td><td></td></tr>"
+    expected = "<br><table><tr><td>Location</td><td>TestSeq</td><td>22-33</td></tr>"
+            + "<tr><td>Type</td><td>variant</td><td></td></tr>"
             + "<tr><td>Description</td><td>G,C</td><td></td></tr>"
             + "<tr><td>Score</td><td>12.5</td><td></td></tr>"
             + "<tr><td>Group</td><td>group</td><td></td></tr>"
             + "<tr><td>Child</td><td></td><td>ENSP002</td></tr>"
             + "<tr><td>Parent</td><td></td><td>ENSG001</td></tr></table>";
-    assertEquals(expected, sf.getDetailsReport());
+    assertEquals(expected, sf.getDetailsReport(seqName));
 
     /*
      * feature with embedded html link in description
      */
     String desc = "<html>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></html>";
     sf = new SequenceFeature("Pfam", desc, 8, 83, "Uniprot");
-    expected = "<br><table><tr><td>Type</td><td>Pfam</td><td></td></tr>"
-            + "<tr><td>Start/end</td><td>8-83</td><td></td></tr>"
+    expected = "<br><table><tr><td>Location</td><td>TestSeq</td><td>8-83</td></tr>"
+            + "<tr><td>Type</td><td>Pfam</td><td></td></tr>"
             + "<tr><td>Description</td><td>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></td><td></td></tr>"
             + "<tr><td>Group</td><td>Uniprot</td><td></td></tr></table>";
-    assertEquals(expected, sf.getDetailsReport());
+    assertEquals(expected, sf.getDetailsReport(seqName));
   }
 }
index 27008f5..a4d6d92 100644 (file)
 package jalview.ext.ensembl;
 
 import static org.testng.AssertJUnit.assertEquals;
-import static org.testng.AssertJUnit.assertSame;
 
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.JvOptionPane;
 import jalview.io.DataSourceType;
 import jalview.io.FastaFile;
@@ -34,8 +32,6 @@ import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyLite;
 
 import java.lang.reflect.Method;
-import java.util.Arrays;
-import java.util.List;
 
 import org.testng.Assert;
 import org.testng.annotations.AfterClass;
@@ -223,7 +219,6 @@ public class EnsemblSeqProxyTest
     SequenceFeature sf = new SequenceFeature("sequence_variant", alleles,
             1, 2, 0f, null);
     sf.setValue("alleles", alleles);
-    sf.setAttributes("x=y,z;alleles=" + alleles + ";a=b,c");
 
     EnsemblSeqProxy.reverseComplementAlleles(sf);
     String revcomp = "G,C,GTA-,HGMD_MUTATION,gtc";
@@ -231,7 +226,5 @@ public class EnsemblSeqProxyTest
     assertEquals(revcomp, sf.getDescription());
     // verify alleles attribute is updated with reverse complement
     assertEquals(revcomp, sf.getValue("alleles"));
-    // verify attributes string is updated with reverse complement
-    assertEquals("x=y,z;alleles=" + revcomp + ";a=b,c", sf.getAttributes());
   }
 }
index d43f257..7ef0d16 100644 (file)
@@ -165,7 +165,7 @@ public class JmolViewerTest
     final String _inFile = "examples/3W5V.pdb";
     inFile = _inFile;
     FileLoader fl = new FileLoader();
-    fl.loadFile(af.getCurrentView(), _inFile, DataSourceType.FILE,
+    fl.LoadFile(af.getCurrentView(), _inFile, DataSourceType.FILE,
             FileFormat.PDB);
     try
     {
index bf4889a..24be581 100644 (file)
@@ -59,16 +59,16 @@ public class ChimeraCommandsTest
   public void testBuildColourCommands()
   {
 
-    Map<Object, AtomSpecModel> map = new LinkedHashMap<>();
-    ChimeraCommands.addColourRange(map, Color.blue, 0, 2, 5, "A");
-    ChimeraCommands.addColourRange(map, Color.blue, 0, 7, 7, "B");
-    ChimeraCommands.addColourRange(map, Color.blue, 0, 9, 23, "A");
-    ChimeraCommands.addColourRange(map, Color.blue, 1, 1, 1, "A");
-    ChimeraCommands.addColourRange(map, Color.blue, 1, 4, 7, "B");
-    ChimeraCommands.addColourRange(map, Color.yellow, 1, 8, 8, "A");
-    ChimeraCommands.addColourRange(map, Color.yellow, 1, 3, 5, "A");
-    ChimeraCommands.addColourRange(map, Color.red, 0, 3, 5, "A");
-    ChimeraCommands.addColourRange(map, Color.red, 0, 6, 9, "A");
+    Map<Object, AtomSpecModel> map = new LinkedHashMap<Object, AtomSpecModel>();
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 2, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 7, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 9, 23, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 1, 1, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 4, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 8, 8, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 6, 9, "A");
 
     // Colours should appear in the Chimera command in the order in which
     // they were added; within colour, by model, by chain, ranges in start order
@@ -84,14 +84,14 @@ public class ChimeraCommandsTest
     /*
      * make a map of { featureType, {featureValue, {residue range specification } } }
      */
-    Map<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<>();
-    Map<Object, AtomSpecModel> featureValues = new HashMap<>();
+    Map<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<String, Map<Object, AtomSpecModel>>();
+    Map<Object, AtomSpecModel> featureValues = new HashMap<Object, AtomSpecModel>();
     
     /*
      * start with just one feature/value...
      */
     featuresMap.put("chain", featureValues);
-    ChimeraCommands.addColourRange(featureValues, "X", 0, 8, 20, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 8, 20, "A");
   
     List<String> commands = ChimeraCommands
             .buildSetAttributeCommands(featuresMap);
@@ -104,24 +104,24 @@ public class ChimeraCommandsTest
     assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:8-20.A");
 
     // add same feature value, overlapping range
-    ChimeraCommands.addColourRange(featureValues, "X", 0, 3, 9, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 3, 9, "A");
     // same feature value, contiguous range
-    ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "A");
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertEquals(1, commands.size());
     assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:3-25.A");
 
     // same feature value and model, different chain
-    ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "B");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "B");
     // same feature value and chain, different model
-    ChimeraCommands.addColourRange(featureValues, "X", 1, 26, 30, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", 1, 26, 30, "A");
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertEquals(1, commands.size());
     assertEquals(commands.get(0),
             "setattr r jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A");
 
     // same feature, different value
-    ChimeraCommands.addColourRange(featureValues, "Y", 0, 40, 50, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "Y", 0, 40, 50, "A");
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertEquals(2, commands.size());
     // commands are ordered by feature type but not by value
@@ -133,7 +133,7 @@ public class ChimeraCommandsTest
     featuresMap.clear();
     featureValues.clear();
     featuresMap.put("side-chain binding!", featureValues);
-    ChimeraCommands.addColourRange(featureValues,
+    ChimeraCommands.addAtomSpecRange(featureValues,
             "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", 0, 7, 15,
             "A");
     // feature names are sanitised to change non-alphanumeric to underscore
index 04ddeb0..36b4edf 100644 (file)
@@ -43,6 +43,7 @@ import jalview.gui.AlignFrame;
 import jalview.gui.Desktop;
 import jalview.gui.JvOptionPane;
 import jalview.schemes.FeatureColour;
+import jalview.structure.StructureSelectionManager;
 import jalview.util.matcher.Condition;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
@@ -61,7 +62,6 @@ import org.testng.annotations.Test;
 
 public class FeaturesFileTest
 {
-  private static final String LINE_SEPARATOR = System.getProperty("line.separator");
   private static String simpleGffFile = "examples/testdata/simpleGff3.gff";
 
   @AfterClass(alwaysRun = true)
@@ -70,7 +70,9 @@ public class FeaturesFileTest
     /*
      * remove any sequence mappings created so they don't pollute other tests
      */
-    Desktop.getStructureSelectionManager().resetAll();
+    StructureSelectionManager ssm = StructureSelectionManager
+            .getStructureSelectionManager(Desktop.getInstance());
+    ssm.resetAll();
   }
 
   @BeforeClass(alwaysRun = true)
@@ -266,10 +268,12 @@ public class FeaturesFileTest
     AlignFrame af = new AlignFrame(al, 500, 500);
     Map<String, FeatureColourI> colours = af.getFeatureRenderer()
             .getFeatureColours();
-    // GFF3 uses '=' separator for name/value pairs in colum 9
+    // GFF3 uses '=' separator for name/value pairs in column 9
+    // comma (%2C) equals (%3D) or semi-colon (%3B) should be url-escaped in values
     String gffData = "##gff-version 3\n"
             + "FER_CAPAA\tuniprot\tMETAL\t39\t39\t0.0\t.\t.\t"
-            + "Note=Iron-sulfur (2Fe-2S);Note=another note;evidence=ECO:0000255|PROSITE-ProRule:PRU00465\n"
+            + "Note=Iron-sulfur (2Fe-2S);Note=another note,and another;evidence=ECO%3B0000255%2CPROSITE%3DProRule:PRU00465;"
+            + "CSQ=AF=21,POLYPHEN=benign,possibly_damaging,clin_sig=Benign%3Dgood\n"
             + "FER1_SOLLC\tuniprot\tPfam\t55\t130\t3.0\t.\t.\tID=$23";
     FeaturesFile featuresFile = new FeaturesFile(gffData,
             DataSourceType.PASTE);
@@ -282,14 +286,25 @@ public class FeaturesFileTest
     assertEquals(1, sfs.size());
     SequenceFeature sf = sfs.get(0);
     // description parsed from Note attribute
-    assertEquals("Iron-sulfur (2Fe-2S),another note", sf.description);
+    assertEquals("Iron-sulfur (2Fe-2S),another note,and another",
+            sf.description);
     assertEquals(39, sf.begin);
     assertEquals(39, sf.end);
     assertEquals("uniprot", sf.featureGroup);
     assertEquals("METAL", sf.type);
-    assertEquals(
-            "Note=Iron-sulfur (2Fe-2S);Note=another note;evidence=ECO:0000255|PROSITE-ProRule:PRU00465",
-            sf.getValue("ATTRIBUTES"));
+    assertEquals(5, sf.otherDetails.size());
+    assertEquals("ECO;0000255,PROSITE=ProRule:PRU00465", // url decoded
+            sf.getValue("evidence"));
+    assertEquals("Iron-sulfur (2Fe-2S),another note,and another",
+            sf.getValue("Note"));
+    assertEquals("21", sf.getValueAsString("CSQ", "AF"));
+    assertEquals("benign,possibly_damaging",
+            sf.getValueAsString("CSQ", "POLYPHEN"));
+    assertEquals("Benign=good", sf.getValueAsString("CSQ", "clin_sig")); // url decoded
+    // todo change STRAND and !Phase into fields of SequenceFeature instead
+    assertEquals(".", sf.otherDetails.get("STRAND"));
+    assertEquals(0, sf.getStrand());
+    assertEquals(".", sf.getPhase());
 
     // verify feature on FER1_SOLLC1
     sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures();
@@ -474,7 +489,7 @@ public class FeaturesFileTest
      */
     FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
     String exported = featuresFile
-            .printJalviewFormat(al.getSequencesArray(), fr, false);
+            .printJalviewFormat(al.getSequencesArray(), fr, false, false);
     String expected = "No Features Visible";
     assertEquals(expected, exported);
 
@@ -483,15 +498,13 @@ public class FeaturesFileTest
      */
     fr.setGroupVisibility("uniprot", true);
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            true);
+            true, false);
     expected = "\nSTARTGROUP\tuniprot\n"
             + "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\t0.0\n"
             + "ENDGROUP\tuniprot\n\n"
             + "desc1\tFER_CAPAN\t-1\t0\t0\tPfam\t1.3\n\n"
             + "desc3\tFER1_SOLLC\t-1\t0\t0\tPfam\n"; // NaN is not output
-    assertEquals(
-            expected.replace("\n", LINE_SEPARATOR),
-            exported);
+    assertEquals(expected, exported);
 
     /*
      * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam
@@ -499,21 +512,21 @@ public class FeaturesFileTest
     fr.setVisible("METAL");
     fr.setVisible("GAMMA-TURN");
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     expected = "METAL\tcc9900\n"
             + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
-            + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
+            + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
             + "ENDGROUP\tuniprot\n";
-    assertEquals(fixLineEnd(expected), exported);
+    assertEquals(expected, exported);
 
     /*
      * now set Pfam visible
      */
     fr.setVisible("Pfam");
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     /*
      * features are output within group, ordered by sequence and type
      */
@@ -521,14 +534,14 @@ public class FeaturesFileTest
             + "Pfam\tff0000\n"
             + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
-            + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
             + "<html>Pfam domain<a href=\"http://pfam.xfam.org/family/PF00111\">Pfam_3_4</a></html>\tFER_CAPAA\t-1\t20\t20\tPfam\t0.0\n"
+            + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
             + "ENDGROUP\tuniprot\n"
             // null / empty group features are output after named groups
             + "\ndesc2\tFER_CAPAN\t-1\t4\t9\tPfam\n"
             + "\ndesc4\tFER1_SOLLC\t-1\t5\t8\tPfam\t-2.6\n";
-    assertEquals(fixLineEnd(expected), exported);
+    assertEquals(expected, exported);
 
     /*
      * hide uniprot group
@@ -539,14 +552,14 @@ public class FeaturesFileTest
             + "\ndesc2\tFER_CAPAN\t-1\t4\t9\tPfam\n"
             + "\ndesc4\tFER1_SOLLC\t-1\t5\t8\tPfam\t-2.6\n";
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            false);
-    assertEquals(fixLineEnd(expected), exported);
+            false, false);
+    assertEquals(expected, exported);
 
     /*
      * include non-positional (overrides group not shown)
      */
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            true);
+            true, false);
     expected = "METAL\tcc9900\n" + "Pfam\tff0000\n"
             + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
@@ -556,7 +569,7 @@ public class FeaturesFileTest
             + "desc2\tFER_CAPAN\t-1\t4\t9\tPfam\n"
             + "\ndesc3\tFER1_SOLLC\t-1\t0\t0\tPfam\n"
             + "desc4\tFER1_SOLLC\t-1\t5\t8\tPfam\t-2.6\n";
-    assertEquals(fixLineEnd(expected), exported);
+    assertEquals(expected, exported);
   }
 
   @Test(groups = { "Functional" })
@@ -573,16 +586,12 @@ public class FeaturesFileTest
     FeatureRendererModel fr = (FeatureRendererModel) af.alignPanel
             .getFeatureRenderer();
     String exported = featuresFile.printGffFormat(al.getSequencesArray(),
-            fr, false);
+            fr, false, false);
     String gffHeader = "##gff-version 2\n";
-    assertEquals(
-            fixLineEnd(gffHeader),
-            exported);
+    assertEquals(gffHeader, exported);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            true);
-    assertEquals(
-            fixLineEnd(gffHeader),
-            exported);
+            true, false);
+    assertEquals(gffHeader, exported);
 
     /*
      * add some features
@@ -597,9 +606,14 @@ public class FeaturesFileTest
                             "s3dm"));
     SequenceFeature sf = new SequenceFeature("Pfam", "", 20, 20, 0f,
             "Uniprot");
-    sf.setAttributes("x=y;black=white");
     sf.setStrand("+");
     sf.setPhase("2");
+    sf.setValue("x", "y");
+    sf.setValue("black", "white");
+    Map<String, String> csq = new HashMap<>();
+    csq.put("SIFT", "benign,mostly benign,cloudy, with meatballs");
+    csq.put("consequence", "missense_variant");
+    sf.setValue("CSQ", csq);
     al.getSequenceAt(1).addSequenceFeature(sf);
 
     /*
@@ -618,10 +632,8 @@ public class FeaturesFileTest
      * with no features displayed, exclude non-positional features
      */
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
-    assertEquals(
-            fixLineEnd(gffHeader),
-            exported);
+            false, false);
+    assertEquals(gffHeader, exported);
 
     /*
      * include non-positional features
@@ -629,12 +641,10 @@ public class FeaturesFileTest
     fr.setGroupVisibility("Uniprot", true);
     fr.setGroupVisibility("s3dm", false);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            true);
+            true, false);
     String expected = gffHeader
             + "FER_CAPAA\tUniprot\tDomain\t0\t0\t0.0\t.\t.\n";
-    assertEquals(
-            fixLineEnd(expected),
-            exported);
+    assertEquals(expected, exported);
 
     /*
      * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam
@@ -643,43 +653,38 @@ public class FeaturesFileTest
     fr.setVisible("METAL");
     fr.setVisible("GAMMA-TURN");
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     // METAL feature has null group: description used for column 2
     expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n";
-    assertEquals(
-            expected.replace("\n", LINE_SEPARATOR),
-            exported);
+    assertEquals(expected, exported);
 
     /*
      * set s3dm group visible
      */
     fr.setGroupVisibility("s3dm", true);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     // METAL feature has null group: description used for column 2
     expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
             + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n";
-    assertEquals(
-            expected.replace("\n", LINE_SEPARATOR),
-            exported);
+    assertEquals(expected, exported);
 
     /*
      * now set Pfam visible
      */
     fr.setVisible("Pfam");
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     // Pfam feature columns include strand(+), phase(2), attributes
     expected = gffHeader
             + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
-            + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n"
-            + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white\n";
-    assertEquals(fixLineEnd(expected), exported);
-  }
-
-  private String fixLineEnd(String s)
-  {
-    return s.replace("\n", LINE_SEPARATOR);
+            // CSQ output as CSQ=att1=value1,att2=value2
+            // note all commas are encoded here which is wrong - it should be
+            // SIFT=benign,mostly benign,cloudy%2C with meatballs
+            + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white;"
+            + "CSQ=SIFT=benign%2Cmostly benign%2Ccloudy%2C with meatballs,consequence=missense_variant\n"
+            + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n";
+    assertEquals(expected, exported);
   }
 
   /**
@@ -754,7 +759,7 @@ public class FeaturesFileTest
     visible.put("foobar", new FeatureColour(Color.blue));
     ff.outputFeatureFilters(sb, visible, featureFilters);
     String expected = "\nSTARTFILTERS\nfoobar\tLabel Present\npfam\t(CSQ:PolyPhen Present) AND (Score LE -2.4)\nENDFILTERS\n";
-    assertEquals(fixLineEnd(expected), sb.toString());
+    assertEquals(expected, sb.toString());
   }
 
   /**
@@ -787,12 +792,11 @@ public class FeaturesFileTest
     fr.setVisible("METAL");
     fr.setColour("METAL", new FeatureColour(Color.PINK));
     String exported = featuresFile.printGffFormat(al.getSequencesArray(),
-            fr, false);
+            fr, false, false);
     String expected = gffHeader
-            + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
-            + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n";
-    assertEquals(
-            fixLineEnd(expected), exported);
+            + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\tclin_sig=Likely Pathogenic;AF=24\n"
+            + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\tclin_sig=Benign;AF=46\n";
+    assertEquals(expected, exported);
 
     /*
      * now threshold to Score > 1.1 - should exclude sf2
@@ -803,19 +807,21 @@ public class FeaturesFileTest
     fc.setThreshold(1.1f);
     fr.setColour("METAL", fc);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
-    expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n";
-    assertEquals(fixLineEnd(expected), exported);
+            false, false);
+    expected = gffHeader
+            + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\tclin_sig=Likely Pathogenic;AF=24\n";
+    assertEquals(expected, exported);
 
     /*
      * remove threshold and check sf2 is exported
      */
     fc.setAboveThreshold(false);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
-    expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
-            + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n";
-    assertEquals(fixLineEnd(expected), exported);
+            false, false);
+    expected = gffHeader
+            + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\tclin_sig=Likely Pathogenic;AF=24\n"
+            + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\tclin_sig=Benign;AF=46\n";
+    assertEquals(expected, exported);
 
     /*
      * filter on (clin_sig contains Benign) - should include sf2 and exclude sf1
@@ -825,9 +831,10 @@ public class FeaturesFileTest
             "clin_sig"));
     fr.setFeatureFilter("METAL", filter);
     exported = featuresFile.printGffFormat(al.getSequencesArray(), fr,
-            false);
-    expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n";
-    assertEquals(fixLineEnd(expected), exported);
+            false, false);
+    expected = gffHeader
+            + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\tclin_sig=Benign;AF=46\n";
+    assertEquals(expected, exported);
   }
 
   /**
@@ -861,15 +868,13 @@ public class FeaturesFileTest
     fr.setColour("METAL", new FeatureColour(Color.PINK));
     String exported = featuresFile.printJalviewFormat(
             al.getSequencesArray(),
-            fr, false);
+            fr, false, false);
     String expected = "METAL\tffafaf\n\nSTARTGROUP\tgrp1\n"
             + "Cath\tFER_CAPAA\t-1\t39\t39\tMETAL\t1.2\n"
             + "ENDGROUP\tgrp1\n\nSTARTGROUP\tgrp2\n"
             + "Cath\tFER_CAPAA\t-1\t41\t41\tMETAL\t0.6\n"
             + "ENDGROUP\tgrp2\n";
-    assertEquals(
-            fixLineEnd(expected),
-            exported);
+    assertEquals(expected, exported);
   
     /*
      * now threshold to Score > 1.1 - should exclude sf2
@@ -881,28 +886,26 @@ public class FeaturesFileTest
     fc.setThreshold(1.1f);
     fr.setColour("METAL", fc);
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     expected = "METAL\tscore|ffffff|000000|noValueMin|abso|0.0|2.0|above|1.1\n\n"
             + "STARTGROUP\tgrp1\n"
             + "Cath\tFER_CAPAA\t-1\t39\t39\tMETAL\t1.2\n"
             + "ENDGROUP\tgrp1\n";
-    assertEquals(
-            expected.replace("\n", LINE_SEPARATOR),
-            exported);
+    assertEquals(expected, exported);
   
     /*
      * remove threshold and check sf2 is exported
      */
     fc.setAboveThreshold(false);
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     expected = "METAL\tscore|ffffff|000000|noValueMin|abso|0.0|2.0|none\n\n"
             + "STARTGROUP\tgrp1\n"
             + "Cath\tFER_CAPAA\t-1\t39\t39\tMETAL\t1.2\n"
             + "ENDGROUP\tgrp1\n\nSTARTGROUP\tgrp2\n"
             + "Cath\tFER_CAPAA\t-1\t41\t41\tMETAL\t0.6\n"
             + "ENDGROUP\tgrp2\n";
-    assertEquals(fixLineEnd(expected), exported);
+    assertEquals(expected, exported);
   
     /*
      * filter on (clin_sig contains Benign) - should include sf2 and exclude sf1
@@ -912,13 +915,13 @@ public class FeaturesFileTest
             "clin_sig"));
     fr.setFeatureFilter("METAL", filter);
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr,
-            false);
+            false, false);
     expected = "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n";
     expected = "METAL\tscore|ffffff|000000|noValueMin|abso|0.0|2.0|none\n\n"
             + "STARTFILTERS\nMETAL\tclin_sig Contains benign\nENDFILTERS\n\n"
             + "STARTGROUP\tgrp2\n"
             + "Cath\tFER_CAPAA\t-1\t41\t41\tMETAL\t0.6\n"
             + "ENDGROUP\tgrp2\n";
-    assertEquals(fixLineEnd(expected), exported);
+    assertEquals(expected, exported);
   }
 }
index 983207a..968901f 100644 (file)
@@ -11,7 +11,7 @@ public class FileLoaderTest
   {
     String urlFile = "http://www.jalview.org/builds/develop/examples/3W5V.pdb";
     FileLoader fileLoader = new FileLoader();
-    fileLoader.loadFileWaitTillLoaded(urlFile, DataSourceType.URL,
+    fileLoader.LoadFileWaitTillLoaded(urlFile, DataSourceType.URL,
             FileFormat.PDB);
     Assert.assertNotNull(fileLoader.file);
     // The FileLoader's file is expected to be same as the original URL.
index cf3c7e5..111fadb 100644 (file)
@@ -35,6 +35,8 @@ import jalview.schemes.FeatureColour;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 import org.testng.annotations.BeforeClass;
@@ -62,22 +64,60 @@ public class SequenceAnnotationReportTest
             3, 1.2f, "group");
 
     // residuePos == 2 does not match start or end of feature, nothing done:
-    sar.appendFeature(sb, 2, null, sf);
+    sar.appendFeature(sb, 2, null, sf, null);
     assertEquals("123456", sb.toString());
 
-    // residuePos == 1 matches start of feature, text appended (but no <br>)
+    // residuePos == 1 matches start of feature, text appended (but no <br/>)
     // feature score is not included
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     assertEquals("123456disulfide bond 1:3", sb.toString());
 
     // residuePos == 3 matches end of feature, text appended
-    // <br> is prefixed once sb.length() > 6
-    sar.appendFeature(sb, 3, null, sf);
-    assertEquals("123456disulfide bond 1:3<br>disulfide bond 1:3",
+    // <br/> is prefixed once sb.length() > 6
+    sar.appendFeature(sb, 3, null, sf, null);
+    assertEquals("123456disulfide bond 1:3<br/>disulfide bond 1:3",
             sb.toString());
   }
 
   @Test(groups = "Functional")
+  public void testAppendFeatures_longText()
+  {
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    StringBuilder sb = new StringBuilder();
+    String longString = // java11!"Abcd".repeat(50);
+            "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"
+                    + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"
+                    + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"
+                    + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"
+                    + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd";
+
+    SequenceFeature sf = new SequenceFeature("sequence", longString, 1, 3,
+            "group");
+
+    sar.appendFeature(sb, 1, null, sf, null);
+    assertTrue(sb.length() < 100);
+
+    List<SequenceFeature> sfl = new ArrayList<>();
+    sb.setLength(0);
+    sfl.add(sf);
+    sfl.add(sf);
+    sfl.add(sf);
+    sfl.add(sf);
+    sfl.add(sf);
+    sfl.add(sf);
+    sfl.add(sf);
+    sfl.add(sf);
+    sfl.add(sf);
+    sfl.add(sf);
+    int n = sar.appendFeaturesLengthLimit(sb, 1, sfl,
+            new FeatureRenderer(null), 200); // text should terminate before 200 characters
+    String s = sb.toString();
+    assertTrue(s.length() < 200);
+    assertEquals(n, 7); // should be 7 features left over
+
+  }
+
+  @Test(groups = "Functional")
   public void testAppendFeature_status()
   {
     SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
@@ -86,7 +126,7 @@ public class SequenceAnnotationReportTest
             Float.NaN, "group");
     sf.setStatus("Confirmed");
 
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     assertEquals("METAL 1 3; Fe2-S; (Confirmed)", sb.toString());
   }
 
@@ -100,7 +140,7 @@ public class SequenceAnnotationReportTest
 
     FeatureRendererModel fr = new FeatureRenderer(null);
     Map<String, float[][]> minmax = fr.getMinMax();
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
     /*
      * map has no entry for this feature type - score is not shown:
      */
@@ -110,9 +150,9 @@ public class SequenceAnnotationReportTest
      * map has entry for this feature type - score is shown:
      */
     minmax.put("METAL", new float[][] { { 0f, 1f }, null });
-    sar.appendFeature(sb, 1, fr, sf);
-    // <br> is appended to a buffer > 6 in length
-    assertEquals("METAL 1 3; Fe2-S<br>METAL 1 3; Fe2-S Score=1.3",
+    sar.appendFeature(sb, 1, fr, sf, null);
+    // <br/> is appended to a buffer > 6 in length
+    assertEquals("METAL 1 3; Fe2-S<br/>METAL 1 3; Fe2-S Score=1.3",
             sb.toString());
 
     /*
@@ -120,7 +160,7 @@ public class SequenceAnnotationReportTest
      */
     minmax.put("METAL", new float[][] { { 2f, 2f }, null });
     sb.setLength(0);
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
   }
 
@@ -132,7 +172,7 @@ public class SequenceAnnotationReportTest
     SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3,
             Float.NaN, "group");
 
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
   }
 
@@ -152,7 +192,7 @@ public class SequenceAnnotationReportTest
      * first with no colour by attribute
      */
     FeatureRendererModel fr = new FeatureRenderer(null);
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
 
     /*
@@ -163,7 +203,7 @@ public class SequenceAnnotationReportTest
     fc.setAttributeName("Pfam");
     fr.setColour("METAL", fc);
     sb.setLength(0);
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
     assertEquals("METAL 1 3; Fe2-S", sb.toString()); // no change
 
     /*
@@ -171,7 +211,7 @@ public class SequenceAnnotationReportTest
      */
     fc.setAttributeName("clinical_significance");
     sb.setLength(0);
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
     assertEquals("METAL 1 3; Fe2-S; clinical_significance=Benign",
             sb.toString());
   }
@@ -193,7 +233,7 @@ public class SequenceAnnotationReportTest
     fc.setAttributeName("clinical_significance");
     fr.setColour("METAL", fc);
     minmax.put("METAL", new float[][] { { 0f, 1f }, null });
-    sar.appendFeature(sb, 1, fr, sf);
+    sar.appendFeature(sb, 1, fr, sf, null);
 
     assertEquals(
             "METAL 1 3; Fe2-S Score=1.3; (Confirmed); clinical_significance=Benign",
@@ -209,13 +249,13 @@ public class SequenceAnnotationReportTest
             Float.NaN, "group");
 
     // description is not included if it duplicates type:
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     assertEquals("METAL 1 3", sb.toString());
 
     sb.setLength(0);
     sf.setDescription("Metal");
     // test is case-sensitive:
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     assertEquals("METAL 1 3; Metal", sb.toString());
   }
 
@@ -228,13 +268,13 @@ public class SequenceAnnotationReportTest
             "<html><body>hello<em>world</em></body></html>", 1, 3,
             Float.NaN, "group");
 
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     // !! strips off </body> but not <body> ??
     assertEquals("METAL 1 3; <body>hello<em>world</em>", sb.toString());
 
     sb.setLength(0);
     sf.setDescription("<br>&kHD>6");
-    sar.appendFeature(sb, 1, null, sf);
+    sar.appendFeature(sb, 1, null, sf, null);
     // if no <html> tag, html-encodes > and < (only):
     assertEquals("METAL 1 3; &lt;br&gt;&kHD&gt;6", sb.toString());
   }
@@ -248,14 +288,13 @@ public class SequenceAnnotationReportTest
     SequenceI seq = new Sequence("s1", "MAKLKRFQSSTLL");
     seq.setDescription("SeqDesc");
 
-    sar.createSequenceAnnotationReport(sb, seq, true, true, null);
-
     /*
      * positional features are ignored
      */
     seq.addSequenceFeature(new SequenceFeature("Domain", "Ferredoxin", 5,
             10, 1f, null));
-    assertEquals("<i><br>SeqDesc</i>", sb.toString());
+    sar.createSequenceAnnotationReport(sb, seq, true, true, null);
+    assertEquals("<i>SeqDesc</i>", sb.toString());
 
     /*
      * non-positional feature
@@ -264,7 +303,7 @@ public class SequenceAnnotationReportTest
             null));
     sb.setLength(0);
     sar.createSequenceAnnotationReport(sb, seq, true, true, null);
-    String expected = "<i><br>SeqDesc<br>Type1 ; Nonpos Score=1.0</i>";
+    String expected = "<i>SeqDesc<br/>Type1 ; Nonpos Score=1.0</i>";
     assertEquals(expected, sb.toString());
 
     /*
@@ -272,7 +311,7 @@ public class SequenceAnnotationReportTest
      */
     sb.setLength(0);
     sar.createSequenceAnnotationReport(sb, seq, true, false, null);
-    assertEquals("<i><br>SeqDesc</i>", sb.toString());
+    assertEquals("<i>SeqDesc</i>", sb.toString());
 
     /*
      * add non-pos feature with score inside min-max range for feature type
@@ -289,7 +328,7 @@ public class SequenceAnnotationReportTest
 
     sb.setLength(0);
     sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
-    expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos</i>";
+    expected = "<i>SeqDesc<br/>Metal ; Desc<br/>Type1 ; Nonpos</i>";
     assertEquals(expected, sb.toString());
     
     /*
@@ -305,8 +344,8 @@ public class SequenceAnnotationReportTest
     assertEquals(expected, sb.toString()); // unchanged!
 
     /*
-     * 'clinical_significance' attribute only included when
-     * used for feature colouring
+     * 'clinical_significance' attribute is only included in description 
+     * when used for feature colouring
      */
     SequenceFeature sf2 = new SequenceFeature("Variant", "Havana", 0, 0,
             5f, null);
@@ -314,7 +353,7 @@ public class SequenceAnnotationReportTest
     seq.addSequenceFeature(sf2);
     sb.setLength(0);
     sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
-    expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana</i>";
+    expected = "<i>SeqDesc<br/>Metal ; Desc<br/>Type1 ; Nonpos<br/>Variant ; Havana</i>";
     assertEquals(expected, sb.toString());
 
     /*
@@ -335,13 +374,23 @@ public class SequenceAnnotationReportTest
     fc.setAttributeName("clinical_significance");
     fr.setColour("Variant", fc);
     sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
-    expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1<br>Metal ; Desc<br>"
-            + "Type1 ; Nonpos<br>Variant ; Havana; clinical_significance=benign</i>";
+    expected = "<i>SeqDesc<br/>UNIPROT P30419<br/>PDB 3iu1<br/>Metal ; Desc<br/>"
+            + "Type1 ; Nonpos<br/>Variant ; Havana; clinical_significance=benign</i>";
     assertEquals(expected, sb.toString());
     // with showNonPositionalFeatures = false
     sb.setLength(0);
     sar.createSequenceAnnotationReport(sb, seq, true, false, fr);
-    expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1</i>";
+    expected = "<i>SeqDesc<br/>UNIPROT P30419<br/>PDB 3iu1</i>";
+    assertEquals(expected, sb.toString());
+
+    /*
+     * long feature description is truncated with ellipsis
+     */
+    sb.setLength(0);
+    sf2.setDescription(
+            "This is a very long description which should be truncated");
+    sar.createSequenceAnnotationReport(sb, seq, false, true, fr);
+    expected = "<i>SeqDesc<br/>Metal ; Desc<br/>Type1 ; Nonpos<br/>Variant ; This is a very long description which sh...; clinical_significance=benign</i>";
     assertEquals(expected, sb.toString());
 
     // see other tests for treatment of status and html
@@ -375,8 +424,10 @@ public class SequenceAnnotationReportTest
     sar.createSequenceAnnotationReport(sb, seq, true, true, null, true);
     String report = sb.toString();
     assertTrue(report
-            .startsWith("<i><br>UNIPROT P30410, P30411, P30412, P30413,...<br>PDB0 3iu1"));
+            .startsWith(
+                    "<i><br/>UNIPROT P30410, P30411, P30412, P30413,...<br/>PDB0 3iu1"));
     assertTrue(report
-            .endsWith("<br>PDB7 3iu1<br>PDB8,...<br>(Output Sequence Details to list all database references)</i>"));
+            .endsWith(
+                    "<br/>PDB7 3iu1<br/>PDB8,...<br/>(Output Sequence Details to list all database references)</i>"));
   }
 }
index f8461f8..1970ff2 100644 (file)
@@ -247,7 +247,7 @@ public class Mapping
   public void mapFer1From3W5V() throws Exception
   {
     AlignFrame seqf = new FileLoader(false)
-            .loadFileWaitTillLoaded(
+            .LoadFileWaitTillLoaded(
                     ">FER1_MAIZE/1-150 Ferredoxin-1, chloroplast precursor\nMATVLGSPRAPAFFFSSSSLRAAPAPTAVALPAAKVGIMGRSASSRRRLRAQATYNVKLITPEGEVELQVPD\nDVYILDQAEEDGIDLPYSCRAGSCSSCAGKVVSGSVDQSDQSYLDDGQIADGWVLTCHAYPTSDVVIETHKE\nEELTGA",
                     DataSourceType.PASTE, FileFormat.Fasta);
     SequenceI newseq = seqf.getViewport().getAlignment().getSequenceAt(0);