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.util.ColorUtils;
38 import jalview.util.Comparison;
40 import java.awt.Color;
41 import java.util.ArrayList;
42 import java.util.HashMap;
43 import java.util.LinkedHashMap;
44 import java.util.List;
48 * Routines for generating Chimera commands for Jalview/Chimera binding
53 public class ChimeraCommands
56 public static final String NAMESPACE_PREFIX = "jv_";
59 * Constructs Chimera commands to colour residues as per the Jalview alignment
70 public static StructureMappingcommandSet[] getColourBySequenceCommand(
71 StructureSelectionManager ssm, String[] files,
72 SequenceI[][] sequence, SequenceRenderer sr,
73 AlignmentViewPanel viewPanel, boolean isChimeraX)
75 Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, files,
76 sequence, sr, viewPanel, isChimeraX);
78 List<String> colourCommands = buildColourCommands(colourMap,
81 StructureMappingcommandSet cs = new StructureMappingcommandSet(
82 ChimeraCommands.class, null,
83 colourCommands.toArray(new String[colourCommands.size()]));
85 return new StructureMappingcommandSet[] { cs };
89 * Traverse the map of colours/models/chains/positions to construct a list of
90 * 'color' commands (one per distinct colour used). The format of each command
95 * color colorname #modelnumber:range.chain
96 * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
104 protected static List<String> buildColourCommands(
105 Map<Object, AtomSpecModel> colourMap, boolean isChimeraX)
108 * This version concatenates all commands into a single String (semi-colon
109 * delimited). If length limit issues arise, refactor to return one color
110 * command per colour.
112 List<String> commands = new ArrayList<>();
113 StringBuilder sb = new StringBuilder(256);
114 boolean firstColour = true;
115 for (Object key : colourMap.keySet())
117 Color colour = (Color) key;
118 String colourCode = ColorUtils.toTkCode(colour);
125 final AtomSpecModel colourData = colourMap.get(colour);
128 sb.append(colourData.getAtomSpecX()).append(" ").append(colourCode);
132 sb.append(colourCode).append(" ").append(colourData.getAtomSpec());
135 commands.add(sb.toString());
140 * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and
141 * builds a Chimera format atom spec
143 * @param modelAndChainRanges
145 protected static String getAtomSpec(
146 Map<Integer, Map<String, List<int[]>>> modelAndChainRanges)
148 StringBuilder sb = new StringBuilder(128);
149 boolean firstModelForColour = true;
150 for (Integer model : modelAndChainRanges.keySet())
152 boolean firstPositionForModel = true;
153 if (!firstModelForColour)
157 firstModelForColour = false;
158 sb.append("#").append(model).append(":");
160 final Map<String, List<int[]>> modelData = modelAndChainRanges
162 for (String chain : modelData.keySet())
164 boolean hasChain = !"".equals(chain.trim());
165 for (int[] range : modelData.get(chain))
167 if (!firstPositionForModel)
171 if (range[0] == range[1])
177 sb.append(range[0]).append("-").append(range[1]);
181 sb.append(".").append(chain);
183 firstPositionForModel = false;
187 return sb.toString();
192 * Build a data structure which records contiguous subsequences for each colour.
193 * From this we can easily generate the Chimera command for colour by sequence.
197 * list of start/end ranges
198 * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
209 protected static Map<Object, AtomSpecModel> buildColoursMap(
210 StructureSelectionManager ssm, String[] files,
211 SequenceI[][] sequence, SequenceRenderer sr,
212 AlignmentViewPanel viewPanel, boolean isChimeraX)
214 FeatureRenderer fr = viewPanel.getFeatureRenderer();
215 FeatureColourFinder finder = new FeatureColourFinder(fr);
216 AlignViewportI viewport = viewPanel.getAlignViewport();
217 HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
218 AlignmentI al = viewport.getAlignment();
219 Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
220 Color lastColour = null;
222 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
224 final int modelNumber = pdbfnum + (isChimeraX ? 1 : 0);
225 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
227 if (mapping == null || mapping.length < 1)
232 int startPos = -1, lastPos = -1;
233 String lastChain = "";
234 for (int s = 0; s < sequence[pdbfnum].length; s++)
236 for (int sp, m = 0; m < mapping.length; m++)
238 final SequenceI seq = sequence[pdbfnum][s];
239 if (mapping[m].getSequence() == seq
240 && (sp = al.findIndex(seq)) > -1)
242 SequenceI asp = al.getSequenceAt(sp);
243 for (int r = 0; r < asp.getLength(); r++)
245 // no mapping to gaps in sequence
246 if (Comparison.isGap(asp.getCharAt(r)))
250 int pos = mapping[m].getPDBResNum(asp.findPosition(r));
252 if (pos < 1 || pos == lastPos)
257 Color colour = sr.getResidueColour(seq, r, finder);
260 * darker colour for hidden regions
262 if (!cs.isVisible(r))
267 final String chain = mapping[m].getChain();
270 * Just keep incrementing the end position for this colour range
271 * _unless_ colour, PDB model or chain has changed, or there is a
272 * gap in the mapped residue sequence
274 final boolean newColour = !colour.equals(lastColour);
275 final boolean nonContig = lastPos + 1 != pos;
276 final boolean newChain = !chain.equals(lastChain);
277 if (newColour || nonContig || newChain)
281 addAtomSpecRange(colourMap, lastColour, modelNumber,
282 startPos, lastPos, lastChain);
290 // final colour range
291 if (lastColour != null)
293 addAtomSpecRange(colourMap, lastColour, modelNumber, startPos,
305 * Helper method to add one contiguous range to the AtomSpec model for the given
306 * value (creating the model if necessary). As used by Jalview, {@code value} is
308 * <li>a colour, when building a 'colour structure by sequence' command</li>
309 * <li>a feature value, when building a 'set Chimera attributes from features'
320 protected static void addAtomSpecRange(Map<Object, AtomSpecModel> map,
321 Object value, int model, int startPos, int endPos, String chain)
324 * Get/initialize map of data for the colour
326 AtomSpecModel atomSpec = map.get(value);
327 if (atomSpec == null)
329 atomSpec = new AtomSpecModel();
330 map.put(value, atomSpec);
333 atomSpec.addRange(model, startPos, endPos, chain);
337 * Constructs and returns Chimera commands to set attributes on residues
338 * corresponding to features in Jalview. Attribute names are the Jalview feature
339 * type, with a "jv_" prefix.
348 public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
349 StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
350 AlignmentViewPanel viewPanel, boolean isChimeraX)
352 Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
353 ssm, files, seqs, viewPanel);
355 List<String> commands = buildSetAttributeCommands(featureMap,
358 StructureMappingcommandSet cs = new StructureMappingcommandSet(
359 ChimeraCommands.class, null,
360 commands.toArray(new String[commands.size()]));
367 * Helper method to build a map of
368 * { featureType, { feature value, AtomSpecModel } }
377 protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
378 StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
379 AlignmentViewPanel viewPanel)
381 Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
383 FeatureRenderer fr = viewPanel.getFeatureRenderer();
389 AlignViewportI viewport = viewPanel.getAlignViewport();
390 List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
393 * if alignment is showing features from complement, we also transfer
394 * these features to the corresponding mapped structure residues
396 boolean showLinkedFeatures = viewport.isShowComplementFeatures();
397 List<String> complementFeatures = new ArrayList<>();
398 FeatureRenderer complementRenderer = null;
399 if (showLinkedFeatures)
401 AlignViewportI comp = fr.getViewport().getCodingComplement();
404 complementRenderer = Desktop.getAlignFrameFor(comp)
405 .getFeatureRenderer();
406 complementFeatures = complementRenderer.getDisplayedFeatureTypes();
409 if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
414 AlignmentI alignment = viewPanel.getAlignment();
415 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
417 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
419 if (mapping == null || mapping.length < 1)
424 for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
426 for (int m = 0; m < mapping.length; m++)
428 final SequenceI seq = seqs[pdbfnum][seqNo];
429 int sp = alignment.findIndex(seq);
430 StructureMapping structureMapping = mapping[m];
431 if (structureMapping.getSequence() == seq && sp > -1)
434 * found a sequence with a mapping to a structure;
435 * now scan its features
437 if (!visibleFeatures.isEmpty())
439 scanSequenceFeatures(visibleFeatures, structureMapping, seq,
442 if (showLinkedFeatures)
444 scanComplementFeatures(complementRenderer, structureMapping,
445 seq, theMap, pdbfnum);
455 * Scans visible features in mapped positions of the CDS/peptide complement, and
456 * adds any found to the map of attribute values/structure positions
458 * @param complementRenderer
459 * @param structureMapping
464 protected static void scanComplementFeatures(
465 FeatureRenderer complementRenderer,
466 StructureMapping structureMapping, SequenceI seq,
467 Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
470 * for each sequence residue mapped to a structure position...
472 for (int seqPos : structureMapping.getMapping().keySet())
475 * find visible complementary features at mapped position(s)
477 MappedFeatures mf = complementRenderer
478 .findComplementFeaturesAtResidue(seq, seqPos);
481 for (SequenceFeature sf : mf.features)
483 String type = sf.getType();
486 * Don't copy features which originated from Chimera
488 if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
489 .equals(sf.getFeatureGroup()))
495 * record feature 'value' (score/description/type) as at the
496 * corresponding structure position
498 List<int[]> mappedRanges = structureMapping
499 .getPDBResNumRanges(seqPos, seqPos);
501 if (!mappedRanges.isEmpty())
503 String value = sf.getDescription();
504 if (value == null || value.length() == 0)
508 float score = sf.getScore();
509 if (score != 0f && !Float.isNaN(score))
511 value = Float.toString(score);
513 Map<Object, AtomSpecModel> featureValues = theMap.get(type);
514 if (featureValues == null)
516 featureValues = new HashMap<>();
517 theMap.put(type, featureValues);
519 for (int[] range : mappedRanges)
521 addAtomSpecRange(featureValues, value, modelNumber, range[0],
522 range[1], structureMapping.getChain());
531 * Inspect features on the sequence; for each feature that is visible, determine
532 * its mapped ranges in the structure (if any) according to the given mapping,
533 * and add them to the map.
535 * @param visibleFeatures
541 protected static void scanSequenceFeatures(List<String> visibleFeatures,
542 StructureMapping mapping, SequenceI seq,
543 Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
545 List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
546 visibleFeatures.toArray(new String[visibleFeatures.size()]));
547 for (SequenceFeature sf : sfs)
549 String type = sf.getType();
552 * Don't copy features which originated from Chimera
554 if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
555 .equals(sf.getFeatureGroup()))
560 List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
563 if (!mappedRanges.isEmpty())
565 String value = sf.getDescription();
566 if (value == null || value.length() == 0)
570 float score = sf.getScore();
571 if (score != 0f && !Float.isNaN(score))
573 value = Float.toString(score);
575 Map<Object, AtomSpecModel> featureValues = theMap.get(type);
576 if (featureValues == null)
578 featureValues = new HashMap<>();
579 theMap.put(type, featureValues);
581 for (int[] range : mappedRanges)
583 addAtomSpecRange(featureValues, value, modelNumber, range[0],
584 range[1], mapping.getChain());
591 * Traverse the map of features/values/models/chains/positions to construct a
592 * list of 'setattr' commands (one per distinct feature type and value).
594 * The format of each command is
597 * <blockquote> setattr r <featureName> " " #modelnumber:range.chain
598 * e.g. setattr r jv:chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
606 protected static List<String> buildSetAttributeCommands(
607 Map<String, Map<Object, AtomSpecModel>> featureMap,
610 List<String> commands = new ArrayList<>();
611 for (String featureType : featureMap.keySet())
613 String attributeName = makeAttributeName(featureType);
616 * clear down existing attributes for this feature
618 // 'problem' - sets attribute to None on all residues - overkill?
619 // commands.add("~setattr r " + attributeName + " :*");
621 Map<Object, AtomSpecModel> values = featureMap.get(featureType);
622 for (Object value : values.keySet())
625 * for each distinct value recorded for this feature type,
626 * add a command to set the attribute on the mapped residues
627 * Put values in single quotes, encoding any embedded single quotes
629 StringBuilder sb = new StringBuilder(128);
630 String featureValue = value.toString();
631 featureValue = featureValue.replaceAll("\\'", "'");
632 sb.append("setattr r ").append(attributeName).append(" '")
633 .append(featureValue).append("' ");
634 AtomSpecModel atomSpecModel = values.get(value);
635 sb.append(isChimeraX ? atomSpecModel.getAtomSpecX()
636 : atomSpecModel.getAtomSpec());
637 commands.add(sb.toString());
645 * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
646 * for a 'Jalview' namespace, and any non-alphanumeric character is converted
653 * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
656 protected static String makeAttributeName(String featureType)
658 StringBuilder sb = new StringBuilder();
659 if (featureType != null)
661 for (char c : featureType.toCharArray())
663 sb.append(Character.isLetterOrDigit(c) ? c : '_');
666 String attName = NAMESPACE_PREFIX + sb.toString();
669 * Chimera treats an attribute name ending in 'color' as colour-valued;
670 * Jalview doesn't, so prevent this by appending an underscore
672 if (attName.toUpperCase().endsWith("COLOR"))