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