Clover icon

Coverage Report

  1. Project Clover database Mon Dec 8 2025 13:36:16 GMT
  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
737
512
90
0.47
7.42
26
3.46

Classes

Class Line # Actions
EBIAlfaFold 69 193 90
0.35880435.9%
 

Contributing tests

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