Clover icon

Coverage Report

  1. Project Clover database Wed May 27 2026 17:27:00 BST
  2. Package jalview.ws.dbsources

File EBIAlfaFold.java

 

Coverage histogram

../../../img/srcFileCovDistChart4.png
49% of files have more coverage

Code metrics

82
193
26
1
739
512
90
0.47
7.42
26
3.46

Classes

Class Line # Actions
EBIAlfaFold 69 193 90
0.3787375437.9%
 

Contributing tests

This file is covered by 56 tests. .

Source view

1   
2    /*
3    * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
4    * Copyright (C) $$Year-Rel$$ The Jalview Authors
5    *
6    * This file is part of Jalview.
7    *
8    * Jalview is free software: you can redistribute it and/or
9    * modify it under the terms of the GNU General Public License
10    * as published by the Free Software Foundation, either version 3
11    * of the License, or (at your option) any later version.
12    *
13    * Jalview is distributed in the hope that it will be useful, but
14    * WITHOUT ANY WARRANTY; without even the implied warranty
15    * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
16    * PURPOSE. See the GNU General Public License for more details.
17    *
18    * You should have received a copy of the GNU General Public License
19    * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
20    * The Jalview Authors are detailed in the 'AUTHORS' file.
21    */
22    package jalview.ws.dbsources;
23   
24    import java.io.File;
25    import java.io.FileInputStream;
26    import java.io.FileNotFoundException;
27    import java.io.IOException;
28    import java.io.InputStream;
29    import java.net.URL;
30    import java.util.ArrayList;
31    import java.util.Date;
32    import java.util.HashMap;
33    import java.util.List;
34    import java.util.Map;
35   
36    import org.json.simple.JSONArray;
37    import org.json.simple.JSONObject;
38    import org.json.simple.parser.ParseException;
39   
40    import com.stevesoft.pat.Regex;
41   
42    import jalview.api.FeatureSettingsModelI;
43    import jalview.bin.Console;
44    import jalview.datamodel.AlignmentAnnotation;
45    import jalview.datamodel.AlignmentI;
46    import jalview.datamodel.ContactMatrixI;
47    import jalview.datamodel.DBRefEntry;
48    import jalview.datamodel.PDBEntry;
49    import jalview.datamodel.SequenceFeature;
50    import jalview.datamodel.SequenceI;
51    import jalview.gui.Desktop;
52    import jalview.io.DataSourceType;
53    import jalview.io.FileFormat;
54    import jalview.io.FileFormatI;
55    import jalview.io.FormatAdapter;
56    import jalview.io.PDBFeatureSettings;
57    import jalview.structure.StructureImportSettings.TFType;
58    import jalview.structure.StructureMapping;
59    import jalview.structure.StructureSelectionManager;
60    import jalview.util.MessageManager;
61    import jalview.util.Platform;
62    import jalview.ws.datamodel.alphafold.PAEContactMatrix;
63    import jalview.ws.utils.UrlDownloadClient;
64   
65    /**
66    * @author JimP
67    *
68    */
 
