JAL-629 Fix --structureimage and options
[jalview.git] / src / jalview / project / Jalview2XML.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.project;
22
23 import static jalview.math.RotatableMatrix.Axis.X;
24 import static jalview.math.RotatableMatrix.Axis.Y;
25 import static jalview.math.RotatableMatrix.Axis.Z;
26
27 import java.awt.Color;
28 import java.awt.Font;
29 import java.awt.Rectangle;
30 import java.io.BufferedReader;
31 import java.io.ByteArrayInputStream;
32 import java.io.File;
33 import java.io.FileInputStream;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.InputStreamReader;
38 import java.io.OutputStream;
39 import java.io.OutputStreamWriter;
40 import java.io.PrintWriter;
41 import java.lang.reflect.InvocationTargetException;
42 import java.math.BigInteger;
43 import java.net.MalformedURLException;
44 import java.net.URL;
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.BitSet;
48 import java.util.Collections;
49 import java.util.Enumeration;
50 import java.util.GregorianCalendar;
51 import java.util.HashMap;
52 import java.util.HashSet;
53 import java.util.Hashtable;
54 import java.util.IdentityHashMap;
55 import java.util.Iterator;
56 import java.util.LinkedHashMap;
57 import java.util.List;
58 import java.util.Locale;
59 import java.util.Map;
60 import java.util.Map.Entry;
61 import java.util.Set;
62 import java.util.Vector;
63 import java.util.jar.JarEntry;
64 import java.util.jar.JarInputStream;
65 import java.util.jar.JarOutputStream;
66
67 import javax.swing.JInternalFrame;
68 import javax.swing.SwingUtilities;
69 import javax.xml.bind.JAXBContext;
70 import javax.xml.bind.JAXBElement;
71 import javax.xml.bind.Marshaller;
72 import javax.xml.datatype.DatatypeConfigurationException;
73 import javax.xml.datatype.DatatypeFactory;
74 import javax.xml.datatype.XMLGregorianCalendar;
75 import javax.xml.stream.XMLInputFactory;
76 import javax.xml.stream.XMLStreamReader;
77
78 import jalview.analysis.Conservation;
79 import jalview.analysis.PCA;
80 import jalview.analysis.scoremodels.ScoreModels;
81 import jalview.analysis.scoremodels.SimilarityParams;
82 import jalview.api.FeatureColourI;
83 import jalview.api.ViewStyleI;
84 import jalview.api.analysis.ScoreModelI;
85 import jalview.api.analysis.SimilarityParamsI;
86 import jalview.api.structures.JalviewStructureDisplayI;
87 import jalview.bin.Cache;
88 import jalview.bin.Console;
89 import jalview.bin.Jalview;
90 import jalview.datamodel.AlignedCodonFrame;
91 import jalview.datamodel.Alignment;
92 import jalview.datamodel.AlignmentAnnotation;
93 import jalview.datamodel.AlignmentI;
94 import jalview.datamodel.ContactMatrix;
95 import jalview.datamodel.ContactMatrixI;
96 import jalview.datamodel.DBRefEntry;
97 import jalview.datamodel.GeneLocus;
98 import jalview.datamodel.GraphLine;
99 import jalview.datamodel.GroupSet;
100 import jalview.datamodel.PDBEntry;
101 import jalview.datamodel.Point;
102 import jalview.datamodel.RnaViewerModel;
103 import jalview.datamodel.SequenceFeature;
104 import jalview.datamodel.SequenceGroup;
105 import jalview.datamodel.SequenceI;
106 import jalview.datamodel.StructureViewerModel;
107 import jalview.datamodel.StructureViewerModel.StructureData;
108 import jalview.datamodel.features.FeatureMatcher;
109 import jalview.datamodel.features.FeatureMatcherI;
110 import jalview.datamodel.features.FeatureMatcherSet;
111 import jalview.datamodel.features.FeatureMatcherSetI;
112 import jalview.ext.varna.RnaModel;
113 import jalview.gui.AlignFrame;
114 import jalview.gui.AlignViewport;
115 import jalview.gui.AlignmentPanel;
116 import jalview.gui.AppVarna;
117 import jalview.gui.Desktop;
118 import jalview.gui.JvOptionPane;
119 import jalview.gui.OOMWarning;
120 import jalview.gui.OverviewPanel;
121 import jalview.gui.PCAPanel;
122 import jalview.gui.PaintRefresher;
123 import jalview.gui.SplitFrame;
124 import jalview.gui.StructureViewer;
125 import jalview.gui.StructureViewer.ViewerType;
126 import jalview.gui.StructureViewerBase;
127 import jalview.gui.TreePanel;
128 import jalview.io.BackupFiles;
129 import jalview.io.DataSourceType;
130 import jalview.io.FileFormat;
131 import jalview.io.NewickFile;
132 import jalview.math.Matrix;
133 import jalview.math.MatrixI;
134 import jalview.renderer.ResidueShaderI;
135 import jalview.schemes.AnnotationColourGradient;
136 import jalview.schemes.ColourSchemeI;
137 import jalview.schemes.ColourSchemeProperty;
138 import jalview.schemes.FeatureColour;
139 import jalview.schemes.ResidueProperties;
140 import jalview.schemes.UserColourScheme;
141 import jalview.structure.StructureSelectionManager;
142 import jalview.structures.models.AAStructureBindingModel;
143 import jalview.util.Format;
144 import jalview.util.HttpUtils;
145 import jalview.util.MessageManager;
146 import jalview.util.Platform;
147 import jalview.util.StringUtils;
148 import jalview.util.jarInputStreamProvider;
149 import jalview.util.matcher.Condition;
150 import jalview.viewmodel.AlignmentViewport;
151 import jalview.viewmodel.PCAModel;
152 import jalview.viewmodel.ViewportRanges;
153 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
154 import jalview.viewmodel.seqfeatures.FeatureRendererSettings;
155 import jalview.viewmodel.seqfeatures.FeaturesDisplayed;
156 import jalview.ws.datamodel.MappableContactMatrixI;
157 import jalview.ws.datamodel.alphafold.PAEContactMatrix;
158 import jalview.ws.jws2.Jws2Discoverer;
159 import jalview.ws.jws2.dm.AAConSettings;
160 import jalview.ws.jws2.jabaws2.Jws2Instance;
161 import jalview.ws.params.ArgumentI;
162 import jalview.ws.params.AutoCalcSetting;
163 import jalview.ws.params.WsParamSetI;
164 import jalview.xml.binding.jalview.AlcodonFrame;
165 import jalview.xml.binding.jalview.AlcodonFrame.AlcodMap;
166 import jalview.xml.binding.jalview.Annotation;
167 import jalview.xml.binding.jalview.Annotation.ThresholdLine;
168 import jalview.xml.binding.jalview.AnnotationColourScheme;
169 import jalview.xml.binding.jalview.AnnotationElement;
170 import jalview.xml.binding.jalview.DoubleMatrix;
171 import jalview.xml.binding.jalview.DoubleVector;
172 import jalview.xml.binding.jalview.Feature;
173 import jalview.xml.binding.jalview.Feature.OtherData;
174 import jalview.xml.binding.jalview.FeatureMatcherSet.CompoundMatcher;
175 import jalview.xml.binding.jalview.FilterBy;
176 import jalview.xml.binding.jalview.JalviewModel;
177 import jalview.xml.binding.jalview.JalviewModel.FeatureSettings;
178 import jalview.xml.binding.jalview.JalviewModel.FeatureSettings.Group;
179 import jalview.xml.binding.jalview.JalviewModel.FeatureSettings.Setting;
180 import jalview.xml.binding.jalview.JalviewModel.JGroup;
181 import jalview.xml.binding.jalview.JalviewModel.JSeq;
182 import jalview.xml.binding.jalview.JalviewModel.JSeq.Pdbids;
183 import jalview.xml.binding.jalview.JalviewModel.JSeq.Pdbids.StructureState;
184 import jalview.xml.binding.jalview.JalviewModel.JSeq.RnaViewer;
185 import jalview.xml.binding.jalview.JalviewModel.JSeq.RnaViewer.SecondaryStructure;
186 import jalview.xml.binding.jalview.JalviewModel.PcaViewer;
187 import jalview.xml.binding.jalview.JalviewModel.PcaViewer.Axis;
188 import jalview.xml.binding.jalview.JalviewModel.PcaViewer.SeqPointMax;
189 import jalview.xml.binding.jalview.JalviewModel.PcaViewer.SeqPointMin;
190 import jalview.xml.binding.jalview.JalviewModel.PcaViewer.SequencePoint;
191 import jalview.xml.binding.jalview.JalviewModel.Tree;
192 import jalview.xml.binding.jalview.JalviewModel.UserColours;
193 import jalview.xml.binding.jalview.JalviewModel.Viewport;
194 import jalview.xml.binding.jalview.JalviewModel.Viewport.CalcIdParam;
195 import jalview.xml.binding.jalview.JalviewModel.Viewport.HiddenColumns;
196 import jalview.xml.binding.jalview.JalviewModel.Viewport.Overview;
197 import jalview.xml.binding.jalview.JalviewUserColours;
198 import jalview.xml.binding.jalview.JalviewUserColours.Colour;
199 import jalview.xml.binding.jalview.MapListType;
200 import jalview.xml.binding.jalview.MapListType.MapListFrom;
201 import jalview.xml.binding.jalview.MapListType.MapListTo;
202 import jalview.xml.binding.jalview.Mapping;
203 import jalview.xml.binding.jalview.MatrixType;
204 import jalview.xml.binding.jalview.NoValueColour;
205 import jalview.xml.binding.jalview.ObjectFactory;
206 import jalview.xml.binding.jalview.PcaDataType;
207 import jalview.xml.binding.jalview.Pdbentry.Property;
208 import jalview.xml.binding.jalview.Sequence;
209 import jalview.xml.binding.jalview.Sequence.DBRef;
210 import jalview.xml.binding.jalview.SequenceSet;
211 import jalview.xml.binding.jalview.SequenceSet.SequenceSetProperties;
212 import jalview.xml.binding.jalview.ThresholdType;
213 import jalview.xml.binding.jalview.VAMSAS;
214
215 /**
216  * Write out the current jalview desktop state as a Jalview XML stream.
217  * 
218  * Note: the vamsas objects referred to here are primitive versions of the
219  * VAMSAS project schema elements - they are not the same and most likely never
220  * will be :)
221  * 
222  * @author $author$
223  * @version $Revision: 1.134 $
224  */
225 public class Jalview2XML
226 {
227
228   // BH 2018 we add the .jvp binary extension to J2S so that
229   // it will declare that binary when we do the file save from the browser
230
231   static
232   {
233     Platform.addJ2SBinaryType(".jvp?");
234   }
235
236   private static final String VIEWER_PREFIX = "viewer_";
237
238   private static final String RNA_PREFIX = "rna_";
239
240   private static final String UTF_8 = "UTF-8";
241
242   /**
243    * used in decision if quit confirmation should be issued
244    */
245   private static boolean stateSavedUpToDate = false;
246
247   /**
248    * prefix for recovering datasets for alignments with multiple views where
249    * non-existent dataset IDs were written for some views
250    */
251   private static final String UNIQSEQSETID = "uniqueSeqSetId.";
252
253   // use this with nextCounter() to make unique names for entities
254   private int counter = 0;
255
256   /*
257    * SequenceI reference -> XML ID string in jalview XML. Populated as XML reps
258    * of sequence objects are created.
259    */
260   IdentityHashMap<SequenceI, String> seqsToIds = null;
261
262   /**
263    * jalview XML Sequence ID to jalview sequence object reference (both dataset
264    * and alignment sequences. Populated as XML reps of sequence objects are
265    * created.)
266    */
267   Map<String, SequenceI> seqRefIds = null;
268
269   Map<String, SequenceI> incompleteSeqs = null;
270
271   List<SeqFref> frefedSequence = null;
272
273   boolean raiseGUI = true; // whether errors are raised in dialog boxes or not
274
275   /*
276    * Map of reconstructed AlignFrame objects that appear to have come from
277    * SplitFrame objects (have a dna/protein complement view).
278    */
279   private Map<Viewport, AlignFrame> splitFrameCandidates = new HashMap<>();
280
281   /*
282    * Map from displayed rna structure models to their saved session state jar
283    * entry names
284    */
285   private Map<RnaModel, String> rnaSessions = new HashMap<>();
286
287   /**
288    * A helper method for safely using the value of an optional attribute that
289    * may be null if not present in the XML. Answers the boolean value, or false
290    * if null.
291    * 
292    * @param b
293    * @return
294    */
295   public static boolean safeBoolean(Boolean b)
296   {
297     return b == null ? false : b.booleanValue();
298   }
299
300   /**
301    * A helper method for safely using the value of an optional attribute that
302    * may be null if not present in the XML. Answers the integer value, or zero
303    * if null.
304    * 
305    * @param i
306    * @return
307    */
308   public static int safeInt(Integer i)
309   {
310     return i == null ? 0 : i.intValue();
311   }
312
313   /**
314    * A helper method for safely using the value of an optional attribute that
315    * may be null if not present in the XML. Answers the float value, or zero if
316    * null.
317    * 
318    * @param f
319    * @return
320    */
321   public static float safeFloat(Float f)
322   {
323     return f == null ? 0f : f.floatValue();
324   }
325
326   /**
327    * create/return unique hash string for sq
328    * 
329    * @param sq
330    * @return new or existing unique string for sq
331    */
332   String seqHash(SequenceI sq)
333   {
334     if (seqsToIds == null)
335     {
336       initSeqRefs();
337     }
338     if (seqsToIds.containsKey(sq))
339     {
340       return seqsToIds.get(sq);
341     }
342     else
343     {
344       // create sequential key
345       String key = "sq" + (seqsToIds.size() + 1);
346       key = makeHashCode(sq, key); // check we don't have an external reference
347       // for it already.
348       seqsToIds.put(sq, key);
349       return key;
350     }
351   }
352
353   void initSeqRefs()
354   {
355     if (seqsToIds == null)
356     {
357       seqsToIds = new IdentityHashMap<>();
358     }
359     if (seqRefIds == null)
360     {
361       seqRefIds = new HashMap<>();
362     }
363     if (incompleteSeqs == null)
364     {
365       incompleteSeqs = new HashMap<>();
366     }
367     if (frefedSequence == null)
368     {
369       frefedSequence = new ArrayList<>();
370     }
371   }
372
373   public Jalview2XML()
374   {
375   }
376
377   public Jalview2XML(boolean raiseGUI)
378   {
379     this.raiseGUI = raiseGUI;
380   }
381
382   /**
383    * base class for resolving forward references to sequences by their ID
384    * 
385    * @author jprocter
386    *
387    */
388   abstract class SeqFref
389   {
390     String sref;
391
392     String type;
393
394     public SeqFref(String _sref, String type)
395     {
396       sref = _sref;
397       this.type = type;
398     }
399
400     public String getSref()
401     {
402       return sref;
403     }
404
405     public SequenceI getSrefSeq()
406     {
407       return seqRefIds.get(sref);
408     }
409
410     public boolean isResolvable()
411     {
412       return seqRefIds.get(sref) != null;
413     }
414
415     public SequenceI getSrefDatasetSeq()
416     {
417       SequenceI sq = seqRefIds.get(sref);
418       if (sq != null)
419       {
420         while (sq.getDatasetSequence() != null)
421         {
422           sq = sq.getDatasetSequence();
423         }
424       }
425       return sq;
426     }
427
428     /**
429      * @return true if the forward reference was fully resolved
430      */
431     abstract boolean resolve();
432
433     @Override
434     public String toString()
435     {
436       return type + " reference to " + sref;
437     }
438   }
439
440   /**
441    * create forward reference for a mapping
442    * 
443    * @param sref
444    * @param _jmap
445    * @return
446    */
447   public SeqFref newMappingRef(final String sref,
448           final jalview.datamodel.Mapping _jmap)
449   {
450     SeqFref fref = new SeqFref(sref, "Mapping")
451     {
452       public jalview.datamodel.Mapping jmap = _jmap;
453
454       @Override
455       boolean resolve()
456       {
457         SequenceI seq = getSrefDatasetSeq();
458         if (seq == null)
459         {
460           return false;
461         }
462         jmap.setTo(seq);
463         return true;
464       }
465     };
466     return fref;
467   }
468
469   public SeqFref newAlcodMapRef(final String sref,
470           final AlignedCodonFrame _cf,
471           final jalview.datamodel.Mapping _jmap)
472   {
473
474     SeqFref fref = new SeqFref(sref, "Codon Frame")
475     {
476       AlignedCodonFrame cf = _cf;
477
478       public jalview.datamodel.Mapping mp = _jmap;
479
480       @Override
481       public boolean isResolvable()
482       {
483         return super.isResolvable() && mp.getTo() != null;
484       }
485
486       @Override
487       boolean resolve()
488       {
489         SequenceI seq = getSrefDatasetSeq();
490         if (seq == null)
491         {
492           return false;
493         }
494         cf.addMap(seq, mp.getTo(), mp.getMap());
495         return true;
496       }
497     };
498     return fref;
499   }
500
501   public void resolveFrefedSequences()
502   {
503     Iterator<SeqFref> nextFref = frefedSequence.iterator();
504     int toresolve = frefedSequence.size();
505     int unresolved = 0, failedtoresolve = 0;
506     while (nextFref.hasNext())
507     {
508       SeqFref ref = nextFref.next();
509       if (ref.isResolvable())
510       {
511         try
512         {
513           if (ref.resolve())
514           {
515             nextFref.remove();
516           }
517           else
518           {
519             failedtoresolve++;
520           }
521         } catch (Exception x)
522         {
523           jalview.bin.Console.errPrintln(
524                   "IMPLEMENTATION ERROR: Failed to resolve forward reference for sequence "
525                           + ref.getSref());
526           x.printStackTrace();
527           failedtoresolve++;
528         }
529       }
530       else
531       {
532         unresolved++;
533       }
534     }
535     if (unresolved > 0)
536     {
537       jalview.bin.Console.errPrintln("Jalview Project Import: There were "
538               + unresolved
539               + " forward references left unresolved on the stack.");
540     }
541     if (failedtoresolve > 0)
542     {
543       jalview.bin.Console.errPrintln("SERIOUS! " + failedtoresolve
544               + " resolvable forward references failed to resolve.");
545     }
546     if (incompleteSeqs != null && incompleteSeqs.size() > 0)
547     {
548       jalview.bin.Console.errPrintln(
549               "Jalview Project Import: There are " + incompleteSeqs.size()
550                       + " sequences which may have incomplete metadata.");
551       if (incompleteSeqs.size() < 10)
552       {
553         for (SequenceI s : incompleteSeqs.values())
554         {
555           jalview.bin.Console.errPrintln(s.toString());
556         }
557       }
558       else
559       {
560         jalview.bin.Console.errPrintln(
561                 "Too many to report. Skipping output of incomplete sequences.");
562       }
563     }
564   }
565
566   /**
567    * This maintains a map of viewports, the key being the seqSetId. Important to
568    * set historyItem and redoList for multiple views
569    */
570   Map<String, AlignViewport> viewportsAdded = new HashMap<>();
571
572   Map<String, AlignmentAnnotation> annotationIds = new HashMap<>();
573
574   String uniqueSetSuffix = "";
575
576   /**
577    * List of pdbfiles added to Jar
578    */
579   List<String> pdbfiles = null;
580
581   // SAVES SEVERAL ALIGNMENT WINDOWS TO SAME JARFILE
582   public void saveState(File statefile)
583   {
584     FileOutputStream fos = null;
585
586     try
587     {
588
589       fos = new FileOutputStream(statefile);
590
591       JarOutputStream jout = new JarOutputStream(fos);
592       saveState(jout);
593       fos.close();
594
595     } catch (Exception e)
596     {
597       Console.error("Couln't write Jalview state to " + statefile, e);
598       // TODO: inform user of the problem - they need to know if their data was
599       // not saved !
600       if (errorMessage == null)
601       {
602         errorMessage = "Did't write Jalview Archive to output file '"
603                 + statefile + "' - See console error log for details";
604       }
605       else
606       {
607         errorMessage += "(Didn't write Jalview Archive to output file '"
608                 + statefile + ")";
609       }
610       e.printStackTrace();
611     } finally
612     {
613       if (fos != null)
614       {
615         try
616         {
617           fos.close();
618         } catch (IOException e)
619         {
620           // ignore
621         }
622       }
623     }
624     reportErrors();
625   }
626
627   /**
628    * Writes a jalview project archive to the given Jar output stream.
629    * 
630    * @param jout
631    */
632   public void saveState(JarOutputStream jout)
633   {
634     AlignFrame[] frames = Desktop.getAlignFrames();
635
636     setStateSavedUpToDate(true);
637
638     if (Cache.getDefault("DEBUG_DELAY_SAVE", false))
639     {
640       int n = debugDelaySave;
641       int i = 0;
642       while (i < n)
643       {
644         Console.debug("***** debugging save sleep " + i + "/" + n);
645         try
646         {
647           Thread.sleep(1000);
648         } catch (InterruptedException e)
649         {
650           // TODO Auto-generated catch block
651           e.printStackTrace();
652         }
653         i++;
654       }
655     }
656
657     if (frames == null)
658     {
659       return;
660     }
661     saveAllFrames(Arrays.asList(frames), jout);
662   }
663
664   /**
665    * core method for storing state for a set of AlignFrames.
666    * 
667    * @param frames
668    *          - frames involving all data to be exported (including containing
669    *          splitframes)
670    * @param jout
671    *          - project output stream
672    */
673   private void saveAllFrames(List<AlignFrame> frames, JarOutputStream jout)
674   {
675     Hashtable<String, AlignFrame> dsses = new Hashtable<>();
676
677     /*
678      * ensure cached data is clear before starting
679      */
680     // todo tidy up seqRefIds, seqsToIds initialisation / reset
681     rnaSessions.clear();
682     splitFrameCandidates.clear();
683
684     try
685     {
686
687       // NOTE UTF-8 MUST BE USED FOR WRITING UNICODE CHARS
688       // //////////////////////////////////////////////////
689
690       List<String> shortNames = new ArrayList<>();
691       List<String> viewIds = new ArrayList<>();
692
693       // REVERSE ORDER
694       for (int i = frames.size() - 1; i > -1; i--)
695       {
696         AlignFrame af = frames.get(i);
697         // skip ?
698         if (skipList != null && skipList
699                 .containsKey(af.getViewport().getSequenceSetId()))
700         {
701           continue;
702         }
703
704         String shortName = makeFilename(af, shortNames);
705
706         int apSize = af.getAlignPanels().size();
707
708         for (int ap = 0; ap < apSize; ap++)
709         {
710           AlignmentPanel apanel = (AlignmentPanel) af.getAlignPanels()
711                   .get(ap);
712           String fileName = apSize == 1 ? shortName : ap + shortName;
713           if (!fileName.endsWith(".xml"))
714           {
715             fileName = fileName + ".xml";
716           }
717
718           saveState(apanel, fileName, jout, viewIds);
719
720           String dssid = getDatasetIdRef(
721                   af.getViewport().getAlignment().getDataset());
722           if (!dsses.containsKey(dssid))
723           {
724             dsses.put(dssid, af);
725           }
726         }
727       }
728
729       writeDatasetFor(dsses, "" + jout.hashCode() + " " + uniqueSetSuffix,
730               jout);
731
732       try
733       {
734         jout.flush();
735       } catch (Exception foo)
736       {
737       }
738       jout.close();
739     } catch (Exception ex)
740     {
741       // TODO: inform user of the problem - they need to know if their data was
742       // not saved !
743       if (errorMessage == null)
744       {
745         errorMessage = "Couldn't write Jalview Archive - see error output for details";
746       }
747       ex.printStackTrace();
748     }
749   }
750
751   /**
752    * Generates a distinct file name, based on the title of the AlignFrame, by
753    * appending _n for increasing n until an unused name is generated. The new
754    * name (without its extension) is added to the list.
755    * 
756    * @param af
757    * @param namesUsed
758    * @return the generated name, with .xml extension
759    */
760   protected String makeFilename(AlignFrame af, List<String> namesUsed)
761   {
762     String shortName = af.getTitle();
763
764     if (shortName.indexOf(File.separatorChar) > -1)
765     {
766       shortName = shortName
767               .substring(shortName.lastIndexOf(File.separatorChar) + 1);
768     }
769
770     int count = 1;
771
772     while (namesUsed.contains(shortName))
773     {
774       if (shortName.endsWith("_" + (count - 1)))
775       {
776         shortName = shortName.substring(0, shortName.lastIndexOf("_"));
777       }
778
779       shortName = shortName.concat("_" + count);
780       count++;
781     }
782
783     namesUsed.add(shortName);
784
785     if (!shortName.endsWith(".xml"))
786     {
787       shortName = shortName + ".xml";
788     }
789     return shortName;
790   }
791
792   // USE THIS METHOD TO SAVE A SINGLE ALIGNMENT WINDOW
793   public boolean saveAlignment(AlignFrame af, String jarFile,
794           String fileName)
795   {
796     try
797     {
798       // create backupfiles object and get new temp filename destination
799       boolean doBackup = BackupFiles.getEnabled();
800       BackupFiles backupfiles = doBackup ? new BackupFiles(jarFile) : null;
801       FileOutputStream fos = new FileOutputStream(
802               doBackup ? backupfiles.getTempFilePath() : jarFile);
803
804       if (Cache.getDefault("DEBUG_DELAY_SAVE", false))
805       {
806         int n = debugDelaySave;
807         int i = 0;
808         while (i < n)
809         {
810           Console.debug("***** debugging save sleep " + i + "/" + n);
811           try
812           {
813             Thread.sleep(1000);
814           } catch (InterruptedException e)
815           {
816             // TODO Auto-generated catch block
817             e.printStackTrace();
818           }
819           i++;
820         }
821       }
822
823       JarOutputStream jout = new JarOutputStream(fos);
824       List<AlignFrame> frames = new ArrayList<>();
825
826       // resolve splitframes
827       if (af.getViewport().getCodingComplement() != null)
828       {
829         frames = ((SplitFrame) af.getSplitViewContainer()).getAlignFrames();
830       }
831       else
832       {
833         frames.add(af);
834       }
835       saveAllFrames(frames, jout);
836       try
837       {
838         jout.flush();
839       } catch (Exception foo)
840       {
841       }
842       jout.close();
843       boolean success = true;
844
845       if (doBackup)
846       {
847         backupfiles.setWriteSuccess(success);
848         success = backupfiles.rollBackupsAndRenameTempFile();
849       }
850
851       return success;
852     } catch (Exception ex)
853     {
854       errorMessage = "Couldn't Write alignment view to Jalview Archive - see error output for details";
855       ex.printStackTrace();
856       return false;
857     }
858   }
859
860   private void writeDatasetFor(Hashtable<String, AlignFrame> dsses,
861           String fileName, JarOutputStream jout)
862   {
863
864     for (String dssids : dsses.keySet())
865     {
866       AlignFrame _af = dsses.get(dssids);
867       String jfileName = fileName + " Dataset for " + _af.getTitle();
868       if (!jfileName.endsWith(".xml"))
869       {
870         jfileName = jfileName + ".xml";
871       }
872       saveState(_af.alignPanel, jfileName, true, jout, null);
873     }
874   }
875
876   /**
877    * create a JalviewModel from an alignment view and marshall it to a
878    * JarOutputStream
879    * 
880    * @param ap
881    *          panel to create jalview model for
882    * @param fileName
883    *          name of alignment panel written to output stream
884    * @param jout
885    *          jar output stream
886    * @param viewIds
887    * @param out
888    *          jar entry name
889    */
890   public JalviewModel saveState(AlignmentPanel ap, String fileName,
891           JarOutputStream jout, List<String> viewIds)
892   {
893     return saveState(ap, fileName, false, jout, viewIds);
894   }
895
896   /**
897    * create a JalviewModel from an alignment view and marshall it to a
898    * JarOutputStream
899    * 
900    * @param ap
901    *          panel to create jalview model for
902    * @param fileName
903    *          name of alignment panel written to output stream
904    * @param storeDS
905    *          when true, only write the dataset for the alignment, not the data
906    *          associated with the view.
907    * @param jout
908    *          jar output stream
909    * @param out
910    *          jar entry name
911    */
912   public JalviewModel saveState(AlignmentPanel ap, String fileName,
913           boolean storeDS, JarOutputStream jout, List<String> viewIds)
914   {
915     if (viewIds == null)
916     {
917       viewIds = new ArrayList<>();
918     }
919
920     initSeqRefs();
921
922     List<UserColourScheme> userColours = new ArrayList<>();
923
924     AlignViewport av = ap.av;
925     ViewportRanges vpRanges = av.getRanges();
926
927     final ObjectFactory objectFactory = new ObjectFactory();
928     JalviewModel object = objectFactory.createJalviewModel();
929     object.setVamsasModel(new VAMSAS());
930
931     // object.setCreationDate(new java.util.Date(System.currentTimeMillis()));
932     try
933     {
934       GregorianCalendar c = new GregorianCalendar();
935       DatatypeFactory datatypeFactory = DatatypeFactory.newInstance();
936       XMLGregorianCalendar now = datatypeFactory.newXMLGregorianCalendar(c);// gregorianCalendar);
937       object.setCreationDate(now);
938     } catch (DatatypeConfigurationException e)
939     {
940       jalview.bin.Console.errPrintln("error writing date: " + e.toString());
941     }
942     object.setVersion(Cache.getDefault("VERSION", "Development Build"));
943
944     /**
945      * rjal is full height alignment, jal is actual alignment with full metadata
946      * but excludes hidden sequences.
947      */
948     jalview.datamodel.AlignmentI rjal = av.getAlignment(), jal = rjal;
949
950     if (av.hasHiddenRows())
951     {
952       rjal = jal.getHiddenSequences().getFullAlignment();
953     }
954
955     SequenceSet vamsasSet = new SequenceSet();
956     Sequence vamsasSeq;
957     // JalviewModelSequence jms = new JalviewModelSequence();
958
959     vamsasSet.setGapChar(jal.getGapCharacter() + "");
960
961     if (jal.getDataset() != null)
962     {
963       // dataset id is the dataset's hashcode
964       vamsasSet.setDatasetId(getDatasetIdRef(jal.getDataset()));
965       if (storeDS)
966       {
967         // switch jal and the dataset
968         jal = jal.getDataset();
969         rjal = jal;
970       }
971     }
972     if (jal.getProperties() != null)
973     {
974       Enumeration en = jal.getProperties().keys();
975       while (en.hasMoreElements())
976       {
977         String key = en.nextElement().toString();
978         SequenceSetProperties ssp = new SequenceSetProperties();
979         ssp.setKey(key);
980         ssp.setValue(jal.getProperties().get(key).toString());
981         // vamsasSet.addSequenceSetProperties(ssp);
982         vamsasSet.getSequenceSetProperties().add(ssp);
983       }
984     }
985
986     JSeq jseq;
987     Set<String> calcIdSet = new HashSet<>();
988     // record the set of vamsas sequence XML POJO we create.
989     HashMap<String, Sequence> vamsasSetIds = new HashMap<>();
990     // SAVE SEQUENCES
991     for (final SequenceI jds : rjal.getSequences())
992     {
993       final SequenceI jdatasq = jds.getDatasetSequence() == null ? jds
994               : jds.getDatasetSequence();
995       String id = seqHash(jds);
996       if (vamsasSetIds.get(id) == null)
997       {
998         if (seqRefIds.get(id) != null && !storeDS)
999         {
1000           // This happens for two reasons: 1. multiple views are being
1001           // serialised.
1002           // 2. the hashCode has collided with another sequence's code. This
1003           // DOES
1004           // HAPPEN! (PF00072.15.stk does this)
1005           // JBPNote: Uncomment to debug writing out of files that do not read
1006           // back in due to ArrayOutOfBoundExceptions.
1007           // jalview.bin.Console.errPrintln("vamsasSeq backref: "+id+"");
1008           // jalview.bin.Console.errPrintln(jds.getName()+"
1009           // "+jds.getStart()+"-"+jds.getEnd()+" "+jds.getSequenceAsString());
1010           // jalview.bin.Console.errPrintln("Hashcode: "+seqHash(jds));
1011           // SequenceI rsq = (SequenceI) seqRefIds.get(id + "");
1012           // jalview.bin.Console.errPrintln(rsq.getName()+"
1013           // "+rsq.getStart()+"-"+rsq.getEnd()+" "+rsq.getSequenceAsString());
1014           // jalview.bin.Console.errPrintln("Hashcode: "+seqHash(rsq));
1015         }
1016         else
1017         {
1018           vamsasSeq = createVamsasSequence(id, jds);
1019           // vamsasSet.addSequence(vamsasSeq);
1020           vamsasSet.getSequence().add(vamsasSeq);
1021           vamsasSetIds.put(id, vamsasSeq);
1022           seqRefIds.put(id, jds);
1023         }
1024       }
1025       jseq = new JSeq();
1026       jseq.setStart(jds.getStart());
1027       jseq.setEnd(jds.getEnd());
1028       jseq.setColour(av.getSequenceColour(jds).getRGB());
1029
1030       jseq.setId(id); // jseq id should be a string not a number
1031       if (!storeDS)
1032       {
1033         // Store any sequences this sequence represents
1034         if (av.hasHiddenRows())
1035         {
1036           // use rjal, contains the full height alignment
1037           jseq.setHidden(
1038                   av.getAlignment().getHiddenSequences().isHidden(jds));
1039
1040           if (av.isHiddenRepSequence(jds))
1041           {
1042             jalview.datamodel.SequenceI[] reps = av
1043                     .getRepresentedSequences(jds).getSequencesInOrder(rjal);
1044
1045             for (int h = 0; h < reps.length; h++)
1046             {
1047               if (reps[h] != jds)
1048               {
1049                 // jseq.addHiddenSequences(rjal.findIndex(reps[h]));
1050                 jseq.getHiddenSequences().add(rjal.findIndex(reps[h]));
1051               }
1052             }
1053           }
1054         }
1055         // mark sequence as reference - if it is the reference for this view
1056         if (jal.hasSeqrep())
1057         {
1058           jseq.setViewreference(jds == jal.getSeqrep());
1059         }
1060       }
1061
1062       // TODO: omit sequence features from each alignment view's XML dump if we
1063       // are storing dataset
1064       List<SequenceFeature> sfs = jds.getSequenceFeatures();
1065       for (SequenceFeature sf : sfs)
1066       {
1067         // Features features = new Features();
1068         Feature features = new Feature();
1069
1070         features.setBegin(sf.getBegin());
1071         features.setEnd(sf.getEnd());
1072         features.setDescription(sf.getDescription());
1073         features.setType(sf.getType());
1074         features.setFeatureGroup(sf.getFeatureGroup());
1075         features.setScore(sf.getScore());
1076         if (sf.links != null)
1077         {
1078           for (int l = 0; l < sf.links.size(); l++)
1079           {
1080             OtherData keyValue = new OtherData();
1081             keyValue.setKey("LINK_" + l);
1082             keyValue.setValue(sf.links.elementAt(l).toString());
1083             // features.addOtherData(keyValue);
1084             features.getOtherData().add(keyValue);
1085           }
1086         }
1087         if (sf.otherDetails != null)
1088         {
1089           /*
1090            * save feature attributes, which may be simple strings or
1091            * map valued (have sub-attributes)
1092            */
1093           for (Entry<String, Object> entry : sf.otherDetails.entrySet())
1094           {
1095             String key = entry.getKey();
1096             Object value = entry.getValue();
1097             if (value instanceof Map<?, ?>)
1098             {
1099               for (Entry<String, Object> subAttribute : ((Map<String, Object>) value)
1100                       .entrySet())
1101               {
1102                 OtherData otherData = new OtherData();
1103                 otherData.setKey(key);
1104                 otherData.setKey2(subAttribute.getKey());
1105                 otherData.setValue(subAttribute.getValue().toString());
1106                 // features.addOtherData(otherData);
1107                 features.getOtherData().add(otherData);
1108               }
1109             }
1110             else
1111             {
1112               OtherData otherData = new OtherData();
1113               otherData.setKey(key);
1114               otherData.setValue(value.toString());
1115               // features.addOtherData(otherData);
1116               features.getOtherData().add(otherData);
1117             }
1118           }
1119         }
1120
1121         // jseq.addFeatures(features);
1122         jseq.getFeatures().add(features);
1123       }
1124
1125       if (jdatasq.getAllPDBEntries() != null)
1126       {
1127         Enumeration<PDBEntry> en = jdatasq.getAllPDBEntries().elements();
1128         while (en.hasMoreElements())
1129         {
1130           Pdbids pdb = new Pdbids();
1131           jalview.datamodel.PDBEntry entry = en.nextElement();
1132
1133           String pdbId = entry.getId();
1134           pdb.setId(pdbId);
1135           pdb.setType(entry.getType());
1136
1137           /*
1138            * Store any structure views associated with this sequence. This
1139            * section copes with duplicate entries in the project, so a dataset
1140            * only view *should* be coped with sensibly.
1141            */
1142           // This must have been loaded, is it still visible?
1143           JInternalFrame[] frames = null;
1144           if (Desktop.desktop != null)
1145           {
1146             frames = Desktop.desktop.getAllFrames();
1147           }
1148           else if (Jalview.isHeadlessMode()
1149                   && Jalview.getInstance().getCommands() != null)
1150           {
1151             // TODO
1152           }
1153           else
1154           {
1155             frames = null;
1156           }
1157           String matchedFile = null;
1158           if (frames != null)
1159           {
1160             for (int f = frames.length - 1; f > -1; f--)
1161             {
1162               if (frames[f] instanceof StructureViewerBase)
1163               {
1164                 StructureViewerBase viewFrame = (StructureViewerBase) frames[f];
1165                 matchedFile = saveStructureViewer(ap, jds, pdb, entry,
1166                         viewIds, matchedFile, viewFrame);
1167                 /*
1168                  * Only store each structure viewer's state once in the project
1169                  * jar. First time through only (storeDS==false)
1170                  */
1171                 String viewId = viewFrame.getViewId();
1172                 String viewerType = viewFrame.getViewerType().toString();
1173                 if (!storeDS && !viewIds.contains(viewId))
1174                 {
1175                   viewIds.add(viewId);
1176                   File viewerState = viewFrame.saveSession();
1177                   if (viewerState != null)
1178                   {
1179                     copyFileToJar(jout, viewerState.getPath(),
1180                             getViewerJarEntryName(viewId), viewerType);
1181                   }
1182                   else
1183                   {
1184                     Console.error("Failed to save viewer state for "
1185                             + viewerType);
1186                   }
1187                 }
1188               }
1189             }
1190           }
1191
1192           if (matchedFile != null || entry.getFile() != null)
1193           {
1194             if (entry.getFile() != null)
1195             {
1196               // use entry's file
1197               matchedFile = entry.getFile();
1198             }
1199             pdb.setFile(matchedFile); // entry.getFile());
1200             if (pdbfiles == null)
1201             {
1202               pdbfiles = new ArrayList<>();
1203             }
1204
1205             if (!pdbfiles.contains(pdbId))
1206             {
1207               pdbfiles.add(pdbId);
1208               copyFileToJar(jout, matchedFile, pdbId, pdbId);
1209             }
1210           }
1211
1212           Enumeration<String> props = entry.getProperties();
1213           if (props.hasMoreElements())
1214           {
1215             // PdbentryItem item = new PdbentryItem();
1216             while (props.hasMoreElements())
1217             {
1218               Property prop = new Property();
1219               String key = props.nextElement();
1220               prop.setName(key);
1221               prop.setValue(entry.getProperty(key).toString());
1222               // item.addProperty(prop);
1223               pdb.getProperty().add(prop);
1224             }
1225             // pdb.addPdbentryItem(item);
1226           }
1227
1228           // jseq.addPdbids(pdb);
1229           jseq.getPdbids().add(pdb);
1230         }
1231       }
1232
1233       saveRnaViewers(jout, jseq, jds, viewIds, ap, storeDS);
1234
1235       // jms.addJSeq(jseq);
1236       object.getJSeq().add(jseq);
1237     }
1238
1239     if (!storeDS && av.hasHiddenRows())
1240     {
1241       jal = av.getAlignment();
1242     }
1243     // SAVE MAPPINGS
1244     // FOR DATASET
1245     if (storeDS && jal.getCodonFrames() != null)
1246     {
1247       List<AlignedCodonFrame> jac = jal.getCodonFrames();
1248       for (AlignedCodonFrame acf : jac)
1249       {
1250         AlcodonFrame alc = new AlcodonFrame();
1251         if (acf.getProtMappings() != null
1252                 && acf.getProtMappings().length > 0)
1253         {
1254           boolean hasMap = false;
1255           SequenceI[] dnas = acf.getdnaSeqs();
1256           jalview.datamodel.Mapping[] pmaps = acf.getProtMappings();
1257           for (int m = 0; m < pmaps.length; m++)
1258           {
1259             AlcodMap alcmap = new AlcodMap();
1260             alcmap.setDnasq(seqHash(dnas[m]));
1261             alcmap.setMapping(
1262                     createVamsasMapping(pmaps[m], dnas[m], null, false));
1263             // alc.addAlcodMap(alcmap);
1264             alc.getAlcodMap().add(alcmap);
1265             hasMap = true;
1266           }
1267           if (hasMap)
1268           {
1269             // vamsasSet.addAlcodonFrame(alc);
1270             vamsasSet.getAlcodonFrame().add(alc);
1271           }
1272         }
1273         // TODO: delete this ? dead code from 2.8.3->2.9 ?
1274         // {
1275         // AlcodonFrame alc = new AlcodonFrame();
1276         // vamsasSet.addAlcodonFrame(alc);
1277         // for (int p = 0; p < acf.aaWidth; p++)
1278         // {
1279         // Alcodon cmap = new Alcodon();
1280         // if (acf.codons[p] != null)
1281         // {
1282         // // Null codons indicate a gapped column in the translated peptide
1283         // // alignment.
1284         // cmap.setPos1(acf.codons[p][0]);
1285         // cmap.setPos2(acf.codons[p][1]);
1286         // cmap.setPos3(acf.codons[p][2]);
1287         // }
1288         // alc.addAlcodon(cmap);
1289         // }
1290         // if (acf.getProtMappings() != null
1291         // && acf.getProtMappings().length > 0)
1292         // {
1293         // SequenceI[] dnas = acf.getdnaSeqs();
1294         // jalview.datamodel.Mapping[] pmaps = acf.getProtMappings();
1295         // for (int m = 0; m < pmaps.length; m++)
1296         // {
1297         // AlcodMap alcmap = new AlcodMap();
1298         // alcmap.setDnasq(seqHash(dnas[m]));
1299         // alcmap.setMapping(createVamsasMapping(pmaps[m], dnas[m], null,
1300         // false));
1301         // alc.addAlcodMap(alcmap);
1302         // }
1303         // }
1304       }
1305     }
1306
1307     // SAVE TREES
1308     // /////////////////////////////////
1309     if (!storeDS && av.getCurrentTree() != null)
1310     {
1311       // FIND ANY ASSOCIATED TREES
1312       // NOT IMPLEMENTED FOR HEADLESS STATE AT PRESENT
1313       if (Desktop.desktop != null)
1314       {
1315         JInternalFrame[] frames = Desktop.desktop.getAllFrames();
1316
1317         for (int t = 0; t < frames.length; t++)
1318         {
1319           if (frames[t] instanceof TreePanel)
1320           {
1321             TreePanel tp = (TreePanel) frames[t];
1322
1323             if (tp.getTreeCanvas().getViewport().getAlignment() == jal)
1324             {
1325               JalviewModel.Tree tree = new JalviewModel.Tree();
1326               tree.setTitle(tp.getTitle());
1327               tree.setCurrentTree((av.getCurrentTree() == tp.getTree()));
1328               tree.setNewick(tp.getTree().print());
1329               tree.setThreshold(tp.getTreeCanvas().getThreshold());
1330
1331               tree.setFitToWindow(tp.fitToWindow.getState());
1332               tree.setFontName(tp.getTreeFont().getName());
1333               tree.setFontSize(tp.getTreeFont().getSize());
1334               tree.setFontStyle(tp.getTreeFont().getStyle());
1335               tree.setMarkUnlinked(tp.placeholdersMenu.getState());
1336
1337               tree.setShowBootstrap(tp.bootstrapMenu.getState());
1338               tree.setShowDistances(tp.distanceMenu.getState());
1339
1340               tree.setHeight(tp.getHeight());
1341               tree.setWidth(tp.getWidth());
1342               tree.setXpos(tp.getX());
1343               tree.setYpos(tp.getY());
1344               tree.setId(makeHashCode(tp, null));
1345               tree.setLinkToAllViews(
1346                       tp.getTreeCanvas().isApplyToAllViews());
1347
1348               // columnWiseTree
1349               if (tp.isColumnWise())
1350               {
1351                 tree.setColumnWise(true);
1352                 String annId = tp.getAssocAnnotation().annotationId;
1353                 tree.setColumnReference(annId);
1354               }
1355               // jms.addTree(tree);
1356               object.getTree().add(tree);
1357             }
1358           }
1359         }
1360       }
1361     }
1362
1363     /*
1364      * save PCA viewers
1365      */
1366     if (!storeDS && Desktop.desktop != null)
1367     {
1368       for (JInternalFrame frame : Desktop.desktop.getAllFrames())
1369       {
1370         if (frame instanceof PCAPanel)
1371         {
1372           PCAPanel panel = (PCAPanel) frame;
1373           if (panel.getAlignViewport().getAlignment() == jal)
1374           {
1375             savePCA(panel, object);
1376           }
1377         }
1378       }
1379     }
1380
1381     // SAVE ANNOTATIONS
1382     /**
1383      * store forward refs from an annotationRow to any groups
1384      */
1385     IdentityHashMap<SequenceGroup, String> groupRefs = new IdentityHashMap<>();
1386     if (storeDS)
1387     {
1388       for (SequenceI sq : jal.getSequences())
1389       {
1390         // Store annotation on dataset sequences only
1391         AlignmentAnnotation[] aa = sq.getAnnotation();
1392         if (aa != null && aa.length > 0)
1393         {
1394           storeAlignmentAnnotation(aa, groupRefs, av, calcIdSet, storeDS,
1395                   vamsasSet);
1396         }
1397       }
1398     }
1399     else
1400     {
1401       if (jal.getAlignmentAnnotation() != null)
1402       {
1403         // Store the annotation shown on the alignment.
1404         AlignmentAnnotation[] aa = jal.getAlignmentAnnotation();
1405         storeAlignmentAnnotation(aa, groupRefs, av, calcIdSet, storeDS,
1406                 vamsasSet);
1407       }
1408     }
1409     // SAVE GROUPS
1410     if (jal.getGroups() != null)
1411     {
1412       JGroup[] groups = new JGroup[jal.getGroups().size()];
1413       int i = -1;
1414       for (jalview.datamodel.SequenceGroup sg : jal.getGroups())
1415       {
1416         JGroup jGroup = new JGroup();
1417         groups[++i] = jGroup;
1418
1419         jGroup.setStart(sg.getStartRes());
1420         jGroup.setEnd(sg.getEndRes());
1421         jGroup.setName(sg.getName());
1422         if (groupRefs.containsKey(sg))
1423         {
1424           // group has references so set its ID field
1425           jGroup.setId(groupRefs.get(sg));
1426         }
1427         ColourSchemeI colourScheme = sg.getColourScheme();
1428         if (colourScheme != null)
1429         {
1430           ResidueShaderI groupColourScheme = sg.getGroupColourScheme();
1431           if (groupColourScheme.conservationApplied())
1432           {
1433             jGroup.setConsThreshold(groupColourScheme.getConservationInc());
1434
1435             if (colourScheme instanceof jalview.schemes.UserColourScheme)
1436             {
1437               jGroup.setColour(setUserColourScheme(colourScheme,
1438                       userColours, object));
1439             }
1440             else
1441             {
1442               jGroup.setColour(colourScheme.getSchemeName());
1443             }
1444           }
1445           else if (colourScheme instanceof jalview.schemes.AnnotationColourGradient)
1446           {
1447             jGroup.setColour("AnnotationColourGradient");
1448             jGroup.setAnnotationColours(constructAnnotationColours(
1449                     (jalview.schemes.AnnotationColourGradient) colourScheme,
1450                     userColours, object));
1451           }
1452           else if (colourScheme instanceof jalview.schemes.UserColourScheme)
1453           {
1454             jGroup.setColour(
1455                     setUserColourScheme(colourScheme, userColours, object));
1456           }
1457           else
1458           {
1459             jGroup.setColour(colourScheme.getSchemeName());
1460           }
1461
1462           jGroup.setPidThreshold(groupColourScheme.getThreshold());
1463         }
1464
1465         jGroup.setOutlineColour(sg.getOutlineColour().getRGB());
1466         jGroup.setDisplayBoxes(sg.getDisplayBoxes());
1467         jGroup.setDisplayText(sg.getDisplayText());
1468         jGroup.setColourText(sg.getColourText());
1469         jGroup.setTextCol1(sg.textColour.getRGB());
1470         jGroup.setTextCol2(sg.textColour2.getRGB());
1471         jGroup.setTextColThreshold(sg.thresholdTextColour);
1472         jGroup.setShowUnconserved(sg.getShowNonconserved());
1473         jGroup.setIgnoreGapsinConsensus(sg.getIgnoreGapsConsensus());
1474         jGroup.setShowConsensusHistogram(sg.isShowConsensusHistogram());
1475         jGroup.setShowSequenceLogo(sg.isShowSequenceLogo());
1476         jGroup.setNormaliseSequenceLogo(sg.isNormaliseSequenceLogo());
1477         for (SequenceI seq : sg.getSequences())
1478         {
1479           // jGroup.addSeq(seqHash(seq));
1480           jGroup.getSeq().add(seqHash(seq));
1481         }
1482       }
1483
1484       // jms.setJGroup(groups);
1485       Object group;
1486       for (JGroup grp : groups)
1487       {
1488         object.getJGroup().add(grp);
1489       }
1490     }
1491     if (!storeDS)
1492     {
1493       // /////////SAVE VIEWPORT
1494       Viewport view = new Viewport();
1495       view.setTitle(ap.alignFrame.getTitle());
1496       view.setSequenceSetId(
1497               makeHashCode(av.getSequenceSetId(), av.getSequenceSetId()));
1498       view.setId(av.getViewId());
1499       if (av.getCodingComplement() != null)
1500       {
1501         view.setComplementId(av.getCodingComplement().getViewId());
1502       }
1503       view.setViewName(av.getViewName());
1504       view.setGatheredViews(av.isGatherViewsHere());
1505
1506       Rectangle size = ap.av.getExplodedGeometry();
1507       Rectangle position = size;
1508       if (size == null)
1509       {
1510         size = ap.alignFrame.getBounds();
1511         if (av.getCodingComplement() != null)
1512         {
1513           position = ((SplitFrame) ap.alignFrame.getSplitViewContainer())
1514                   .getBounds();
1515         }
1516         else
1517         {
1518           position = size;
1519         }
1520       }
1521       view.setXpos(position.x);
1522       view.setYpos(position.y);
1523
1524       view.setWidth(size.width);
1525       view.setHeight(size.height);
1526
1527       view.setStartRes(vpRanges.getStartRes());
1528       view.setStartSeq(vpRanges.getStartSeq());
1529
1530       OverviewPanel ov = ap.getOverviewPanel();
1531       if (ov != null)
1532       {
1533         Overview overview = new Overview();
1534         overview.setTitle(ov.getTitle());
1535         Rectangle bounds = ov.getFrameBounds();
1536         overview.setXpos(bounds.x);
1537         overview.setYpos(bounds.y);
1538         overview.setWidth(bounds.width);
1539         overview.setHeight(bounds.height);
1540         overview.setShowHidden(ov.isShowHiddenRegions());
1541         overview.setGapColour(ov.getCanvas().getGapColour().getRGB());
1542         overview.setResidueColour(
1543                 ov.getCanvas().getResidueColour().getRGB());
1544         overview.setHiddenColour(ov.getCanvas().getHiddenColour().getRGB());
1545         view.setOverview(overview);
1546       }
1547       if (av.getGlobalColourScheme() instanceof jalview.schemes.UserColourScheme)
1548       {
1549         view.setBgColour(setUserColourScheme(av.getGlobalColourScheme(),
1550                 userColours, object));
1551       }
1552       else if (av
1553               .getGlobalColourScheme() instanceof jalview.schemes.AnnotationColourGradient)
1554       {
1555         AnnotationColourScheme ac = constructAnnotationColours(
1556                 (jalview.schemes.AnnotationColourGradient) av
1557                         .getGlobalColourScheme(),
1558                 userColours, object);
1559
1560         view.setAnnotationColours(ac);
1561         view.setBgColour("AnnotationColourGradient");
1562       }
1563       else
1564       {
1565         view.setBgColour(ColourSchemeProperty
1566                 .getColourName(av.getGlobalColourScheme()));
1567       }
1568
1569       ResidueShaderI vcs = av.getResidueShading();
1570       ColourSchemeI cs = av.getGlobalColourScheme();
1571
1572       if (cs != null)
1573       {
1574         if (vcs.conservationApplied())
1575         {
1576           view.setConsThreshold(vcs.getConservationInc());
1577           if (cs instanceof jalview.schemes.UserColourScheme)
1578           {
1579             view.setBgColour(setUserColourScheme(cs, userColours, object));
1580           }
1581         }
1582         view.setPidThreshold(vcs.getThreshold());
1583       }
1584
1585       view.setConservationSelected(av.getConservationSelected());
1586       view.setPidSelected(av.getAbovePIDThreshold());
1587       view.setCharHeight(av.getCharHeight());
1588       view.setCharWidth(av.getCharWidth());
1589       final Font font = av.getFont();
1590       view.setFontName(font.getName());
1591       view.setFontSize(font.getSize());
1592       view.setFontStyle(font.getStyle());
1593       view.setScaleProteinAsCdna(av.getViewStyle().isScaleProteinAsCdna());
1594       view.setRenderGaps(av.isRenderGaps());
1595       view.setShowAnnotation(av.isShowAnnotation());
1596       view.setShowBoxes(av.getShowBoxes());
1597       view.setShowColourText(av.getColourText());
1598       view.setShowFullId(av.getShowJVSuffix());
1599       view.setRightAlignIds(av.isRightAlignIds());
1600       view.setShowSequenceFeatures(av.isShowSequenceFeatures());
1601       view.setShowText(av.getShowText());
1602       view.setShowUnconserved(av.getShowUnconserved());
1603       view.setWrapAlignment(av.getWrapAlignment());
1604       view.setTextCol1(av.getTextColour().getRGB());
1605       view.setTextCol2(av.getTextColour2().getRGB());
1606       view.setTextColThreshold(av.getThresholdTextColour());
1607       view.setShowConsensusHistogram(av.isShowConsensusHistogram());
1608       view.setShowSequenceLogo(av.isShowSequenceLogo());
1609       view.setNormaliseSequenceLogo(av.isNormaliseSequenceLogo());
1610       view.setShowGroupConsensus(av.isShowGroupConsensus());
1611       view.setShowGroupConservation(av.isShowGroupConservation());
1612       view.setShowNPfeatureTooltip(av.isShowNPFeats());
1613       view.setShowDbRefTooltip(av.isShowDBRefs());
1614       view.setFollowHighlight(av.isFollowHighlight());
1615       view.setFollowSelection(av.followSelection);
1616       view.setIgnoreGapsinConsensus(av.isIgnoreGapsConsensus());
1617       view.setShowComplementFeatures(av.isShowComplementFeatures());
1618       view.setShowComplementFeaturesOnTop(
1619               av.isShowComplementFeaturesOnTop());
1620       if (av.getFeaturesDisplayed() != null)
1621       {
1622         FeatureSettings fs = new FeatureSettings();
1623
1624         FeatureRendererModel fr = ap.getSeqPanel().seqCanvas
1625                 .getFeatureRenderer();
1626         String[] renderOrder = fr.getRenderOrder().toArray(new String[0]);
1627
1628         Vector<String> settingsAdded = new Vector<>();
1629         if (renderOrder != null)
1630         {
1631           for (String featureType : renderOrder)
1632           {
1633             FeatureSettings.Setting setting = new FeatureSettings.Setting();
1634             setting.setType(featureType);
1635
1636             /*
1637              * save any filter for the feature type
1638              */
1639             FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
1640             if (filter != null)
1641             {
1642               Iterator<FeatureMatcherI> filters = filter.getMatchers()
1643                       .iterator();
1644               FeatureMatcherI firstFilter = filters.next();
1645               setting.setMatcherSet(Jalview2XML.marshalFilter(firstFilter,
1646                       filters, filter.isAnded()));
1647             }
1648
1649             /*
1650              * save colour scheme for the feature type
1651              */
1652             FeatureColourI fcol = fr.getFeatureStyle(featureType);
1653             if (!fcol.isSimpleColour())
1654             {
1655               setting.setColour(fcol.getMaxColour().getRGB());
1656               setting.setMincolour(fcol.getMinColour().getRGB());
1657               setting.setMin(fcol.getMin());
1658               setting.setMax(fcol.getMax());
1659               setting.setColourByLabel(fcol.isColourByLabel());
1660               if (fcol.isColourByAttribute())
1661               {
1662                 String[] attName = fcol.getAttributeName();
1663                 setting.getAttributeName().add(attName[0]);
1664                 if (attName.length > 1)
1665                 {
1666                   setting.getAttributeName().add(attName[1]);
1667                 }
1668               }
1669               setting.setAutoScale(fcol.isAutoScaled());
1670               setting.setThreshold(fcol.getThreshold());
1671               Color noColour = fcol.getNoColour();
1672               if (noColour == null)
1673               {
1674                 setting.setNoValueColour(NoValueColour.NONE);
1675               }
1676               else if (noColour.equals(fcol.getMaxColour()))
1677               {
1678                 setting.setNoValueColour(NoValueColour.MAX);
1679               }
1680               else
1681               {
1682                 setting.setNoValueColour(NoValueColour.MIN);
1683               }
1684               // -1 = No threshold, 0 = Below, 1 = Above
1685               setting.setThreshstate(fcol.isAboveThreshold() ? 1
1686                       : (fcol.isBelowThreshold() ? 0 : -1));
1687             }
1688             else
1689             {
1690               setting.setColour(fcol.getColour().getRGB());
1691             }
1692
1693             setting.setDisplay(
1694                     av.getFeaturesDisplayed().isVisible(featureType));
1695             float rorder = fr.getOrder(featureType);
1696             if (rorder > -1)
1697             {
1698               setting.setOrder(rorder);
1699             }
1700             /// fs.addSetting(setting);
1701             fs.getSetting().add(setting);
1702             settingsAdded.addElement(featureType);
1703           }
1704         }
1705
1706         // is groups actually supposed to be a map here ?
1707         Iterator<String> en = fr.getFeatureGroups().iterator();
1708         Vector<String> groupsAdded = new Vector<>();
1709         while (en.hasNext())
1710         {
1711           String grp = en.next();
1712           if (groupsAdded.contains(grp))
1713           {
1714             continue;
1715           }
1716           Group g = new Group();
1717           g.setName(grp);
1718           g.setDisplay(((Boolean) fr.checkGroupVisibility(grp, false))
1719                   .booleanValue());
1720           // fs.addGroup(g);
1721           fs.getGroup().add(g);
1722           groupsAdded.addElement(grp);
1723         }
1724         // jms.setFeatureSettings(fs);
1725         object.setFeatureSettings(fs);
1726       }
1727
1728       if (av.hasHiddenColumns())
1729       {
1730         jalview.datamodel.HiddenColumns hidden = av.getAlignment()
1731                 .getHiddenColumns();
1732         if (hidden == null)
1733         {
1734           Console.warn(
1735                   "REPORT BUG: avoided null columnselection bug (DMAM reported). Please contact Jim about this.");
1736         }
1737         else
1738         {
1739           Iterator<int[]> hiddenRegions = hidden.iterator();
1740           while (hiddenRegions.hasNext())
1741           {
1742             int[] region = hiddenRegions.next();
1743             HiddenColumns hc = new HiddenColumns();
1744             hc.setStart(region[0]);
1745             hc.setEnd(region[1]);
1746             // view.addHiddenColumns(hc);
1747             view.getHiddenColumns().add(hc);
1748           }
1749         }
1750       }
1751       if (calcIdSet.size() > 0)
1752       {
1753         for (String calcId : calcIdSet)
1754         {
1755           if (calcId.trim().length() > 0)
1756           {
1757             CalcIdParam cidp = createCalcIdParam(calcId, av);
1758             // Some calcIds have no parameters.
1759             if (cidp != null)
1760             {
1761               // view.addCalcIdParam(cidp);
1762               view.getCalcIdParam().add(cidp);
1763             }
1764           }
1765         }
1766       }
1767
1768       // jms.addViewport(view);
1769       object.getViewport().add(view);
1770     }
1771     // object.setJalviewModelSequence(jms);
1772     // object.getVamsasModel().addSequenceSet(vamsasSet);
1773     object.getVamsasModel().getSequenceSet().add(vamsasSet);
1774
1775     if (jout != null && fileName != null)
1776     {
1777       // We may not want to write the object to disk,
1778       // eg we can copy the alignViewport to a new view object
1779       // using save and then load
1780       try
1781       {
1782         fileName = fileName.replace('\\', '/');
1783         jalview.bin.Console.outPrintln("Writing jar entry " + fileName);
1784         JarEntry entry = new JarEntry(fileName);
1785         jout.putNextEntry(entry);
1786         PrintWriter pout = new PrintWriter(
1787                 new OutputStreamWriter(jout, UTF_8));
1788         JAXBContext jaxbContext = JAXBContext
1789                 .newInstance(JalviewModel.class);
1790         Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
1791
1792         // output pretty printed
1793         // jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
1794         jaxbMarshaller.marshal(
1795                 new ObjectFactory().createJalviewModel(object), pout);
1796
1797         // jaxbMarshaller.marshal(object, pout);
1798         // marshaller.marshal(object);
1799         pout.flush();
1800         jout.closeEntry();
1801       } catch (Exception ex)
1802       {
1803         // TODO: raise error in GUI if marshalling failed.
1804         jalview.bin.Console.errPrintln("Error writing Jalview project");
1805         ex.printStackTrace();
1806       }
1807     }
1808     return object;
1809   }
1810
1811   /**
1812    * Writes PCA viewer attributes and computed values to an XML model object and
1813    * adds it to the JalviewModel. Any exceptions are reported by logging.
1814    */
1815   protected void savePCA(PCAPanel panel, JalviewModel object)
1816   {
1817     try
1818     {
1819       PcaViewer viewer = new PcaViewer();
1820       viewer.setHeight(panel.getHeight());
1821       viewer.setWidth(panel.getWidth());
1822       viewer.setXpos(panel.getX());
1823       viewer.setYpos(panel.getY());
1824       viewer.setTitle(panel.getTitle());
1825       PCAModel pcaModel = panel.getPcaModel();
1826       viewer.setScoreModelName(pcaModel.getScoreModelName());
1827       viewer.setXDim(panel.getSelectedDimensionIndex(X));
1828       viewer.setYDim(panel.getSelectedDimensionIndex(Y));
1829       viewer.setZDim(panel.getSelectedDimensionIndex(Z));
1830       viewer.setBgColour(
1831               panel.getRotatableCanvas().getBackgroundColour().getRGB());
1832       viewer.setScaleFactor(panel.getRotatableCanvas().getScaleFactor());
1833       float[] spMin = panel.getRotatableCanvas().getSeqMin();
1834       SeqPointMin spmin = new SeqPointMin();
1835       spmin.setXPos(spMin[0]);
1836       spmin.setYPos(spMin[1]);
1837       spmin.setZPos(spMin[2]);
1838       viewer.setSeqPointMin(spmin);
1839       float[] spMax = panel.getRotatableCanvas().getSeqMax();
1840       SeqPointMax spmax = new SeqPointMax();
1841       spmax.setXPos(spMax[0]);
1842       spmax.setYPos(spMax[1]);
1843       spmax.setZPos(spMax[2]);
1844       viewer.setSeqPointMax(spmax);
1845       viewer.setShowLabels(panel.getRotatableCanvas().isShowLabels());
1846       viewer.setLinkToAllViews(
1847               panel.getRotatableCanvas().isApplyToAllViews());
1848       SimilarityParamsI sp = pcaModel.getSimilarityParameters();
1849       viewer.setIncludeGaps(sp.includeGaps());
1850       viewer.setMatchGaps(sp.matchGaps());
1851       viewer.setIncludeGappedColumns(sp.includeGappedColumns());
1852       viewer.setDenominateByShortestLength(sp.denominateByShortestLength());
1853
1854       /*
1855        * sequence points on display
1856        */
1857       for (jalview.datamodel.SequencePoint spt : pcaModel
1858               .getSequencePoints())
1859       {
1860         SequencePoint point = new SequencePoint();
1861         point.setSequenceRef(seqHash(spt.getSequence()));
1862         point.setXPos(spt.coord.x);
1863         point.setYPos(spt.coord.y);
1864         point.setZPos(spt.coord.z);
1865         viewer.getSequencePoint().add(point);
1866       }
1867
1868       /*
1869        * (end points of) axes on display
1870        */
1871       for (Point p : panel.getRotatableCanvas().getAxisEndPoints())
1872       {
1873
1874         Axis axis = new Axis();
1875         axis.setXPos(p.x);
1876         axis.setYPos(p.y);
1877         axis.setZPos(p.z);
1878         viewer.getAxis().add(axis);
1879       }
1880
1881       /*
1882        * raw PCA data (note we are not restoring PCA inputs here -
1883        * alignment view, score model, similarity parameters)
1884        */
1885       PcaDataType data = new PcaDataType();
1886       viewer.setPcaData(data);
1887       PCA pca = pcaModel.getPcaData();
1888
1889       DoubleMatrix pm = new DoubleMatrix();
1890       saveDoubleMatrix(pca.getPairwiseScores(), pm);
1891       data.setPairwiseMatrix(pm);
1892
1893       DoubleMatrix tm = new DoubleMatrix();
1894       saveDoubleMatrix(pca.getTridiagonal(), tm);
1895       data.setTridiagonalMatrix(tm);
1896
1897       DoubleMatrix eigenMatrix = new DoubleMatrix();
1898       data.setEigenMatrix(eigenMatrix);
1899       saveDoubleMatrix(pca.getEigenmatrix(), eigenMatrix);
1900
1901       object.getPcaViewer().add(viewer);
1902     } catch (Throwable t)
1903     {
1904       Console.error("Error saving PCA: " + t.getMessage());
1905     }
1906   }
1907
1908   /**
1909    * Stores values from a matrix into an XML element, including (if present) the
1910    * D or E vectors
1911    * 
1912    * @param m
1913    * @param xmlMatrix
1914    * @see #loadDoubleMatrix(DoubleMatrix)
1915    */
1916   protected void saveDoubleMatrix(MatrixI m, DoubleMatrix xmlMatrix)
1917   {
1918     xmlMatrix.setRows(m.height());
1919     xmlMatrix.setColumns(m.width());
1920     for (int i = 0; i < m.height(); i++)
1921     {
1922       DoubleVector row = new DoubleVector();
1923       for (int j = 0; j < m.width(); j++)
1924       {
1925         row.getV().add(m.getValue(i, j));
1926       }
1927       xmlMatrix.getRow().add(row);
1928     }
1929     if (m.getD() != null)
1930     {
1931       DoubleVector dVector = new DoubleVector();
1932       for (double d : m.getD())
1933       {
1934         dVector.getV().add(d);
1935       }
1936       xmlMatrix.setD(dVector);
1937     }
1938     if (m.getE() != null)
1939     {
1940       DoubleVector eVector = new DoubleVector();
1941       for (double e : m.getE())
1942       {
1943         eVector.getV().add(e);
1944       }
1945       xmlMatrix.setE(eVector);
1946     }
1947   }
1948
1949   /**
1950    * Loads XML matrix data into a new Matrix object, including the D and/or E
1951    * vectors (if present)
1952    * 
1953    * @param mData
1954    * @return
1955    * @see Jalview2XML#saveDoubleMatrix(MatrixI, DoubleMatrix)
1956    */
1957   protected MatrixI loadDoubleMatrix(DoubleMatrix mData)
1958   {
1959     int rows = mData.getRows();
1960     double[][] vals = new double[rows][];
1961
1962     for (int i = 0; i < rows; i++)
1963     {
1964       List<Double> dVector = mData.getRow().get(i).getV();
1965       vals[i] = new double[dVector.size()];
1966       int dvi = 0;
1967       for (Double d : dVector)
1968       {
1969         vals[i][dvi++] = d;
1970       }
1971     }
1972
1973     MatrixI m = new Matrix(vals);
1974
1975     if (mData.getD() != null)
1976     {
1977       List<Double> dVector = mData.getD().getV();
1978       double[] vec = new double[dVector.size()];
1979       int dvi = 0;
1980       for (Double d : dVector)
1981       {
1982         vec[dvi++] = d;
1983       }
1984       m.setD(vec);
1985     }
1986     if (mData.getE() != null)
1987     {
1988       List<Double> dVector = mData.getE().getV();
1989       double[] vec = new double[dVector.size()];
1990       int dvi = 0;
1991       for (Double d : dVector)
1992       {
1993         vec[dvi++] = d;
1994       }
1995       m.setE(vec);
1996     }
1997
1998     return m;
1999   }
2000
2001   /**
2002    * Save any Varna viewers linked to this sequence. Writes an rnaViewer element
2003    * for each viewer, with
2004    * <ul>
2005    * <li>viewer geometry (position, size, split pane divider location)</li>
2006    * <li>index of the selected structure in the viewer (currently shows gapped
2007    * or ungapped)</li>
2008    * <li>the id of the annotation holding RNA secondary structure</li>
2009    * <li>(currently only one SS is shown per viewer, may be more in future)</li>
2010    * </ul>
2011    * Varna viewer state is also written out (in native Varna XML) to separate
2012    * project jar entries. A separate entry is written for each RNA structure
2013    * displayed, with the naming convention
2014    * <ul>
2015    * <li>rna_viewId_sequenceId_annotationId_[gapped|trimmed]</li>
2016    * </ul>
2017    * 
2018    * @param jout
2019    * @param jseq
2020    * @param jds
2021    * @param viewIds
2022    * @param ap
2023    * @param storeDataset
2024    */
2025   protected void saveRnaViewers(JarOutputStream jout, JSeq jseq,
2026           final SequenceI jds, List<String> viewIds, AlignmentPanel ap,
2027           boolean storeDataset)
2028   {
2029     if (Desktop.desktop == null)
2030     {
2031       return;
2032     }
2033     JInternalFrame[] frames = Desktop.desktop.getAllFrames();
2034     for (int f = frames.length - 1; f > -1; f--)
2035     {
2036       if (frames[f] instanceof AppVarna)
2037       {
2038         AppVarna varna = (AppVarna) frames[f];
2039         /*
2040          * link the sequence to every viewer that is showing it and is linked to
2041          * its alignment panel
2042          */
2043         if (varna.isListeningFor(jds) && ap == varna.getAlignmentPanel())
2044         {
2045           String viewId = varna.getViewId();
2046           RnaViewer rna = new RnaViewer();
2047           rna.setViewId(viewId);
2048           rna.setTitle(varna.getTitle());
2049           rna.setXpos(varna.getX());
2050           rna.setYpos(varna.getY());
2051           rna.setWidth(varna.getWidth());
2052           rna.setHeight(varna.getHeight());
2053           rna.setDividerLocation(varna.getDividerLocation());
2054           rna.setSelectedRna(varna.getSelectedIndex());
2055           // jseq.addRnaViewer(rna);
2056           jseq.getRnaViewer().add(rna);
2057
2058           /*
2059            * Store each Varna panel's state once in the project per sequence.
2060            * First time through only (storeDataset==false)
2061            */
2062           // boolean storeSessions = false;
2063           // String sequenceViewId = viewId + seqsToIds.get(jds);
2064           // if (!storeDataset && !viewIds.contains(sequenceViewId))
2065           // {
2066           // viewIds.add(sequenceViewId);
2067           // storeSessions = true;
2068           // }
2069           for (RnaModel model : varna.getModels())
2070           {
2071             if (model.seq == jds)
2072             {
2073               /*
2074                * VARNA saves each view (sequence or alignment secondary
2075                * structure, gapped or trimmed) as a separate XML file
2076                */
2077               String jarEntryName = rnaSessions.get(model);
2078               if (jarEntryName == null)
2079               {
2080
2081                 String varnaStateFile = varna.getStateInfo(model.rna);
2082                 jarEntryName = RNA_PREFIX + viewId + "_" + nextCounter();
2083                 copyFileToJar(jout, varnaStateFile, jarEntryName, "Varna");
2084                 rnaSessions.put(model, jarEntryName);
2085               }
2086               SecondaryStructure ss = new SecondaryStructure();
2087               String annotationId = varna.getAnnotation(jds).annotationId;
2088               ss.setAnnotationId(annotationId);
2089               ss.setViewerState(jarEntryName);
2090               ss.setGapped(model.gapped);
2091               ss.setTitle(model.title);
2092               // rna.addSecondaryStructure(ss);
2093               rna.getSecondaryStructure().add(ss);
2094             }
2095           }
2096         }
2097       }
2098     }
2099   }
2100
2101   /**
2102    * Copy the contents of a file to a new entry added to the output jar
2103    * 
2104    * @param jout
2105    * @param infilePath
2106    * @param jarEntryName
2107    * @param msg
2108    *          additional identifying info to log to the console
2109    */
2110   protected void copyFileToJar(JarOutputStream jout, String infilePath,
2111           String jarEntryName, String msg)
2112   {
2113     try (InputStream is = new FileInputStream(infilePath))
2114     {
2115       File file = new File(infilePath);
2116       if (file.exists() && jout != null)
2117       {
2118         jalview.bin.Console.outPrintln(
2119                 "Writing jar entry " + jarEntryName + " (" + msg + ")");
2120         jout.putNextEntry(new JarEntry(jarEntryName));
2121         copyAll(is, jout);
2122         jout.closeEntry();
2123         // dis = new DataInputStream(new FileInputStream(file));
2124         // byte[] data = new byte[(int) file.length()];
2125         // dis.readFully(data);
2126         // writeJarEntry(jout, jarEntryName, data);
2127       }
2128     } catch (Exception ex)
2129     {
2130       ex.printStackTrace();
2131     }
2132   }
2133
2134   /**
2135    * Copies input to output, in 4K buffers; handles any data (text or binary)
2136    * 
2137    * @param in
2138    * @param out
2139    * @throws IOException
2140    */
2141   protected void copyAll(InputStream in, OutputStream out)
2142           throws IOException
2143   {
2144     byte[] buffer = new byte[4096];
2145     int bytesRead = 0;
2146     while ((bytesRead = in.read(buffer)) != -1)
2147     {
2148       out.write(buffer, 0, bytesRead);
2149     }
2150   }
2151
2152   /**
2153    * Save the state of a structure viewer
2154    * 
2155    * @param ap
2156    * @param jds
2157    * @param pdb
2158    *          the archive XML element under which to save the state
2159    * @param entry
2160    * @param viewIds
2161    * @param matchedFile
2162    * @param viewFrame
2163    * @return
2164    */
2165   protected String saveStructureViewer(AlignmentPanel ap, SequenceI jds,
2166           Pdbids pdb, PDBEntry entry, List<String> viewIds,
2167           String matchedFile, StructureViewerBase viewFrame)
2168   {
2169     final AAStructureBindingModel bindingModel = viewFrame.getBinding();
2170
2171     /*
2172      * Look for any bindings for this viewer to the PDB file of interest
2173      * (including part matches excluding chain id)
2174      */
2175     for (int peid = 0; peid < bindingModel.getPdbCount(); peid++)
2176     {
2177       final PDBEntry pdbentry = bindingModel.getPdbEntry(peid);
2178       final String pdbId = pdbentry.getId();
2179       if (!pdbId.equals(entry.getId()) && !(entry.getId().length() > 4
2180               && entry.getId().toLowerCase(Locale.ROOT)
2181                       .startsWith(pdbId.toLowerCase(Locale.ROOT))))
2182       {
2183         /*
2184          * not interested in a binding to a different PDB entry here
2185          */
2186         continue;
2187       }
2188       if (matchedFile == null)
2189       {
2190         matchedFile = pdbentry.getFile();
2191       }
2192       else if (!matchedFile.equals(pdbentry.getFile()))
2193       {
2194         Console.warn(
2195                 "Probably lost some PDB-Sequence mappings for this structure file (which apparently has same PDB Entry code): "
2196                         + pdbentry.getFile());
2197       }
2198       // record the
2199       // file so we
2200       // can get at it if the ID
2201       // match is ambiguous (e.g.
2202       // 1QIP==1qipA)
2203
2204       for (int smap = 0; smap < viewFrame.getBinding()
2205               .getSequence()[peid].length; smap++)
2206       {
2207         // if (jal.findIndex(jmol.jmb.sequence[peid][smap]) > -1)
2208         if (jds == viewFrame.getBinding().getSequence()[peid][smap])
2209         {
2210           StructureState state = new StructureState();
2211           state.setVisible(true);
2212           state.setXpos(viewFrame.getX());
2213           state.setYpos(viewFrame.getY());
2214           state.setWidth(viewFrame.getWidth());
2215           state.setHeight(viewFrame.getHeight());
2216           final String viewId = viewFrame.getViewId();
2217           state.setViewId(viewId);
2218           state.setAlignwithAlignPanel(viewFrame.isUsedforaligment(ap));
2219           state.setColourwithAlignPanel(viewFrame.isUsedForColourBy(ap));
2220           state.setColourByJmol(viewFrame.isColouredByViewer());
2221           state.setType(viewFrame.getViewerType().toString());
2222           // pdb.addStructureState(state);
2223           pdb.getStructureState().add(state);
2224         }
2225       }
2226     }
2227     return matchedFile;
2228   }
2229
2230   /**
2231    * Populates the AnnotationColourScheme xml for save. This captures the
2232    * settings of the options in the 'Colour by Annotation' dialog.
2233    * 
2234    * @param acg
2235    * @param userColours
2236    * @param jm
2237    * @return
2238    */
2239   private AnnotationColourScheme constructAnnotationColours(
2240           AnnotationColourGradient acg, List<UserColourScheme> userColours,
2241           JalviewModel jm)
2242   {
2243     AnnotationColourScheme ac = new AnnotationColourScheme();
2244     ac.setAboveThreshold(acg.getAboveThreshold());
2245     ac.setThreshold(acg.getAnnotationThreshold());
2246     // 2.10.2 save annotationId (unique) not annotation label
2247     ac.setAnnotation(acg.getAnnotation().annotationId);
2248     if (acg.getBaseColour() instanceof UserColourScheme)
2249     {
2250       ac.setColourScheme(
2251               setUserColourScheme(acg.getBaseColour(), userColours, jm));
2252     }
2253     else
2254     {
2255       ac.setColourScheme(
2256               ColourSchemeProperty.getColourName(acg.getBaseColour()));
2257     }
2258
2259     ac.setMaxColour(acg.getMaxColour().getRGB());
2260     ac.setMinColour(acg.getMinColour().getRGB());
2261     ac.setPerSequence(acg.isSeqAssociated());
2262     ac.setPredefinedColours(acg.isPredefinedColours());
2263     return ac;
2264   }
2265
2266   private void storeAlignmentAnnotation(AlignmentAnnotation[] aa,
2267           IdentityHashMap<SequenceGroup, String> groupRefs,
2268           AlignmentViewport av, Set<String> calcIdSet, boolean storeDS,
2269           SequenceSet vamsasSet)
2270   {
2271
2272     for (int i = 0; i < aa.length; i++)
2273     {
2274       Annotation an = new Annotation();
2275
2276       AlignmentAnnotation annotation = aa[i];
2277       if (annotation.annotationId != null)
2278       {
2279         annotationIds.put(annotation.annotationId, annotation);
2280       }
2281
2282       an.setId(annotation.annotationId);
2283
2284       an.setVisible(annotation.visible);
2285
2286       an.setDescription(annotation.description);
2287
2288       if (annotation.sequenceRef != null)
2289       {
2290         // 2.9 JAL-1781 xref on sequence id rather than name
2291         an.setSequenceRef(seqsToIds.get(annotation.sequenceRef));
2292       }
2293       if (annotation.groupRef != null)
2294       {
2295         String groupIdr = groupRefs.get(annotation.groupRef);
2296         if (groupIdr == null)
2297         {
2298           // make a locally unique String
2299           groupRefs.put(annotation.groupRef,
2300                   groupIdr = ("" + System.currentTimeMillis()
2301                           + annotation.groupRef.getName()
2302                           + groupRefs.size()));
2303         }
2304         an.setGroupRef(groupIdr.toString());
2305       }
2306
2307       // store all visualization attributes for annotation
2308       an.setGraphHeight(annotation.graphHeight);
2309       an.setCentreColLabels(annotation.centreColLabels);
2310       an.setScaleColLabels(annotation.scaleColLabel);
2311       an.setShowAllColLabels(annotation.showAllColLabels);
2312       an.setBelowAlignment(annotation.belowAlignment);
2313
2314       if (annotation.graph > 0)
2315       {
2316         an.setGraph(true);
2317         an.setGraphType(annotation.graph);
2318         an.setGraphGroup(annotation.graphGroup);
2319         if (annotation.getThreshold() != null)
2320         {
2321           ThresholdLine line = new ThresholdLine();
2322           line.setLabel(annotation.getThreshold().label);
2323           line.setValue(annotation.getThreshold().value);
2324           line.setColour(annotation.getThreshold().colour.getRGB());
2325           an.setThresholdLine(line);
2326         }
2327         if (annotation.graph == AlignmentAnnotation.CONTACT_MAP)
2328         {
2329           if (annotation.sequenceRef.getContactMaps() != null)
2330           {
2331             ContactMatrixI cm = annotation.sequenceRef
2332                     .getContactMatrixFor(annotation);
2333             if (cm != null)
2334             {
2335               MatrixType xmlmat = new MatrixType();
2336               xmlmat.setType(cm.getType());
2337               xmlmat.setRows(BigInteger.valueOf(cm.getWidth()));
2338               xmlmat.setCols(BigInteger.valueOf(cm.getHeight()));
2339               // consider using an opaque to/from -> allow instance to control
2340               // its representation ?
2341               xmlmat.setElements(ContactMatrix.contactToFloatString(cm));
2342               if (cm.hasGroups())
2343               {
2344                 for (BitSet gp : cm.getGroups())
2345                 {
2346                   xmlmat.getGroups().add(stringifyBitset(gp));
2347                 }
2348               }
2349               if (cm.hasTree())
2350               {
2351                 // provenance object for tree ?
2352                 xmlmat.getNewick().add(cm.getNewick());
2353                 xmlmat.setTreeMethod(cm.getTreeMethod());
2354               }
2355               if (cm.hasCutHeight())
2356               {
2357                 xmlmat.setCutHeight(cm.getCutHeight());
2358               }
2359               // set/get properties
2360               if (cm instanceof MappableContactMatrixI)
2361               {
2362                 jalview.util.MapList mlst = ((MappableContactMatrixI) cm)
2363                         .getMapFor(annotation.sequenceRef);
2364                 if (mlst != null)
2365                 {
2366                   MapListType mp = new MapListType();
2367                   List<int[]> r = mlst.getFromRanges();
2368                   for (int[] range : r)
2369                   {
2370                     MapListFrom mfrom = new MapListFrom();
2371                     mfrom.setStart(range[0]);
2372                     mfrom.setEnd(range[1]);
2373                     // mp.addMapListFrom(mfrom);
2374                     mp.getMapListFrom().add(mfrom);
2375                   }
2376                   r = mlst.getToRanges();
2377                   for (int[] range : r)
2378                   {
2379                     MapListTo mto = new MapListTo();
2380                     mto.setStart(range[0]);
2381                     mto.setEnd(range[1]);
2382                     // mp.addMapListTo(mto);
2383                     mp.getMapListTo().add(mto);
2384                   }
2385                   mp.setMapFromUnit(
2386                           BigInteger.valueOf(mlst.getFromRatio()));
2387                   mp.setMapToUnit(BigInteger.valueOf(mlst.getToRatio()));
2388                   xmlmat.setMapping(mp);
2389                 }
2390               }
2391               // and add to model
2392               an.getContactmatrix().add(xmlmat);
2393             }
2394           }
2395         }
2396       }
2397       else
2398       {
2399         an.setGraph(false);
2400       }
2401
2402       an.setLabel(annotation.label);
2403
2404       if (annotation == av.getAlignmentQualityAnnot()
2405               || annotation == av.getAlignmentConservationAnnotation()
2406               || annotation == av.getAlignmentConsensusAnnotation()
2407               || annotation.autoCalculated)
2408       {
2409         // new way of indicating autocalculated annotation -
2410         an.setAutoCalculated(annotation.autoCalculated);
2411       }
2412       if (annotation.hasScore())
2413       {
2414         an.setScore(annotation.getScore());
2415       }
2416
2417       if (annotation.getCalcId() != null)
2418       {
2419         calcIdSet.add(annotation.getCalcId());
2420         an.setCalcId(annotation.getCalcId());
2421       }
2422       if (annotation.hasProperties())
2423       {
2424         for (String pr : annotation.getProperties())
2425         {
2426           jalview.xml.binding.jalview.Property prop = new jalview.xml.binding.jalview.Property();
2427           prop.setName(pr);
2428           prop.setValue(annotation.getProperty(pr));
2429           an.getProperty().add(prop);
2430         }
2431       }
2432
2433       AnnotationElement ae;
2434       if (annotation.annotations != null)
2435       {
2436         an.setScoreOnly(false);
2437         for (int a = 0; a < annotation.annotations.length; a++)
2438         {
2439           if ((annotation == null) || (annotation.annotations[a] == null))
2440           {
2441             continue;
2442           }
2443
2444           ae = new AnnotationElement();
2445           if (annotation.annotations[a].description != null)
2446           {
2447             ae.setDescription(annotation.annotations[a].description);
2448           }
2449           if (annotation.annotations[a].displayCharacter != null)
2450           {
2451             ae.setDisplayCharacter(
2452                     annotation.annotations[a].displayCharacter);
2453           }
2454
2455           if (!Float.isNaN(annotation.annotations[a].value))
2456           {
2457             ae.setValue(annotation.annotations[a].value);
2458           }
2459
2460           ae.setPosition(a);
2461           if (annotation.annotations[a].secondaryStructure > ' ')
2462           {
2463             ae.setSecondaryStructure(
2464                     annotation.annotations[a].secondaryStructure + "");
2465           }
2466
2467           if (annotation.annotations[a].colour != null
2468                   && annotation.annotations[a].colour != java.awt.Color.black)
2469           {
2470             ae.setColour(annotation.annotations[a].colour.getRGB());
2471           }
2472
2473           // an.addAnnotationElement(ae);
2474           an.getAnnotationElement().add(ae);
2475           if (annotation.autoCalculated)
2476           {
2477             // only write one non-null entry into the annotation row -
2478             // sufficient to get the visualization attributes necessary to
2479             // display data
2480             continue;
2481           }
2482         }
2483       }
2484       else
2485       {
2486         an.setScoreOnly(true);
2487       }
2488       if (!storeDS || (storeDS && !annotation.autoCalculated))
2489       {
2490         // skip autocalculated annotation - these are only provided for
2491         // alignments
2492         // vamsasSet.addAnnotation(an);
2493         vamsasSet.getAnnotation().add(an);
2494       }
2495     }
2496
2497   }
2498
2499   private String stringifyBitset(BitSet gp)
2500   {
2501     StringBuilder sb = new StringBuilder();
2502     for (long val : gp.toLongArray())
2503     {
2504       if (sb.length() > 0)
2505       {
2506         sb.append(",");
2507       }
2508       sb.append(val);
2509     }
2510     return sb.toString();
2511   }
2512
2513   private BitSet deStringifyBitset(String stringified)
2514   {
2515     if ("".equals(stringified) || stringified == null)
2516     {
2517       return new BitSet();
2518     }
2519     String[] longvals = stringified.split(",");
2520     long[] newlongvals = new long[longvals.length];
2521     for (int lv = 0; lv < longvals.length; lv++)
2522     {
2523       try
2524       {
2525         newlongvals[lv] = Long.valueOf(longvals[lv]);
2526       } catch (Exception x)
2527       {
2528         errorMessage += "Couldn't destringify bitset from: '" + stringified
2529                 + "'";
2530         newlongvals[lv] = 0;
2531       }
2532     }
2533     return BitSet.valueOf(newlongvals);
2534
2535   }
2536
2537   private CalcIdParam createCalcIdParam(String calcId, AlignViewport av)
2538   {
2539     AutoCalcSetting settings = av.getCalcIdSettingsFor(calcId);
2540     if (settings != null)
2541     {
2542       CalcIdParam vCalcIdParam = new CalcIdParam();
2543       vCalcIdParam.setCalcId(calcId);
2544       // vCalcIdParam.addServiceURL(settings.getServiceURI());
2545       vCalcIdParam.getServiceURL().add(settings.getServiceURI());
2546       // generic URI allowing a third party to resolve another instance of the
2547       // service used for this calculation
2548       for (String url : settings.getServiceURLs())
2549       {
2550         // vCalcIdParam.addServiceURL(urls);
2551         vCalcIdParam.getServiceURL().add(url);
2552       }
2553       vCalcIdParam.setVersion("1.0");
2554       if (settings.getPreset() != null)
2555       {
2556         WsParamSetI setting = settings.getPreset();
2557         vCalcIdParam.setName(setting.getName());
2558         vCalcIdParam.setDescription(setting.getDescription());
2559       }
2560       else
2561       {
2562         vCalcIdParam.setName("");
2563         vCalcIdParam.setDescription("Last used parameters");
2564       }
2565       // need to be able to recover 1) settings 2) user-defined presets or
2566       // recreate settings from preset 3) predefined settings provided by
2567       // service - or settings that can be transferred (or discarded)
2568       vCalcIdParam.setParameters(
2569               settings.getWsParamFile().replace("\n", "|\\n|"));
2570       vCalcIdParam.setAutoUpdate(settings.isAutoUpdate());
2571       // todo - decide if updateImmediately is needed for any projects.
2572
2573       return vCalcIdParam;
2574     }
2575     return null;
2576   }
2577
2578   private boolean recoverCalcIdParam(CalcIdParam calcIdParam,
2579           AlignViewport av)
2580   {
2581     if (calcIdParam.getVersion().equals("1.0"))
2582     {
2583       final String[] calcIds = calcIdParam.getServiceURL()
2584               .toArray(new String[0]);
2585       Jws2Instance service = Jws2Discoverer.getDiscoverer()
2586               .getPreferredServiceFor(calcIds);
2587       if (service != null)
2588       {
2589         WsParamSetI parmSet = null;
2590         try
2591         {
2592           parmSet = service.getParamStore().parseServiceParameterFile(
2593                   calcIdParam.getName(), calcIdParam.getDescription(),
2594                   calcIds,
2595                   calcIdParam.getParameters().replace("|\\n|", "\n"));
2596         } catch (IOException x)
2597         {
2598           Console.warn("Couldn't parse parameter data for "
2599                   + calcIdParam.getCalcId(), x);
2600           return false;
2601         }
2602         List<ArgumentI> argList = null;
2603         if (calcIdParam.getName().length() > 0)
2604         {
2605           parmSet = service.getParamStore()
2606                   .getPreset(calcIdParam.getName());
2607           if (parmSet != null)
2608           {
2609             // TODO : check we have a good match with settings in AACon -
2610             // otherwise we'll need to create a new preset
2611           }
2612         }
2613         else
2614         {
2615           argList = parmSet.getArguments();
2616           parmSet = null;
2617         }
2618         AAConSettings settings = new AAConSettings(
2619                 calcIdParam.isAutoUpdate(), service, parmSet, argList);
2620         av.setCalcIdSettingsFor(calcIdParam.getCalcId(), settings,
2621                 calcIdParam.isNeedsUpdate());
2622         return true;
2623       }
2624       else
2625       {
2626         Console.warn(
2627                 "Cannot resolve a service for the parameters used in this project. Try configuring a JABAWS server.");
2628         return false;
2629       }
2630     }
2631     throw new Error(MessageManager.formatMessage(
2632             "error.unsupported_version_calcIdparam", new Object[]
2633             { calcIdParam.toString() }));
2634   }
2635
2636   /**
2637    * External mapping between jalview objects and objects yielding a valid and
2638    * unique object ID string. This is null for normal Jalview project IO, but
2639    * non-null when a jalview project is being read or written as part of a
2640    * vamsas session.
2641    */
2642   IdentityHashMap jv2vobj = null;
2643
2644   /**
2645    * Construct a unique ID for jvobj using either existing bindings or if none
2646    * exist, the result of the hashcode call for the object.
2647    * 
2648    * @param jvobj
2649    *          jalview data object
2650    * @return unique ID for referring to jvobj
2651    */
2652   private String makeHashCode(Object jvobj, String altCode)
2653   {
2654     if (jv2vobj != null)
2655     {
2656       Object id = jv2vobj.get(jvobj);
2657       if (id != null)
2658       {
2659         return id.toString();
2660       }
2661       // check string ID mappings
2662       if (jvids2vobj != null && jvobj instanceof String)
2663       {
2664         id = jvids2vobj.get(jvobj);
2665       }
2666       if (id != null)
2667       {
2668         return id.toString();
2669       }
2670       // give up and warn that something has gone wrong
2671       Console.warn(
2672               "Cannot find ID for object in external mapping : " + jvobj);
2673     }
2674     return altCode;
2675   }
2676
2677   /**
2678    * return local jalview object mapped to ID, if it exists
2679    * 
2680    * @param idcode
2681    *          (may be null)
2682    * @return null or object bound to idcode
2683    */
2684   private Object retrieveExistingObj(String idcode)
2685   {
2686     if (idcode != null && vobj2jv != null)
2687     {
2688       return vobj2jv.get(idcode);
2689     }
2690     return null;
2691   }
2692
2693   /**
2694    * binding from ID strings from external mapping table to jalview data model
2695    * objects.
2696    */
2697   private Hashtable vobj2jv;
2698
2699   private Sequence createVamsasSequence(String id, SequenceI jds)
2700   {
2701     return createVamsasSequence(true, id, jds, null);
2702   }
2703
2704   private Sequence createVamsasSequence(boolean recurse, String id,
2705           SequenceI jds, SequenceI parentseq)
2706   {
2707     Sequence vamsasSeq = new Sequence();
2708     vamsasSeq.setId(id);
2709     vamsasSeq.setName(jds.getName());
2710     vamsasSeq.setSequence(jds.getSequenceAsString());
2711     vamsasSeq.setDescription(jds.getDescription());
2712     List<DBRefEntry> dbrefs = null;
2713     if (jds.getDatasetSequence() != null)
2714     {
2715       vamsasSeq.setDsseqid(seqHash(jds.getDatasetSequence()));
2716     }
2717     else
2718     {
2719       // seqId==dsseqid so we can tell which sequences really are
2720       // dataset sequences only
2721       vamsasSeq.setDsseqid(id);
2722       dbrefs = jds.getDBRefs();
2723       if (parentseq == null)
2724       {
2725         parentseq = jds;
2726       }
2727     }
2728
2729     /*
2730      * save any dbrefs; special subclass GeneLocus is flagged as 'locus'
2731      */
2732     if (dbrefs != null)
2733     {
2734       for (int d = 0, nd = dbrefs.size(); d < nd; d++)
2735       {
2736         DBRef dbref = new DBRef();
2737         DBRefEntry ref = dbrefs.get(d);
2738         dbref.setSource(ref.getSource());
2739         dbref.setVersion(ref.getVersion());
2740         dbref.setAccessionId(ref.getAccessionId());
2741         dbref.setCanonical(ref.isCanonical());
2742         if (ref instanceof GeneLocus)
2743         {
2744           dbref.setLocus(true);
2745         }
2746         if (ref.hasMap())
2747         {
2748           Mapping mp = createVamsasMapping(ref.getMap(), parentseq, jds,
2749                   recurse);
2750           dbref.setMapping(mp);
2751         }
2752         vamsasSeq.getDBRef().add(dbref);
2753       }
2754     }
2755     return vamsasSeq;
2756   }
2757
2758   private Mapping createVamsasMapping(jalview.datamodel.Mapping jmp,
2759           SequenceI parentseq, SequenceI jds, boolean recurse)
2760   {
2761     Mapping mp = null;
2762     if (jmp.getMap() != null)
2763     {
2764       mp = new Mapping();
2765
2766       jalview.util.MapList mlst = jmp.getMap();
2767       List<int[]> r = mlst.getFromRanges();
2768       for (int[] range : r)
2769       {
2770         MapListFrom mfrom = new MapListFrom();
2771         mfrom.setStart(range[0]);
2772         mfrom.setEnd(range[1]);
2773         // mp.addMapListFrom(mfrom);
2774         mp.getMapListFrom().add(mfrom);
2775       }
2776       r = mlst.getToRanges();
2777       for (int[] range : r)
2778       {
2779         MapListTo mto = new MapListTo();
2780         mto.setStart(range[0]);
2781         mto.setEnd(range[1]);
2782         // mp.addMapListTo(mto);
2783         mp.getMapListTo().add(mto);
2784       }
2785       mp.setMapFromUnit(BigInteger.valueOf(mlst.getFromRatio()));
2786       mp.setMapToUnit(BigInteger.valueOf(mlst.getToRatio()));
2787       if (jmp.getTo() != null)
2788       {
2789         // MappingChoice mpc = new MappingChoice();
2790
2791         // check/create ID for the sequence referenced by getTo()
2792
2793         String jmpid = "";
2794         SequenceI ps = null;
2795         if (parentseq != jmp.getTo()
2796                 && parentseq.getDatasetSequence() != jmp.getTo())
2797         {
2798           // chaining dbref rather than a handshaking one
2799           jmpid = seqHash(ps = jmp.getTo());
2800         }
2801         else
2802         {
2803           jmpid = seqHash(ps = parentseq);
2804         }
2805         // mpc.setDseqFor(jmpid);
2806         mp.setDseqFor(jmpid);
2807         if (!seqRefIds.containsKey(jmpid))
2808         {
2809           Console.debug("creatign new DseqFor ID");
2810           seqRefIds.put(jmpid, ps);
2811         }
2812         else
2813         {
2814           Console.debug("reusing DseqFor ID");
2815         }
2816
2817         // mp.setMappingChoice(mpc);
2818       }
2819     }
2820     return mp;
2821   }
2822
2823   String setUserColourScheme(jalview.schemes.ColourSchemeI cs,
2824           List<UserColourScheme> userColours, JalviewModel jm)
2825   {
2826     String id = null;
2827     jalview.schemes.UserColourScheme ucs = (jalview.schemes.UserColourScheme) cs;
2828     boolean newucs = false;
2829     if (!userColours.contains(ucs))
2830     {
2831       userColours.add(ucs);
2832       newucs = true;
2833     }
2834     id = "ucs" + userColours.indexOf(ucs);
2835     if (newucs)
2836     {
2837       // actually create the scheme's entry in the XML model
2838       java.awt.Color[] colours = ucs.getColours();
2839       UserColours uc = new UserColours();
2840       // UserColourScheme jbucs = new UserColourScheme();
2841       JalviewUserColours jbucs = new JalviewUserColours();
2842
2843       for (int i = 0; i < colours.length; i++)
2844       {
2845         Colour col = new Colour();
2846         col.setName(ResidueProperties.aa[i]);
2847         col.setRGB(jalview.util.Format.getHexString(colours[i]));
2848         // jbucs.addColour(col);
2849         jbucs.getColour().add(col);
2850       }
2851       if (ucs.getLowerCaseColours() != null)
2852       {
2853         colours = ucs.getLowerCaseColours();
2854         for (int i = 0; i < colours.length; i++)
2855         {
2856           Colour col = new Colour();
2857           col.setName(ResidueProperties.aa[i].toLowerCase(Locale.ROOT));
2858           col.setRGB(jalview.util.Format.getHexString(colours[i]));
2859           // jbucs.addColour(col);
2860           jbucs.getColour().add(col);
2861         }
2862       }
2863
2864       uc.setId(id);
2865       uc.setUserColourScheme(jbucs);
2866       // jm.addUserColours(uc);
2867       jm.getUserColours().add(uc);
2868     }
2869
2870     return id;
2871   }
2872
2873   jalview.schemes.UserColourScheme getUserColourScheme(JalviewModel jm,
2874           String id)
2875   {
2876     List<UserColours> uc = jm.getUserColours();
2877     UserColours colours = null;
2878     /*
2879     for (int i = 0; i < uc.length; i++)
2880     {
2881       if (uc[i].getId().equals(id))
2882       {
2883         colours = uc[i];
2884         break;
2885       }
2886     }
2887     */
2888     for (UserColours c : uc)
2889     {
2890       if (c.getId().equals(id))
2891       {
2892         colours = c;
2893         break;
2894       }
2895     }
2896
2897     java.awt.Color[] newColours = new java.awt.Color[24];
2898
2899     for (int i = 0; i < 24; i++)
2900     {
2901       newColours[i] = new java.awt.Color(Integer.parseInt(
2902               // colours.getUserColourScheme().getColour(i).getRGB(), 16));
2903               colours.getUserColourScheme().getColour().get(i).getRGB(),
2904               16));
2905     }
2906
2907     jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme(
2908             newColours);
2909
2910     if (colours.getUserColourScheme().getColour().size()/*Count()*/ > 24)
2911     {
2912       newColours = new java.awt.Color[23];
2913       for (int i = 0; i < 23; i++)
2914       {
2915         newColours[i] = new java.awt.Color(
2916                 Integer.parseInt(colours.getUserColourScheme().getColour()
2917                         .get(i + 24).getRGB(), 16));
2918       }
2919       ucs.setLowerCaseColours(newColours);
2920     }
2921
2922     return ucs;
2923   }
2924
2925   /**
2926    * contains last error message (if any) encountered by XML loader.
2927    */
2928   String errorMessage = null;
2929
2930   /**
2931    * flag to control whether the Jalview2XML_V1 parser should be deferred to if
2932    * exceptions are raised during project XML parsing
2933    */
2934   public boolean attemptversion1parse = false;
2935
2936   /**
2937    * Load a jalview project archive from a jar file
2938    * 
2939    * @param file
2940    *          - HTTP URL or filename
2941    */
2942   public AlignFrame loadJalviewAlign(final Object file)
2943   {
2944
2945     jalview.gui.AlignFrame af = null;
2946
2947     try
2948     {
2949       // create list to store references for any new Jmol viewers created
2950       newStructureViewers = new Vector<>();
2951       // UNMARSHALLER SEEMS TO CLOSE JARINPUTSTREAM, MOST ANNOYING
2952       // Workaround is to make sure caller implements the JarInputStreamProvider
2953       // interface
2954       // so we can re-open the jar input stream for each entry.
2955
2956       jarInputStreamProvider jprovider = createjarInputStreamProvider(file);
2957       af = loadJalviewAlign(jprovider);
2958       if (af != null)
2959       {
2960         af.setMenusForViewport();
2961       }
2962     } catch (MalformedURLException e)
2963     {
2964       errorMessage = "Invalid URL format for '" + file + "'";
2965       reportErrors();
2966     } finally
2967     {
2968       try
2969       {
2970         SwingUtilities.invokeAndWait(new Runnable()
2971         {
2972           @Override
2973           public void run()
2974           {
2975             setLoadingFinishedForNewStructureViewers();
2976           }
2977         });
2978       } catch (Exception x)
2979       {
2980         jalview.bin.Console
2981                 .errPrintln("Error loading alignment: " + x.getMessage());
2982       }
2983     }
2984     return af;
2985   }
2986
2987   @SuppressWarnings("unused")
2988   private jarInputStreamProvider createjarInputStreamProvider(
2989           final Object ofile) throws MalformedURLException
2990   {
2991
2992     // BH 2018 allow for bytes already attached to File object
2993     try
2994     {
2995       String file = (ofile instanceof File
2996               ? ((File) ofile).getCanonicalPath()
2997               : ofile.toString());
2998       byte[] bytes = Platform.isJS() ? Platform.getFileBytes((File) ofile)
2999               : null;
3000       URL url = null;
3001       errorMessage = null;
3002       uniqueSetSuffix = null;
3003       seqRefIds = null;
3004       viewportsAdded.clear();
3005       frefedSequence = null;
3006
3007       if (HttpUtils.startsWithHttpOrHttps(file))
3008       {
3009         url = new URL(file);
3010       }
3011       final URL _url = url;
3012       return new jarInputStreamProvider()
3013       {
3014
3015         @Override
3016         public JarInputStream getJarInputStream() throws IOException
3017         {
3018           if (bytes != null)
3019           {
3020             // jalview.bin.Console.outPrintln("Jalview2XML: opening byte
3021             // jarInputStream for
3022             // bytes.length=" + bytes.length);
3023             return new JarInputStream(new ByteArrayInputStream(bytes));
3024           }
3025           if (_url != null)
3026           {
3027             // jalview.bin.Console.outPrintln("Jalview2XML: opening url
3028             // jarInputStream for "
3029             // + _url);
3030             return new JarInputStream(_url.openStream());
3031           }
3032           else
3033           {
3034             // jalview.bin.Console.outPrintln("Jalview2XML: opening file
3035             // jarInputStream for
3036             // " + file);
3037             return new JarInputStream(new FileInputStream(file));
3038           }
3039         }
3040
3041         @Override
3042         public String getFilename()
3043         {
3044           return file;
3045         }
3046       };
3047     } catch (IOException e)
3048     {
3049       e.printStackTrace();
3050       return null;
3051     }
3052   }
3053
3054   /**
3055    * Recover jalview session from a jalview project archive. Caller may
3056    * initialise uniqueSetSuffix, seqRefIds, viewportsAdded and frefedSequence
3057    * themselves. Any null fields will be initialised with default values,
3058    * non-null fields are left alone.
3059    * 
3060    * @param jprovider
3061    * @return
3062    */
3063   public AlignFrame loadJalviewAlign(final jarInputStreamProvider jprovider)
3064   {
3065     errorMessage = null;
3066     if (uniqueSetSuffix == null)
3067     {
3068       uniqueSetSuffix = System.currentTimeMillis() % 100000 + "";
3069     }
3070     if (seqRefIds == null)
3071     {
3072       initSeqRefs();
3073     }
3074     AlignFrame af = null, _af = null;
3075     IdentityHashMap<AlignmentI, AlignmentI> importedDatasets = new IdentityHashMap<>();
3076     Map<String, AlignFrame> gatherToThisFrame = new HashMap<>();
3077     final String file = jprovider.getFilename();
3078     try
3079     {
3080       JarInputStream jin = null;
3081       JarEntry jarentry = null;
3082       int entryCount = 1;
3083
3084       do
3085       {
3086         jin = jprovider.getJarInputStream();
3087         for (int i = 0; i < entryCount; i++)
3088         {
3089           jarentry = jin.getNextJarEntry();
3090         }
3091
3092         if (jarentry != null && jarentry.getName().endsWith(".xml"))
3093         {
3094           JAXBContext jc = JAXBContext
3095                   .newInstance("jalview.xml.binding.jalview");
3096           XMLStreamReader streamReader = XMLInputFactory.newInstance()
3097                   .createXMLStreamReader(jin);
3098           javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
3099           JAXBElement<JalviewModel> jbe = um.unmarshal(streamReader,
3100                   JalviewModel.class);
3101           JalviewModel object = jbe.getValue();
3102
3103           if (true) // !skipViewport(object))
3104           {
3105             _af = loadFromObject(object, file, true, jprovider);
3106             if (_af != null && object.getViewport().size() > 0)
3107             // getJalviewModelSequence().getViewportCount() > 0)
3108             {
3109               if (af == null)
3110               {
3111                 // store a reference to the first view
3112                 af = _af;
3113               }
3114               if (_af.getViewport().isGatherViewsHere())
3115               {
3116                 // if this is a gathered view, keep its reference since
3117                 // after gathering views, only this frame will remain
3118                 af = _af;
3119                 gatherToThisFrame.put(_af.getViewport().getSequenceSetId(),
3120                         _af);
3121               }
3122               // Save dataset to register mappings once all resolved
3123               importedDatasets.put(
3124                       af.getViewport().getAlignment().getDataset(),
3125                       af.getViewport().getAlignment().getDataset());
3126             }
3127           }
3128           entryCount++;
3129         }
3130         else if (jarentry != null)
3131         {
3132           // Some other file here.
3133           entryCount++;
3134         }
3135       } while (jarentry != null);
3136       jin.close();
3137       resolveFrefedSequences();
3138     } catch (IOException ex)
3139     {
3140       ex.printStackTrace();
3141       errorMessage = "Couldn't locate Jalview XML file : " + file;
3142       jalview.bin.Console.errPrintln(
3143               "Exception whilst loading jalview XML file : " + ex + "\n");
3144     } catch (Exception ex)
3145     {
3146       jalview.bin.Console
3147               .errPrintln("Parsing as Jalview Version 2 file failed.");
3148       ex.printStackTrace(System.err);
3149       if (attemptversion1parse)
3150       {
3151         // used to attempt to parse as V1 castor-generated xml
3152       }
3153       if (Desktop.instance != null)
3154       {
3155         Desktop.instance.stopLoading();
3156       }
3157       if (af != null)
3158       {
3159         jalview.bin.Console.outPrintln("Successfully loaded archive file");
3160         return af;
3161       }
3162       ex.printStackTrace();
3163
3164       jalview.bin.Console.errPrintln(
3165               "Exception whilst loading jalview XML file : " + ex + "\n");
3166     } catch (OutOfMemoryError e)
3167     {
3168       // Don't use the OOM Window here
3169       errorMessage = "Out of memory loading jalview XML file";
3170       jalview.bin.Console
3171               .errPrintln("Out of memory whilst loading jalview XML file");
3172       e.printStackTrace();
3173     }
3174
3175     /*
3176      * Regather multiple views (with the same sequence set id) to the frame (if
3177      * any) that is flagged as the one to gather to, i.e. convert them to tabbed
3178      * views instead of separate frames. Note this doesn't restore a state where
3179      * some expanded views in turn have tabbed views - the last "first tab" read
3180      * in will play the role of gatherer for all.
3181      */
3182     for (AlignFrame fr : gatherToThisFrame.values())
3183     {
3184       Desktop.instance.gatherViews(fr);
3185     }
3186
3187     restoreSplitFrames();
3188     for (AlignmentI ds : importedDatasets.keySet())
3189     {
3190       if (ds.getCodonFrames() != null)
3191       {
3192         StructureSelectionManager
3193                 .getStructureSelectionManager(Desktop.instance)
3194                 .registerMappings(ds.getCodonFrames());
3195       }
3196     }
3197     if (errorMessage != null)
3198     {
3199       reportErrors();
3200     }
3201
3202     if (Desktop.instance != null)
3203     {
3204       Desktop.instance.stopLoading();
3205     }
3206
3207     return af;
3208   }
3209
3210   /**
3211    * Try to reconstruct and display SplitFrame windows, where each contains
3212    * complementary dna and protein alignments. Done by pairing up AlignFrame
3213    * objects (created earlier) which have complementary viewport ids associated.
3214    */
3215   protected void restoreSplitFrames()
3216   {
3217     List<SplitFrame> gatherTo = new ArrayList<>();
3218     List<AlignFrame> addedToSplitFrames = new ArrayList<>();
3219     Map<String, AlignFrame> dna = new HashMap<>();
3220
3221     /*
3222      * Identify the DNA alignments
3223      */
3224     for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
3225             .entrySet())
3226     {
3227       AlignFrame af = candidate.getValue();
3228       if (af.getViewport().getAlignment().isNucleotide())
3229       {
3230         dna.put(candidate.getKey().getId(), af);
3231       }
3232     }
3233
3234     /*
3235      * Try to match up the protein complements
3236      */
3237     for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
3238             .entrySet())
3239     {
3240       AlignFrame af = candidate.getValue();
3241       if (!af.getViewport().getAlignment().isNucleotide())
3242       {
3243         String complementId = candidate.getKey().getComplementId();
3244         // only non-null complements should be in the Map
3245         if (complementId != null && dna.containsKey(complementId))
3246         {
3247           final AlignFrame dnaFrame = dna.get(complementId);
3248           SplitFrame sf = createSplitFrame(dnaFrame, af);
3249           addedToSplitFrames.add(dnaFrame);
3250           addedToSplitFrames.add(af);
3251           dnaFrame.setMenusForViewport();
3252           af.setMenusForViewport();
3253           if (af.getViewport().isGatherViewsHere())
3254           {
3255             gatherTo.add(sf);
3256           }
3257         }
3258       }
3259     }
3260
3261     /*
3262      * Open any that we failed to pair up (which shouldn't happen!) as
3263      * standalone AlignFrame's.
3264      */
3265     for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
3266             .entrySet())
3267     {
3268       AlignFrame af = candidate.getValue();
3269       if (!addedToSplitFrames.contains(af))
3270       {
3271         Viewport view = candidate.getKey();
3272         Desktop.addInternalFrame(af, view.getTitle(),
3273                 safeInt(view.getWidth()), safeInt(view.getHeight()));
3274         af.setMenusForViewport();
3275         jalview.bin.Console.errPrintln("Failed to restore view "
3276                 + view.getTitle() + " to split frame");
3277       }
3278     }
3279
3280     /*
3281      * Gather back into tabbed views as flagged.
3282      */
3283     for (SplitFrame sf : gatherTo)
3284     {
3285       Desktop.instance.gatherViews(sf);
3286     }
3287
3288     splitFrameCandidates.clear();
3289   }
3290
3291   /**
3292    * Construct and display one SplitFrame holding DNA and protein alignments.
3293    * 
3294    * @param dnaFrame
3295    * @param proteinFrame
3296    * @return
3297    */
3298   protected SplitFrame createSplitFrame(AlignFrame dnaFrame,
3299           AlignFrame proteinFrame)
3300   {
3301     SplitFrame splitFrame = new SplitFrame(dnaFrame, proteinFrame);
3302     String title = MessageManager.getString("label.linked_view_title");
3303     int width = (int) dnaFrame.getBounds().getWidth();
3304     int height = (int) (dnaFrame.getBounds().getHeight()
3305             + proteinFrame.getBounds().getHeight() + 50);
3306
3307     /*
3308      * SplitFrame location is saved to both enclosed frames
3309      */
3310     splitFrame.setLocation(dnaFrame.getX(), dnaFrame.getY());
3311     Desktop.addInternalFrame(splitFrame, title, width, height);
3312
3313     /*
3314      * And compute cDNA consensus (couldn't do earlier with consensus as
3315      * mappings were not yet present)
3316      */
3317     proteinFrame.getViewport().alignmentChanged(proteinFrame.alignPanel);
3318
3319     return splitFrame;
3320   }
3321
3322   /**
3323    * check errorMessage for a valid error message and raise an error box in the
3324    * GUI or write the current errorMessage to stderr and then clear the error
3325    * state.
3326    */
3327   protected void reportErrors()
3328   {
3329     reportErrors(false);
3330   }
3331
3332   protected void reportErrors(final boolean saving)
3333   {
3334     if (errorMessage != null)
3335     {
3336       final String finalErrorMessage = errorMessage;
3337       if (raiseGUI)
3338       {
3339         javax.swing.SwingUtilities.invokeLater(new Runnable()
3340         {
3341           @Override
3342           public void run()
3343           {
3344             JvOptionPane.showInternalMessageDialog(Desktop.desktop,
3345                     finalErrorMessage,
3346                     "Error " + (saving ? "saving" : "loading")
3347                             + " Jalview file",
3348                     JvOptionPane.WARNING_MESSAGE);
3349           }
3350         });
3351       }
3352       else
3353       {
3354         jalview.bin.Console.errPrintln(
3355                 "Problem loading Jalview file: " + errorMessage);
3356       }
3357     }
3358     errorMessage = null;
3359   }
3360
3361   Map<String, String> alreadyLoadedPDB = new HashMap<>();
3362
3363   /**
3364    * when set, local views will be updated from view stored in JalviewXML
3365    * Currently (28th Sep 2008) things will go horribly wrong in vamsas document
3366    * sync if this is set to true.
3367    */
3368   private final boolean updateLocalViews = false;
3369
3370   /**
3371    * Returns the path to a temporary file holding the PDB file for the given PDB
3372    * id. The first time of asking, searches for a file of that name in the
3373    * Jalview project jar, and copies it to a new temporary file. Any repeat
3374    * requests just return the path to the file previously created.
3375    * 
3376    * @param jprovider
3377    * @param pdbId
3378    * @return
3379    */
3380   String loadPDBFile(jarInputStreamProvider jprovider, String pdbId,
3381           String origFile)
3382   {
3383     if (alreadyLoadedPDB.containsKey(pdbId))
3384     {
3385       return alreadyLoadedPDB.get(pdbId).toString();
3386     }
3387
3388     String tempFile = copyJarEntry(jprovider, pdbId, "jalview_pdb",
3389             origFile);
3390     if (tempFile != null)
3391     {
3392       alreadyLoadedPDB.put(pdbId, tempFile);
3393     }
3394     return tempFile;
3395   }
3396
3397   /**
3398    * Copies the jar entry of given name to a new temporary file and returns the
3399    * path to the file, or null if the entry is not found.
3400    * 
3401    * @param jprovider
3402    * @param jarEntryName
3403    * @param prefix
3404    *          a prefix for the temporary file name, must be at least three
3405    *          characters long
3406    * @param suffixModel
3407    *          null or original file - so new file can be given the same suffix
3408    *          as the old one
3409    * @return
3410    */
3411   protected String copyJarEntry(jarInputStreamProvider jprovider,
3412           String jarEntryName, String prefix, String suffixModel)
3413   {
3414     String suffix = ".tmp";
3415     if (suffixModel == null)
3416     {
3417       suffixModel = jarEntryName;
3418     }
3419     int sfpos = suffixModel.lastIndexOf(".");
3420     if (sfpos > -1 && sfpos < (suffixModel.length() - 1))
3421     {
3422       suffix = "." + suffixModel.substring(sfpos + 1);
3423     }
3424
3425     try (JarInputStream jin = jprovider.getJarInputStream())
3426     {
3427       JarEntry entry = null;
3428       do
3429       {
3430         entry = jin.getNextJarEntry();
3431       } while (entry != null && !entry.getName().equals(jarEntryName));
3432
3433       if (entry != null)
3434       {
3435         // in = new BufferedReader(new InputStreamReader(jin, UTF_8));
3436         File outFile = File.createTempFile(prefix, suffix);
3437         outFile.deleteOnExit();
3438         try (OutputStream os = new FileOutputStream(outFile))
3439         {
3440           copyAll(jin, os);
3441         }
3442         String t = outFile.getAbsolutePath();
3443         return t;
3444       }
3445       else
3446       {
3447         Console.warn(
3448                 "Couldn't find entry in Jalview Jar for " + jarEntryName);
3449       }
3450     } catch (Exception ex)
3451     {
3452       ex.printStackTrace();
3453     }
3454
3455     return null;
3456   }
3457
3458   private class JvAnnotRow
3459   {
3460     public JvAnnotRow(int i, AlignmentAnnotation jaa)
3461     {
3462       order = i;
3463       template = jaa;
3464     }
3465
3466     /**
3467      * persisted version of annotation row from which to take vis properties
3468      */
3469     public jalview.datamodel.AlignmentAnnotation template;
3470
3471     /**
3472      * original position of the annotation row in the alignment
3473      */
3474     public int order;
3475   }
3476
3477   /**
3478    * Load alignment frame from jalview XML DOM object
3479    * 
3480    * @param jalviewModel
3481    *          DOM
3482    * @param file
3483    *          filename source string
3484    * @param loadTreesAndStructures
3485    *          when false only create Viewport
3486    * @param jprovider
3487    *          data source provider
3488    * @return alignment frame created from view stored in DOM
3489    */
3490   AlignFrame loadFromObject(JalviewModel jalviewModel, String file,
3491           boolean loadTreesAndStructures, jarInputStreamProvider jprovider)
3492   {
3493     SequenceSet vamsasSet = jalviewModel.getVamsasModel().getSequenceSet()
3494             .get(0);
3495     List<Sequence> vamsasSeqs = vamsasSet.getSequence();
3496
3497     // JalviewModelSequence jms = object.getJalviewModelSequence();
3498
3499     // Viewport view = (jms.getViewportCount() > 0) ? jms.getViewport(0)
3500     // : null;
3501     Viewport view = (jalviewModel.getViewport().size() > 0)
3502             ? jalviewModel.getViewport().get(0)
3503             : null;
3504
3505     // ////////////////////////////////
3506     // INITIALISE ALIGNMENT SEQUENCESETID AND VIEWID
3507     //
3508     //
3509     // If we just load in the same jar file again, the sequenceSetId
3510     // will be the same, and we end up with multiple references
3511     // to the same sequenceSet. We must modify this id on load
3512     // so that each load of the file gives a unique id
3513
3514     /**
3515      * used to resolve correct alignment dataset for alignments with multiple
3516      * views
3517      */
3518     String uniqueSeqSetId = null;
3519     String viewId = null;
3520     if (view != null)
3521     {
3522       uniqueSeqSetId = view.getSequenceSetId() + uniqueSetSuffix;
3523       viewId = (view.getId() == null ? null
3524               : view.getId() + uniqueSetSuffix);
3525     }
3526
3527     // ////////////////////////////////
3528     // LOAD SEQUENCES
3529
3530     List<SequenceI> hiddenSeqs = null;
3531
3532     List<SequenceI> tmpseqs = new ArrayList<>();
3533
3534     boolean multipleView = false;
3535     SequenceI referenceseqForView = null;
3536     // JSeq[] jseqs = object.getJalviewModelSequence().getJSeq();
3537     List<JSeq> jseqs = jalviewModel.getJSeq();
3538     int vi = 0; // counter in vamsasSeq array
3539     for (int i = 0; i < jseqs.size(); i++)
3540     {
3541       JSeq jseq = jseqs.get(i);
3542       String seqId = jseq.getId();
3543
3544       SequenceI tmpSeq = seqRefIds.get(seqId);
3545       if (tmpSeq != null)
3546       {
3547         if (!incompleteSeqs.containsKey(seqId))
3548         {
3549           // may not need this check, but keep it for at least 2.9,1 release
3550           if (tmpSeq.getStart() != jseq.getStart()
3551                   || tmpSeq.getEnd() != jseq.getEnd())
3552           {
3553             jalview.bin.Console.errPrintln(String.format(
3554                     "Warning JAL-2154 regression: updating start/end for sequence %s from %d/%d to %d/%d",
3555                     tmpSeq.getName(), tmpSeq.getStart(), tmpSeq.getEnd(),
3556                     jseq.getStart(), jseq.getEnd()));
3557           }
3558         }
3559         else
3560         {
3561           incompleteSeqs.remove(seqId);
3562         }
3563         if (vamsasSeqs.size() > vi
3564                 && vamsasSeqs.get(vi).getId().equals(seqId))
3565         {
3566           // most likely we are reading a dataset XML document so
3567           // update from vamsasSeq section of XML for this sequence
3568           tmpSeq.setName(vamsasSeqs.get(vi).getName());
3569           tmpSeq.setDescription(vamsasSeqs.get(vi).getDescription());
3570           tmpSeq.setSequence(vamsasSeqs.get(vi).getSequence());
3571           vi++;
3572         }
3573         else
3574         {
3575           // reading multiple views, so vamsasSeq set is a subset of JSeq
3576           multipleView = true;
3577         }
3578         tmpSeq.setStart(jseq.getStart());
3579         tmpSeq.setEnd(jseq.getEnd());
3580         tmpseqs.add(tmpSeq);
3581       }
3582       else
3583       {
3584         Sequence vamsasSeq = vamsasSeqs.get(vi);
3585         tmpSeq = new jalview.datamodel.Sequence(vamsasSeq.getName(),
3586                 vamsasSeq.getSequence());
3587         tmpSeq.setDescription(vamsasSeq.getDescription());
3588         tmpSeq.setStart(jseq.getStart());
3589         tmpSeq.setEnd(jseq.getEnd());
3590         tmpSeq.setVamsasId(uniqueSetSuffix + seqId);
3591         seqRefIds.put(vamsasSeq.getId(), tmpSeq);
3592         tmpseqs.add(tmpSeq);
3593         vi++;
3594       }
3595
3596       if (safeBoolean(jseq.isViewreference()))
3597       {
3598         referenceseqForView = tmpseqs.get(tmpseqs.size() - 1);
3599       }
3600
3601       if (jseq.isHidden() != null && jseq.isHidden().booleanValue())
3602       {
3603         if (hiddenSeqs == null)
3604         {
3605           hiddenSeqs = new ArrayList<>();
3606         }
3607
3608         hiddenSeqs.add(tmpSeq);
3609       }
3610     }
3611
3612     // /
3613     // Create the alignment object from the sequence set
3614     // ///////////////////////////////
3615     SequenceI[] orderedSeqs = tmpseqs
3616             .toArray(new SequenceI[tmpseqs.size()]);
3617
3618     AlignmentI al = null;
3619     // so we must create or recover the dataset alignment before going further
3620     // ///////////////////////////////
3621     if (vamsasSet.getDatasetId() == null || vamsasSet.getDatasetId() == "")
3622     {
3623       // older jalview projects do not have a dataset - so creat alignment and
3624       // dataset
3625       al = new Alignment(orderedSeqs);
3626       al.setDataset(null);
3627     }
3628     else
3629     {
3630       boolean isdsal = jalviewModel.getViewport().isEmpty();
3631       if (isdsal)
3632       {
3633         // we are importing a dataset record, so
3634         // recover reference to an alignment already materialsed as dataset
3635         al = getDatasetFor(vamsasSet.getDatasetId());
3636       }
3637       if (al == null)
3638       {
3639         // materialse the alignment
3640         al = new Alignment(orderedSeqs);
3641       }
3642       if (isdsal)
3643       {
3644         addDatasetRef(vamsasSet.getDatasetId(), al);
3645       }
3646
3647       // finally, verify all data in vamsasSet is actually present in al
3648       // passing on flag indicating if it is actually a stored dataset
3649       recoverDatasetFor(vamsasSet, al, isdsal, uniqueSeqSetId);
3650     }
3651
3652     if (referenceseqForView != null)
3653     {
3654       al.setSeqrep(referenceseqForView);
3655     }
3656     // / Add the alignment properties
3657     for (int i = 0; i < vamsasSet.getSequenceSetProperties().size(); i++)
3658     {
3659       SequenceSetProperties ssp = vamsasSet.getSequenceSetProperties()
3660               .get(i);
3661       al.setProperty(ssp.getKey(), ssp.getValue());
3662     }
3663
3664     // ///////////////////////////////
3665
3666     Hashtable pdbloaded = new Hashtable(); // TODO nothing writes to this??
3667     if (!multipleView)
3668     {
3669       // load sequence features, database references and any associated PDB
3670       // structures for the alignment
3671       //
3672       // prior to 2.10, this part would only be executed the first time a
3673       // sequence was encountered, but not afterwards.
3674       // now, for 2.10 projects, this is also done if the xml doc includes
3675       // dataset sequences not actually present in any particular view.
3676       //
3677       for (int i = 0; i < vamsasSeqs.size(); i++)
3678       {
3679         JSeq jseq = jseqs.get(i);
3680         if (jseq.getFeatures().size() > 0)
3681         {
3682           List<Feature> features = jseq.getFeatures();
3683           for (int f = 0; f < features.size(); f++)
3684           {
3685             Feature feat = features.get(f);
3686             SequenceFeature sf = new SequenceFeature(feat.getType(),
3687                     feat.getDescription(), feat.getBegin(), feat.getEnd(),
3688                     safeFloat(feat.getScore()), feat.getFeatureGroup());
3689             sf.setStatus(feat.getStatus());
3690
3691             /*
3692              * load any feature attributes - include map-valued attributes
3693              */
3694             Map<String, Map<String, String>> mapAttributes = new HashMap<>();
3695             for (int od = 0; od < feat.getOtherData().size(); od++)
3696             {
3697               OtherData keyValue = feat.getOtherData().get(od);
3698               String attributeName = keyValue.getKey();
3699               String attributeValue = keyValue.getValue();
3700               if (attributeName.startsWith("LINK"))
3701               {
3702                 sf.addLink(attributeValue);
3703               }
3704               else
3705               {
3706                 String subAttribute = keyValue.getKey2();
3707                 if (subAttribute == null)
3708                 {
3709                   // simple string-valued attribute
3710                   sf.setValue(attributeName, attributeValue);
3711                 }
3712                 else
3713                 {
3714                   // attribute 'key' has sub-attribute 'key2'
3715                   if (!mapAttributes.containsKey(attributeName))
3716                   {
3717                     mapAttributes.put(attributeName, new HashMap<>());
3718                   }
3719                   mapAttributes.get(attributeName).put(subAttribute,
3720                           attributeValue);
3721                 }
3722               }
3723             }
3724             for (Entry<String, Map<String, String>> mapAttribute : mapAttributes
3725                     .entrySet())
3726             {
3727               sf.setValue(mapAttribute.getKey(), mapAttribute.getValue());
3728             }
3729
3730             // adds feature to datasequence's feature set (since Jalview 2.10)
3731             al.getSequenceAt(i).addSequenceFeature(sf);
3732           }
3733         }
3734         if (vamsasSeqs.get(i).getDBRef().size() > 0)
3735         {
3736           // adds dbrefs to datasequence's set (since Jalview 2.10)
3737           addDBRefs(
3738                   al.getSequenceAt(i).getDatasetSequence() == null
3739                           ? al.getSequenceAt(i)
3740                           : al.getSequenceAt(i).getDatasetSequence(),
3741                   vamsasSeqs.get(i));
3742         }
3743         if (jseq.getPdbids().size() > 0)
3744         {
3745           List<Pdbids> ids = jseq.getPdbids();
3746           for (int p = 0; p < ids.size(); p++)
3747           {
3748             Pdbids pdbid = ids.get(p);
3749             jalview.datamodel.PDBEntry entry = new jalview.datamodel.PDBEntry();
3750             entry.setId(pdbid.getId());
3751             if (pdbid.getType() != null)
3752             {
3753               if (PDBEntry.Type.getType(pdbid.getType()) != null)
3754               {
3755                 entry.setType(PDBEntry.Type.getType(pdbid.getType()));
3756               }
3757               else
3758               {
3759                 entry.setType(PDBEntry.Type.FILE);
3760               }
3761             }
3762             // jprovider is null when executing 'New View'
3763             if (pdbid.getFile() != null && jprovider != null)
3764             {
3765               if (!pdbloaded.containsKey(pdbid.getFile()))
3766               {
3767                 entry.setFile(loadPDBFile(jprovider, pdbid.getId(),
3768                         pdbid.getFile()));
3769               }
3770               else
3771               {
3772                 entry.setFile(pdbloaded.get(pdbid.getId()).toString());
3773               }
3774             }
3775             /*
3776             if (pdbid.getPdbentryItem() != null)
3777             {
3778               for (PdbentryItem item : pdbid.getPdbentryItem())
3779               {
3780                 for (Property pr : item.getProperty())
3781                 {
3782                   entry.setProperty(pr.getName(), pr.getValue());
3783                 }
3784               }
3785             }
3786             */
3787             for (Property prop : pdbid.getProperty())
3788             {
3789               entry.setProperty(prop.getName(), prop.getValue());
3790             }
3791             StructureSelectionManager
3792                     .getStructureSelectionManager(Desktop.instance)
3793                     .registerPDBEntry(entry);
3794             // adds PDBEntry to datasequence's set (since Jalview 2.10)
3795             if (al.getSequenceAt(i).getDatasetSequence() != null)
3796             {
3797               al.getSequenceAt(i).getDatasetSequence().addPDBId(entry);
3798             }
3799             else
3800             {
3801               al.getSequenceAt(i).addPDBId(entry);
3802             }
3803           }
3804         }
3805       }
3806     } // end !multipleview
3807
3808     // ///////////////////////////////
3809     // LOAD SEQUENCE MAPPINGS
3810
3811     if (vamsasSet.getAlcodonFrame().size() > 0)
3812     {
3813       // TODO Potentially this should only be done once for all views of an
3814       // alignment
3815       List<AlcodonFrame> alc = vamsasSet.getAlcodonFrame();
3816       for (int i = 0; i < alc.size(); i++)
3817       {
3818         AlignedCodonFrame cf = new AlignedCodonFrame();
3819         if (alc.get(i).getAlcodMap().size() > 0)
3820         {
3821           List<AlcodMap> maps = alc.get(i).getAlcodMap();
3822           for (int m = 0; m < maps.size(); m++)
3823           {
3824             AlcodMap map = maps.get(m);
3825             SequenceI dnaseq = seqRefIds.get(map.getDnasq());
3826             // Load Mapping
3827             jalview.datamodel.Mapping mapping = null;
3828             // attach to dna sequence reference.
3829             if (map.getMapping() != null)
3830             {
3831               mapping = addMapping(map.getMapping());
3832               if (dnaseq != null && mapping.getTo() != null)
3833               {
3834                 cf.addMap(dnaseq, mapping.getTo(), mapping.getMap());
3835               }
3836               else
3837               {
3838                 // defer to later
3839                 frefedSequence
3840                         .add(newAlcodMapRef(map.getDnasq(), cf, mapping));
3841               }
3842             }
3843           }
3844           al.addCodonFrame(cf);
3845         }
3846       }
3847     }
3848
3849     // ////////////////////////////////
3850     // LOAD ANNOTATIONS
3851     List<JvAnnotRow> autoAlan = new ArrayList<>();
3852
3853     /*
3854      * store any annotations which forward reference a group's ID
3855      */
3856     Map<String, List<AlignmentAnnotation>> groupAnnotRefs = new Hashtable<>();
3857
3858     if (vamsasSet.getAnnotation().size()/*Count()*/ > 0)
3859     {
3860       List<Annotation> an = vamsasSet.getAnnotation();
3861
3862       for (int i = 0; i < an.size(); i++)
3863       {
3864         Annotation annotation = an.get(i);
3865
3866         /**
3867          * test if annotation is automatically calculated for this view only
3868          */
3869         boolean autoForView = false;
3870         if (annotation.getLabel().equals("Quality")
3871                 || annotation.getLabel().equals("Conservation")
3872                 || annotation.getLabel().equals("Consensus"))
3873         {
3874           // Kludge for pre 2.5 projects which lacked the autocalculated flag
3875           autoForView = true;
3876           // JAXB has no has() test; schema defaults value to false
3877           // if (!annotation.hasAutoCalculated())
3878           // {
3879           // annotation.setAutoCalculated(true);
3880           // }
3881         }
3882         if (autoForView || annotation.isAutoCalculated())
3883         {
3884           // remove ID - we don't recover annotation from other views for
3885           // view-specific annotation
3886           annotation.setId(null);
3887         }
3888
3889         // set visibility for other annotation in this view
3890         String annotationId = annotation.getId();
3891         if (annotationId != null && annotationIds.containsKey(annotationId))
3892         {
3893           AlignmentAnnotation jda = annotationIds.get(annotationId);
3894           // in principle Visible should always be true for annotation displayed
3895           // in multiple views
3896           if (annotation.isVisible() != null)
3897           {
3898             jda.visible = annotation.isVisible();
3899           }
3900
3901           al.addAnnotation(jda);
3902
3903           continue;
3904         }
3905         // Construct new annotation from model.
3906         List<AnnotationElement> ae = annotation.getAnnotationElement();
3907         jalview.datamodel.Annotation[] anot = null;
3908         java.awt.Color firstColour = null;
3909         int anpos;
3910         if (!annotation.isScoreOnly())
3911         {
3912           anot = new jalview.datamodel.Annotation[al.getWidth()];
3913           for (int aa = 0; aa < ae.size() && aa < anot.length; aa++)
3914           {
3915             AnnotationElement annElement = ae.get(aa);
3916             anpos = annElement.getPosition();
3917
3918             if (anpos >= anot.length)
3919             {
3920               continue;
3921             }
3922
3923             float value = safeFloat(annElement.getValue());
3924             anot[anpos] = new jalview.datamodel.Annotation(
3925                     annElement.getDisplayCharacter(),
3926                     annElement.getDescription(),
3927                     (annElement.getSecondaryStructure() == null
3928                             || annElement.getSecondaryStructure()
3929                                     .length() == 0)
3930                                             ? ' '
3931                                             : annElement
3932                                                     .getSecondaryStructure()
3933                                                     .charAt(0),
3934                     value);
3935             anot[anpos].colour = new Color(safeInt(annElement.getColour()));
3936             if (firstColour == null)
3937             {
3938               firstColour = anot[anpos].colour;
3939             }
3940           }
3941         }
3942         jalview.datamodel.AlignmentAnnotation jaa = null;
3943
3944         if (annotation.isGraph())
3945         {
3946           float llim = 0, hlim = 0;
3947           // if (autoForView || an[i].isAutoCalculated()) {
3948           // hlim=11f;
3949           // }
3950           jaa = new jalview.datamodel.AlignmentAnnotation(
3951                   annotation.getLabel(), annotation.getDescription(), anot,
3952                   llim, hlim, safeInt(annotation.getGraphType()));
3953
3954           jaa.graphGroup = safeInt(annotation.getGraphGroup());
3955           jaa._linecolour = firstColour;
3956           if (annotation.getThresholdLine() != null)
3957           {
3958             jaa.setThreshold(new jalview.datamodel.GraphLine(
3959                     safeFloat(annotation.getThresholdLine().getValue()),
3960                     annotation.getThresholdLine().getLabel(),
3961                     new java.awt.Color(safeInt(
3962                             annotation.getThresholdLine().getColour()))));
3963           }
3964           if (autoForView || annotation.isAutoCalculated())
3965           {
3966             // Hardwire the symbol display line to ensure that labels for
3967             // histograms are displayed
3968             jaa.hasText = true;
3969           }
3970         }
3971         else
3972         {
3973           jaa = new jalview.datamodel.AlignmentAnnotation(
3974                   annotation.getLabel(), annotation.getDescription(), anot);
3975           jaa._linecolour = firstColour;
3976         }
3977         // register new annotation
3978         if (annotation.getId() != null)
3979         {
3980           annotationIds.put(annotation.getId(), jaa);
3981           jaa.annotationId = annotation.getId();
3982         }
3983         // recover sequence association
3984         String sequenceRef = annotation.getSequenceRef();
3985         if (sequenceRef != null)
3986         {
3987           // from 2.9 sequenceRef is to sequence id (JAL-1781)
3988           SequenceI sequence = seqRefIds.get(sequenceRef);
3989           if (sequence == null)
3990           {
3991             // in pre-2.9 projects sequence ref is to sequence name
3992             sequence = al.findName(sequenceRef);
3993           }
3994           if (sequence != null)
3995           {
3996             jaa.createSequenceMapping(sequence, 1, true);
3997             sequence.addAlignmentAnnotation(jaa);
3998           }
3999         }
4000         // and make a note of any group association
4001         if (annotation.getGroupRef() != null
4002                 && annotation.getGroupRef().length() > 0)
4003         {
4004           List<jalview.datamodel.AlignmentAnnotation> aal = groupAnnotRefs
4005                   .get(annotation.getGroupRef());
4006           if (aal == null)
4007           {
4008             aal = new ArrayList<>();
4009             groupAnnotRefs.put(annotation.getGroupRef(), aal);
4010           }
4011           aal.add(jaa);
4012         }
4013
4014         if (annotation.getScore() != null)
4015         {
4016           jaa.setScore(annotation.getScore().doubleValue());
4017         }
4018         if (annotation.isVisible() != null)
4019         {
4020           jaa.visible = annotation.isVisible().booleanValue();
4021         }
4022
4023         if (annotation.isCentreColLabels() != null)
4024         {
4025           jaa.centreColLabels = annotation.isCentreColLabels()
4026                   .booleanValue();
4027         }
4028
4029         if (annotation.isScaleColLabels() != null)
4030         {
4031           jaa.scaleColLabel = annotation.isScaleColLabels().booleanValue();
4032         }
4033         if (annotation.isAutoCalculated())
4034         {
4035           // newer files have an 'autoCalculated' flag and store calculation
4036           // state in viewport properties
4037           jaa.autoCalculated = true; // means annotation will be marked for
4038           // update at end of load.
4039         }
4040         if (annotation.getGraphHeight() != null)
4041         {
4042           jaa.graphHeight = annotation.getGraphHeight().intValue();
4043         }
4044         jaa.belowAlignment = annotation.isBelowAlignment();
4045         jaa.setCalcId(annotation.getCalcId());
4046         if (annotation.getProperty().size() > 0)
4047         {
4048           for (jalview.xml.binding.jalview.Property prop : annotation
4049                   .getProperty())
4050           {
4051             jaa.setProperty(prop.getName(), prop.getValue());
4052           }
4053         }
4054         if (jaa.graph == AlignmentAnnotation.CONTACT_MAP)
4055         {
4056           if (annotation.getContactmatrix() != null
4057                   && annotation.getContactmatrix().size() > 0)
4058           {
4059             for (MatrixType xmlmat : annotation.getContactmatrix())
4060             {
4061               if (PAEContactMatrix.PAEMATRIX.equals(xmlmat.getType()))
4062               {
4063                 if (!xmlmat.getRows().equals(xmlmat.getCols()))
4064                 {
4065                   Console.error("Can't handle non square PAE Matrices");
4066                 }
4067                 else
4068                 {
4069                   float[][] elements = ContactMatrix
4070                           .fromFloatStringToContacts(xmlmat.getElements(),
4071                                   xmlmat.getCols().intValue(),
4072                                   xmlmat.getRows().intValue());
4073                   jalview.util.MapList mapping = null;
4074                   if (xmlmat.getMapping() != null)
4075                   {
4076                     MapListType m = xmlmat.getMapping();
4077                     // Mapping m = dr.getMapping();
4078                     int fr[] = new int[m.getMapListFrom().size() * 2];
4079                     Iterator<MapListFrom> from = m.getMapListFrom()
4080                             .iterator();// enumerateMapListFrom();
4081                     for (int _i = 0; from.hasNext(); _i += 2)
4082                     {
4083                       MapListFrom mf = from.next();
4084                       fr[_i] = mf.getStart();
4085                       fr[_i + 1] = mf.getEnd();
4086                     }
4087                     int fto[] = new int[m.getMapListTo().size() * 2];
4088                     Iterator<MapListTo> to = m.getMapListTo().iterator();// enumerateMapListTo();
4089                     for (int _i = 0; to.hasNext(); _i += 2)
4090                     {
4091                       MapListTo mf = to.next();
4092                       fto[_i] = mf.getStart();
4093                       fto[_i + 1] = mf.getEnd();
4094                     }
4095
4096                     mapping = new jalview.util.MapList(fr, fto,
4097                             m.getMapFromUnit().intValue(),
4098                             m.getMapToUnit().intValue());
4099                   }
4100                   List<BitSet> newgroups = new ArrayList<BitSet>();
4101                   if (xmlmat.getGroups().size() > 0)
4102                   {
4103                     for (String sgroup : xmlmat.getGroups())
4104                     {
4105                       newgroups.add(deStringifyBitset(sgroup));
4106                     }
4107                   }
4108                   String nwk = xmlmat.getNewick().size() > 0
4109                           ? xmlmat.getNewick().get(0)
4110                           : null;
4111                   if (xmlmat.getNewick().size() > 1)
4112                   {
4113                     Console.log.info(
4114                             "Ignoring additional clusterings for contact matrix");
4115                   }
4116                   String treeMethod = xmlmat.getTreeMethod();
4117                   double thresh = xmlmat.getCutHeight() != null
4118                           ? xmlmat.getCutHeight()
4119                           : 0;
4120                   GroupSet grpset = new GroupSet();
4121                   grpset.restoreGroups(newgroups, treeMethod, nwk, thresh);
4122                   PAEContactMatrix newpae = new PAEContactMatrix(
4123                           jaa.sequenceRef, mapping, elements, grpset);
4124                   jaa.sequenceRef.addContactListFor(jaa, newpae);
4125                 }
4126               }
4127               else
4128               {
4129                 Console.error("Ignoring CONTACT_MAP annotation with type "
4130                         + xmlmat.getType());
4131               }
4132             }
4133           }
4134         }
4135
4136         if (jaa.autoCalculated)
4137         {
4138           autoAlan.add(new JvAnnotRow(i, jaa));
4139         }
4140         else
4141         // if (!autoForView)
4142         {
4143           // add autocalculated group annotation and any user created annotation
4144           // for the view
4145           al.addAnnotation(jaa);
4146         }
4147       }
4148     }
4149     // ///////////////////////
4150     // LOAD GROUPS
4151     // Create alignment markup and styles for this view
4152     if (jalviewModel.getJGroup().size() > 0)
4153     {
4154       List<JGroup> groups = jalviewModel.getJGroup();
4155       boolean addAnnotSchemeGroup = false;
4156       for (int i = 0; i < groups.size(); i++)
4157       {
4158         JGroup jGroup = groups.get(i);
4159         ColourSchemeI cs = null;
4160         if (jGroup.getColour() != null)
4161         {
4162           if (jGroup.getColour().startsWith("ucs"))
4163           {
4164             cs = getUserColourScheme(jalviewModel, jGroup.getColour());
4165           }
4166           else if (jGroup.getColour().equals("AnnotationColourGradient")
4167                   && jGroup.getAnnotationColours() != null)
4168           {
4169             addAnnotSchemeGroup = true;
4170           }
4171           else
4172           {
4173             cs = ColourSchemeProperty.getColourScheme(null, al,
4174                     jGroup.getColour());
4175           }
4176         }
4177         int pidThreshold = safeInt(jGroup.getPidThreshold());
4178
4179         Vector<SequenceI> seqs = new Vector<>();
4180
4181         for (int s = 0; s < jGroup.getSeq().size(); s++)
4182         {
4183           String seqId = jGroup.getSeq().get(s);
4184           SequenceI ts = seqRefIds.get(seqId);
4185
4186           if (ts != null)
4187           {
4188             seqs.addElement(ts);
4189           }
4190         }
4191
4192         if (seqs.size() < 1)
4193         {
4194           continue;
4195         }
4196
4197         SequenceGroup sg = new SequenceGroup(seqs, jGroup.getName(), cs,
4198                 safeBoolean(jGroup.isDisplayBoxes()),
4199                 safeBoolean(jGroup.isDisplayText()),
4200                 safeBoolean(jGroup.isColourText()),
4201                 safeInt(jGroup.getStart()), safeInt(jGroup.getEnd()));
4202         sg.getGroupColourScheme().setThreshold(pidThreshold, true);
4203         sg.getGroupColourScheme()
4204                 .setConservationInc(safeInt(jGroup.getConsThreshold()));
4205         sg.setOutlineColour(new Color(safeInt(jGroup.getOutlineColour())));
4206
4207         sg.textColour = new Color(safeInt(jGroup.getTextCol1()));
4208         sg.textColour2 = new Color(safeInt(jGroup.getTextCol2()));
4209         sg.setShowNonconserved(safeBoolean(jGroup.isShowUnconserved()));
4210         sg.thresholdTextColour = safeInt(jGroup.getTextColThreshold());
4211         // attributes with a default in the schema are never null
4212         sg.setShowConsensusHistogram(jGroup.isShowConsensusHistogram());
4213         sg.setshowSequenceLogo(jGroup.isShowSequenceLogo());
4214         sg.setNormaliseSequenceLogo(jGroup.isNormaliseSequenceLogo());
4215         sg.setIgnoreGapsConsensus(jGroup.isIgnoreGapsinConsensus());
4216         if (jGroup.getConsThreshold() != null
4217                 && jGroup.getConsThreshold().intValue() != 0)
4218         {
4219           Conservation c = new Conservation("All", sg.getSequences(null), 0,
4220                   sg.getWidth() - 1);
4221           c.calculate();
4222           c.verdict(false, 25);
4223           sg.cs.setConservation(c);
4224         }
4225
4226         if (jGroup.getId() != null && groupAnnotRefs.size() > 0)
4227         {
4228           // re-instate unique group/annotation row reference
4229           List<AlignmentAnnotation> jaal = groupAnnotRefs
4230                   .get(jGroup.getId());
4231           if (jaal != null)
4232           {
4233             for (AlignmentAnnotation jaa : jaal)
4234             {
4235               jaa.groupRef = sg;
4236               if (jaa.autoCalculated)
4237               {
4238                 // match up and try to set group autocalc alignment row for this
4239                 // annotation
4240                 if (jaa.label.startsWith("Consensus for "))
4241                 {
4242                   sg.setConsensus(jaa);
4243                 }
4244                 // match up and try to set group autocalc alignment row for this
4245                 // annotation
4246                 if (jaa.label.startsWith("Conservation for "))
4247                 {
4248                   sg.setConservationRow(jaa);
4249                 }
4250               }
4251             }
4252           }
4253         }
4254         al.addGroup(sg);
4255         if (addAnnotSchemeGroup)
4256         {
4257           // reconstruct the annotation colourscheme
4258           sg.setColourScheme(
4259                   constructAnnotationColour(jGroup.getAnnotationColours(),
4260                           null, al, jalviewModel, false));
4261         }
4262       }
4263     }
4264     if (view == null)
4265     {
4266       // only dataset in this model, so just return.
4267       return null;
4268     }
4269     // ///////////////////////////////
4270     // LOAD VIEWPORT
4271
4272     AlignFrame af = null;
4273     AlignViewport av = null;
4274     // now check to see if we really need to create a new viewport.
4275     if (multipleView && viewportsAdded.size() == 0)
4276     {
4277       // We recovered an alignment for which a viewport already exists.
4278       // TODO: fix up any settings necessary for overlaying stored state onto
4279       // state recovered from another document. (may not be necessary).
4280       // we may need a binding from a viewport in memory to one recovered from
4281       // XML.
4282       // and then recover its containing af to allow the settings to be applied.
4283       // TODO: fix for vamsas demo
4284       jalview.bin.Console.errPrintln(
4285               "About to recover a viewport for existing alignment: Sequence set ID is "
4286                       + uniqueSeqSetId);
4287       Object seqsetobj = retrieveExistingObj(uniqueSeqSetId);
4288       if (seqsetobj != null)
4289       {
4290         if (seqsetobj instanceof String)
4291         {
4292           uniqueSeqSetId = (String) seqsetobj;
4293           jalview.bin.Console.errPrintln(
4294                   "Recovered extant sequence set ID mapping for ID : New Sequence set ID is "
4295                           + uniqueSeqSetId);
4296         }
4297         else
4298         {
4299           jalview.bin.Console.errPrintln(
4300                   "Warning : Collision between sequence set ID string and existing jalview object mapping.");
4301         }
4302
4303       }
4304     }
4305     /**
4306      * indicate that annotation colours are applied across all groups (pre
4307      * Jalview 2.8.1 behaviour)
4308      */
4309     boolean doGroupAnnColour = Jalview2XML.isVersionStringLaterThan("2.8.1",
4310             jalviewModel.getVersion());
4311
4312     AlignmentPanel ap = null;
4313     boolean isnewview = true;
4314     if (viewId != null)
4315     {
4316       // Check to see if this alignment already has a view id == viewId
4317       jalview.gui.AlignmentPanel views[] = Desktop
4318               .getAlignmentPanels(uniqueSeqSetId);
4319       if (views != null && views.length > 0)
4320       {
4321         for (int v = 0; v < views.length; v++)
4322         {
4323           if (views[v].av.getViewId().equalsIgnoreCase(viewId))
4324           {
4325             // recover the existing alignpanel, alignframe, viewport
4326             af = views[v].alignFrame;
4327             av = views[v].av;
4328             ap = views[v];
4329             // TODO: could even skip resetting view settings if we don't want to
4330             // change the local settings from other jalview processes
4331             isnewview = false;
4332           }
4333         }
4334       }
4335     }
4336
4337     if (isnewview)
4338     {
4339       af = loadViewport(file, jseqs, hiddenSeqs, al, jalviewModel, view,
4340               uniqueSeqSetId, viewId, autoAlan);
4341       av = af.getViewport();
4342       ap = af.alignPanel;
4343     }
4344
4345     /*
4346      * Load any trees, PDB structures and viewers, Overview
4347      * 
4348      * Not done if flag is false (when this method is used for New View)
4349      */
4350     if (loadTreesAndStructures)
4351     {
4352       loadTrees(jalviewModel, view, af, av, ap);
4353       loadPCAViewers(jalviewModel, ap);
4354       loadPDBStructures(jprovider, jseqs, af, ap);
4355       loadRnaViewers(jprovider, jseqs, ap);
4356       loadOverview(view, jalviewModel.getVersion(), af);
4357     }
4358     // and finally return.
4359     return af;
4360   }
4361
4362   /**
4363    * Load Overview window, restoring colours, 'show hidden regions' flag, title
4364    * and geometry as saved
4365    * 
4366    * @param view
4367    * @param af
4368    */
4369   protected void loadOverview(Viewport view, String version, AlignFrame af)
4370   {
4371     if (!isVersionStringLaterThan("2.11.3", version)
4372             && view.getOverview() == null)
4373     {
4374       return;
4375     }
4376     /*
4377      * first close any Overview that was opened automatically
4378      * (if so configured in Preferences) so that the view is
4379      * restored in the same state as saved
4380      */
4381     af.alignPanel.closeOverviewPanel();
4382
4383     Overview overview = view.getOverview();
4384     if (overview != null)
4385     {
4386       OverviewPanel overviewPanel = af
4387               .openOverviewPanel(overview.isShowHidden());
4388       overviewPanel.setTitle(overview.getTitle());
4389       overviewPanel.setFrameBounds(overview.getXpos(), overview.getYpos(),
4390               overview.getWidth(), overview.getHeight());
4391       Color gap = new Color(overview.getGapColour());
4392       Color residue = new Color(overview.getResidueColour());
4393       Color hidden = new Color(overview.getHiddenColour());
4394       overviewPanel.getCanvas().setColours(gap, residue, hidden);
4395     }
4396   }
4397
4398   /**
4399    * Instantiate and link any saved RNA (Varna) viewers. The state of the Varna
4400    * panel is restored from separate jar entries, two (gapped and trimmed) per
4401    * sequence and secondary structure.
4402    * 
4403    * Currently each viewer shows just one sequence and structure (gapped and
4404    * trimmed), however this method is designed to support multiple sequences or
4405    * structures in viewers if wanted in future.
4406    * 
4407    * @param jprovider
4408    * @param jseqs
4409    * @param ap
4410    */
4411   private void loadRnaViewers(jarInputStreamProvider jprovider,
4412           List<JSeq> jseqs, AlignmentPanel ap)
4413   {
4414     /*
4415      * scan the sequences for references to viewers; create each one the first
4416      * time it is referenced, add Rna models to existing viewers
4417      */
4418     for (JSeq jseq : jseqs)
4419     {
4420       for (int i = 0; i < jseq.getRnaViewer().size(); i++)
4421       {
4422         RnaViewer viewer = jseq.getRnaViewer().get(i);
4423         AppVarna appVarna = findOrCreateVarnaViewer(viewer, uniqueSetSuffix,
4424                 ap);
4425
4426         for (int j = 0; j < viewer.getSecondaryStructure().size(); j++)
4427         {
4428           SecondaryStructure ss = viewer.getSecondaryStructure().get(j);
4429           SequenceI seq = seqRefIds.get(jseq.getId());
4430           AlignmentAnnotation ann = this.annotationIds
4431                   .get(ss.getAnnotationId());
4432
4433           /*
4434            * add the structure to the Varna display (with session state copied
4435            * from the jar to a temporary file)
4436            */
4437           boolean gapped = safeBoolean(ss.isGapped());
4438           String rnaTitle = ss.getTitle();
4439           String sessionState = ss.getViewerState();
4440           String tempStateFile = copyJarEntry(jprovider, sessionState,
4441                   "varna", null);
4442           RnaModel rna = new RnaModel(rnaTitle, ann, seq, null, gapped);
4443           appVarna.addModelSession(rna, rnaTitle, tempStateFile);
4444         }
4445         appVarna.setInitialSelection(safeInt(viewer.getSelectedRna()));
4446       }
4447     }
4448   }
4449
4450   /**
4451    * Locate and return an already instantiated matching AppVarna, or create one
4452    * if not found
4453    * 
4454    * @param viewer
4455    * @param viewIdSuffix
4456    * @param ap
4457    * @return
4458    */
4459   protected AppVarna findOrCreateVarnaViewer(RnaViewer viewer,
4460           String viewIdSuffix, AlignmentPanel ap)
4461   {
4462     /*
4463      * on each load a suffix is appended to the saved viewId, to avoid conflicts
4464      * if load is repeated
4465      */
4466     String postLoadId = viewer.getViewId() + viewIdSuffix;
4467     for (JInternalFrame frame : getAllFrames())
4468     {
4469       if (frame instanceof AppVarna)
4470       {
4471         AppVarna varna = (AppVarna) frame;
4472         if (postLoadId.equals(varna.getViewId()))
4473         {
4474           // this viewer is already instantiated
4475           // could in future here add ap as another 'parent' of the
4476           // AppVarna window; currently just 1-to-many
4477           return varna;
4478         }
4479       }
4480     }
4481
4482     /*
4483      * viewer not found - make it
4484      */
4485     RnaViewerModel model = new RnaViewerModel(postLoadId, viewer.getTitle(),
4486             safeInt(viewer.getXpos()), safeInt(viewer.getYpos()),
4487             safeInt(viewer.getWidth()), safeInt(viewer.getHeight()),
4488             safeInt(viewer.getDividerLocation()));
4489     AppVarna varna = new AppVarna(model, ap);
4490
4491     return varna;
4492   }
4493
4494   /**
4495    * Load any saved trees
4496    * 
4497    * @param jm
4498    * @param view
4499    * @param af
4500    * @param av
4501    * @param ap
4502    */
4503   protected void loadTrees(JalviewModel jm, Viewport view, AlignFrame af,
4504           AlignViewport av, AlignmentPanel ap)
4505   {
4506     // TODO result of automated refactoring - are all these parameters needed?
4507     try
4508     {
4509       for (int t = 0; t < jm.getTree().size(); t++)
4510       {
4511
4512         Tree tree = jm.getTree().get(t);
4513
4514         TreePanel tp = (TreePanel) retrieveExistingObj(tree.getId());
4515         if (tp == null)
4516         {
4517           if (tree.isColumnWise())
4518           {
4519             AlignmentAnnotation aa = annotationIds
4520                     .get(tree.getColumnReference());
4521             if (aa == null)
4522             {
4523               Console.warn(
4524                       "Null alignment annotation when restoring columnwise tree");
4525             }
4526             tp = af.showColumnWiseTree(new NewickFile(tree.getNewick()), aa,
4527                     tree.getTitle(), safeInt(tree.getWidth()),
4528                     safeInt(tree.getHeight()), safeInt(tree.getXpos()),
4529                     safeInt(tree.getYpos()));
4530
4531           }
4532           else
4533           {
4534             tp = af.showNewickTree(new NewickFile(tree.getNewick()),
4535                     tree.getTitle(), safeInt(tree.getWidth()),
4536                     safeInt(tree.getHeight()), safeInt(tree.getXpos()),
4537                     safeInt(tree.getYpos()));
4538           }
4539           if (tree.getId() != null)
4540           {
4541             // perhaps bind the tree id to something ?
4542           }
4543         }
4544         else
4545         {
4546           // update local tree attributes ?
4547           // TODO: should check if tp has been manipulated by user - if so its
4548           // settings shouldn't be modified
4549           tp.setTitle(tree.getTitle());
4550           tp.setBounds(new Rectangle(safeInt(tree.getXpos()),
4551                   safeInt(tree.getYpos()), safeInt(tree.getWidth()),
4552                   safeInt(tree.getHeight())));
4553           tp.setViewport(av); // af.viewport;
4554           // TODO: verify 'associate with all views' works still
4555           tp.getTreeCanvas().setViewport(av); // af.viewport;
4556           tp.getTreeCanvas().setAssociatedPanel(ap); // af.alignPanel;
4557         }
4558         tp.getTreeCanvas().setApplyToAllViews(tree.isLinkToAllViews());
4559         if (tp == null)
4560         {
4561           Console.warn(
4562                   "There was a problem recovering stored Newick tree: \n"
4563                           + tree.getNewick());
4564           continue;
4565         }
4566
4567         tp.fitToWindow.setState(safeBoolean(tree.isFitToWindow()));
4568         tp.fitToWindow_actionPerformed(null);
4569
4570         if (tree.getFontName() != null)
4571         {
4572           tp.setTreeFont(
4573                   new Font(tree.getFontName(), safeInt(tree.getFontStyle()),
4574                           safeInt(tree.getFontSize())));
4575         }
4576         else
4577         {
4578           tp.setTreeFont(
4579                   new Font(view.getFontName(), safeInt(view.getFontStyle()),
4580                           safeInt(view.getFontSize())));
4581         }
4582
4583         tp.showPlaceholders(safeBoolean(tree.isMarkUnlinked()));
4584         tp.showBootstrap(safeBoolean(tree.isShowBootstrap()));
4585         tp.showDistances(safeBoolean(tree.isShowDistances()));
4586
4587         tp.getTreeCanvas().setThreshold(safeFloat(tree.getThreshold()));
4588
4589         if (safeBoolean(tree.isCurrentTree()))
4590         {
4591           af.getViewport().setCurrentTree(tp.getTree());
4592         }
4593       }
4594
4595     } catch (Exception ex)
4596     {
4597       ex.printStackTrace();
4598     }
4599   }
4600
4601   /**
4602    * Load and link any saved structure viewers.
4603    * 
4604    * @param jprovider
4605    * @param jseqs
4606    * @param af
4607    * @param ap
4608    */
4609   protected void loadPDBStructures(jarInputStreamProvider jprovider,
4610           List<JSeq> jseqs, AlignFrame af, AlignmentPanel ap)
4611   {
4612     /*
4613      * Run through all PDB ids on the alignment, and collect mappings between
4614      * distinct view ids and all sequences referring to that view.
4615      */
4616     Map<String, StructureViewerModel> structureViewers = new LinkedHashMap<>();
4617
4618     for (int i = 0; i < jseqs.size(); i++)
4619     {
4620       JSeq jseq = jseqs.get(i);
4621       if (jseq.getPdbids().size() > 0)
4622       {
4623         List<Pdbids> ids = jseq.getPdbids();
4624         for (int p = 0; p < ids.size(); p++)
4625         {
4626           Pdbids pdbid = ids.get(p);
4627           final int structureStateCount = pdbid.getStructureState().size();
4628           for (int s = 0; s < structureStateCount; s++)
4629           {
4630             // check to see if we haven't already created this structure view
4631             final StructureState structureState = pdbid.getStructureState()
4632                     .get(s);
4633             String sviewid = (structureState.getViewId() == null) ? null
4634                     : structureState.getViewId() + uniqueSetSuffix;
4635             jalview.datamodel.PDBEntry jpdb = new jalview.datamodel.PDBEntry();
4636             // Originally : pdbid.getFile()
4637             // : TODO: verify external PDB file recovery still works in normal
4638             // jalview project load
4639             jpdb.setFile(
4640                     loadPDBFile(jprovider, pdbid.getId(), pdbid.getFile()));
4641             jpdb.setId(pdbid.getId());
4642
4643             int x = safeInt(structureState.getXpos());
4644             int y = safeInt(structureState.getYpos());
4645             int width = safeInt(structureState.getWidth());
4646             int height = safeInt(structureState.getHeight());
4647
4648             // Probably don't need to do this anymore...
4649             // Desktop.desktop.getComponentAt(x, y);
4650             // TODO: NOW: check that this recovers the PDB file correctly.
4651             String pdbFile = loadPDBFile(jprovider, pdbid.getId(),
4652                     pdbid.getFile());
4653             jalview.datamodel.SequenceI seq = seqRefIds
4654                     .get(jseq.getId() + "");
4655             if (sviewid == null)
4656             {
4657               sviewid = "_jalview_pre2_4_" + x + "," + y + "," + width + ","
4658                       + height;
4659             }
4660             if (!structureViewers.containsKey(sviewid))
4661             {
4662               String viewerType = structureState.getType();
4663               if (viewerType == null) // pre Jalview 2.9
4664               {
4665                 viewerType = ViewerType.JMOL.toString();
4666               }
4667               structureViewers.put(sviewid,
4668                       new StructureViewerModel(x, y, width, height, false,
4669                               false, true, structureState.getViewId(),
4670                               viewerType));
4671               // Legacy pre-2.7 conversion JAL-823 :
4672               // do not assume any view has to be linked for colour by
4673               // sequence
4674             }
4675
4676             // assemble String[] { pdb files }, String[] { id for each
4677             // file }, orig_fileloc, SequenceI[][] {{ seqs_file 1 }, {
4678             // seqs_file 2}, boolean[] {
4679             // linkAlignPanel,superposeWithAlignpanel}} from hash
4680             StructureViewerModel jmoldat = structureViewers.get(sviewid);
4681             jmoldat.setAlignWithPanel(jmoldat.isAlignWithPanel()
4682                     || structureState.isAlignwithAlignPanel());
4683
4684             /*
4685              * Default colour by linked panel to false if not specified (e.g.
4686              * for pre-2.7 projects)
4687              */
4688             boolean colourWithAlignPanel = jmoldat.isColourWithAlignPanel();
4689             colourWithAlignPanel |= structureState.isColourwithAlignPanel();
4690             jmoldat.setColourWithAlignPanel(colourWithAlignPanel);
4691
4692             /*
4693              * Default colour by viewer to true if not specified (e.g. for
4694              * pre-2.7 projects)
4695              */
4696             boolean colourByViewer = jmoldat.isColourByViewer();
4697             colourByViewer &= structureState.isColourByJmol();
4698             jmoldat.setColourByViewer(colourByViewer);
4699
4700             if (jmoldat.getStateData().length() < structureState.getValue()
4701                     /*Content()*/.length())
4702             {
4703               jmoldat.setStateData(structureState.getValue());// Content());
4704             }
4705             if (pdbid.getFile() != null)
4706             {
4707               File mapkey = new File(pdbid.getFile());
4708               StructureData seqstrmaps = jmoldat.getFileData().get(mapkey);
4709               if (seqstrmaps == null)
4710               {
4711                 jmoldat.getFileData().put(mapkey,
4712                         seqstrmaps = jmoldat.new StructureData(pdbFile,
4713                                 pdbid.getId()));
4714               }
4715               if (!seqstrmaps.getSeqList().contains(seq))
4716               {
4717                 seqstrmaps.getSeqList().add(seq);
4718                 // TODO and chains?
4719               }
4720             }
4721             else
4722             {
4723               errorMessage = ("The Jmol views in this project were imported\nfrom an older version of Jalview.\nPlease review the sequence colour associations\nin the Colour by section of the Jmol View menu.\n\nIn the case of problems, see note at\nhttp://issues.jalview.org/browse/JAL-747");
4724               Console.warn(errorMessage);
4725             }
4726           }
4727         }
4728       }
4729     }
4730     // Instantiate the associated structure views
4731     for (Entry<String, StructureViewerModel> entry : structureViewers
4732             .entrySet())
4733     {
4734       try
4735       {
4736         createOrLinkStructureViewer(entry, af, ap, jprovider);
4737       } catch (Exception e)
4738       {
4739         jalview.bin.Console.errPrintln(
4740                 "Error loading structure viewer: " + e.getMessage());
4741         // failed - try the next one
4742       }
4743     }
4744   }
4745
4746   /**
4747    * 
4748    * @param viewerData
4749    * @param af
4750    * @param ap
4751    * @param jprovider
4752    */
4753   protected void createOrLinkStructureViewer(
4754           Entry<String, StructureViewerModel> viewerData, AlignFrame af,
4755           AlignmentPanel ap, jarInputStreamProvider jprovider)
4756   {
4757     final StructureViewerModel stateData = viewerData.getValue();
4758
4759     /*
4760      * Search for any viewer windows already open from other alignment views
4761      * that exactly match the stored structure state
4762      */
4763     StructureViewerBase comp = findMatchingViewer(viewerData);
4764
4765     if (comp != null)
4766     {
4767       linkStructureViewer(ap, comp, stateData);
4768       return;
4769     }
4770
4771     String type = stateData.getType();
4772     try
4773     {
4774       ViewerType viewerType = ViewerType.valueOf(type);
4775       createStructureViewer(viewerType, viewerData, af, jprovider);
4776     } catch (IllegalArgumentException | NullPointerException e)
4777     {
4778       // TODO JAL-3619 show error dialog / offer an alternative viewer
4779       Console.error("Invalid structure viewer type: " + type);
4780     }
4781   }
4782
4783   /**
4784    * Generates a name for the entry in the project jar file to hold state
4785    * information for a structure viewer
4786    * 
4787    * @param viewId
4788    * @return
4789    */
4790   protected String getViewerJarEntryName(String viewId)
4791   {
4792     return VIEWER_PREFIX + viewId;
4793   }
4794
4795   /**
4796    * Returns any open frame that matches given structure viewer data. The match
4797    * is based on the unique viewId, or (for older project versions) the frame's
4798    * geometry.
4799    * 
4800    * @param viewerData
4801    * @return
4802    */
4803   protected StructureViewerBase findMatchingViewer(
4804           Entry<String, StructureViewerModel> viewerData)
4805   {
4806     final String sviewid = viewerData.getKey();
4807     final StructureViewerModel svattrib = viewerData.getValue();
4808     StructureViewerBase comp = null;
4809     JInternalFrame[] frames = getAllFrames();
4810     for (JInternalFrame frame : frames)
4811     {
4812       if (frame instanceof StructureViewerBase)
4813       {
4814         /*
4815          * Post jalview 2.4 schema includes structure view id
4816          */
4817         if (sviewid != null && ((StructureViewerBase) frame).getViewId()
4818                 .equals(sviewid))
4819         {
4820           comp = (StructureViewerBase) frame;
4821           break; // break added in 2.9
4822         }
4823         /*
4824          * Otherwise test for matching position and size of viewer frame
4825          */
4826         else if (frame.getX() == svattrib.getX()
4827                 && frame.getY() == svattrib.getY()
4828                 && frame.getHeight() == svattrib.getHeight()
4829                 && frame.getWidth() == svattrib.getWidth())
4830         {
4831           comp = (StructureViewerBase) frame;
4832           // no break in faint hope of an exact match on viewId
4833         }
4834       }
4835     }
4836     return comp;
4837   }
4838
4839   /**
4840    * Link an AlignmentPanel to an existing structure viewer.
4841    * 
4842    * @param ap
4843    * @param viewer
4844    * @param oldFiles
4845    * @param useinViewerSuperpos
4846    * @param usetoColourbyseq
4847    * @param viewerColouring
4848    */
4849   protected void linkStructureViewer(AlignmentPanel ap,
4850           StructureViewerBase viewer, StructureViewerModel stateData)
4851   {
4852     // NOTE: if the jalview project is part of a shared session then
4853     // view synchronization should/could be done here.
4854
4855     final boolean useinViewerSuperpos = stateData.isAlignWithPanel();
4856     final boolean usetoColourbyseq = stateData.isColourWithAlignPanel();
4857     final boolean viewerColouring = stateData.isColourByViewer();
4858     Map<File, StructureData> oldFiles = stateData.getFileData();
4859
4860     /*
4861      * Add mapping for sequences in this view to an already open viewer
4862      */
4863     final AAStructureBindingModel binding = viewer.getBinding();
4864     for (File id : oldFiles.keySet())
4865     {
4866       // add this and any other pdb files that should be present in the
4867       // viewer
4868       StructureData filedat = oldFiles.get(id);
4869       String pdbFile = filedat.getFilePath();
4870       SequenceI[] seq = filedat.getSeqList().toArray(new SequenceI[0]);
4871       binding.getSsm().setMapping(seq, null, pdbFile, DataSourceType.FILE,
4872               null);
4873       binding.addSequenceForStructFile(pdbFile, seq);
4874     }
4875     // and add the AlignmentPanel's reference to the view panel
4876     viewer.addAlignmentPanel(ap);
4877     if (useinViewerSuperpos)
4878     {
4879       viewer.useAlignmentPanelForSuperposition(ap);
4880     }
4881     else
4882     {
4883       viewer.excludeAlignmentPanelForSuperposition(ap);
4884     }
4885     if (usetoColourbyseq)
4886     {
4887       viewer.useAlignmentPanelForColourbyseq(ap, !viewerColouring);
4888     }
4889     else
4890     {
4891       viewer.excludeAlignmentPanelForColourbyseq(ap);
4892     }
4893   }
4894
4895   /**
4896    * Get all frames within the Desktop.
4897    * 
4898    * @return
4899    */
4900   protected JInternalFrame[] getAllFrames()
4901   {
4902     JInternalFrame[] frames = null;
4903     // TODO is this necessary - is it safe - risk of hanging?
4904     do
4905     {
4906       try
4907       {
4908         frames = Desktop.desktop.getAllFrames();
4909       } catch (ArrayIndexOutOfBoundsException e)
4910       {
4911         // occasional No such child exceptions are thrown here...
4912         try
4913         {
4914           Thread.sleep(10);
4915         } catch (InterruptedException f)
4916         {
4917         }
4918       }
4919     } while (frames == null);
4920     return frames;
4921   }
4922
4923   /**
4924    * Answers true if 'version' is equal to or later than 'supported', where each
4925    * is formatted as major/minor versions like "2.8.3" or "2.3.4b1" for bugfix
4926    * changes. Development and test values for 'version' are leniently treated
4927    * i.e. answer true.
4928    * 
4929    * @param supported
4930    *          - minimum version we are comparing against
4931    * @param version
4932    *          - version of data being processsed
4933    * @return true if version is equal to or later than supported
4934    */
4935   public static boolean isVersionStringLaterThan(String supported,
4936           String version)
4937   {
4938     if (supported == null || version == null
4939             || version.equalsIgnoreCase("DEVELOPMENT BUILD")
4940             || version.equalsIgnoreCase("Test")
4941             || version.equalsIgnoreCase("AUTOMATED BUILD"))
4942     {
4943       jalview.bin.Console.errPrintln("Assuming project file with "
4944               + (version == null ? "null" : version)
4945               + " is compatible with Jalview version " + supported);
4946       return true;
4947     }
4948     else
4949     {
4950       return StringUtils.compareVersions(version, supported, "b") >= 0;
4951     }
4952   }
4953
4954   Vector<JalviewStructureDisplayI> newStructureViewers = null;
4955
4956   protected void addNewStructureViewer(JalviewStructureDisplayI sview)
4957   {
4958     if (newStructureViewers != null)
4959     {
4960       sview.getBinding().setFinishedLoadingFromArchive(false);
4961       newStructureViewers.add(sview);
4962     }
4963   }
4964
4965   protected void setLoadingFinishedForNewStructureViewers()
4966   {
4967     if (newStructureViewers != null)
4968     {
4969       for (JalviewStructureDisplayI sview : newStructureViewers)
4970       {
4971         sview.getBinding().setFinishedLoadingFromArchive(true);
4972       }
4973       newStructureViewers.clear();
4974       newStructureViewers = null;
4975     }
4976   }
4977
4978   AlignFrame loadViewport(String file, List<JSeq> JSEQ,
4979           List<SequenceI> hiddenSeqs, AlignmentI al, JalviewModel jm,
4980           Viewport view, String uniqueSeqSetId, String viewId,
4981           List<JvAnnotRow> autoAlan)
4982   {
4983     AlignFrame af = null;
4984     af = new AlignFrame(al, safeInt(view.getWidth()),
4985             safeInt(view.getHeight()), uniqueSeqSetId, viewId)
4986     // {
4987     //
4988     // @Override
4989     // protected void processKeyEvent(java.awt.event.KeyEvent e) {
4990     // jalview.bin.Console.outPrintln("Jalview2XML AF " + e);
4991     // super.processKeyEvent(e);
4992     //
4993     // }
4994     //
4995     // }
4996     ;
4997
4998     af.setFileName(file, FileFormat.Jalview);
4999
5000     final AlignViewport viewport = af.getViewport();
5001     for (int i = 0; i < JSEQ.size(); i++)
5002     {
5003       int colour = safeInt(JSEQ.get(i).getColour());
5004       viewport.setSequenceColour(viewport.getAlignment().getSequenceAt(i),
5005               new Color(colour));
5006     }
5007
5008     if (al.hasSeqrep())
5009     {
5010       viewport.setColourByReferenceSeq(true);
5011       viewport.setDisplayReferenceSeq(true);
5012     }
5013
5014     viewport.setGatherViewsHere(safeBoolean(view.isGatheredViews()));
5015
5016     if (view.getSequenceSetId() != null)
5017     {
5018       AlignmentViewport av = viewportsAdded.get(uniqueSeqSetId);
5019
5020       viewport.setSequenceSetId(uniqueSeqSetId);
5021       if (av != null)
5022       {
5023         // propagate shared settings to this new view
5024         viewport.setHistoryList(av.getHistoryList());
5025         viewport.setRedoList(av.getRedoList());
5026       }
5027       else
5028       {
5029         viewportsAdded.put(uniqueSeqSetId, viewport);
5030       }
5031       // TODO: check if this method can be called repeatedly without
5032       // side-effects if alignpanel already registered.
5033       PaintRefresher.Register(af.alignPanel, uniqueSeqSetId);
5034     }
5035     // apply Hidden regions to view.
5036     if (hiddenSeqs != null)
5037     {
5038       for (int s = 0; s < JSEQ.size(); s++)
5039       {
5040         SequenceGroup hidden = new SequenceGroup();
5041         boolean isRepresentative = false;
5042         for (int r = 0; r < JSEQ.get(s).getHiddenSequences().size(); r++)
5043         {
5044           isRepresentative = true;
5045           SequenceI sequenceToHide = al
5046                   .getSequenceAt(JSEQ.get(s).getHiddenSequences().get(r));
5047           hidden.addSequence(sequenceToHide, false);
5048           // remove from hiddenSeqs list so we don't try to hide it twice
5049           hiddenSeqs.remove(sequenceToHide);
5050         }
5051         if (isRepresentative)
5052         {
5053           SequenceI representativeSequence = al.getSequenceAt(s);
5054           hidden.addSequence(representativeSequence, false);
5055           viewport.hideRepSequences(representativeSequence, hidden);
5056         }
5057       }
5058
5059       SequenceI[] hseqs = hiddenSeqs
5060               .toArray(new SequenceI[hiddenSeqs.size()]);
5061       viewport.hideSequence(hseqs);
5062
5063     }
5064     // recover view properties and display parameters
5065
5066     viewport.setShowAnnotation(safeBoolean(view.isShowAnnotation()));
5067     viewport.setAbovePIDThreshold(safeBoolean(view.isPidSelected()));
5068     final int pidThreshold = safeInt(view.getPidThreshold());
5069     viewport.setThreshold(pidThreshold);
5070
5071     viewport.setColourText(safeBoolean(view.isShowColourText()));
5072
5073     viewport.setConservationSelected(
5074             safeBoolean(view.isConservationSelected()));
5075     viewport.setIncrement(safeInt(view.getConsThreshold()));
5076     viewport.setShowJVSuffix(safeBoolean(view.isShowFullId()));
5077     viewport.setRightAlignIds(safeBoolean(view.isRightAlignIds()));
5078     viewport.setFont(
5079             new Font(view.getFontName(), safeInt(view.getFontStyle()),
5080                     safeInt(view.getFontSize())),
5081             (view.getCharWidth() != null) ? false : true);
5082     if (view.getCharWidth() != null)
5083     {
5084       viewport.setCharWidth(view.getCharWidth());
5085       viewport.setCharHeight(view.getCharHeight());
5086     }
5087     ViewStyleI vs = viewport.getViewStyle();
5088     vs.setScaleProteinAsCdna(view.isScaleProteinAsCdna());
5089     viewport.setViewStyle(vs);
5090     // TODO: allow custom charWidth/Heights to be restored by updating them
5091     // after setting font - which means set above to false
5092     viewport.setRenderGaps(safeBoolean(view.isRenderGaps()));
5093     viewport.setWrapAlignment(safeBoolean(view.isWrapAlignment()));
5094     viewport.setShowAnnotation(safeBoolean(view.isShowAnnotation()));
5095
5096     viewport.setShowBoxes(safeBoolean(view.isShowBoxes()));
5097
5098     viewport.setShowText(safeBoolean(view.isShowText()));
5099
5100     viewport.setTextColour(new Color(safeInt(view.getTextCol1())));
5101     viewport.setTextColour2(new Color(safeInt(view.getTextCol2())));
5102     viewport.setThresholdTextColour(safeInt(view.getTextColThreshold()));
5103     viewport.setShowUnconserved(view.isShowUnconserved());
5104     viewport.getRanges().setStartRes(safeInt(view.getStartRes()));
5105
5106     if (view.getViewName() != null)
5107     {
5108       viewport.setViewName(view.getViewName());
5109       af.setInitialTabVisible();
5110     }
5111     af.setBounds(safeInt(view.getXpos()), safeInt(view.getYpos()),
5112             safeInt(view.getWidth()), safeInt(view.getHeight()));
5113     // startSeq set in af.alignPanel.updateLayout below
5114     af.alignPanel.updateLayout();
5115     ColourSchemeI cs = null;
5116     // apply colourschemes
5117     if (view.getBgColour() != null)
5118     {
5119       if (view.getBgColour().startsWith("ucs"))
5120       {
5121         cs = getUserColourScheme(jm, view.getBgColour());
5122       }
5123       else if (view.getBgColour().startsWith("Annotation"))
5124       {
5125         AnnotationColourScheme viewAnnColour = view.getAnnotationColours();
5126         cs = constructAnnotationColour(viewAnnColour, af, al, jm, true);
5127
5128         // annpos
5129
5130       }
5131       else
5132       {
5133         cs = ColourSchemeProperty.getColourScheme(af.getViewport(), al,
5134                 view.getBgColour());
5135       }
5136     }
5137
5138     /*
5139      * turn off 'alignment colour applies to all groups'
5140      * while restoring global colour scheme
5141      */
5142     viewport.setColourAppliesToAllGroups(false);
5143     viewport.setGlobalColourScheme(cs);
5144     viewport.getResidueShading().setThreshold(pidThreshold,
5145             view.isIgnoreGapsinConsensus());
5146     viewport.getResidueShading()
5147             .setConsensus(viewport.getSequenceConsensusHash());
5148     if (safeBoolean(view.isConservationSelected()) && cs != null)
5149     {
5150       viewport.getResidueShading()
5151               .setConservationInc(safeInt(view.getConsThreshold()));
5152     }
5153     af.changeColour(cs);
5154     viewport.setColourAppliesToAllGroups(true);
5155
5156     viewport.setShowSequenceFeatures(
5157             safeBoolean(view.isShowSequenceFeatures()));
5158
5159     viewport.setCentreColumnLabels(view.isCentreColumnLabels());
5160     viewport.setIgnoreGapsConsensus(view.isIgnoreGapsinConsensus(), null);
5161     viewport.setFollowHighlight(view.isFollowHighlight());
5162     viewport.followSelection = view.isFollowSelection();
5163     viewport.setShowConsensusHistogram(view.isShowConsensusHistogram());
5164     viewport.setShowSequenceLogo(view.isShowSequenceLogo());
5165     viewport.setNormaliseSequenceLogo(view.isNormaliseSequenceLogo());
5166     viewport.setShowDBRefs(safeBoolean(view.isShowDbRefTooltip()));
5167     viewport.setShowNPFeats(safeBoolean(view.isShowNPfeatureTooltip()));
5168     viewport.setShowGroupConsensus(view.isShowGroupConsensus());
5169     viewport.setShowGroupConservation(view.isShowGroupConservation());
5170     viewport.setShowComplementFeatures(view.isShowComplementFeatures());
5171     viewport.setShowComplementFeaturesOnTop(
5172             view.isShowComplementFeaturesOnTop());
5173
5174     // recover feature settings
5175     if (jm.getFeatureSettings() != null)
5176     {
5177       FeatureRendererModel fr = af.alignPanel.getSeqPanel().seqCanvas
5178               .getFeatureRenderer();
5179       FeaturesDisplayed fdi;
5180       viewport.setFeaturesDisplayed(fdi = new FeaturesDisplayed());
5181       String[] renderOrder = new String[jm.getFeatureSettings().getSetting()
5182               .size()];
5183       Map<String, FeatureColourI> featureColours = new Hashtable<>();
5184       Map<String, Float> featureOrder = new Hashtable<>();
5185
5186       for (int fs = 0; fs < jm.getFeatureSettings().getSetting()
5187               .size(); fs++)
5188       {
5189         Setting setting = jm.getFeatureSettings().getSetting().get(fs);
5190         String featureType = setting.getType();
5191
5192         /*
5193          * restore feature filters (if any)
5194          */
5195         jalview.xml.binding.jalview.FeatureMatcherSet filters = setting
5196                 .getMatcherSet();
5197         if (filters != null)
5198         {
5199           FeatureMatcherSetI filter = Jalview2XML.parseFilter(featureType,
5200                   filters);
5201           if (!filter.isEmpty())
5202           {
5203             fr.setFeatureFilter(featureType, filter);
5204           }
5205         }
5206
5207         /*
5208          * restore feature colour scheme
5209          */
5210         Color maxColour = new Color(setting.getColour());
5211         if (setting.getMincolour() != null)
5212         {
5213           /*
5214            * minColour is always set unless a simple colour
5215            * (including for colour by label though it doesn't use it)
5216            */
5217           Color minColour = new Color(setting.getMincolour().intValue());
5218           Color noValueColour = minColour;
5219           NoValueColour noColour = setting.getNoValueColour();
5220           if (noColour == NoValueColour.NONE)
5221           {
5222             noValueColour = null;
5223           }
5224           else if (noColour == NoValueColour.MAX)
5225           {
5226             noValueColour = maxColour;
5227           }
5228           float min = safeFloat(safeFloat(setting.getMin()));
5229           float max = setting.getMax() == null ? 1f
5230                   : setting.getMax().floatValue();
5231           FeatureColourI gc = new FeatureColour(maxColour, minColour,
5232                   maxColour, noValueColour, min, max);
5233           if (setting.getAttributeName().size() > 0)
5234           {
5235             gc.setAttributeName(setting.getAttributeName().toArray(
5236                     new String[setting.getAttributeName().size()]));
5237           }
5238           if (setting.getThreshold() != null)
5239           {
5240             gc.setThreshold(setting.getThreshold().floatValue());
5241             int threshstate = safeInt(setting.getThreshstate());
5242             // -1 = None, 0 = Below, 1 = Above threshold
5243             if (threshstate == 0)
5244             {
5245               gc.setBelowThreshold(true);
5246             }
5247             else if (threshstate == 1)
5248             {
5249               gc.setAboveThreshold(true);
5250             }
5251           }
5252           gc.setAutoScaled(true); // default
5253           if (setting.isAutoScale() != null)
5254           {
5255             gc.setAutoScaled(setting.isAutoScale());
5256           }
5257           if (setting.isColourByLabel() != null)
5258           {
5259             gc.setColourByLabel(setting.isColourByLabel());
5260           }
5261           // and put in the feature colour table.
5262           featureColours.put(featureType, gc);
5263         }
5264         else
5265         {
5266           featureColours.put(featureType, new FeatureColour(maxColour));
5267         }
5268         renderOrder[fs] = featureType;
5269         if (setting.getOrder() != null)
5270         {
5271           featureOrder.put(featureType, setting.getOrder().floatValue());
5272         }
5273         else
5274         {
5275           featureOrder.put(featureType, Float.valueOf(
5276                   fs / jm.getFeatureSettings().getSetting().size()));
5277         }
5278         if (safeBoolean(setting.isDisplay()))
5279         {
5280           fdi.setVisible(featureType);
5281         }
5282       }
5283       Map<String, Boolean> fgtable = new Hashtable<>();
5284       for (int gs = 0; gs < jm.getFeatureSettings().getGroup().size(); gs++)
5285       {
5286         Group grp = jm.getFeatureSettings().getGroup().get(gs);
5287         fgtable.put(grp.getName(), Boolean.valueOf(grp.isDisplay()));
5288       }
5289       // FeatureRendererSettings frs = new FeatureRendererSettings(renderOrder,
5290       // fgtable, featureColours, jms.getFeatureSettings().hasTransparency() ?
5291       // jms.getFeatureSettings().getTransparency() : 0.0, featureOrder);
5292       FeatureRendererSettings frs = new FeatureRendererSettings(renderOrder,
5293               fgtable, featureColours, 1.0f, featureOrder);
5294       fr.transferSettings(frs);
5295     }
5296
5297     if (view.getHiddenColumns().size() > 0)
5298     {
5299       for (int c = 0; c < view.getHiddenColumns().size(); c++)
5300       {
5301         final HiddenColumns hc = view.getHiddenColumns().get(c);
5302         viewport.hideColumns(safeInt(hc.getStart()),
5303                 safeInt(hc.getEnd()) /* +1 */);
5304       }
5305     }
5306     if (view.getCalcIdParam() != null)
5307     {
5308       for (CalcIdParam calcIdParam : view.getCalcIdParam())
5309       {
5310         if (calcIdParam != null)
5311         {
5312           if (recoverCalcIdParam(calcIdParam, viewport))
5313           {
5314           }
5315           else
5316           {
5317             Console.warn("Couldn't recover parameters for "
5318                     + calcIdParam.getCalcId());
5319           }
5320         }
5321       }
5322     }
5323     af.setMenusFromViewport(viewport);
5324     af.setTitle(view.getTitle());
5325     // TODO: we don't need to do this if the viewport is aready visible.
5326     /*
5327      * Add the AlignFrame to the desktop (it may be 'gathered' later), unless it
5328      * has a 'cdna/protein complement' view, in which case save it in order to
5329      * populate a SplitFrame once all views have been read in.
5330      */
5331     String complementaryViewId = view.getComplementId();
5332     if (complementaryViewId == null)
5333     {
5334       Desktop.addInternalFrame(af, view.getTitle(),
5335               safeInt(view.getWidth()), safeInt(view.getHeight()));
5336       // recompute any autoannotation
5337       af.alignPanel.updateAnnotation(false, true);
5338       reorderAutoannotation(af, al, autoAlan);
5339       af.alignPanel.alignmentChanged();
5340     }
5341     else
5342     {
5343       splitFrameCandidates.put(view, af);
5344     }
5345
5346     return af;
5347   }
5348
5349   /**
5350    * Reads saved data to restore Colour by Annotation settings
5351    * 
5352    * @param viewAnnColour
5353    * @param af
5354    * @param al
5355    * @param model
5356    * @param checkGroupAnnColour
5357    * @return
5358    */
5359   private ColourSchemeI constructAnnotationColour(
5360           AnnotationColourScheme viewAnnColour, AlignFrame af,
5361           AlignmentI al, JalviewModel model, boolean checkGroupAnnColour)
5362   {
5363     boolean propagateAnnColour = false;
5364     AlignmentI annAlignment = af != null ? af.getViewport().getAlignment()
5365             : al;
5366     if (checkGroupAnnColour && al.getGroups() != null
5367             && al.getGroups().size() > 0)
5368     {
5369       // pre 2.8.1 behaviour
5370       // check to see if we should transfer annotation colours
5371       propagateAnnColour = true;
5372       for (SequenceGroup sg : al.getGroups())
5373       {
5374         if (sg.getColourScheme() instanceof AnnotationColourGradient)
5375         {
5376           propagateAnnColour = false;
5377         }
5378       }
5379     }
5380
5381     /*
5382      * 2.10.2- : saved annotationId is AlignmentAnnotation.annotationId
5383      */
5384     String annotationId = viewAnnColour.getAnnotation();
5385     AlignmentAnnotation matchedAnnotation = annotationIds.get(annotationId);
5386
5387     /*
5388      * pre 2.10.2: saved annotationId is AlignmentAnnotation.label
5389      */
5390     if (matchedAnnotation == null
5391             && annAlignment.getAlignmentAnnotation() != null)
5392     {
5393       for (int i = 0; i < annAlignment.getAlignmentAnnotation().length; i++)
5394       {
5395         if (annotationId
5396                 .equals(annAlignment.getAlignmentAnnotation()[i].label))
5397         {
5398           matchedAnnotation = annAlignment.getAlignmentAnnotation()[i];
5399           break;
5400         }
5401       }
5402     }
5403     if (matchedAnnotation == null)
5404     {
5405       jalview.bin.Console
5406               .errPrintln("Failed to match annotation colour scheme for "
5407                       + annotationId);
5408       return null;
5409     }
5410     // belt-and-braces create a threshold line if the
5411     // colourscheme needs one but the matchedAnnotation doesn't have one
5412     if (safeInt(viewAnnColour.getAboveThreshold()) != 0
5413             && matchedAnnotation.getThreshold() == null)
5414     {
5415       matchedAnnotation.setThreshold(
5416               new GraphLine(safeFloat(viewAnnColour.getThreshold()),
5417                       "Threshold", Color.black));
5418     }
5419
5420     AnnotationColourGradient cs = null;
5421     if (viewAnnColour.getColourScheme().equals("None"))
5422     {
5423       cs = new AnnotationColourGradient(matchedAnnotation,
5424               new Color(safeInt(viewAnnColour.getMinColour())),
5425               new Color(safeInt(viewAnnColour.getMaxColour())),
5426               safeInt(viewAnnColour.getAboveThreshold()));
5427     }
5428     else if (viewAnnColour.getColourScheme().startsWith("ucs"))
5429     {
5430       cs = new AnnotationColourGradient(matchedAnnotation,
5431               getUserColourScheme(model, viewAnnColour.getColourScheme()),
5432               safeInt(viewAnnColour.getAboveThreshold()));
5433     }
5434     else
5435     {
5436       cs = new AnnotationColourGradient(matchedAnnotation,
5437               ColourSchemeProperty.getColourScheme(af.getViewport(), al,
5438                       viewAnnColour.getColourScheme()),
5439               safeInt(viewAnnColour.getAboveThreshold()));
5440     }
5441
5442     boolean perSequenceOnly = safeBoolean(viewAnnColour.isPerSequence());
5443     boolean useOriginalColours = safeBoolean(
5444             viewAnnColour.isPredefinedColours());
5445     cs.setSeqAssociated(perSequenceOnly);
5446     cs.setPredefinedColours(useOriginalColours);
5447
5448     if (propagateAnnColour && al.getGroups() != null)
5449     {
5450       // Also use these settings for all the groups
5451       for (int g = 0; g < al.getGroups().size(); g++)
5452       {
5453         SequenceGroup sg = al.getGroups().get(g);
5454         if (sg.getGroupColourScheme() == null)
5455         {
5456           continue;
5457         }
5458
5459         AnnotationColourGradient groupScheme = new AnnotationColourGradient(
5460                 matchedAnnotation, sg.getColourScheme(),
5461                 safeInt(viewAnnColour.getAboveThreshold()));
5462         sg.setColourScheme(groupScheme);
5463         groupScheme.setSeqAssociated(perSequenceOnly);
5464         groupScheme.setPredefinedColours(useOriginalColours);
5465       }
5466     }
5467     return cs;
5468   }
5469
5470   private void reorderAutoannotation(AlignFrame af, AlignmentI al,
5471           List<JvAnnotRow> autoAlan)
5472   {
5473     // copy over visualization settings for autocalculated annotation in the
5474     // view
5475     if (al.getAlignmentAnnotation() != null)
5476     {
5477       /**
5478        * Kludge for magic autoannotation names (see JAL-811)
5479        */
5480       String[] magicNames = new String[] { "Consensus", "Quality",
5481           "Conservation" };
5482       JvAnnotRow nullAnnot = new JvAnnotRow(-1, null);
5483       Hashtable<String, JvAnnotRow> visan = new Hashtable<>();
5484       for (String nm : magicNames)
5485       {
5486         visan.put(nm, nullAnnot);
5487       }
5488       for (JvAnnotRow auan : autoAlan)
5489       {
5490         visan.put(auan.template.label
5491                 + (auan.template.getCalcId() == null ? ""
5492                         : "\t" + auan.template.getCalcId()),
5493                 auan);
5494       }
5495       int hSize = al.getAlignmentAnnotation().length;
5496       List<JvAnnotRow> reorder = new ArrayList<>();
5497       // work through any autoCalculated annotation already on the view
5498       // removing it if it should be placed in a different location on the
5499       // annotation panel.
5500       List<String> remains = new ArrayList<>(visan.keySet());
5501       for (int h = 0; h < hSize; h++)
5502       {
5503         jalview.datamodel.AlignmentAnnotation jalan = al
5504                 .getAlignmentAnnotation()[h];
5505         if (jalan.autoCalculated)
5506         {
5507           String k;
5508           JvAnnotRow valan = visan.get(k = jalan.label);
5509           if (jalan.getCalcId() != null)
5510           {
5511             valan = visan.get(k = jalan.label + "\t" + jalan.getCalcId());
5512           }
5513
5514           if (valan != null)
5515           {
5516             // delete the auto calculated row from the alignment
5517             al.deleteAnnotation(jalan, false);
5518             remains.remove(k);
5519             hSize--;
5520             h--;
5521             if (valan != nullAnnot)
5522             {
5523               if (jalan != valan.template)
5524               {
5525                 // newly created autoannotation row instance
5526                 // so keep a reference to the visible annotation row
5527                 // and copy over all relevant attributes
5528                 if (valan.template.graphHeight >= 0)
5529
5530                 {
5531                   jalan.graphHeight = valan.template.graphHeight;
5532                 }
5533                 jalan.visible = valan.template.visible;
5534               }
5535               reorder.add(new JvAnnotRow(valan.order, jalan));
5536             }
5537           }
5538         }
5539       }
5540       // Add any (possibly stale) autocalculated rows that were not appended to
5541       // the view during construction
5542       for (String other : remains)
5543       {
5544         JvAnnotRow othera = visan.get(other);
5545         if (othera != nullAnnot && othera.template.getCalcId() != null
5546                 && othera.template.getCalcId().length() > 0)
5547         {
5548           reorder.add(othera);
5549         }
5550       }
5551       // now put the automatic annotation in its correct place
5552       int s = 0, srt[] = new int[reorder.size()];
5553       JvAnnotRow[] rws = new JvAnnotRow[reorder.size()];
5554       for (JvAnnotRow jvar : reorder)
5555       {
5556         rws[s] = jvar;
5557         srt[s++] = jvar.order;
5558       }
5559       reorder.clear();
5560       jalview.util.QuickSort.sort(srt, rws);
5561       // and re-insert the annotation at its correct position
5562       for (JvAnnotRow jvar : rws)
5563       {
5564         al.addAnnotation(jvar.template, jvar.order);
5565       }
5566       af.alignPanel.adjustAnnotationHeight();
5567     }
5568   }
5569
5570   Hashtable skipList = null;
5571
5572   /**
5573    * TODO remove this method
5574    * 
5575    * @param view
5576    * @return AlignFrame bound to sequenceSetId from view, if one exists. private
5577    *         AlignFrame getSkippedFrame(Viewport view) { if (skipList==null) {
5578    *         throw new Error("Implementation Error. No skipList defined for this
5579    *         Jalview2XML instance."); } return (AlignFrame)
5580    *         skipList.get(view.getSequenceSetId()); }
5581    */
5582
5583   /**
5584    * Check if the Jalview view contained in object should be skipped or not.
5585    * 
5586    * @param object
5587    * @return true if view's sequenceSetId is a key in skipList
5588    */
5589   private boolean skipViewport(JalviewModel object)
5590   {
5591     if (skipList == null)
5592     {
5593       return false;
5594     }
5595     String id = object.getViewport().get(0).getSequenceSetId();
5596     if (skipList.containsKey(id))
5597     {
5598       Console.debug("Skipping seuqence set id " + id);
5599       return true;
5600     }
5601     return false;
5602   }
5603
5604   public void addToSkipList(AlignFrame af)
5605   {
5606     if (skipList == null)
5607     {
5608       skipList = new Hashtable();
5609     }
5610     skipList.put(af.getViewport().getSequenceSetId(), af);
5611   }
5612
5613   public void clearSkipList()
5614   {
5615     if (skipList != null)
5616     {
5617       skipList.clear();
5618       skipList = null;
5619     }
5620   }
5621
5622   private void recoverDatasetFor(SequenceSet vamsasSet, AlignmentI al,
5623           boolean ignoreUnrefed, String uniqueSeqSetId)
5624   {
5625     jalview.datamodel.AlignmentI ds = getDatasetFor(
5626             vamsasSet.getDatasetId());
5627     AlignmentI xtant_ds = ds;
5628     if (xtant_ds == null)
5629     {
5630       // good chance we are about to create a new dataset, but check if we've
5631       // seen some of the dataset sequence IDs before.
5632       // TODO: skip this check if we are working with project generated by
5633       // version 2.11 or later
5634       xtant_ds = checkIfHasDataset(vamsasSet.getSequence());
5635       if (xtant_ds != null)
5636       {
5637         ds = xtant_ds;
5638         addDatasetRef(vamsasSet.getDatasetId(), ds);
5639       }
5640     }
5641     Vector<SequenceI> dseqs = null;
5642     if (!ignoreUnrefed)
5643     {
5644       // recovering an alignment View
5645       AlignmentI seqSetDS = getDatasetFor(UNIQSEQSETID + uniqueSeqSetId);
5646       if (seqSetDS != null)
5647       {
5648         if (ds != null && ds != seqSetDS)
5649         {
5650           Console.warn(
5651                   "JAL-3171 regression: Overwriting a dataset reference for an alignment"
5652                           + " - CDS/Protein crossreference data may be lost");
5653           if (xtant_ds != null)
5654           {
5655             // This can only happen if the unique sequence set ID was bound to a
5656             // dataset that did not contain any of the sequences in the view
5657             // currently being restored.
5658             Console.warn(
5659                     "JAL-3171 SERIOUS!  TOTAL CONFUSION - please consider contacting the Jalview Development team so they can investigate why your project caused this message to be displayed.");
5660           }
5661         }
5662         ds = seqSetDS;
5663         addDatasetRef(vamsasSet.getDatasetId(), ds);
5664       }
5665     }
5666     if (ds == null)
5667     {
5668       // try even harder to restore dataset
5669       AlignmentI xtantDS = checkIfHasDataset(vamsasSet.getSequence());
5670       // create a list of new dataset sequences
5671       dseqs = new Vector<>();
5672     }
5673     for (int i = 0, iSize = vamsasSet.getSequence().size(); i < iSize; i++)
5674     {
5675       Sequence vamsasSeq = vamsasSet.getSequence().get(i);
5676       ensureJalviewDatasetSequence(vamsasSeq, ds, dseqs, ignoreUnrefed, i);
5677     }
5678     // create a new dataset
5679     if (ds == null)
5680     {
5681       SequenceI[] dsseqs = new SequenceI[dseqs.size()];
5682       dseqs.copyInto(dsseqs);
5683       ds = new jalview.datamodel.Alignment(dsseqs);
5684       Console.debug("Created new dataset " + vamsasSet.getDatasetId()
5685               + " for alignment " + System.identityHashCode(al));
5686       addDatasetRef(vamsasSet.getDatasetId(), ds);
5687     }
5688     // set the dataset for the newly imported alignment.
5689     if (al.getDataset() == null && !ignoreUnrefed)
5690     {
5691       al.setDataset(ds);
5692       // register dataset for the alignment's uniqueSeqSetId for legacy projects
5693       addDatasetRef(UNIQSEQSETID + uniqueSeqSetId, ds);
5694     }
5695     updateSeqDatasetBinding(vamsasSet.getSequence(), ds);
5696   }
5697
5698   /**
5699    * XML dataset sequence ID to materialised dataset reference
5700    */
5701   HashMap<String, AlignmentI> seqToDataset = new HashMap<>();
5702
5703   /**
5704    * @return the first materialised dataset reference containing a dataset
5705    *         sequence referenced in the given view
5706    * @param list
5707    *          - sequences from the view
5708    */
5709   AlignmentI checkIfHasDataset(List<Sequence> list)
5710   {
5711     for (Sequence restoredSeq : list)
5712     {
5713       AlignmentI datasetFor = seqToDataset.get(restoredSeq.getDsseqid());
5714       if (datasetFor != null)
5715       {
5716         return datasetFor;
5717       }
5718     }
5719     return null;
5720   }
5721
5722   /**
5723    * Register ds as the containing dataset for the dataset sequences referenced
5724    * by sequences in list
5725    * 
5726    * @param list
5727    *          - sequences in a view
5728    * @param ds
5729    */
5730   void updateSeqDatasetBinding(List<Sequence> list, AlignmentI ds)
5731   {
5732     for (Sequence restoredSeq : list)
5733     {
5734       AlignmentI prevDS = seqToDataset.put(restoredSeq.getDsseqid(), ds);
5735       if (prevDS != null && prevDS != ds)
5736       {
5737         Console.warn("Dataset sequence appears in many datasets: "
5738                 + restoredSeq.getDsseqid());
5739         // TODO: try to merge!
5740       }
5741     }
5742   }
5743
5744   /**
5745    * 
5746    * @param vamsasSeq
5747    *          sequence definition to create/merge dataset sequence for
5748    * @param ds
5749    *          dataset alignment
5750    * @param dseqs
5751    *          vector to add new dataset sequence to
5752    * @param ignoreUnrefed
5753    *          - when true, don't create new sequences from vamsasSeq if it's id
5754    *          doesn't already have an asssociated Jalview sequence.
5755    * @param vseqpos
5756    *          - used to reorder the sequence in the alignment according to the
5757    *          vamsasSeq array ordering, to preserve ordering of dataset
5758    */
5759   private void ensureJalviewDatasetSequence(Sequence vamsasSeq,
5760           AlignmentI ds, Vector<SequenceI> dseqs, boolean ignoreUnrefed,
5761           int vseqpos)
5762   {
5763     // JBP TODO: Check this is called for AlCodonFrames to support recovery of
5764     // xRef Codon Maps
5765     SequenceI sq = seqRefIds.get(vamsasSeq.getId());
5766     boolean reorder = false;
5767     SequenceI dsq = null;
5768     if (sq != null && sq.getDatasetSequence() != null)
5769     {
5770       dsq = sq.getDatasetSequence();
5771     }
5772     else
5773     {
5774       reorder = true;
5775     }
5776     if (sq == null && ignoreUnrefed)
5777     {
5778       return;
5779     }
5780     String sqid = vamsasSeq.getDsseqid();
5781     if (dsq == null)
5782     {
5783       // need to create or add a new dataset sequence reference to this sequence
5784       if (sqid != null)
5785       {
5786         dsq = seqRefIds.get(sqid);
5787       }
5788       // check again
5789       if (dsq == null)
5790       {
5791         // make a new dataset sequence
5792         dsq = sq.createDatasetSequence();
5793         if (sqid == null)
5794         {
5795           // make up a new dataset reference for this sequence
5796           sqid = seqHash(dsq);
5797         }
5798         dsq.setVamsasId(uniqueSetSuffix + sqid);
5799         seqRefIds.put(sqid, dsq);
5800         if (ds == null)
5801         {
5802           if (dseqs != null)
5803           {
5804             dseqs.addElement(dsq);
5805           }
5806         }
5807         else
5808         {
5809           ds.addSequence(dsq);
5810         }
5811       }
5812       else
5813       {
5814         if (sq != dsq)
5815         { // make this dataset sequence sq's dataset sequence
5816           sq.setDatasetSequence(dsq);
5817           // and update the current dataset alignment
5818           if (ds == null)
5819           {
5820             if (dseqs != null)
5821             {
5822               if (!dseqs.contains(dsq))
5823               {
5824                 dseqs.add(dsq);
5825               }
5826             }
5827             else
5828             {
5829               if (ds.findIndex(dsq) < 0)
5830               {
5831                 ds.addSequence(dsq);
5832               }
5833             }
5834           }
5835         }
5836       }
5837     }
5838     // TODO: refactor this as a merge dataset sequence function
5839     // now check that sq (the dataset sequence) sequence really is the union of
5840     // all references to it
5841     // boolean pre = sq.getStart() < dsq.getStart();
5842     // boolean post = sq.getEnd() > dsq.getEnd();
5843     // if (pre || post)
5844     if (sq != dsq)
5845     {
5846       // StringBuffer sb = new StringBuffer();
5847       String newres = jalview.analysis.AlignSeq.extractGaps(
5848               jalview.util.Comparison.GapChars, sq.getSequenceAsString());
5849       if (!newres.equalsIgnoreCase(dsq.getSequenceAsString())
5850               && newres.length() > dsq.getLength())
5851       {
5852         // Update with the longer sequence.
5853         synchronized (dsq)
5854         {
5855           /*
5856            * if (pre) { sb.insert(0, newres .substring(0, dsq.getStart() -
5857            * sq.getStart())); dsq.setStart(sq.getStart()); } if (post) {
5858            * sb.append(newres.substring(newres.length() - sq.getEnd() -
5859            * dsq.getEnd())); dsq.setEnd(sq.getEnd()); }
5860            */
5861           dsq.setSequence(newres);
5862         }
5863         // TODO: merges will never happen if we 'know' we have the real dataset
5864         // sequence - this should be detected when id==dssid
5865         jalview.bin.Console.errPrintln(
5866                 "DEBUG Notice:  Merged dataset sequence (if you see this often, post at http://issues.jalview.org/browse/JAL-1474)"); // ("
5867         // + (pre ? "prepended" : "") + " "
5868         // + (post ? "appended" : ""));
5869       }
5870     }
5871     else
5872     {
5873       // sequence refs are identical. We may need to update the existing dataset
5874       // alignment with this one, though.
5875       if (ds != null && dseqs == null)
5876       {
5877         int opos = ds.findIndex(dsq);
5878         SequenceI tseq = null;
5879         if (opos != -1 && vseqpos != opos)
5880         {
5881           // remove from old position
5882           ds.deleteSequence(dsq);
5883         }
5884         if (vseqpos < ds.getHeight())
5885         {
5886           if (vseqpos != opos)
5887           {
5888             // save sequence at destination position
5889             tseq = ds.getSequenceAt(vseqpos);
5890             ds.replaceSequenceAt(vseqpos, dsq);
5891             ds.addSequence(tseq);
5892           }
5893         }
5894         else
5895         {
5896           ds.addSequence(dsq);
5897         }
5898       }
5899     }
5900   }
5901
5902   /*
5903    * TODO use AlignmentI here and in related methods - needs
5904    * AlignmentI.getDataset() changed to return AlignmentI instead of Alignment
5905    */
5906   Hashtable<String, AlignmentI> datasetIds = null;
5907
5908   IdentityHashMap<AlignmentI, String> dataset2Ids = null;
5909
5910   private AlignmentI getDatasetFor(String datasetId)
5911   {
5912     if (datasetIds == null)
5913     {
5914       datasetIds = new Hashtable<>();
5915       return null;
5916     }
5917     if (datasetIds.containsKey(datasetId))
5918     {
5919       return datasetIds.get(datasetId);
5920     }
5921     return null;
5922   }
5923
5924   private void addDatasetRef(String datasetId, AlignmentI dataset)
5925   {
5926     if (datasetIds == null)
5927     {
5928       datasetIds = new Hashtable<>();
5929     }
5930     datasetIds.put(datasetId, dataset);
5931   }
5932
5933   /**
5934    * make a new dataset ID for this jalview dataset alignment
5935    * 
5936    * @param dataset
5937    * @return
5938    */
5939   private String getDatasetIdRef(AlignmentI dataset)
5940   {
5941     if (dataset.getDataset() != null)
5942     {
5943       Console.warn(
5944               "Serious issue!  Dataset Object passed to getDatasetIdRef is not a Jalview DATASET alignment...");
5945     }
5946     String datasetId = makeHashCode(dataset, null);
5947     if (datasetId == null)
5948     {
5949       // make a new datasetId and record it
5950       if (dataset2Ids == null)
5951       {
5952         dataset2Ids = new IdentityHashMap<>();
5953       }
5954       else
5955       {
5956         datasetId = dataset2Ids.get(dataset);
5957       }
5958       if (datasetId == null)
5959       {
5960         datasetId = "ds" + dataset2Ids.size() + 1;
5961         dataset2Ids.put(dataset, datasetId);
5962       }
5963     }
5964     return datasetId;
5965   }
5966
5967   /**
5968    * Add any saved DBRefEntry's to the sequence. An entry flagged as 'locus' is
5969    * constructed as a special subclass GeneLocus.
5970    * 
5971    * @param datasetSequence
5972    * @param sequence
5973    */
5974   private void addDBRefs(SequenceI datasetSequence, Sequence sequence)
5975   {
5976     for (int d = 0; d < sequence.getDBRef().size(); d++)
5977     {
5978       DBRef dr = sequence.getDBRef().get(d);
5979       DBRefEntry entry;
5980       if (dr.isLocus())
5981       {
5982         entry = new GeneLocus(dr.getSource(), dr.getVersion(),
5983                 dr.getAccessionId());
5984       }
5985       else
5986       {
5987         entry = new DBRefEntry(dr.getSource(), dr.getVersion(),
5988                 dr.getAccessionId());
5989       }
5990       if (dr.getMapping() != null)
5991       {
5992         entry.setMap(addMapping(dr.getMapping()));
5993       }
5994       entry.setCanonical(dr.isCanonical());
5995       datasetSequence.addDBRef(entry);
5996     }
5997   }
5998
5999   private jalview.datamodel.Mapping addMapping(Mapping m)
6000   {
6001     SequenceI dsto = null;
6002     // Mapping m = dr.getMapping();
6003     int fr[] = new int[m.getMapListFrom().size() * 2];
6004     Iterator<MapListFrom> from = m.getMapListFrom().iterator();// enumerateMapListFrom();
6005     for (int _i = 0; from.hasNext(); _i += 2)
6006     {
6007       MapListFrom mf = from.next();
6008       fr[_i] = mf.getStart();
6009       fr[_i + 1] = mf.getEnd();
6010     }
6011     int fto[] = new int[m.getMapListTo().size() * 2];
6012     Iterator<MapListTo> to = m.getMapListTo().iterator();// enumerateMapListTo();
6013     for (int _i = 0; to.hasNext(); _i += 2)
6014     {
6015       MapListTo mf = to.next();
6016       fto[_i] = mf.getStart();
6017       fto[_i + 1] = mf.getEnd();
6018     }
6019     jalview.datamodel.Mapping jmap = new jalview.datamodel.Mapping(dsto, fr,
6020             fto, m.getMapFromUnit().intValue(),
6021             m.getMapToUnit().intValue());
6022
6023     /*
6024      * (optional) choice of dseqFor or Sequence
6025      */
6026     if (m.getDseqFor() != null)
6027     {
6028       String dsfor = m.getDseqFor();
6029       if (seqRefIds.containsKey(dsfor))
6030       {
6031         /*
6032          * recover from hash
6033          */
6034         jmap.setTo(seqRefIds.get(dsfor));
6035       }
6036       else
6037       {
6038         frefedSequence.add(newMappingRef(dsfor, jmap));
6039       }
6040     }
6041     else if (m.getSequence() != null)
6042     {
6043       /*
6044        * local sequence definition
6045        */
6046       Sequence ms = m.getSequence();
6047       SequenceI djs = null;
6048       String sqid = ms.getDsseqid();
6049       if (sqid != null && sqid.length() > 0)
6050       {
6051         /*
6052          * recover dataset sequence
6053          */
6054         djs = seqRefIds.get(sqid);
6055       }
6056       else
6057       {
6058         jalview.bin.Console.errPrintln(
6059                 "Warning - making up dataset sequence id for DbRef sequence map reference");
6060         sqid = ((Object) ms).toString(); // make up a new hascode for
6061         // undefined dataset sequence hash
6062         // (unlikely to happen)
6063       }
6064
6065       if (djs == null)
6066       {
6067         /**
6068          * make a new dataset sequence and add it to refIds hash
6069          */
6070         djs = new jalview.datamodel.Sequence(ms.getName(),
6071                 ms.getSequence());
6072         djs.setStart(jmap.getMap().getToLowest());
6073         djs.setEnd(jmap.getMap().getToHighest());
6074         djs.setVamsasId(uniqueSetSuffix + sqid);
6075         jmap.setTo(djs);
6076         incompleteSeqs.put(sqid, djs);
6077         seqRefIds.put(sqid, djs);
6078
6079       }
6080       Console.debug("about to recurse on addDBRefs.");
6081       addDBRefs(djs, ms);
6082
6083     }
6084
6085     return jmap;
6086   }
6087
6088   /**
6089    * Provides a 'copy' of an alignment view (on action New View) by 'saving' the
6090    * view as XML (but not to file), and then reloading it
6091    * 
6092    * @param ap
6093    * @return
6094    */
6095   public AlignmentPanel copyAlignPanel(AlignmentPanel ap)
6096   {
6097     initSeqRefs();
6098     JalviewModel jm = saveState(ap, null, null, null);
6099
6100     addDatasetRef(
6101             jm.getVamsasModel().getSequenceSet().get(0).getDatasetId(),
6102             ap.getAlignment().getDataset());
6103
6104     uniqueSetSuffix = "";
6105     // jm.getJalviewModelSequence().getViewport(0).setId(null);
6106     jm.getViewport().get(0).setId(null);
6107     // we don't overwrite the view we just copied
6108
6109     if (this.frefedSequence == null)
6110     {
6111       frefedSequence = new Vector<>();
6112     }
6113
6114     viewportsAdded.clear();
6115
6116     AlignFrame af = loadFromObject(jm, null, false, null);
6117     af.getAlignPanels().clear();
6118     af.closeMenuItem_actionPerformed(true);
6119
6120     /*
6121      * if(ap.av.getAlignment().getAlignmentAnnotation()!=null) { for(int i=0;
6122      * i<ap.av.getAlignment().getAlignmentAnnotation().length; i++) {
6123      * if(!ap.av.getAlignment().getAlignmentAnnotation()[i].autoCalculated) {
6124      * af.alignPanel.av.getAlignment().getAlignmentAnnotation()[i] =
6125      * ap.av.getAlignment().getAlignmentAnnotation()[i]; } } }
6126      */
6127
6128     return af.alignPanel;
6129   }
6130
6131   private Hashtable jvids2vobj;
6132
6133   /**
6134    * set the object to ID mapping tables used to write/recover objects and XML
6135    * ID strings for the jalview project. If external tables are provided then
6136    * finalize and clearSeqRefs will not clear the tables when the Jalview2XML
6137    * object goes out of scope. - also populates the datasetIds hashtable with
6138    * alignment objects containing dataset sequences
6139    * 
6140    * @param vobj2jv
6141    *          Map from ID strings to jalview datamodel
6142    * @param jv2vobj
6143    *          Map from jalview datamodel to ID strings
6144    * 
6145    * 
6146    */
6147   public void setObjectMappingTables(Hashtable vobj2jv,
6148           IdentityHashMap jv2vobj)
6149   {
6150     this.jv2vobj = jv2vobj;
6151     this.vobj2jv = vobj2jv;
6152     Iterator ds = jv2vobj.keySet().iterator();
6153     String id;
6154     while (ds.hasNext())
6155     {
6156       Object jvobj = ds.next();
6157       id = jv2vobj.get(jvobj).toString();
6158       if (jvobj instanceof jalview.datamodel.Alignment)
6159       {
6160         if (((jalview.datamodel.Alignment) jvobj).getDataset() == null)
6161         {
6162           addDatasetRef(id, (jalview.datamodel.Alignment) jvobj);
6163         }
6164       }
6165       else if (jvobj instanceof jalview.datamodel.Sequence)
6166       {
6167         // register sequence object so the XML parser can recover it.
6168         if (seqRefIds == null)
6169         {
6170           seqRefIds = new HashMap<>();
6171         }
6172         if (seqsToIds == null)
6173         {
6174           seqsToIds = new IdentityHashMap<>();
6175         }
6176         seqRefIds.put(jv2vobj.get(jvobj).toString(), (SequenceI) jvobj);
6177         seqsToIds.put((SequenceI) jvobj, id);
6178       }
6179       else if (jvobj instanceof jalview.datamodel.AlignmentAnnotation)
6180       {
6181         String anid;
6182         AlignmentAnnotation jvann = (AlignmentAnnotation) jvobj;
6183         annotationIds.put(anid = jv2vobj.get(jvobj).toString(), jvann);
6184         if (jvann.annotationId == null)
6185         {
6186           jvann.annotationId = anid;
6187         }
6188         if (!jvann.annotationId.equals(anid))
6189         {
6190           // TODO verify that this is the correct behaviour
6191           Console.warn("Overriding Annotation ID for " + anid
6192                   + " from different id : " + jvann.annotationId);
6193           jvann.annotationId = anid;
6194         }
6195       }
6196       else if (jvobj instanceof String)
6197       {
6198         if (jvids2vobj == null)
6199         {
6200           jvids2vobj = new Hashtable();
6201           jvids2vobj.put(jvobj, jv2vobj.get(jvobj).toString());
6202         }
6203       }
6204       else
6205       {
6206         Console.debug("Ignoring " + jvobj.getClass() + " (ID = " + id);
6207       }
6208     }
6209   }
6210
6211   /**
6212    * set the uniqueSetSuffix used to prefix/suffix object IDs for jalview
6213    * objects created from the project archive. If string is null (default for
6214    * construction) then suffix will be set automatically.
6215    * 
6216    * @param string
6217    */
6218   public void setUniqueSetSuffix(String string)
6219   {
6220     uniqueSetSuffix = string;
6221
6222   }
6223
6224   /**
6225    * uses skipList2 as the skipList for skipping views on sequence sets
6226    * associated with keys in the skipList
6227    * 
6228    * @param skipList2
6229    */
6230   public void setSkipList(Hashtable skipList2)
6231   {
6232     skipList = skipList2;
6233   }
6234
6235   /**
6236    * Reads the jar entry of given name and returns its contents, or null if the
6237    * entry is not found.
6238    * 
6239    * @param jprovider
6240    * @param jarEntryName
6241    * @return
6242    */
6243   protected String readJarEntry(jarInputStreamProvider jprovider,
6244           String jarEntryName)
6245   {
6246     String result = null;
6247     BufferedReader in = null;
6248
6249     try
6250     {
6251       /*
6252        * Reopen the jar input stream and traverse its entries to find a matching
6253        * name
6254        */
6255       JarInputStream jin = jprovider.getJarInputStream();
6256       JarEntry entry = null;
6257       do
6258       {
6259         entry = jin.getNextJarEntry();
6260       } while (entry != null && !entry.getName().equals(jarEntryName));
6261
6262       if (entry != null)
6263       {
6264         StringBuilder out = new StringBuilder(256);
6265         in = new BufferedReader(new InputStreamReader(jin, UTF_8));
6266         String data;
6267
6268         while ((data = in.readLine()) != null)
6269         {
6270           out.append(data);
6271         }
6272         result = out.toString();
6273       }
6274       else
6275       {
6276         Console.warn(
6277                 "Couldn't find entry in Jalview Jar for " + jarEntryName);
6278       }
6279     } catch (Exception ex)
6280     {
6281       ex.printStackTrace();
6282     } finally
6283     {
6284       if (in != null)
6285       {
6286         try
6287         {
6288           in.close();
6289         } catch (IOException e)
6290         {
6291           // ignore
6292         }
6293       }
6294     }
6295
6296     return result;
6297   }
6298
6299   /**
6300    * Returns an incrementing counter (0, 1, 2...)
6301    * 
6302    * @return
6303    */
6304   private synchronized int nextCounter()
6305   {
6306     return counter++;
6307   }
6308
6309   /**
6310    * Loads any saved PCA viewers
6311    * 
6312    * @param jms
6313    * @param ap
6314    */
6315   protected void loadPCAViewers(JalviewModel model, AlignmentPanel ap)
6316   {
6317     try
6318     {
6319       List<PcaViewer> pcaviewers = model.getPcaViewer();
6320       for (PcaViewer viewer : pcaviewers)
6321       {
6322         String modelName = viewer.getScoreModelName();
6323         SimilarityParamsI params = new SimilarityParams(
6324                 viewer.isIncludeGappedColumns(), viewer.isMatchGaps(),
6325                 viewer.isIncludeGaps(),
6326                 viewer.isDenominateByShortestLength());
6327
6328         /*
6329          * create the panel (without computing the PCA)
6330          */
6331         PCAPanel panel = new PCAPanel(ap, modelName, params);
6332
6333         panel.setTitle(viewer.getTitle());
6334         panel.setBounds(new Rectangle(viewer.getXpos(), viewer.getYpos(),
6335                 viewer.getWidth(), viewer.getHeight()));
6336
6337         boolean showLabels = viewer.isShowLabels();
6338         panel.setShowLabels(showLabels);
6339         panel.getRotatableCanvas().setShowLabels(showLabels);
6340         panel.getRotatableCanvas()
6341                 .setBgColour(new Color(viewer.getBgColour()));
6342         panel.getRotatableCanvas()
6343                 .setApplyToAllViews(viewer.isLinkToAllViews());
6344
6345         /*
6346          * load PCA output data
6347          */
6348         ScoreModelI scoreModel = ScoreModels.getInstance()
6349                 .getScoreModel(modelName, ap);
6350         PCA pca = new PCA(null, scoreModel, params);
6351         PcaDataType pcaData = viewer.getPcaData();
6352
6353         MatrixI pairwise = loadDoubleMatrix(pcaData.getPairwiseMatrix());
6354         pca.setPairwiseScores(pairwise);
6355
6356         MatrixI triDiag = loadDoubleMatrix(pcaData.getTridiagonalMatrix());
6357         pca.setTridiagonal(triDiag);
6358
6359         MatrixI result = loadDoubleMatrix(pcaData.getEigenMatrix());
6360         pca.setEigenmatrix(result);
6361
6362         panel.getPcaModel().setPCA(pca);
6363
6364         /*
6365          * we haven't saved the input data! (JAL-2647 to do)
6366          */
6367         panel.setInputData(null);
6368
6369         /*
6370          * add the sequence points for the PCA display
6371          */
6372         List<jalview.datamodel.SequencePoint> seqPoints = new ArrayList<>();
6373         for (SequencePoint sp : viewer.getSequencePoint())
6374         {
6375           String seqId = sp.getSequenceRef();
6376           SequenceI seq = seqRefIds.get(seqId);
6377           if (seq == null)
6378           {
6379             throw new IllegalStateException(
6380                     "Unmatched seqref for PCA: " + seqId);
6381           }
6382           Point pt = new Point(sp.getXPos(), sp.getYPos(), sp.getZPos());
6383           jalview.datamodel.SequencePoint seqPoint = new jalview.datamodel.SequencePoint(
6384                   seq, pt);
6385           seqPoints.add(seqPoint);
6386         }
6387         panel.getRotatableCanvas().setPoints(seqPoints, seqPoints.size());
6388
6389         /*
6390          * set min-max ranges and scale after setPoints (which recomputes them)
6391          */
6392         panel.getRotatableCanvas().setScaleFactor(viewer.getScaleFactor());
6393         SeqPointMin spMin = viewer.getSeqPointMin();
6394         float[] min = new float[] { spMin.getXPos(), spMin.getYPos(),
6395             spMin.getZPos() };
6396         SeqPointMax spMax = viewer.getSeqPointMax();
6397         float[] max = new float[] { spMax.getXPos(), spMax.getYPos(),
6398             spMax.getZPos() };
6399         panel.getRotatableCanvas().setSeqMinMax(min, max);
6400
6401         // todo: hold points list in PCAModel only
6402         panel.getPcaModel().setSequencePoints(seqPoints);
6403
6404         panel.setSelectedDimensionIndex(viewer.getXDim(), X);
6405         panel.setSelectedDimensionIndex(viewer.getYDim(), Y);
6406         panel.setSelectedDimensionIndex(viewer.getZDim(), Z);
6407
6408         // is this duplication needed?
6409         panel.setTop(seqPoints.size() - 1);
6410         panel.getPcaModel().setTop(seqPoints.size() - 1);
6411
6412         /*
6413          * add the axes' end points for the display
6414          */
6415         for (int i = 0; i < 3; i++)
6416         {
6417           Axis axis = viewer.getAxis().get(i);
6418           panel.getRotatableCanvas().getAxisEndPoints()[i] = new Point(
6419                   axis.getXPos(), axis.getYPos(), axis.getZPos());
6420         }
6421
6422         Desktop.addInternalFrame(panel, MessageManager.formatMessage(
6423                 "label.calc_title", "PCA", modelName), 475, 450);
6424       }
6425     } catch (Exception ex)
6426     {
6427       Console.error("Error loading PCA: " + ex.toString());
6428     }
6429   }
6430
6431   /**
6432    * Creates a new structure viewer window
6433    * 
6434    * @param viewerType
6435    * @param viewerData
6436    * @param af
6437    * @param jprovider
6438    */
6439   protected void createStructureViewer(ViewerType viewerType,
6440           final Entry<String, StructureViewerModel> viewerData,
6441           AlignFrame af, jarInputStreamProvider jprovider)
6442   {
6443     final StructureViewerModel viewerModel = viewerData.getValue();
6444     String sessionFilePath = null;
6445
6446     if (viewerType == ViewerType.JMOL)
6447     {
6448       sessionFilePath = rewriteJmolSession(viewerModel, jprovider);
6449     }
6450     else
6451     {
6452       String viewerJarEntryName = getViewerJarEntryName(
6453               viewerModel.getViewId());
6454       sessionFilePath = copyJarEntry(jprovider, viewerJarEntryName,
6455               "viewerSession", ".tmp");
6456     }
6457     final String sessionPath = sessionFilePath;
6458     final String sviewid = viewerData.getKey();
6459     try
6460     {
6461       SwingUtilities.invokeAndWait(new Runnable()
6462       {
6463         @Override
6464         public void run()
6465         {
6466           JalviewStructureDisplayI sview = null;
6467           try
6468           {
6469             sview = StructureViewer.createView(viewerType, af.alignPanel,
6470                     viewerModel, sessionPath, sviewid);
6471             addNewStructureViewer(sview);
6472           } catch (OutOfMemoryError ex)
6473           {
6474             new OOMWarning("Restoring structure view for " + viewerType,
6475                     (OutOfMemoryError) ex.getCause());
6476             if (sview != null && sview.isVisible())
6477             {
6478               sview.closeViewer(false);
6479               sview.setVisible(false);
6480               sview.dispose();
6481             }
6482           }
6483         }
6484       });
6485     } catch (InvocationTargetException | InterruptedException ex)
6486     {
6487       Console.warn("Unexpected error when opening " + viewerType
6488               + " structure viewer", ex);
6489     }
6490   }
6491
6492   /**
6493    * Rewrites a Jmol session script, saves it to a temporary file, and returns
6494    * the path of the file. "load file" commands are rewritten to change the
6495    * original PDB file names to those created as the Jalview project is loaded.
6496    * 
6497    * @param svattrib
6498    * @param jprovider
6499    * @return
6500    */
6501   private String rewriteJmolSession(StructureViewerModel svattrib,
6502           jarInputStreamProvider jprovider)
6503   {
6504     String state = svattrib.getStateData(); // Jalview < 2.9
6505     if (state == null || state.isEmpty()) // Jalview >= 2.9
6506     {
6507       String jarEntryName = getViewerJarEntryName(svattrib.getViewId());
6508       state = readJarEntry(jprovider, jarEntryName);
6509     }
6510     // TODO or simpler? for each key in oldFiles,
6511     // replace key.getPath() in state with oldFiles.get(key).getFilePath()
6512     // (allowing for different path escapings)
6513     StringBuilder rewritten = new StringBuilder(state.length());
6514     int cp = 0, ncp, ecp;
6515     Map<File, StructureData> oldFiles = svattrib.getFileData();
6516     while ((ncp = state.indexOf("load ", cp)) > -1)
6517     {
6518       do
6519       {
6520         // look for next filename in load statement
6521         rewritten.append(state.substring(cp,
6522                 ncp = (state.indexOf("\"", ncp + 1) + 1)));
6523         String oldfilenam = state.substring(ncp,
6524                 ecp = state.indexOf("\"", ncp));
6525         // recover the new mapping data for this old filename
6526         // have to normalize filename - since Jmol and jalview do
6527         // filename translation differently.
6528         StructureData filedat = oldFiles.get(new File(oldfilenam));
6529         if (filedat == null)
6530         {
6531           String reformatedOldFilename = oldfilenam.replaceAll("/", "\\\\");
6532           filedat = oldFiles.get(new File(reformatedOldFilename));
6533         }
6534         rewritten.append(Platform.escapeBackslashes(filedat.getFilePath()));
6535         rewritten.append("\"");
6536         cp = ecp + 1; // advance beyond last \" and set cursor so we can
6537                       // look for next file statement.
6538       } while ((ncp = state.indexOf("/*file*/", cp)) > -1);
6539     }
6540     if (cp > 0)
6541     {
6542       // just append rest of state
6543       rewritten.append(state.substring(cp));
6544     }
6545     else
6546     {
6547       System.err.print("Ignoring incomplete Jmol state for PDB ids: ");
6548       rewritten = new StringBuilder(state);
6549       rewritten.append("; load append ");
6550       for (File id : oldFiles.keySet())
6551       {
6552         // add pdb files that should be present in the viewer
6553         StructureData filedat = oldFiles.get(id);
6554         rewritten.append(" \"").append(filedat.getFilePath()).append("\"");
6555       }
6556       rewritten.append(";");
6557     }
6558
6559     if (rewritten.length() == 0)
6560     {
6561       return null;
6562     }
6563     final String history = "history = ";
6564     int historyIndex = rewritten.indexOf(history);
6565     if (historyIndex > -1)
6566     {
6567       /*
6568        * change "history = [true|false];" to "history = [1|0];"
6569        */
6570       historyIndex += history.length();
6571       String val = rewritten.substring(historyIndex, historyIndex + 5);
6572       if (val.startsWith("true"))
6573       {
6574         rewritten.replace(historyIndex, historyIndex + 4, "1");
6575       }
6576       else if (val.startsWith("false"))
6577       {
6578         rewritten.replace(historyIndex, historyIndex + 5, "0");
6579       }
6580     }
6581
6582     try
6583     {
6584       File tmp = File.createTempFile("viewerSession", ".tmp");
6585       try (OutputStream os = new FileOutputStream(tmp))
6586       {
6587         InputStream is = new ByteArrayInputStream(
6588                 rewritten.toString().getBytes());
6589         copyAll(is, os);
6590         return tmp.getAbsolutePath();
6591       }
6592     } catch (IOException e)
6593     {
6594       Console.error("Error restoring Jmol session: " + e.toString());
6595     }
6596     return null;
6597   }
6598
6599   /**
6600    * Populates an XML model of the feature colour scheme for one feature type
6601    * 
6602    * @param featureType
6603    * @param fcol
6604    * @return
6605    */
6606   public static Colour marshalColour(String featureType,
6607           FeatureColourI fcol)
6608   {
6609     Colour col = new Colour();
6610     if (fcol.isSimpleColour())
6611     {
6612       col.setRGB(Format.getHexString(fcol.getColour()));
6613     }
6614     else
6615     {
6616       col.setRGB(Format.getHexString(fcol.getMaxColour()));
6617       col.setMin(fcol.getMin());
6618       col.setMax(fcol.getMax());
6619       col.setMinRGB(jalview.util.Format.getHexString(fcol.getMinColour()));
6620       col.setAutoScale(fcol.isAutoScaled());
6621       col.setThreshold(fcol.getThreshold());
6622       col.setColourByLabel(fcol.isColourByLabel());
6623       col.setThreshType(fcol.isAboveThreshold() ? ThresholdType.ABOVE
6624               : (fcol.isBelowThreshold() ? ThresholdType.BELOW
6625                       : ThresholdType.NONE));
6626       if (fcol.isColourByAttribute())
6627       {
6628         final String[] attName = fcol.getAttributeName();
6629         col.getAttributeName().add(attName[0]);
6630         if (attName.length > 1)
6631         {
6632           col.getAttributeName().add(attName[1]);
6633         }
6634       }
6635       Color noColour = fcol.getNoColour();
6636       if (noColour == null)
6637       {
6638         col.setNoValueColour(NoValueColour.NONE);
6639       }
6640       else if (noColour == fcol.getMaxColour())
6641       {
6642         col.setNoValueColour(NoValueColour.MAX);
6643       }
6644       else
6645       {
6646         col.setNoValueColour(NoValueColour.MIN);
6647       }
6648     }
6649     col.setName(featureType);
6650     return col;
6651   }
6652
6653   /**
6654    * Populates an XML model of the feature filter(s) for one feature type
6655    * 
6656    * @param firstMatcher
6657    *          the first (or only) match condition)
6658    * @param filter
6659    *          remaining match conditions (if any)
6660    * @param and
6661    *          if true, conditions are and-ed, else or-ed
6662    */
6663   public static jalview.xml.binding.jalview.FeatureMatcherSet marshalFilter(
6664           FeatureMatcherI firstMatcher, Iterator<FeatureMatcherI> filters,
6665           boolean and)
6666   {
6667     jalview.xml.binding.jalview.FeatureMatcherSet result = new jalview.xml.binding.jalview.FeatureMatcherSet();
6668
6669     if (filters.hasNext())
6670     {
6671       /*
6672        * compound matcher
6673        */
6674       CompoundMatcher compound = new CompoundMatcher();
6675       compound.setAnd(and);
6676       jalview.xml.binding.jalview.FeatureMatcherSet matcher1 = marshalFilter(
6677               firstMatcher, Collections.emptyIterator(), and);
6678       // compound.addMatcherSet(matcher1);
6679       compound.getMatcherSet().add(matcher1);
6680       FeatureMatcherI nextMatcher = filters.next();
6681       jalview.xml.binding.jalview.FeatureMatcherSet matcher2 = marshalFilter(
6682               nextMatcher, filters, and);
6683       // compound.addMatcherSet(matcher2);
6684       compound.getMatcherSet().add(matcher2);
6685       result.setCompoundMatcher(compound);
6686     }
6687     else
6688     {
6689       /*
6690        * single condition matcher
6691        */
6692       // MatchCondition matcherModel = new MatchCondition();
6693       jalview.xml.binding.jalview.FeatureMatcher matcherModel = new jalview.xml.binding.jalview.FeatureMatcher();
6694       matcherModel.setCondition(
6695               firstMatcher.getMatcher().getCondition().getStableName());
6696       matcherModel.setValue(firstMatcher.getMatcher().getPattern());
6697       if (firstMatcher.isByAttribute())
6698       {
6699         matcherModel.setBy(FilterBy.BY_ATTRIBUTE);
6700         // matcherModel.setAttributeName(firstMatcher.getAttribute());
6701         String[] attName = firstMatcher.getAttribute();
6702         matcherModel.getAttributeName().add(attName[0]); // attribute
6703         if (attName.length > 1)
6704         {
6705           matcherModel.getAttributeName().add(attName[1]); // sub-attribute
6706         }
6707       }
6708       else if (firstMatcher.isByLabel())
6709       {
6710         matcherModel.setBy(FilterBy.BY_LABEL);
6711       }
6712       else if (firstMatcher.isByScore())
6713       {
6714         matcherModel.setBy(FilterBy.BY_SCORE);
6715       }
6716       result.setMatchCondition(matcherModel);
6717     }
6718
6719     return result;
6720   }
6721
6722   /**
6723    * Loads one XML model of a feature filter to a Jalview object
6724    * 
6725    * @param featureType
6726    * @param matcherSetModel
6727    * @return
6728    */
6729   public static FeatureMatcherSetI parseFilter(String featureType,
6730           jalview.xml.binding.jalview.FeatureMatcherSet matcherSetModel)
6731   {
6732     FeatureMatcherSetI result = new FeatureMatcherSet();
6733     try
6734     {
6735       parseFilterConditions(result, matcherSetModel, true);
6736     } catch (IllegalStateException e)
6737     {
6738       // mixing AND and OR conditions perhaps
6739       jalview.bin.Console.errPrintln(
6740               String.format("Error reading filter conditions for '%s': %s",
6741                       featureType, e.getMessage()));
6742       // return as much as was parsed up to the error
6743     }
6744
6745     return result;
6746   }
6747
6748   /**
6749    * Adds feature match conditions to matcherSet as unmarshalled from XML
6750    * (possibly recursively for compound conditions)
6751    * 
6752    * @param matcherSet
6753    * @param matcherSetModel
6754    * @param and
6755    *          if true, multiple conditions are AND-ed, else they are OR-ed
6756    * @throws IllegalStateException
6757    *           if AND and OR conditions are mixed
6758    */
6759   protected static void parseFilterConditions(FeatureMatcherSetI matcherSet,
6760           jalview.xml.binding.jalview.FeatureMatcherSet matcherSetModel,
6761           boolean and)
6762   {
6763     jalview.xml.binding.jalview.FeatureMatcher mc = matcherSetModel
6764             .getMatchCondition();
6765     if (mc != null)
6766     {
6767       /*
6768        * single condition
6769        */
6770       FilterBy filterBy = mc.getBy();
6771       Condition cond = Condition.fromString(mc.getCondition());
6772       String pattern = mc.getValue();
6773       FeatureMatcherI matchCondition = null;
6774       if (filterBy == FilterBy.BY_LABEL)
6775       {
6776         matchCondition = FeatureMatcher.byLabel(cond, pattern);
6777       }
6778       else if (filterBy == FilterBy.BY_SCORE)
6779       {
6780         matchCondition = FeatureMatcher.byScore(cond, pattern);
6781
6782       }
6783       else if (filterBy == FilterBy.BY_ATTRIBUTE)
6784       {
6785         final List<String> attributeName = mc.getAttributeName();
6786         String[] attNames = attributeName
6787                 .toArray(new String[attributeName.size()]);
6788         matchCondition = FeatureMatcher.byAttribute(cond, pattern,
6789                 attNames);
6790       }
6791
6792       /*
6793        * note this throws IllegalStateException if AND-ing to a 
6794        * previously OR-ed compound condition, or vice versa
6795        */
6796       if (and)
6797       {
6798         matcherSet.and(matchCondition);
6799       }
6800       else
6801       {
6802         matcherSet.or(matchCondition);
6803       }
6804     }
6805     else
6806     {
6807       /*
6808        * compound condition
6809        */
6810       List<jalview.xml.binding.jalview.FeatureMatcherSet> matchers = matcherSetModel
6811               .getCompoundMatcher().getMatcherSet();
6812       boolean anded = matcherSetModel.getCompoundMatcher().isAnd();
6813       if (matchers.size() == 2)
6814       {
6815         parseFilterConditions(matcherSet, matchers.get(0), anded);
6816         parseFilterConditions(matcherSet, matchers.get(1), anded);
6817       }
6818       else
6819       {
6820         jalview.bin.Console
6821                 .errPrintln("Malformed compound filter condition");
6822       }
6823     }
6824   }
6825
6826   /**
6827    * Loads one XML model of a feature colour to a Jalview object
6828    * 
6829    * @param colourModel
6830    * @return
6831    */
6832   public static FeatureColourI parseColour(Colour colourModel)
6833   {
6834     FeatureColourI colour = null;
6835
6836     if (colourModel.getMax() != null)
6837     {
6838       Color mincol = null;
6839       Color maxcol = null;
6840       Color noValueColour = null;
6841
6842       try
6843       {
6844         mincol = new Color(Integer.parseInt(colourModel.getMinRGB(), 16));
6845         maxcol = new Color(Integer.parseInt(colourModel.getRGB(), 16));
6846       } catch (Exception e)
6847       {
6848         Console.warn("Couldn't parse out graduated feature color.", e);
6849       }
6850
6851       NoValueColour noCol = colourModel.getNoValueColour();
6852       if (noCol == NoValueColour.MIN)
6853       {
6854         noValueColour = mincol;
6855       }
6856       else if (noCol == NoValueColour.MAX)
6857       {
6858         noValueColour = maxcol;
6859       }
6860
6861       colour = new FeatureColour(maxcol, mincol, maxcol, noValueColour,
6862               safeFloat(colourModel.getMin()),
6863               safeFloat(colourModel.getMax()));
6864       final List<String> attributeName = colourModel.getAttributeName();
6865       String[] attributes = attributeName
6866               .toArray(new String[attributeName.size()]);
6867       if (attributes != null && attributes.length > 0)
6868       {
6869         colour.setAttributeName(attributes);
6870       }
6871       if (colourModel.isAutoScale() != null)
6872       {
6873         colour.setAutoScaled(colourModel.isAutoScale().booleanValue());
6874       }
6875       if (colourModel.isColourByLabel() != null)
6876       {
6877         colour.setColourByLabel(
6878                 colourModel.isColourByLabel().booleanValue());
6879       }
6880       if (colourModel.getThreshold() != null)
6881       {
6882         colour.setThreshold(colourModel.getThreshold().floatValue());
6883       }
6884       ThresholdType ttyp = colourModel.getThreshType();
6885       if (ttyp == ThresholdType.ABOVE)
6886       {
6887         colour.setAboveThreshold(true);
6888       }
6889       else if (ttyp == ThresholdType.BELOW)
6890       {
6891         colour.setBelowThreshold(true);
6892       }
6893     }
6894     else
6895     {
6896       Color color = new Color(Integer.parseInt(colourModel.getRGB(), 16));
6897       colour = new FeatureColour(color);
6898     }
6899
6900     return colour;
6901   }
6902
6903   public static void setStateSavedUpToDate(boolean s)
6904   {
6905     Console.debug("Setting overall stateSavedUpToDate to " + s);
6906     stateSavedUpToDate = s;
6907   }
6908
6909   public static boolean stateSavedUpToDate()
6910   {
6911     Console.debug("Returning overall stateSavedUpToDate value: "
6912             + stateSavedUpToDate);
6913     return stateSavedUpToDate;
6914   }
6915
6916   public static boolean allSavedUpToDate()
6917   {
6918     if (stateSavedUpToDate()) // nothing happened since last project save
6919       return true;
6920
6921     AlignFrame[] frames = Desktop.getAlignFrames();
6922     if (frames != null)
6923     {
6924       for (int i = 0; i < frames.length; i++)
6925       {
6926         if (frames[i] == null)
6927           continue;
6928         if (!frames[i].getViewport().savedUpToDate())
6929           return false; // at least one alignment is not individually saved
6930       }
6931     }
6932     return true;
6933   }
6934
6935   // used for debugging and tests
6936   private static int debugDelaySave = 20;
6937
6938   public static void setDebugDelaySave(int n)
6939   {
6940     debugDelaySave = n;
6941   }
6942 }