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
69 public static StructureMappingcommandSet[] getColourBySequenceCommand(
70 StructureSelectionManager ssm, String[] files,
71 SequenceI[][] sequence, SequenceRenderer sr,
72 AlignmentViewPanel viewPanel)
74 Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, files,
75 sequence, sr, viewPanel);
77 List<String> colourCommands = buildColourCommands(colourMap);
79 StructureMappingcommandSet cs = new StructureMappingcommandSet(
80 ChimeraCommands.class, null,
81 colourCommands.toArray(new String[colourCommands.size()]));
83 return new StructureMappingcommandSet[] { cs };
87 * Traverse the map of colours/models/chains/positions to construct a list of
88 * 'color' commands (one per distinct colour used). The format of each command
93 * color colorname #modelnumber:range.chain
94 * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
101 protected static List<String> buildColourCommands(
102 Map<Object, AtomSpecModel> colourMap)
105 * This version concatenates all commands into a single String (semi-colon
106 * delimited). If length limit issues arise, refactor to return one color
107 * command per colour.
109 List<String> commands = new ArrayList<>();
110 StringBuilder sb = new StringBuilder(256);
111 boolean firstColour = true;
112 for (Object key : colourMap.keySet())
114 Color colour = (Color) key;
115 String colourCode = ColorUtils.toTkCode(colour);
120 sb.append("color ").append(colourCode).append(" ");
122 final AtomSpecModel colourData = colourMap.get(colour);
123 sb.append(colourData.getAtomSpec());
125 commands.add(sb.toString());
130 * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and
131 * builds a Chimera format atom spec
133 * @param modelAndChainRanges
135 protected static String getAtomSpec(
136 Map<Integer, Map<String, List<int[]>>> modelAndChainRanges)
138 StringBuilder sb = new StringBuilder(128);
139 boolean firstModelForColour = true;
140 for (Integer model : modelAndChainRanges.keySet())
142 boolean firstPositionForModel = true;
143 if (!firstModelForColour)
147 firstModelForColour = false;
148 sb.append("#").append(model).append(":");
150 final Map<String, List<int[]>> modelData = modelAndChainRanges
152 for (String chain : modelData.keySet())
154 boolean hasChain = !"".equals(chain.trim());
155 for (int[] range : modelData.get(chain))
157 if (!firstPositionForModel)
161 if (range[0] == range[1])
167 sb.append(range[0]).append("-").append(range[1]);
171 sb.append(".").append(chain);
173 firstPositionForModel = false;
177 return sb.toString();
182 * Build a data structure which records contiguous subsequences for each colour.
183 * From this we can easily generate the Chimera command for colour by sequence.
187 * list of start/end ranges
188 * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
191 protected static Map<Object, AtomSpecModel> buildColoursMap(
192 StructureSelectionManager ssm, String[] files,
193 SequenceI[][] sequence, SequenceRenderer sr,
194 AlignmentViewPanel viewPanel)
196 FeatureRenderer fr = viewPanel.getFeatureRenderer();
197 FeatureColourFinder finder = new FeatureColourFinder(fr);
198 AlignViewportI viewport = viewPanel.getAlignViewport();
199 HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
200 AlignmentI al = viewport.getAlignment();
201 Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
202 Color lastColour = null;
204 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
206 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
208 if (mapping == null || mapping.length < 1)
213 int startPos = -1, lastPos = -1;
214 String lastChain = "";
215 for (int s = 0; s < sequence[pdbfnum].length; s++)
217 for (int sp, m = 0; m < mapping.length; m++)
219 final SequenceI seq = sequence[pdbfnum][s];
220 if (mapping[m].getSequence() == seq
221 && (sp = al.findIndex(seq)) > -1)
223 SequenceI asp = al.getSequenceAt(sp);
224 for (int r = 0; r < asp.getLength(); r++)
226 // no mapping to gaps in sequence
227 if (Comparison.isGap(asp.getCharAt(r)))
231 int pos = mapping[m].getPDBResNum(asp.findPosition(r));
233 if (pos < 1 || pos == lastPos)
238 Color colour = sr.getResidueColour(seq, r, finder);
241 * darker colour for hidden regions
243 if (!cs.isVisible(r))
248 final String chain = mapping[m].getChain();
251 * Just keep incrementing the end position for this colour range
252 * _unless_ colour, PDB model or chain has changed, or there is a
253 * gap in the mapped residue sequence
255 final boolean newColour = !colour.equals(lastColour);
256 final boolean nonContig = lastPos + 1 != pos;
257 final boolean newChain = !chain.equals(lastChain);
258 if (newColour || nonContig || newChain)
262 addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
271 // final colour range
272 if (lastColour != null)
274 addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
286 * Helper method to add one contiguous range to the AtomSpec model for the given
287 * value (creating the model if necessary). As used by Jalview, {@code value} is
289 * <li>a colour, when building a 'colour structure by sequence' command</li>
290 * <li>a feature value, when building a 'set Chimera attributes from features'
301 protected static void addAtomSpecRange(Map<Object, AtomSpecModel> map,
302 Object value, int model, int startPos, int endPos, String chain)
305 * Get/initialize map of data for the colour
307 AtomSpecModel atomSpec = map.get(value);
308 if (atomSpec == null)
310 atomSpec = new AtomSpecModel();
311 map.put(value, atomSpec);
314 atomSpec.addRange(model, startPos, endPos, chain);
318 * Constructs and returns Chimera commands to set attributes on residues
319 * corresponding to features in Jalview. Attribute names are the Jalview
320 * feature type, with a "jv_" prefix.
328 public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
329 StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
330 AlignmentViewPanel viewPanel)
332 Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
333 ssm, files, seqs, viewPanel);
335 List<String> commands = buildSetAttributeCommands(featureMap);
337 StructureMappingcommandSet cs = new StructureMappingcommandSet(
338 ChimeraCommands.class, null,
339 commands.toArray(new String[commands.size()]));
346 * Helper method to build a map of
347 * { featureType, { feature value, AtomSpecModel } }
356 protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
357 StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
358 AlignmentViewPanel viewPanel)
360 Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
362 FeatureRenderer fr = viewPanel.getFeatureRenderer();
368 AlignViewportI viewport = viewPanel.getAlignViewport();
369 List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
372 * if alignment is showing features from complement, we also transfer
373 * these features to the corresponding mapped structure residues
375 boolean showLinkedFeatures = viewport.isShowComplementFeatures();
376 List<String> complementFeatures = new ArrayList<>();
377 FeatureRenderer complementRenderer = null;
378 if (showLinkedFeatures)
380 AlignViewportI comp = fr.getViewport().getCodingComplement();
383 complementRenderer = Desktop.getAlignFrameFor(comp)
384 .getFeatureRenderer();
385 complementFeatures = complementRenderer.getDisplayedFeatureTypes();
388 if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
393 AlignmentI alignment = viewPanel.getAlignment();
394 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
396 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
398 if (mapping == null || mapping.length < 1)
403 for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
405 for (int m = 0; m < mapping.length; m++)
407 final SequenceI seq = seqs[pdbfnum][seqNo];
408 int sp = alignment.findIndex(seq);
409 StructureMapping structureMapping = mapping[m];
410 if (structureMapping.getSequence() == seq && sp > -1)
413 * found a sequence with a mapping to a structure;
414 * now scan its features
416 if (!visibleFeatures.isEmpty())
418 scanSequenceFeatures(visibleFeatures, structureMapping, seq,
421 if (showLinkedFeatures)
423 scanComplementFeatures(complementRenderer, structureMapping,
424 seq, theMap, pdbfnum);
434 * Scans visible features in mapped positions of the CDS/peptide complement, and
435 * adds any found to the map of attribute values/structure positions
437 * @param complementRenderer
438 * @param structureMapping
443 protected static void scanComplementFeatures(
444 FeatureRenderer complementRenderer,
445 StructureMapping structureMapping, SequenceI seq,
446 Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
449 * for each sequence residue mapped to a structure position...
451 for (int seqPos : structureMapping.getMapping().keySet())
454 * find visible complementary features at mapped position(s)
456 MappedFeatures mf = complementRenderer
457 .findComplementFeaturesAtResidue(seq, seqPos);
460 for (SequenceFeature sf : mf.features)
462 String type = sf.getType();
465 * Don't copy features which originated from Chimera
467 if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
468 .equals(sf.getFeatureGroup()))
474 * record feature 'value' (score/description/type) as at the
475 * corresponding structure position
477 List<int[]> mappedRanges = structureMapping
478 .getPDBResNumRanges(seqPos, seqPos);
480 if (!mappedRanges.isEmpty())
482 String value = sf.getDescription();
483 if (value == null || value.length() == 0)
487 float score = sf.getScore();
488 if (score != 0f && !Float.isNaN(score))
490 value = Float.toString(score);
492 Map<Object, AtomSpecModel> featureValues = theMap.get(type);
493 if (featureValues == null)
495 featureValues = new HashMap<>();
496 theMap.put(type, featureValues);
498 for (int[] range : mappedRanges)
500 addAtomSpecRange(featureValues, value, modelNumber, range[0],
501 range[1], structureMapping.getChain());
510 * Inspect features on the sequence; for each feature that is visible, determine
511 * its mapped ranges in the structure (if any) according to the given mapping,
512 * and add them to the map.
514 * @param visibleFeatures
520 protected static void scanSequenceFeatures(List<String> visibleFeatures,
521 StructureMapping mapping, SequenceI seq,
522 Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
524 List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
525 visibleFeatures.toArray(new String[visibleFeatures.size()]));
526 for (SequenceFeature sf : sfs)
528 String type = sf.getType();
531 * Don't copy features which originated from Chimera
533 if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
534 .equals(sf.getFeatureGroup()))
539 List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
542 if (!mappedRanges.isEmpty())
544 String value = sf.getDescription();
545 if (value == null || value.length() == 0)
549 float score = sf.getScore();
550 if (score != 0f && !Float.isNaN(score))
552 value = Float.toString(score);
554 Map<Object, AtomSpecModel> featureValues = theMap.get(type);
555 if (featureValues == null)
557 featureValues = new HashMap<>();
558 theMap.put(type, featureValues);
560 for (int[] range : mappedRanges)
562 addAtomSpecRange(featureValues, value, modelNumber, range[0],
563 range[1], mapping.getChain());
570 * Traverse the map of features/values/models/chains/positions to construct a
571 * list of 'setattr' commands (one per distinct feature type and value).
573 * The format of each command is
576 * <blockquote> setattr r <featureName> " " #modelnumber:range.chain
577 * e.g. setattr r jv:chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
584 protected static List<String> buildSetAttributeCommands(
585 Map<String, Map<Object, AtomSpecModel>> featureMap)
587 List<String> commands = new ArrayList<>();
588 for (String featureType : featureMap.keySet())
590 String attributeName = makeAttributeName(featureType);
593 * clear down existing attributes for this feature
595 // 'problem' - sets attribute to None on all residues - overkill?
596 // commands.add("~setattr r " + attributeName + " :*");
598 Map<Object, AtomSpecModel> values = featureMap.get(featureType);
599 for (Object value : values.keySet())
602 * for each distinct value recorded for this feature type,
603 * add a command to set the attribute on the mapped residues
604 * Put values in single quotes, encoding any embedded single quotes
606 StringBuilder sb = new StringBuilder(128);
607 String featureValue = value.toString();
608 featureValue = featureValue.replaceAll("\\'", "'");
609 sb.append("setattr r ").append(attributeName).append(" '")
610 .append(featureValue).append("' ");
611 sb.append(values.get(value).getAtomSpec());
612 commands.add(sb.toString());
620 * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
621 * for a 'Jalview' namespace, and any non-alphanumeric character is converted
628 * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
631 protected static String makeAttributeName(String featureType)
633 StringBuilder sb = new StringBuilder();
634 if (featureType != null)
636 for (char c : featureType.toCharArray())
638 sb.append(Character.isLetterOrDigit(c) ? c : '_');
641 String attName = NAMESPACE_PREFIX + sb.toString();
644 * Chimera treats an attribute name ending in 'color' as colour-valued;
645 * Jalview doesn't, so prevent this by appending an underscore
647 if (attName.toUpperCase().endsWith("COLOR"))