69    public class EBIAlfaFold extends EbiFileRetrievedProxy
70    {
71    private static final String SEPARATOR = "|";
72   
73    private static final String COLON = ":";
74   
75    private static String AF_VERSION = null;
76   
 
77  204 toggle public EBIAlfaFold()
78    {
79  204 super();
80    }
81   
82    /*
83    * (non-Javadoc)
84    *
85    * @see jalview.ws.DbSourceProxy#getAccessionSeparator()
86    */
 
87  0 toggle @Override
88    public String getAccessionSeparator()
89    {
90  0 return null;
91    }
92   
93    /*
94    * (non-Javadoc)
95    *
96    * @see jalview.ws.DbSourceProxy#getAccessionValidator()
97    */
 
98  204 toggle @Override
99    public Regex getAccessionValidator()
100    {
101    // Since AF Multimer, accessions can be like AF00012344 rather than AF-Q0130e-F1
102    // also need to be able to map _model_v1 in cif files
103  204 Regex validator = new Regex("(AF-[A-Z]*[0-9]+[A-Z0-9]+([-0-9A-Za-z_]*[0-9]+)?)");
104  204 validator.setIgnoreCase(true);
105  204 return validator;
106    }
107   
108    /*
109    * (non-Javadoc)
110    *
111    * @see jalview.ws.DbSourceProxy#getDbSource()
112    */
 
113  0 toggle @Override
114    public String getDbSource()
115    {
116  0 return "ALPHAFOLD";
117    }
118   
119    /*
120    * (non-Javadoc)
121    *
122    * @see jalview.ws.DbSourceProxy#getDbVersion()
123    */
 
124  0 toggle @Override
125    public String getDbVersion()
126    {
127  0 return "1";
128    }
129   
 
130  1 toggle public static String pingAPIVersion()
131    {
132  1 if (AF_VERSION != null)
133    {
134  0 return AF_VERSION;
135    }
136  1 synchronized (EBIAlfaFold.class)
137    {
138  1 if (AF_VERSION != null)
139    {
140  0 return AF_VERSION;
141    }
142   
143  1 String Version = null;
144  1 try
145    {
146  1 URL ping = new URL(
147    "https://alphafold.ebi.ac.uk/api/prediction/Q5VSL9");
148  1 Object resp = Platform.parseJSON(ping.openStream());
149  1 if (resp != null && resp instanceof JSONArray)
150    {
151  1 Version = (((JSONObject) ((JSONArray) resp).get(0))
152    .get("latestVersion")).toString();
153  1 AF_VERSION = Version;
154    }
155    } catch (Throwable x)
156    {
157  0 jalview.bin.Console.errPrintln(
158    "Couldn't get EBI AlphaFold DB latest version!");
159  0 jalview.bin.Console.errPrintln(x);
160    }
161  1 return Version;
162    }
163    }
164   
 
165  0 toggle public static String getAlphaFoldCifDownloadUrl(String id, String vnum)
166    {
167  0 if (vnum == null || vnum.length() == 0)
168    {
169  0 pingAPIVersion();
170  0 vnum = AF_VERSION;
171    }
172  0 return "https://alphafold.ebi.ac.uk/files/" + id + "-model_v" + vnum
173    + ".cif";
174    }
175   
 
176  32 toggle public static String getAlphaFoldPaeDownloadUrl(String id, String vnum)
177    {
178  32 if (vnum == null || vnum.length() == 0)
179    {
180  1 pingAPIVersion();
181  1 vnum = AF_VERSION;
182    }
183  32 return "https://alphafold.ebi.ac.uk/files/" + id
184    + "-predicted_aligned_error_v" + vnum + ".json";
185    }
186   
187    /*
188    * (non-Javadoc)
189    *
190    * @see jalview.ws.DbSourceProxy#getSequenceRecords(java.lang.String[])
191    */
 
192  0 toggle @Override
193    public AlignmentI getSequenceRecords(String queries) throws Exception
194    {
195  0 return getSequenceRecords(queries, null);
196    }
197   
 
198  0 toggle public AlignmentI getSequenceRecords(String queries, String retrievalUrl)
199    throws Exception
200    {
201  0 AlignmentI pdbAlignment = null;
202  0 String chain = null;
203  0 String id = null;
204  0 if (queries.indexOf(COLON) > -1)
205    {
206  0 chain = queries.substring(queries.indexOf(COLON) + 1);
207  0 id = queries.substring(0, queries.indexOf(COLON));
208    }
209    else
210    {
211  0 id = queries;
212    }
213   
214  0 if (!isValidReference(id))
215    {
216  0 jalview.bin.Console.errPrintln(
217    "(AFClient) Ignoring invalid alphafold query: '" + id + "'");
218  0 stopQuery();
219  0 return null;
220    }
221  0 String alphaFoldCif = getAlphaFoldCifDownloadUrl(id, AF_VERSION);
222  0 if (retrievalUrl != null)
223    {
224  0 alphaFoldCif = retrievalUrl;
225    }
226   
227  0 try
228    {
229  0 File tmpFile = File.createTempFile(id, ".cif");
230  0 Console.debug("Retrieving structure file for " + id + " from "
231    + alphaFoldCif);
232  0 UrlDownloadClient.download(alphaFoldCif, tmpFile);
233   
234    // may not need this check ?
235  0 file = tmpFile.getAbsolutePath();
236  0 if (file == null)
237    {
238  0 return null;
239    }
240    // TODO Get the PAE file somewhere around here and remove from JmolParser
241   
242  0 pdbAlignment = importDownloadedStructureFromUrl(alphaFoldCif, tmpFile,
243    id, chain, getDbSource(), getDbVersion());
244   
245  0 if (pdbAlignment == null || pdbAlignment.getHeight() < 1)
246    {
247  0 throw new Exception(MessageManager.formatMessage(
248    "exception.no_pdb_records_for_chain", new String[]
249  0 { id, ((chain == null) ? "' '" : chain) }));
250    }
251    // done during structure retrieval
252    // retrieve_AlphaFold_pAE(id, pdbAlignment, retrievalUrl);
253   
254    } catch (Exception ex) // Problem parsing PDB file
255    {
256  0 stopQuery();
257  0 throw (ex);
258    }
259  0 return pdbAlignment;
260    }
261   
262    /**
263    * get an alphafold pAE for the given id and return the File object of the
264    * downloaded (temp) file
265    *
266    * @param id
267    * @param retrievalUrl
268    * - URL of .mmcif from EBI-AlphaFold - will be used to generate the
269    * pAE URL automatically
270    * @throws IOException
271    * @throws Exception
272    */
 
273  32 toggle public static File fetchAlphaFoldPAE(String id, String retrievalUrl)
274    throws IOException
275    {
276  32 Console.debug(
277  32 "Fetching PAE for "+id+" from "+((retrievalUrl==null) ? "AlphaFoldDB" : retrievalUrl));
278   
279  32 String paeURL=null;
280   
281  32 if (retrievalUrl != null)
282    {
283    // manufacture the PAE url from a url like ...-model-vN.cif
284  0 paeURL = retrievalUrl.replace("model", "predicted_aligned_error")
285    .replace(".cif", ".json");
286    } else {
287    // import PAE as contact matrix - assume this will work if there was a
288    // model
289  32 Console.trace("Resolving PAE URL...");
290  32 paeURL = getAlphaFoldPaeDownloadUrl(id, AF_VERSION);
291  32 Console.trace("Resolved : "+paeURL);
292    }
293  32 return fetchAPAE_from(id, paeURL);
294    }
295   
296    /**
297    * get a PAE file or reuse existing file
298    *
299    * @param id
300    * - null or an alphafold ID if this is an alphafold model - used
301    * only to construct tempfile name
302    * @param retrievalUrl
303    * - URL of .mmcif from EBI-AlphaFold - will be used to generate the
304    * pAE URL automatically
305    * @throws IOException
306    * @throws Exception
307    */
 
308  35 toggle public static File fetchAPAE_from(String id, String paeURL)
309    throws IOException
310    {
311  35 Console.debug(
312    "fetchAPAE called for "+id+" from '" + paeURL + "'");
313   
314    // check the cache
315  35 File pae = paeDownloadCache.get(paeURL);
316  35 if (pae != null && pae.exists() && (new Date().getTime()
317    - pae.lastModified()) < PAE_CACHE_STALE_TIME)
318    {
319  32 Console.debug(
320    "Using existing file in PAE cache for '" + paeURL + "'");
321  32 return pae;
322    }
323   
324  3 try
325    {
326  3 pae = File.createTempFile(id == null ? "af_pae" : id, "pae_json");
327    } catch (IOException e)
328    {
329  0 e.printStackTrace();
330    }
331  3 Console.debug("Downloading pae from " + paeURL + " to " + pae.toString()
332    + "");
333  3 try
334    {
335  3 UrlDownloadClient.download(paeURL, pae);
336    } catch (IOException e)
337    {
338  0 throw e;
339    }
340    // cache and it if successful
341  3 paeDownloadCache.put(paeURL, pae);
342  3 return pae;
343    }
344   
345    /**
346    * get an alphafold pAE for the given id, and add it to sequence 0 in
347    * pdbAlignment (assuming it came from structurefile parser).
348    *
349    * @param id
350    * @param pdbAlignment
351    * @param retrievalUrl
352    * - URL of .mmcif from EBI-AlphaFold - will be used to generate the
353    * pAE URL automatically
354    * @throws IOException
355    * @throws Exception
356    */
 
357  0 toggle public static void retrieve_AlphaFold_pAE(String id,
358    AlignmentI pdbAlignment, String retrievalUrl) throws IOException
359    {
360  0 File pae = fetchAlphaFoldPAE(id, retrievalUrl);
361  0 addAlphaFoldPAE(pdbAlignment, pae, 0, null, false, false, null);
362    }
363   
 
364  81 toggle public static void addAlphaFoldPAE(AlignmentI pdbAlignment, File pae,
365    int index, String id, boolean isStruct, boolean isStructId,
366    String label)
367    {
368  81 FileInputStream paeInput = null;
369  81 try
370    {
371  81 paeInput = new FileInputStream(pae);
372    } catch (FileNotFoundException e)
373    {
374  0 Console.error(
375    "Could not find pAE file '" + pae.getAbsolutePath() + "'", e);
376  0 return;
377    }
378   
379  81 if (isStruct)
380    {
381    // ###### WRITE A TEST for this bit of the logic addAlphaFoldPAE with
382    // different params.
383  0 StructureSelectionManager ssm = StructureSelectionManager
384    .getStructureSelectionManager(Desktop.getInstance());
385  0 if (ssm != null)
386    {
387  0 String structFilename = isStructId ? ssm.findFileForPDBId(id) : id;
388  0 addPAEToStructure(ssm, structFilename, pae, label);
389    }
390   
391    }
392    else
393    {
394    // attach to sequence?!
395  81 try
396    {
397  81 if (!importPaeJSONAsContactMatrixToSequence(pdbAlignment, paeInput,
398    index, id, label))
399    {
400  0 Console.warn("Could not import contact matrix from '"
401    + pae.getAbsolutePath() + "' to sequence.");
402    }
403    } catch (IOException e1)
404    {
405  0 Console.error("Error when importing pAE file '"
406    + pae.getAbsolutePath() + "'", e1);
407    } catch (ParseException e2)
408    {
409  0 Console.error("Error when parsing pAE file '"
410    + pae.getAbsolutePath() + "'", e2);
411    }
412    }
413   
414    }
415   
 
416  0 toggle public static void addPAEToStructure(StructureSelectionManager ssm,
417    String structFilename, File pae, String label)
418    {
419  0 FileInputStream paeInput = null;
420  0 try
421    {
422  0 paeInput = new FileInputStream(pae);
423    } catch (FileNotFoundException e)
424    {
425  0 Console.error(
426    "Could not find pAE file '" + pae.getAbsolutePath() + "'", e);
427  0 return;
428    }
429  0 if (ssm == null)
430    {
431  0 ssm = StructureSelectionManager
432    .getStructureSelectionManager(Desktop.getInstance());
433    }
434  0 if (ssm != null)
435    {
436  0 StructureMapping[] smArray = ssm.getMapping(structFilename);
437   
438  0 try
439    {
440  0 if (!importPaeJSONAsContactMatrixToStructure(smArray, paeInput,
441    label))
442    {
443  0 Console.warn("Could not import contact matrix from '"
444    + pae.getAbsolutePath() + "' to structure.");
445    }
446    } catch (IOException e1)
447    {
448  0 Console.error("Error when importing pAE file '"
449    + pae.getAbsolutePath() + "'", e1);
450    } catch (ParseException e2)
451    {
452  0 Console.error("Error when parsing pAE file '"
453    + pae.getAbsolutePath() + "'", e2);
454    }
455    }
456    }
457   
458    /**
459    * parses the given pAE matrix and adds it to sequence 0 in the given
460    * alignment
461    *
462    * @param pdbAlignment
463    * @param pae_input
464    * @return true if there was a pAE matrix added
465    * @throws ParseException
466    * @throws IOException
467    * @throws Exception
468    */
 
469  81 toggle public static boolean importPaeJSONAsContactMatrixToSequence(
470    AlignmentI pdbAlignment, InputStream pae_input, int index,
471    String seqId, String label) throws IOException, ParseException
472    {
473  81 SequenceI sequence = null;
474  81 if (seqId == null)
475    {
476  81 int seqToGet = index > 0 ? index : 0;
477  81 sequence = pdbAlignment.getSequenceAt(seqToGet);
478    }
479  81 if (sequence == null)
480    {
481  0 SequenceI[] sequences = pdbAlignment.findSequenceMatch(seqId);
482  0 if (sequences == null || sequences.length < 1)
483    {
484  0 Console.warn("Could not find sequence with id '" + seqId
485    + "' to attach pAE matrix to. Ignoring matrix.");
486  0 return false;
487    }
488    else
489    {
490  0 sequence = sequences[0]; // just use the first sequence with this seqId
491    }
492    }
493  81 if (sequence == null)
494    {
495  0 return false;
496    }
497  81 return importPaeJSONAsContactMatrixToSequence(pdbAlignment, pae_input,
498    sequence, label);
499    }
500   
 
501  81 toggle public static boolean importPaeJSONAsContactMatrixToSequence(
502    AlignmentI pdbAlignment, InputStream pae_input,
503    SequenceI sequence, String label)
504    throws IOException, ParseException
505    {
506  81 JSONObject paeDict = parseJSONtoPAEContactMatrix(pae_input);
507  81 if (paeDict == null)
508    {
509  0 Console.debug("JSON file did not parse properly.");
510  0 return false;
511    }
512  81 ContactMatrixI matrix = new PAEContactMatrix(sequence, paeDict);
513   
514  81 AlignmentAnnotation cmannot = sequence.addContactList(matrix);
515  81 if (label != null)
516  0 cmannot.label = label;
517  81 pdbAlignment.addAnnotation(cmannot);
518   
519  81 return true;
520    }
521   
 
522  87 toggle public static JSONObject parseJSONtoPAEContactMatrix(
523    InputStream pae_input) throws IOException, ParseException
524    {
525  87 Object paeJson = Platform.parseJSON(pae_input);
526  87 JSONObject paeDict = null;
527  87 if (paeJson instanceof JSONObject)
528    {
529  20 paeDict = (JSONObject) paeJson;
530    }
531  67 else if (paeJson instanceof JSONArray)
532    {
533  67 JSONArray jsonArray = (JSONArray) paeJson;
534  67 if (jsonArray.size() > 0)
535  67 paeDict = (JSONObject) jsonArray.get(0);
536    }
537   
538  87 return paeDict;
539    }
540   
541    // ###### TEST THIS
 
542  3 toggle public static boolean importPaeJSONAsContactMatrixToStructure(
543    StructureMapping[] smArray, InputStream paeInput, String label)
544    throws IOException, ParseException
545    {
546  3 boolean someDone = false;
547  3 for (StructureMapping sm : smArray)
548    {
549  3 boolean thisDone = importPaeJSONAsContactMatrixToStructure(sm,
550    paeInput, label);
551  3 someDone |= thisDone;
552    }
553  3 return someDone;
554    }
555   
 
556  3 toggle public static boolean importPaeJSONAsContactMatrixToStructure(
557    StructureMapping sm, InputStream paeInput, String label)
558    throws IOException, ParseException
559    {
560  3 JSONObject pae_obj = parseJSONtoPAEContactMatrix(paeInput);
561  3 if (pae_obj == null)
562    {
563  0 Console.debug("JSON file did not parse properly.");
564  0 return false;
565    }
566   
567  3 SequenceI seq = sm.getSequence();
568  3 ContactMatrixI matrix = new PAEContactMatrix(seq, pae_obj);
569  3 AlignmentAnnotation cmannot = sm.getSequence().addContactList(matrix);
570    /* this already happens in Sequence.addContactList()
571    seq.addAlignmentAnnotation(cmannot);
572    */
573  3 return true;
574    }
575   
576    /**
577    * general purpose structure importer - designed to yield alignment useful for
578    * transfer of annotation to associated sequences
579    *
580    * @param alphaFoldCif
581    * @param tmpFile
582    * @param id
583    * @param chain
584    * @param dbSource
585    * @param dbVersion
586    * @return
587    * @throws Exception
588    */
 
589  0 toggle public static AlignmentI importDownloadedStructureFromUrl(
590    String alphaFoldCif, File tmpFile, String id, String chain,
591    String dbSource, String dbVersion) throws Exception
592    {
593  0 String file = tmpFile.getAbsolutePath();
594    // todo get rid of Type and use FileFormatI instead?
595  0 FileFormatI fileFormat = FileFormat.MMCif;
596  0 TFType tempfacType = TFType.PLDDT;
597  0 AlignmentI pdbAlignment = new FormatAdapter().readFile(tmpFile, file,
598    DataSourceType.FILE, fileFormat, tempfacType);
599   
600  0 if (pdbAlignment != null)
601    {
602  0 List<SequenceI> toremove = new ArrayList<SequenceI>();
603  0 for (SequenceI pdbcs : pdbAlignment.getSequences())
604    {
605  0 String chid = null;
606    // Mapping map=null;
607  0 for (PDBEntry pid : pdbcs.getAllPDBEntries())
608    {
609  0 if (pid.getFile() == file)
610    {
611  0 chid = pid.getChainCode();
612    }
613    }
614  0 if (chain == null || (chid != null && (chid.equals(chain)
615    || chid.trim().equals(chain.trim())
616    || (chain.trim().length() == 0 && chid.equals("_")))))
617    {
618    // FIXME seems to result in 'PDB|1QIP|1qip|A' - 1QIP is redundant.
619    // TODO: suggest simplify naming to 1qip|A as default name defined
620  0 pdbcs.setName(id + SEPARATOR + pdbcs.getName());
621    // Might need to add more metadata to the PDBEntry object
622    // like below
623    /*
624    * PDBEntry entry = new PDBEntry(); // Construct the PDBEntry
625    * entry.setId(id); if (entry.getProperty() == null)
626    * entry.setProperty(new Hashtable());
627    * entry.getProperty().put("chains", pdbchain.id + "=" +
628    * sq.getStart() + "-" + sq.getEnd());
629    * sq.getDatasetSequence().addPDBId(entry);
630    */
631    // Add PDB DB Refs
632    // We make a DBRefEtntry because we have obtained the PDB file from
633    // a
634    // verifiable source
635    // JBPNote - PDB DBRefEntry should also carry the chain and mapping
636    // information
637  0 if (dbSource != null)
638    {
639  0 DBRefEntry dbentry = new DBRefEntry(dbSource,
640   
641  0 dbVersion, (chid == null ? id : id + chid));
642    // dbentry.setMap()
643  0 pdbcs.addDBRef(dbentry);
644    // update any feature groups
645  0 List<SequenceFeature> allsf = pdbcs.getFeatures()
646    .getAllFeatures();
647  0 List<SequenceFeature> newsf = new ArrayList<SequenceFeature>();
648  0 if (allsf != null && allsf.size() > 0)
649    {
650  0 for (SequenceFeature f : allsf)
651    {
652  0 if (file.equals(f.getFeatureGroup()))
653    {
654  0 f = new SequenceFeature(f, f.type, f.begin, f.end, id,
655    f.score);
656    }
657  0 newsf.add(f);
658    }
659  0 pdbcs.setSequenceFeatures(newsf);
660    }
661    }
662    }
663    else
664    {
665    // mark this sequence to be removed from the alignment
666    // - since it's not from the right chain
667  0 toremove.add(pdbcs);
668    }
669    }
670    // now remove marked sequences
671  0 for (SequenceI pdbcs : toremove)
672    {
673  0 pdbAlignment.deleteSequence(pdbcs);
674  0 if (pdbcs.getAnnotation() != null)
675    {
676  0 for (AlignmentAnnotation aa : pdbcs.getAnnotation())
677    {
678  0 pdbAlignment.deleteAnnotation(aa);
679    }
680    }
681    }
682    }
683  0 return pdbAlignment;
684    }
685   
686    /*
687    * (non-Javadoc)
688    *
689    * @see jalview.ws.DbSourceProxy#isValidReference(java.lang.String)
690    */
 
691  0 toggle @Override
692    public boolean isValidReference(String accession)
693    {
694  0 Regex r = getAccessionValidator();
695  0 return r.search(accession.trim());
696    }
697   
698    /**
699    * human glyoxalase
700    */
 
701  0 toggle @Override
702    public String getTestQuery()
703    {
704  0 return "AF-O15552-F1";
705    }
706   
 
707  0 toggle @Override
708    public String getDbName()
709    {
710  0 return "ALPHAFOLD"; // getDbSource();
711    }
712   
 
713  0 toggle @Override
714    public int getTier()
715    {
716  0 return 0;
717    }
718   
719    /**
720    * Returns a descriptor for suitable feature display settings with
721    * <ul>
722    * <li>ResNums or insertions features visible</li>
723    * <li>insertions features coloured red</li>
724    * <li>ResNum features coloured by label</li>
725    * <li>Insertions displayed above (on top of) ResNums</li>
726    * </ul>
727    */
 
728  0 toggle @Override
729    public FeatureSettingsModelI getFeatureColourScheme()
730    {
731  0 return new PDBFeatureSettings();
732    }
733   
734    // days * 86400000
735    private static final long PAE_CACHE_STALE_TIME = 1 * 86400000;
736   
737    private static Map<String, File> paeDownloadCache = new HashMap<>();
738   
739    }