2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
21 package jalview.ext.rbvi.chimera;
23 import jalview.api.AlignViewportI;
24 import jalview.api.AlignmentViewPanel;
25 import jalview.api.FeatureRenderer;
26 import jalview.api.SequenceRenderer;
27 import jalview.datamodel.AlignmentI;
28 import jalview.datamodel.HiddenColumns;
29 import jalview.datamodel.MappedFeatures;
30 import jalview.datamodel.SequenceFeature;
31 import jalview.datamodel.SequenceI;
32 import jalview.gui.Desktop;
33 import jalview.renderer.seqfeatures.FeatureColourFinder;
34 import jalview.structure.StructureMapping;
35 import jalview.structure.StructureMappingcommandSet;
36 import jalview.structure.StructureSelectionManager;
37 import jalview.structures.models.AAStructureBindingModel;
38 import jalview.util.ColorUtils;
39 import jalview.util.Comparison;
40 import jalview.util.IntRangeComparator;
42 import java.awt.Color;
43 import java.util.ArrayList;
44 import java.util.Collections;
45 import java.util.HashMap;
46 import java.util.Iterator;
47 import java.util.LinkedHashMap;
48 import java.util.List;
52 * Routines for generating Chimera commands for Jalview/Chimera binding
57 public class ChimeraCommands
59 public static final String NAMESPACE_PREFIX = "jv_";
62 * colour for residues shown in structure but hidden in alignment
64 private static final String COLOR_GRAY_HEX = "color "
65 + ColorUtils.toTkCode(Color.GRAY);
68 * Constructs Chimera commands to colour residues as per the Jalview alignment
74 public static String[] getColourBySequenceCommand(
75 Map<Object, AtomSpecModel> colourMap,
76 AAStructureBindingModel binding)
78 List<String> colourCommands = buildColourCommands(colourMap, binding);
80 return colourCommands.toArray(new String[colourCommands.size()]);
84 * Traverse the map of colours/models/chains/positions to construct a list of
85 * 'color' commands (one per distinct colour used). The format of each command
90 * color colorname #modelnumber:range.chain
91 * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
99 protected static List<String> buildColourCommands(
100 Map<Object, AtomSpecModel> colourMap,
101 AAStructureBindingModel binding)
104 * This version concatenates all commands into a single String (semi-colon
105 * delimited). If length limit issues arise, refactor to return one color
106 * command per colour.
108 List<String> commands = new ArrayList<>();
109 StringBuilder sb = new StringBuilder(256);
110 sb.append(COLOR_GRAY_HEX);
112 for (Object key : colourMap.keySet())
114 Color colour = (Color) key;
115 String colourCode = ColorUtils.toTkCode(colour);
117 sb.append("color ").append(colourCode).append(" ");
118 final AtomSpecModel colourData = colourMap.get(colour);
119 sb.append(getAtomSpec(colourData, binding));
121 commands.add(sb.toString());
126 * Build a data structure which records contiguous subsequences for each colour.
127 * From this we can easily generate the Chimera command for colour by sequence.
133 * list of start/end ranges
136 * Ordering is by order of addition (for colours and positions), natural
137 * ordering (for models and chains)
143 * @param hideHiddenRegions
147 protected static Map<Object, AtomSpecModel> buildColoursMap(
148 StructureSelectionManager ssm, String[] files,
149 SequenceI[][] sequence, SequenceRenderer sr,
150 boolean hideHiddenRegions, AlignmentViewPanel viewPanel)
152 FeatureRenderer fr = viewPanel.getFeatureRenderer();
153 FeatureColourFinder finder = new FeatureColourFinder(fr);
154 AlignViewportI viewport = viewPanel.getAlignViewport();
155 HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
156 AlignmentI al = viewport.getAlignment();
157 Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
158 Color lastColour = null;
160 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
162 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
164 if (mapping == null || mapping.length < 1)
169 int startPos = -1, lastPos = -1;
170 String lastChain = "";
171 for (int s = 0; s < sequence[pdbfnum].length; s++)
173 for (int sp, m = 0; m < mapping.length; m++)
175 final SequenceI seq = sequence[pdbfnum][s];
176 if (mapping[m].getSequence() == seq
177 && (sp = al.findIndex(seq)) > -1)
179 SequenceI asp = al.getSequenceAt(sp);
180 for (int r = 0; r < asp.getLength(); r++)
182 // no mapping to gaps in sequence
183 if (Comparison.isGap(asp.getCharAt(r)))
187 int pos = mapping[m].getPDBResNum(asp.findPosition(r));
189 if (pos < 1 || pos == lastPos)
194 Color colour = sr.getResidueColour(seq, r, finder);
197 * hidden regions are shown gray or, optionally, ignored
199 if (!cs.isVisible(r))
201 if (hideHiddenRegions)
211 final String chain = mapping[m].getChain();
214 * Just keep incrementing the end position for this colour range
215 * _unless_ colour, PDB model or chain has changed, or there is a
216 * gap in the mapped residue sequence
218 final boolean newColour = !colour.equals(lastColour);
219 final boolean nonContig = lastPos + 1 != pos;
220 final boolean newChain = !chain.equals(lastChain);
221 if (newColour || nonContig || newChain)
225 addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
234 // final colour range
235 if (lastColour != null)
237 addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
249 * Helper method to add one contiguous range to the AtomSpec model for the given
250 * value (creating the model if necessary). As used by Jalview, {@code value} is
252 * <li>a colour, when building a 'colour structure by sequence' command</li>
253 * <li>a feature value, when building a 'set Chimera attributes from features'
264 public static void addAtomSpecRange(Map<Object, AtomSpecModel> map,
265 Object value, int model, int startPos, int endPos, String chain)
268 * Get/initialize map of data for the colour
270 AtomSpecModel atomSpec = map.get(value);
271 if (atomSpec == null)
273 atomSpec = new AtomSpecModel();
274 map.put(value, atomSpec);
277 atomSpec.addRange(model, startPos, endPos, chain);
281 * Constructs and returns Chimera commands to set attributes on residues
282 * corresponding to features in Jalview. Attribute names are the Jalview feature
283 * type, with a "jv_" prefix.
292 public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
293 AlignmentViewPanel viewPanel, AAStructureBindingModel binding)
295 StructureSelectionManager ssm = binding.getSsm();
296 String[] files = binding.getStructureFiles();
297 SequenceI[][] seqs = binding.getSequence();
299 Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
300 ssm, files, seqs, viewPanel);
302 List<String> commands = buildSetAttributeCommands(featureMap, binding);
304 StructureMappingcommandSet cs = new StructureMappingcommandSet(
305 ChimeraCommands.class, null,
306 commands.toArray(new String[commands.size()]));
313 * Helper method to build a map of
314 * { featureType, { feature value, AtomSpecModel } }
323 protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
324 StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
325 AlignmentViewPanel viewPanel)
327 Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
329 FeatureRenderer fr = viewPanel.getFeatureRenderer();
335 AlignViewportI viewport = viewPanel.getAlignViewport();
336 List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
339 * if alignment is showing features from complement, we also transfer
340 * these features to the corresponding mapped structure residues
342 boolean showLinkedFeatures = viewport.isShowComplementFeatures();
343 List<String> complementFeatures = new ArrayList<>();
344 FeatureRenderer complementRenderer = null;
345 if (showLinkedFeatures)
347 AlignViewportI comp = fr.getViewport().getCodingComplement();
350 complementRenderer = Desktop.getAlignFrameFor(comp)
351 .getFeatureRenderer();
352 complementFeatures = complementRenderer.getDisplayedFeatureTypes();
355 if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
360 AlignmentI alignment = viewPanel.getAlignment();
361 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
363 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
365 if (mapping == null || mapping.length < 1)
370 for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
372 for (int m = 0; m < mapping.length; m++)
374 final SequenceI seq = seqs[pdbfnum][seqNo];
375 int sp = alignment.findIndex(seq);
376 StructureMapping structureMapping = mapping[m];
377 if (structureMapping.getSequence() == seq && sp > -1)
380 * found a sequence with a mapping to a structure;
381 * now scan its features
383 if (!visibleFeatures.isEmpty())
385 scanSequenceFeatures(visibleFeatures, structureMapping, seq,
388 if (showLinkedFeatures)
390 scanComplementFeatures(complementRenderer, structureMapping,
391 seq, theMap, pdbfnum);
401 * Scans visible features in mapped positions of the CDS/peptide complement, and
402 * adds any found to the map of attribute values/structure positions
404 * @param complementRenderer
405 * @param structureMapping
410 protected static void scanComplementFeatures(
411 FeatureRenderer complementRenderer,
412 StructureMapping structureMapping, SequenceI seq,
413 Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
416 * for each sequence residue mapped to a structure position...
418 for (int seqPos : structureMapping.getMapping().keySet())
421 * find visible complementary features at mapped position(s)
423 MappedFeatures mf = complementRenderer
424 .findComplementFeaturesAtResidue(seq, seqPos);
427 for (SequenceFeature sf : mf.features)
429 String type = sf.getType();
432 * Don't copy features which originated from Chimera
434 if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
435 .equals(sf.getFeatureGroup()))
441 * record feature 'value' (score/description/type) as at the
442 * corresponding structure position
444 List<int[]> mappedRanges = structureMapping
445 .getPDBResNumRanges(seqPos, seqPos);
447 if (!mappedRanges.isEmpty())
449 String value = sf.getDescription();
450 if (value == null || value.length() == 0)
454 float score = sf.getScore();
455 if (score != 0f && !Float.isNaN(score))
457 value = Float.toString(score);
459 Map<Object, AtomSpecModel> featureValues = theMap.get(type);
460 if (featureValues == null)
462 featureValues = new HashMap<>();
463 theMap.put(type, featureValues);
465 for (int[] range : mappedRanges)
467 addAtomSpecRange(featureValues, value, modelNumber, range[0],
468 range[1], structureMapping.getChain());
477 * Inspect features on the sequence; for each feature that is visible, determine
478 * its mapped ranges in the structure (if any) according to the given mapping,
479 * and add them to the map.
481 * @param visibleFeatures
487 protected static void scanSequenceFeatures(List<String> visibleFeatures,
488 StructureMapping mapping, SequenceI seq,
489 Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
491 List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
492 visibleFeatures.toArray(new String[visibleFeatures.size()]));
493 for (SequenceFeature sf : sfs)
495 String type = sf.getType();
498 * Don't copy features which originated from Chimera
500 if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
501 .equals(sf.getFeatureGroup()))
506 List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
509 if (!mappedRanges.isEmpty())
511 String value = sf.getDescription();
512 if (value == null || value.length() == 0)
516 float score = sf.getScore();
517 if (score != 0f && !Float.isNaN(score))
519 value = Float.toString(score);
521 Map<Object, AtomSpecModel> featureValues = theMap.get(type);
522 if (featureValues == null)
524 featureValues = new HashMap<>();
525 theMap.put(type, featureValues);
527 for (int[] range : mappedRanges)
529 addAtomSpecRange(featureValues, value, modelNumber, range[0],
530 range[1], mapping.getChain());
537 * Traverse the map of features/values/models/chains/positions to construct a
538 * list of 'setattr' commands (one per distinct feature type and value).
540 * The format of each command is
543 * <blockquote> setattr r <featureName> " " #modelnumber:range.chain
544 * e.g. setattr r jv:chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
552 protected static List<String> buildSetAttributeCommands(
553 Map<String, Map<Object, AtomSpecModel>> featureMap,
554 AAStructureBindingModel binding)
556 List<String> commands = new ArrayList<>();
557 for (String featureType : featureMap.keySet())
559 String attributeName = makeAttributeName(featureType);
562 * clear down existing attributes for this feature
564 // 'problem' - sets attribute to None on all residues - overkill?
565 // commands.add("~setattr r " + attributeName + " :*");
567 Map<Object, AtomSpecModel> values = featureMap.get(featureType);
568 for (Object value : values.keySet())
571 * for each distinct value recorded for this feature type,
572 * add a command to set the attribute on the mapped residues
573 * Put values in single quotes, encoding any embedded single quotes
575 StringBuilder sb = new StringBuilder(128);
576 String featureValue = value.toString();
577 featureValue = featureValue.replaceAll("\\'", "'");
578 sb.append("setattr r ").append(attributeName).append(" '")
579 .append(featureValue).append("' ");
580 sb.append(getAtomSpec(values.get(value), binding));
581 commands.add(sb.toString());
589 * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
590 * for a 'Jalview' namespace, and any non-alphanumeric character is converted
597 * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
600 protected static String makeAttributeName(String featureType)
602 StringBuilder sb = new StringBuilder();
603 if (featureType != null)
605 for (char c : featureType.toCharArray())
607 sb.append(Character.isLetterOrDigit(c) ? c : '_');
610 String attName = NAMESPACE_PREFIX + sb.toString();
613 * Chimera treats an attribute name ending in 'color' as colour-valued;
614 * Jalview doesn't, so prevent this by appending an underscore
616 if (attName.toUpperCase().endsWith("COLOR"))
625 * Returns the range(s) formatted as a Chimera atomspec
629 public static String getAtomSpec(AtomSpecModel atomSpec,
630 AAStructureBindingModel binding)
632 StringBuilder sb = new StringBuilder(128);
633 boolean firstModel = true;
634 for (Integer model : atomSpec.getModels())
641 // todo use JalviewChimeraBinding.getModelSpec(model)
642 // which means this cannot be static
643 sb.append(binding.getModelSpec(model)).append(":");
645 boolean firstPositionForModel = true;
647 for (String chain : atomSpec.getChains(model))
649 chain = " ".equals(chain) ? chain : chain.trim();
651 List<int[]> rangeList = atomSpec.getRanges(model, chain);
654 * sort ranges into ascending start position order
656 Collections.sort(rangeList, IntRangeComparator.ASCENDING);
658 int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
659 int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
661 Iterator<int[]> iterator = rangeList.iterator();
662 while (iterator.hasNext())
664 int[] range = iterator.next();
665 if (range[0] <= end + 1)
668 * range overlaps or is contiguous with the last one
669 * - so just extend the end position, and carry on
670 * (unless this is the last in the list)
672 end = Math.max(end, range[1]);
677 * we have a break so append the last range
679 appendRange(sb, start, end, chain, firstPositionForModel);
680 firstPositionForModel = false;
687 * and append the last range
689 if (!rangeList.isEmpty())
691 appendRange(sb, start, end, chain, firstPositionForModel);
692 firstPositionForModel = false;
696 return sb.toString();
700 * A helper method that appends one start-end range to a Chimera atomspec
706 * @param firstPositionForModel
708 static void appendRange(StringBuilder sb, int start, int end,
709 String chain, boolean firstPositionForModel)
711 if (!firstPositionForModel)
721 sb.append(start).append("-").append(end);
725 if (!" ".equals(chain))