JAL-2780 JAL-2781 UNASSIGNED positions marked as Integer.Min
[jalview.git] / src / jalview / ws / sifts / SiftsClient.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.ws.sifts;
22
23 import jalview.analysis.AlignSeq;
24 import jalview.analysis.scoremodels.ScoreMatrix;
25 import jalview.analysis.scoremodels.ScoreModels;
26 import jalview.api.DBRefEntryI;
27 import jalview.api.SiftsClientI;
28 import jalview.datamodel.DBRefEntry;
29 import jalview.datamodel.DBRefSource;
30 import jalview.datamodel.SequenceI;
31 import jalview.io.StructureFile;
32 import jalview.schemes.ResidueProperties;
33 import jalview.structure.StructureMapping;
34 import jalview.util.Comparison;
35 import jalview.util.DBRefUtils;
36 import jalview.util.Format;
37 import jalview.xml.binding.sifts.Entry;
38 import jalview.xml.binding.sifts.Entry.Entity;
39 import jalview.xml.binding.sifts.Entry.Entity.Segment;
40 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListMapRegion.MapRegion;
41 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue;
42 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue.CrossRefDb;
43 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue.ResidueDetail;
44
45 import java.io.File;
46 import java.io.FileInputStream;
47 import java.io.FileOutputStream;
48 import java.io.IOException;
49 import java.io.InputStream;
50 import java.io.PrintStream;
51 import java.net.URL;
52 import java.net.URLConnection;
53 import java.nio.file.Files;
54 import java.nio.file.Path;
55 import java.nio.file.attribute.BasicFileAttributes;
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.Collection;
59 import java.util.Collections;
60 import java.util.Date;
61 import java.util.HashMap;
62 import java.util.HashSet;
63 import java.util.List;
64 import java.util.Map;
65 import java.util.Set;
66 import java.util.TreeMap;
67 import java.util.zip.GZIPInputStream;
68
69 import javax.xml.bind.JAXBContext;
70 import javax.xml.bind.Unmarshaller;
71 import javax.xml.stream.XMLInputFactory;
72 import javax.xml.stream.XMLStreamReader;
73
74 import MCview.Atom;
75 import MCview.PDBChain;
76
77 public class SiftsClient implements SiftsClientI
78 {
79   /*
80    * for use in mocking out file fetch for tests only
81    * - reset to null after testing!
82    */
83   private static File mockSiftsFile;
84
85   private Entry siftsEntry;
86
87   private StructureFile pdb;
88
89   private String pdbId;
90
91   private String structId;
92
93   private CoordinateSys seqCoordSys = CoordinateSys.UNIPROT;
94
95   private static final int BUFFER_SIZE = 4096;
96
97   public static final int UNASSIGNED = Integer.MIN_VALUE;
98
99   private static final int PDB_RES_POS = 0;
100
101   private static final int PDB_ATOM_POS = 1;
102
103   private static final String NOT_OBSERVED = "Not_Observed";
104
105   private static final String SIFTS_FTP_BASE_URL = "http://ftp.ebi.ac.uk/pub/databases/msd/sifts/xml/";
106
107   private final static String NEWLINE = System.lineSeparator();
108
109   private String curSourceDBRef;
110
111   private HashSet<String> curDBRefAccessionIdsString;
112
113   private enum CoordinateSys
114   {
115     UNIPROT("UniProt"), PDB("PDBresnum"), PDBe("PDBe");
116     private String name;
117
118     private CoordinateSys(String name)
119     {
120       this.name = name;
121     }
122
123     public String getName()
124     {
125       return name;
126     }
127   };
128
129   private enum ResidueDetailType
130   {
131     NAME_SEC_STRUCTURE("nameSecondaryStructure"),
132     CODE_SEC_STRUCTURE("codeSecondaryStructure"), ANNOTATION("Annotation");
133     private String code;
134
135     private ResidueDetailType(String code)
136     {
137       this.code = code;
138     }
139
140     public String getCode()
141     {
142       return code;
143     }
144   };
145
146   /**
147    * Fetch SIFTs file for the given PDBfile and construct an instance of
148    * SiftsClient
149    * 
150    * @param pdbId
151    * @throws SiftsException
152    */
153   public SiftsClient(StructureFile pdb) throws SiftsException
154   {
155     this.pdb = pdb;
156     this.pdbId = pdb.getId();
157     File siftsFile = getSiftsFile(pdbId);
158     siftsEntry = parseSIFTs(siftsFile);
159   }
160
161   /**
162    * Parse the given SIFTs File and return a JAXB POJO of parsed data
163    * 
164    * @param siftFile
165    *          - the GZipped SIFTs XML file to parse
166    * @return
167    * @throws Exception
168    *           if a problem occurs while parsing the SIFTs XML
169    */
170   private Entry parseSIFTs(File siftFile) throws SiftsException
171   {
172     try (InputStream in = new FileInputStream(siftFile);
173             GZIPInputStream gzis = new GZIPInputStream(in);)
174     {
175       // System.out.println("File : " + siftFile.getAbsolutePath());
176       JAXBContext jc = JAXBContext.newInstance("jalview.xml.binding.sifts");
177       XMLStreamReader streamReader = XMLInputFactory.newInstance()
178               .createXMLStreamReader(gzis);
179       Unmarshaller um = jc.createUnmarshaller();
180       return (Entry) um.unmarshal(streamReader);
181     } catch (Exception e)
182     {
183       e.printStackTrace();
184       throw new SiftsException(e.getMessage());
185     }
186   }
187
188   /**
189    * Get a SIFTs XML file for a given PDB Id from Cache or download from FTP
190    * repository if not found in cache
191    * 
192    * @param pdbId
193    * @return SIFTs XML file
194    * @throws SiftsException
195    */
196   public static File getSiftsFile(String pdbId) throws SiftsException
197   {
198     /*
199      * return mocked file if it has been set
200      */
201     if (mockSiftsFile != null)
202     {
203       return mockSiftsFile;
204     }
205
206     String siftsFileName = SiftsSettings.getSiftDownloadDirectory()
207             + pdbId.toLowerCase() + ".xml.gz";
208     File siftsFile = new File(siftsFileName);
209     if (siftsFile.exists())
210     {
211       // The line below is required for unit testing... don't comment it out!!!
212       System.out.println(">>> SIFTS File already downloaded for " + pdbId);
213
214       if (isFileOlderThanThreshold(siftsFile,
215               SiftsSettings.getCacheThresholdInDays()))
216       {
217         File oldSiftsFile = new File(siftsFileName + "_old");
218         siftsFile.renameTo(oldSiftsFile);
219         try
220         {
221           siftsFile = downloadSiftsFile(pdbId.toLowerCase());
222           oldSiftsFile.delete();
223           return siftsFile;
224         } catch (IOException e)
225         {
226           e.printStackTrace();
227           oldSiftsFile.renameTo(siftsFile);
228           return new File(siftsFileName);
229         }
230       }
231       else
232       {
233         return siftsFile;
234       }
235     }
236     try
237     {
238       siftsFile = downloadSiftsFile(pdbId.toLowerCase());
239     } catch (IOException e)
240     {
241       throw new SiftsException(e.getMessage());
242     }
243     return siftsFile;
244   }
245
246   /**
247    * This method enables checking if a cached file has exceeded a certain
248    * threshold(in days)
249    * 
250    * @param file
251    *          the cached file
252    * @param noOfDays
253    *          the threshold in days
254    * @return
255    */
256   public static boolean isFileOlderThanThreshold(File file, int noOfDays)
257   {
258     Path filePath = file.toPath();
259     BasicFileAttributes attr;
260     int diffInDays = 0;
261     try
262     {
263       attr = Files.readAttributes(filePath, BasicFileAttributes.class);
264       diffInDays = (int) ((new Date().getTime()
265               - attr.lastModifiedTime().toMillis())
266               / (1000 * 60 * 60 * 24));
267       // System.out.println("Diff in days : " + diffInDays);
268     } catch (IOException e)
269     {
270       e.printStackTrace();
271     }
272     return noOfDays <= diffInDays;
273   }
274
275   /**
276    * Download a SIFTs XML file for a given PDB Id from an FTP repository
277    * 
278    * @param pdbId
279    * @return downloaded SIFTs XML file
280    * @throws SiftsException
281    * @throws IOException
282    */
283   public static File downloadSiftsFile(String pdbId)
284           throws SiftsException, IOException
285   {
286     if (pdbId.contains(".cif"))
287     {
288       pdbId = pdbId.replace(".cif", "");
289     }
290     String siftFile = pdbId + ".xml.gz";
291     String siftsFileFTPURL = SIFTS_FTP_BASE_URL + siftFile;
292     String downloadedSiftsFile = SiftsSettings.getSiftDownloadDirectory()
293             + siftFile;
294     File siftsDownloadDir = new File(
295             SiftsSettings.getSiftDownloadDirectory());
296     if (!siftsDownloadDir.exists())
297     {
298       siftsDownloadDir.mkdirs();
299     }
300     // System.out.println(">> Download ftp url : " + siftsFileFTPURL);
301     // long now = System.currentTimeMillis();
302     URL url = new URL(siftsFileFTPURL);
303     URLConnection conn = url.openConnection();
304     InputStream inputStream = conn.getInputStream();
305     FileOutputStream outputStream = new FileOutputStream(
306             downloadedSiftsFile);
307     byte[] buffer = new byte[BUFFER_SIZE];
308     int bytesRead = -1;
309     while ((bytesRead = inputStream.read(buffer)) != -1)
310     {
311       outputStream.write(buffer, 0, bytesRead);
312     }
313     outputStream.close();
314     inputStream.close();
315     // System.out.println(">>> File downloaded : " + downloadedSiftsFile
316     // + " took " + (System.currentTimeMillis() - now) + "ms");
317     return new File(downloadedSiftsFile);
318   }
319
320   /**
321    * Delete the SIFTs file for the given PDB Id in the local SIFTs download
322    * directory
323    * 
324    * @param pdbId
325    * @return true if the file was deleted or doesn't exist
326    */
327   public static boolean deleteSiftsFileByPDBId(String pdbId)
328   {
329     File siftsFile = new File(SiftsSettings.getSiftDownloadDirectory()
330             + pdbId.toLowerCase() + ".xml.gz");
331     if (siftsFile.exists())
332     {
333       return siftsFile.delete();
334     }
335     return true;
336   }
337
338   /**
339    * Get a valid SIFTs DBRef for the given sequence current SIFTs entry
340    * 
341    * @param seq
342    *          - the target sequence for the operation
343    * @return a valid DBRefEntry that is SIFTs compatible
344    * @throws Exception
345    *           if no valid source DBRefEntry was found for the given sequences
346    */
347   public DBRefEntryI getValidSourceDBRef(SequenceI seq)
348           throws SiftsException
349   {
350     List<DBRefEntry> dbRefs = seq.getPrimaryDBRefs();
351     if (dbRefs == null || dbRefs.size() < 1)
352     {
353       throw new SiftsException(
354               "Source DBRef could not be determined. DBRefs might not have been retrieved.");
355     }
356
357     for (DBRefEntry dbRef : dbRefs)
358     {
359       if (dbRef == null || dbRef.getAccessionId() == null
360               || dbRef.getSource() == null)
361       {
362         continue;
363       }
364       String canonicalSource = DBRefUtils
365               .getCanonicalName(dbRef.getSource());
366       if (isValidDBRefEntry(dbRef)
367               && (canonicalSource.equalsIgnoreCase(DBRefSource.UNIPROT)
368                       || canonicalSource.equalsIgnoreCase(DBRefSource.PDB)))
369       {
370         return dbRef;
371       }
372     }
373     throw new SiftsException("Could not get source DB Ref");
374   }
375
376   /**
377    * Check that the DBRef Entry is properly populated and is available in this
378    * SiftClient instance
379    * 
380    * @param entry
381    *          - DBRefEntry to validate
382    * @return true validation is successful otherwise false is returned.
383    */
384   boolean isValidDBRefEntry(DBRefEntryI entry)
385   {
386     return entry != null && entry.getAccessionId() != null
387             && isFoundInSiftsEntry(entry.getAccessionId());
388   }
389
390   @Override
391   public HashSet<String> getAllMappingAccession()
392   {
393     HashSet<String> accessions = new HashSet<String>();
394     List<Entity> entities = siftsEntry.getEntity();
395     for (Entity entity : entities)
396     {
397       List<Segment> segments = entity.getSegment();
398       for (Segment segment : segments)
399       {
400         List<MapRegion> mapRegions = segment.getListMapRegion()
401                 .getMapRegion();
402         for (MapRegion mapRegion : mapRegions)
403         {
404           accessions
405                   .add(mapRegion.getDb().getDbAccessionId().toLowerCase());
406         }
407       }
408     }
409     return accessions;
410   }
411
412   @Override
413   public StructureMapping getSiftsStructureMapping(SequenceI seq,
414           String pdbFile, String chain) throws SiftsException
415   {
416     SequenceI aseq = seq;
417     while (seq.getDatasetSequence() != null)
418     {
419       seq = seq.getDatasetSequence();
420     }
421     structId = (chain == null) ? pdbId : pdbId + "|" + chain;
422     System.out.println("Getting SIFTS mapping for " + structId + ": seq "
423             + seq.getName());
424
425     final StringBuilder mappingDetails = new StringBuilder(128);
426     PrintStream ps = new PrintStream(System.out)
427     {
428       @Override
429       public void print(String x)
430       {
431         mappingDetails.append(x);
432       }
433
434       @Override
435       public void println()
436       {
437         mappingDetails.append(NEWLINE);
438       }
439     };
440     HashMap<Integer, int[]> mapping = getGreedyMapping(chain, seq, ps);
441
442     String mappingOutput = mappingDetails.toString();
443     StructureMapping siftsMapping = new StructureMapping(seq, pdbFile,
444             pdbId, chain, mapping, mappingOutput);
445     return siftsMapping;
446   }
447
448   @Override
449   public HashMap<Integer, int[]> getGreedyMapping(String entityId,
450           SequenceI seq, java.io.PrintStream os) throws SiftsException
451   {
452     List<Integer> omitNonObserved = new ArrayList<Integer>();
453     int nonObservedShiftIndex = 0;
454     // System.out.println("Generating mappings for : " + entityId);
455     Entity entity = null;
456     entity = getEntityById(entityId);
457     String originalSeq = AlignSeq.extractGaps(
458             jalview.util.Comparison.GapChars, seq.getSequenceAsString());
459     HashMap<Integer, int[]> mapping = new HashMap<Integer, int[]>();
460     DBRefEntryI sourceDBRef;
461     sourceDBRef = getValidSourceDBRef(seq);
462     // TODO ensure sequence start/end is in the same coordinate system and
463     // consistent with the choosen sourceDBRef
464
465     // set sequence coordinate system - default value is UniProt
466     if (sourceDBRef.getSource().equalsIgnoreCase(DBRefSource.PDB))
467     {
468       seqCoordSys = CoordinateSys.PDB;
469     }
470
471     HashSet<String> dbRefAccessionIdsString = new HashSet<String>();
472     for (DBRefEntry dbref : seq.getDBRefs())
473     {
474       dbRefAccessionIdsString.add(dbref.getAccessionId().toLowerCase());
475     }
476     dbRefAccessionIdsString.add(sourceDBRef.getAccessionId().toLowerCase());
477
478     curDBRefAccessionIdsString = dbRefAccessionIdsString;
479     curSourceDBRef = sourceDBRef.getAccessionId();
480
481     TreeMap<Integer, String> resNumMap = new TreeMap<Integer, String>();
482     List<Segment> segments = entity.getSegment();
483     SegmentHelperPojo shp = new SegmentHelperPojo(seq, mapping, resNumMap,
484             omitNonObserved, nonObservedShiftIndex);
485     processSegments(segments, shp);
486     try
487     {
488       populateAtomPositions(entityId, mapping);
489     } catch (Exception e)
490     {
491       e.printStackTrace();
492     }
493     if (seqCoordSys == CoordinateSys.UNIPROT)
494     {
495       padWithGaps(resNumMap, omitNonObserved);
496     }
497     int seqStart = UNASSIGNED;
498     int seqEnd = UNASSIGNED;
499     int pdbStart = UNASSIGNED;
500     int pdbEnd = UNASSIGNED;
501
502     if (mapping.isEmpty())
503     {
504       throw new SiftsException("SIFTS mapping failed");
505     }
506
507     Integer[] keys = mapping.keySet().toArray(new Integer[0]);
508     Arrays.sort(keys);
509     seqStart = keys[0];
510     seqEnd = keys[keys.length - 1];
511
512     String matchedSeq = originalSeq;
513     if (seqStart != UNASSIGNED)
514     {
515       pdbStart = mapping.get(seqStart)[PDB_RES_POS];
516       pdbEnd = mapping.get(seqEnd)[PDB_RES_POS];
517       int orignalSeqStart = seq.getStart();
518       if (orignalSeqStart >= 1)
519       {
520         int subSeqStart = (seqStart >= orignalSeqStart)
521                 ? seqStart - orignalSeqStart
522                 : 0;
523         int subSeqEnd = seqEnd - (orignalSeqStart - 1);
524         subSeqEnd = originalSeq.length() < subSeqEnd ? originalSeq.length()
525                 : subSeqEnd;
526         matchedSeq = originalSeq.substring(subSeqStart, subSeqEnd);
527       }
528       else
529       {
530         matchedSeq = originalSeq.substring(1, originalSeq.length());
531       }
532     }
533
534     StringBuilder targetStrucSeqs = new StringBuilder();
535     for (String res : resNumMap.values())
536     {
537       targetStrucSeqs.append(res);
538     }
539
540     if (os != null)
541     {
542       MappingOutputPojo mop = new MappingOutputPojo();
543       mop.setSeqStart(seqStart);
544       mop.setSeqEnd(seqEnd);
545       mop.setSeqName(seq.getName());
546       mop.setSeqResidue(matchedSeq);
547
548       mop.setStrStart(pdbStart);
549       mop.setStrEnd(pdbEnd);
550       mop.setStrName(structId);
551       mop.setStrResidue(targetStrucSeqs.toString());
552
553       mop.setType("pep");
554       os.print(getMappingOutput(mop).toString());
555       os.println();
556     }
557     return mapping;
558   }
559
560   void processSegments(List<Segment> segments, SegmentHelperPojo shp)
561   {
562     SequenceI seq = shp.getSeq();
563     HashMap<Integer, int[]> mapping = shp.getMapping();
564     TreeMap<Integer, String> resNumMap = shp.getResNumMap();
565     List<Integer> omitNonObserved = shp.getOmitNonObserved();
566     int nonObservedShiftIndex = shp.getNonObservedShiftIndex();
567     for (Segment segment : segments)
568     {
569       // System.out.println("Mapping segments : " + segment.getSegId() + "\\"s
570       // + segStartEnd);
571       List<Residue> residues = segment.getListResidue().getResidue();
572       for (Residue residue : residues)
573       {
574         int pdbeIndex = getLeadingIntegerValue(residue.getDbResNum(),
575                 UNASSIGNED);
576         int currSeqIndex = UNASSIGNED;
577         List<CrossRefDb> cRefDbs = residue.getCrossRefDb();
578         CrossRefDb pdbRefDb = null;
579         for (CrossRefDb cRefDb : cRefDbs)
580         {
581           if (cRefDb.getDbSource().equalsIgnoreCase(DBRefSource.PDB))
582           {
583             pdbRefDb = cRefDb;
584           }
585           if (cRefDb.getDbCoordSys().equalsIgnoreCase(seqCoordSys.getName())
586                   && isAccessionMatched(cRefDb.getDbAccessionId()))
587           {
588             currSeqIndex = getLeadingIntegerValue(cRefDb.getDbResNum(),
589                     UNASSIGNED);
590             if (pdbRefDb != null)
591             {
592               break;// exit loop if pdb and uniprot are already found
593             }
594           }
595         }
596         if (seqCoordSys == seqCoordSys.PDB) // FIXME: is seqCoordSys ever PDBe
597                                             // ???
598         {
599           // if the sequence has a primary reference to the PDB, then we are
600           // dealing with a sequence extracted directly from the PDB. In that
601           // case, numbering is PDBe - non-observed residues
602           currSeqIndex = pdbeIndex;
603         }
604         if (currSeqIndex == UNASSIGNED)
605         {
606           continue;
607         }
608         if (!isResidueObserved(residue)
609                 && seqCoordSys != seqCoordSys.UNIPROT)
610         {
611           // mapping to PDB or PDBe so we need to bookkeep for the non-observed
612           // SEQRES positions
613           omitNonObserved.add(currSeqIndex);
614           ++nonObservedShiftIndex;
615         }
616
617         // if (currSeqIndex >= seq.getStart() && currSeqIndex <= seqlength) //
618         // true
619                                                                          // numbering
620                                                                          // is
621                                                                          // not
622                                                                          // up
623                                                                          // to
624                                                                          // seq.getEnd()
625         {
626
627           int resNum = (pdbRefDb == null)
628                   ? getLeadingIntegerValue(residue.getDbResNum(),
629                           UNASSIGNED)
630                   : getLeadingIntegerValue(pdbRefDb.getDbResNum(),
631                           UNASSIGNED);
632
633           if (isResidueObserved(residue)
634                   || seqCoordSys == CoordinateSys.UNIPROT)
635           {
636             char resCharCode = ResidueProperties
637                     .getSingleCharacterCode(ResidueProperties
638                             .getCanonicalAminoAcid(residue.getDbResName()));
639             resNumMap.put(currSeqIndex, String.valueOf(resCharCode));
640           }
641
642           mapping.put(currSeqIndex - nonObservedShiftIndex,
643                   new int[]
644                   { Integer.valueOf(resNum), UNASSIGNED });
645         }
646       }
647     }
648   }
649
650   /**
651    * Get the leading integer part of a string that begins with an integer.
652    * 
653    * @param input
654    *          - the string input to process
655    * @param failValue
656    *          - value returned if unsuccessful
657    * @return
658    */
659   static int getLeadingIntegerValue(String input, int failValue)
660   {
661     if (input == null)
662     {
663       return failValue;
664     }
665     String[] parts = input.split("(?=\\D)(?<=\\d)");
666     if (parts != null && parts.length > 0 && parts[0].matches("[0-9]+"))
667     {
668       return Integer.valueOf(parts[0]);
669     }
670     return failValue;
671   }
672
673   /**
674    * 
675    * @param chainId
676    *          Target chain to populate mapping of its atom positions.
677    * @param mapping
678    *          Two dimension array of residue index versus atom position
679    * @throws IllegalArgumentException
680    *           Thrown if chainId or mapping is null
681    * @throws SiftsException
682    */
683   void populateAtomPositions(String chainId, Map<Integer, int[]> mapping)
684           throws IllegalArgumentException, SiftsException
685   {
686     try
687     {
688       PDBChain chain = pdb.findChain(chainId);
689
690       if (chain == null || mapping == null)
691       {
692         throw new IllegalArgumentException(
693                 "Chain id or mapping must not be null.");
694       }
695       for (int[] map : mapping.values())
696       {
697         if (map[PDB_RES_POS] != UNASSIGNED)
698         {
699           map[PDB_ATOM_POS] = getAtomIndex(map[PDB_RES_POS], chain.atoms);
700         }
701       }
702     } catch (NullPointerException e)
703     {
704       throw new SiftsException(e.getMessage());
705     } catch (Exception e)
706     {
707       throw new SiftsException(e.getMessage());
708     }
709   }
710
711   /**
712    * 
713    * @param residueIndex
714    *          The residue index used for the search
715    * @param atoms
716    *          A collection of Atom to search
717    * @return atom position for the given residue index
718    */
719   int getAtomIndex(int residueIndex, Collection<Atom> atoms)
720   {
721     if (atoms == null)
722     {
723       throw new IllegalArgumentException(
724               "atoms collection must not be null!");
725     }
726     for (Atom atom : atoms)
727     {
728       if (atom.resNumber == residueIndex)
729       {
730         return atom.atomIndex;
731       }
732     }
733     return UNASSIGNED;
734   }
735
736   /**
737    * Checks if the residue instance is marked 'Not_observed' or not
738    * 
739    * @param residue
740    * @return
741    */
742   private boolean isResidueObserved(Residue residue)
743   {
744     Set<String> annotations = getResidueAnnotaitons(residue,
745             ResidueDetailType.ANNOTATION);
746     if (annotations == null || annotations.isEmpty())
747     {
748       return true;
749     }
750     for (String annotation : annotations)
751     {
752       if (annotation.equalsIgnoreCase(NOT_OBSERVED))
753       {
754         return false;
755       }
756     }
757     return true;
758   }
759
760   /**
761    * Get annotation String for a given residue and annotation type
762    * 
763    * @param residue
764    * @param type
765    * @return
766    */
767   private Set<String> getResidueAnnotaitons(Residue residue,
768           ResidueDetailType type)
769   {
770     HashSet<String> foundAnnotations = new HashSet<String>();
771     List<ResidueDetail> resDetails = residue.getResidueDetail();
772     for (ResidueDetail resDetail : resDetails)
773     {
774       if (resDetail.getProperty().equalsIgnoreCase(type.getCode()))
775       {
776         foundAnnotations.add(resDetail.getContent());
777       }
778     }
779     return foundAnnotations;
780   }
781
782   @Override
783   public boolean isAccessionMatched(String accession)
784   {
785     boolean isStrictMatch = true;
786     return isStrictMatch ? curSourceDBRef.equalsIgnoreCase(accession)
787             : curDBRefAccessionIdsString.contains(accession.toLowerCase());
788   }
789
790   private boolean isFoundInSiftsEntry(String accessionId)
791   {
792     Set<String> siftsDBRefs = getAllMappingAccession();
793     return accessionId != null
794             && siftsDBRefs.contains(accessionId.toLowerCase());
795   }
796
797   /**
798    * Pad omitted residue positions in PDB sequence with gaps
799    * 
800    * @param resNumMap
801    */
802   void padWithGaps(Map<Integer, String> resNumMap,
803           List<Integer> omitNonObserved)
804   {
805     if (resNumMap == null || resNumMap.isEmpty())
806     {
807       return;
808     }
809     Integer[] keys = resNumMap.keySet().toArray(new Integer[0]);
810     // Arrays.sort(keys);
811     int firstIndex = keys[0];
812     int lastIndex = keys[keys.length - 1];
813     // System.out.println("Min value " + firstIndex);
814     // System.out.println("Max value " + lastIndex);
815     for (int x = firstIndex; x <= lastIndex; x++)
816     {
817       if (!resNumMap.containsKey(x) && !omitNonObserved.contains(x))
818       {
819         resNumMap.put(x, "-");
820       }
821     }
822   }
823
824   @Override
825   public Entity getEntityById(String id) throws SiftsException
826   {
827     // Determines an entity to process by performing a heuristic matching of all
828     // Entities with the given chainId and choosing the best matching Entity
829     Entity entity = getEntityByMostOptimalMatchedId(id);
830     if (entity != null)
831     {
832       return entity;
833     }
834     throw new SiftsException("Entity " + id + " not found");
835   }
836
837   /**
838    * This method was added because EntityId is NOT always equal to ChainId.
839    * Hence, it provides the logic to greedily detect the "true" Entity for a
840    * given chainId where discrepancies exist.
841    * 
842    * @param chainId
843    * @return
844    */
845   public Entity getEntityByMostOptimalMatchedId(String chainId)
846   {
847     // System.out.println("---> advanced greedy entityId matching block
848     // entered..");
849     List<Entity> entities = siftsEntry.getEntity();
850     SiftsEntitySortPojo[] sPojo = new SiftsEntitySortPojo[entities.size()];
851     int count = 0;
852     for (Entity entity : entities)
853     {
854       sPojo[count] = new SiftsEntitySortPojo();
855       sPojo[count].entityId = entity.getEntityId();
856
857       List<Segment> segments = entity.getSegment();
858       for (Segment segment : segments)
859       {
860         List<Residue> residues = segment.getListResidue().getResidue();
861         for (Residue residue : residues)
862         {
863           List<CrossRefDb> cRefDbs = residue.getCrossRefDb();
864           for (CrossRefDb cRefDb : cRefDbs)
865           {
866             if (!cRefDb.getDbSource().equalsIgnoreCase("PDB"))
867             {
868               continue;
869             }
870             ++sPojo[count].resCount;
871             if (cRefDb.getDbChainId().equalsIgnoreCase(chainId))
872             {
873               ++sPojo[count].chainIdFreq;
874             }
875           }
876         }
877       }
878       sPojo[count].pid = (100 * sPojo[count].chainIdFreq)
879               / sPojo[count].resCount;
880       ++count;
881     }
882     Arrays.sort(sPojo, Collections.reverseOrder());
883     // System.out.println("highest matched entity : " + sPojo[0].entityId);
884     // System.out.println("highest matched pid : " + sPojo[0].pid);
885
886     if (sPojo[0].entityId != null)
887     {
888       if (sPojo[0].pid < 1)
889       {
890         return null;
891       }
892       for (Entity entity : entities)
893       {
894         if (!entity.getEntityId().equalsIgnoreCase(sPojo[0].entityId))
895         {
896           continue;
897         }
898         return entity;
899       }
900     }
901     return null;
902   }
903
904   private class SiftsEntitySortPojo
905           implements Comparable<SiftsEntitySortPojo>
906   {
907     public String entityId;
908
909     public int chainIdFreq;
910
911     public int pid;
912
913     public int resCount;
914
915     @Override
916     public int compareTo(SiftsEntitySortPojo o)
917     {
918       return this.pid - o.pid;
919     }
920   }
921
922   private class SegmentHelperPojo
923   {
924     private SequenceI seq;
925
926     private HashMap<Integer, int[]> mapping;
927
928     private TreeMap<Integer, String> resNumMap;
929
930     private List<Integer> omitNonObserved;
931
932     private int nonObservedShiftIndex;
933
934     public SegmentHelperPojo(SequenceI seq, HashMap<Integer, int[]> mapping,
935             TreeMap<Integer, String> resNumMap,
936             List<Integer> omitNonObserved, int nonObservedShiftIndex)
937     {
938       setSeq(seq);
939       setMapping(mapping);
940       setResNumMap(resNumMap);
941       setOmitNonObserved(omitNonObserved);
942       setNonObservedShiftIndex(nonObservedShiftIndex);
943     }
944
945     public SequenceI getSeq()
946     {
947       return seq;
948     }
949
950     public void setSeq(SequenceI seq)
951     {
952       this.seq = seq;
953     }
954
955     public HashMap<Integer, int[]> getMapping()
956     {
957       return mapping;
958     }
959
960     public void setMapping(HashMap<Integer, int[]> mapping)
961     {
962       this.mapping = mapping;
963     }
964
965     public TreeMap<Integer, String> getResNumMap()
966     {
967       return resNumMap;
968     }
969
970     public void setResNumMap(TreeMap<Integer, String> resNumMap)
971     {
972       this.resNumMap = resNumMap;
973     }
974
975     public List<Integer> getOmitNonObserved()
976     {
977       return omitNonObserved;
978     }
979
980     public void setOmitNonObserved(List<Integer> omitNonObserved)
981     {
982       this.omitNonObserved = omitNonObserved;
983     }
984
985     public int getNonObservedShiftIndex()
986     {
987       return nonObservedShiftIndex;
988     }
989
990     public void setNonObservedShiftIndex(int nonObservedShiftIndex)
991     {
992       this.nonObservedShiftIndex = nonObservedShiftIndex;
993     }
994   }
995
996   @Override
997   public StringBuilder getMappingOutput(MappingOutputPojo mp)
998           throws SiftsException
999   {
1000     String seqRes = mp.getSeqResidue();
1001     String seqName = mp.getSeqName();
1002     int sStart = mp.getSeqStart();
1003     int sEnd = mp.getSeqEnd();
1004
1005     String strRes = mp.getStrResidue();
1006     String strName = mp.getStrName();
1007     int pdbStart = mp.getStrStart();
1008     int pdbEnd = mp.getStrEnd();
1009
1010     String type = mp.getType();
1011
1012     int maxid = (seqName.length() >= strName.length()) ? seqName.length()
1013             : strName.length();
1014     int len = 72 - maxid - 1;
1015
1016     int nochunks = ((seqRes.length()) / len)
1017             + ((seqRes.length()) % len > 0 ? 1 : 0);
1018     // output mappings
1019     StringBuilder output = new StringBuilder(512);
1020     output.append(NEWLINE);
1021     output.append("Sequence \u27f7 Structure mapping details")
1022             .append(NEWLINE);
1023     output.append("Method: SIFTS");
1024     output.append(NEWLINE).append(NEWLINE);
1025
1026     output.append(new Format("%" + maxid + "s").form(seqName));
1027     output.append(" :  ");
1028     output.append(String.valueOf(sStart));
1029     output.append(" - ");
1030     output.append(String.valueOf(sEnd));
1031     output.append(" Maps to ");
1032     output.append(NEWLINE);
1033     output.append(new Format("%" + maxid + "s").form(structId));
1034     output.append(" :  ");
1035     output.append(String.valueOf(pdbStart));
1036     output.append(" - ");
1037     output.append(String.valueOf(pdbEnd));
1038     output.append(NEWLINE).append(NEWLINE);
1039
1040     ScoreMatrix pam250 = ScoreModels.getInstance().getPam250();
1041     int matchedSeqCount = 0;
1042     for (int j = 0; j < nochunks; j++)
1043     {
1044       // Print the first aligned sequence
1045       output.append(new Format("%" + (maxid) + "s").form(seqName))
1046               .append(" ");
1047
1048       for (int i = 0; i < len; i++)
1049       {
1050         if ((i + (j * len)) < seqRes.length())
1051         {
1052           output.append(seqRes.charAt(i + (j * len)));
1053         }
1054       }
1055
1056       output.append(NEWLINE);
1057       output.append(new Format("%" + (maxid) + "s").form(" ")).append(" ");
1058
1059       /*
1060        * Print out the match symbols:
1061        * | for exact match (ignoring case)
1062        * . if PAM250 score is positive
1063        * else a space
1064        */
1065       for (int i = 0; i < len; i++)
1066       {
1067         try
1068         {
1069           if ((i + (j * len)) < seqRes.length())
1070           {
1071             char c1 = seqRes.charAt(i + (j * len));
1072             char c2 = strRes.charAt(i + (j * len));
1073             boolean sameChar = Comparison.isSameResidue(c1, c2, false);
1074             if (sameChar && !Comparison.isGap(c1))
1075             {
1076               matchedSeqCount++;
1077               output.append("|");
1078             }
1079             else if (type.equals("pep"))
1080             {
1081               if (pam250.getPairwiseScore(c1, c2) > 0)
1082               {
1083                 output.append(".");
1084               }
1085               else
1086               {
1087                 output.append(" ");
1088               }
1089             }
1090             else
1091             {
1092               output.append(" ");
1093             }
1094           }
1095         } catch (IndexOutOfBoundsException e)
1096         {
1097           continue;
1098         }
1099       }
1100       // Now print the second aligned sequence
1101       output = output.append(NEWLINE);
1102       output = output.append(new Format("%" + (maxid) + "s").form(strName))
1103               .append(" ");
1104       for (int i = 0; i < len; i++)
1105       {
1106         if ((i + (j * len)) < strRes.length())
1107         {
1108           output.append(strRes.charAt(i + (j * len)));
1109         }
1110       }
1111       output.append(NEWLINE).append(NEWLINE);
1112     }
1113     float pid = (float) matchedSeqCount / seqRes.length() * 100;
1114     if (pid < SiftsSettings.getFailSafePIDThreshold())
1115     {
1116       throw new SiftsException(">>> Low PID detected for SIFTs mapping...");
1117     }
1118     output.append("Length of alignment = " + seqRes.length())
1119             .append(NEWLINE);
1120     output.append(new Format("Percentage ID = %2.2f").form(pid));
1121     return output;
1122   }
1123
1124   @Override
1125   public int getEntityCount()
1126   {
1127     return siftsEntry.getEntity().size();
1128   }
1129
1130   @Override
1131   public String getDbAccessionId()
1132   {
1133     return siftsEntry.getDbAccessionId();
1134   }
1135
1136   @Override
1137   public String getDbCoordSys()
1138   {
1139     return siftsEntry.getDbCoordSys();
1140   }
1141
1142   @Override
1143   public String getDbSource()
1144   {
1145     return siftsEntry.getDbSource();
1146   }
1147
1148   @Override
1149   public String getDbVersion()
1150   {
1151     return siftsEntry.getDbVersion();
1152   }
1153
1154   public static void setMockSiftsFile(File file)
1155   {
1156     mockSiftsFile = file;
1157   }
1158
1159 }