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.datamodel.AlignmentI;
27 import jalview.datamodel.MappedFeatures;
28 import jalview.datamodel.SequenceFeature;
29 import jalview.datamodel.SequenceI;
30 import jalview.gui.Desktop;
31 import jalview.structure.AtomSpecModel;
32 import jalview.structure.StructureCommandsBase;
33 import jalview.structure.StructureMapping;
34 import jalview.structure.StructureSelectionManager;
35 import jalview.util.ColorUtils;
36 import jalview.util.IntRangeComparator;
38 import java.awt.Color;
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.HashMap;
42 import java.util.Iterator;
43 import java.util.LinkedHashMap;
44 import java.util.List;
48 * Routines for generating Chimera commands for Jalview/Chimera binding
53 public class ChimeraCommands extends StructureCommandsBase
55 public static final String NAMESPACE_PREFIX = "jv_";
57 private static final String CMD_COLOUR_BY_CHARGE = "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS";
59 private static final String CMD_COLOUR_BY_CHAIN = "rainbow chain";
61 // Chimera clause to exclude alternate locations in atom selection
62 private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
65 public String[] colourBySequence(Map<Object, AtomSpecModel> colourMap)
67 List<String> colourCommands = buildColourCommands(colourMap);
69 return colourCommands.toArray(new String[colourCommands.size()]);
73 public String getColourCommand(String atomSpec, Color colour)
75 // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/color.html
76 String colourCode = getColourString(colour);
77 return "color " + colourCode + " " + atomSpec;
81 * Returns a colour formatted suitable for use in viewer command syntax
87 protected String getColourString(Color colour)
89 return ColorUtils.toTkCode(colour);
93 * Constructs and returns Chimera commands to set attributes on residues
94 * corresponding to features in Jalview. Attribute names are the Jalview feature
95 * type, with a "jv_" prefix.
104 public String[] setAttributesForFeatures(
105 StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
106 AlignmentViewPanel viewPanel)
108 Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
109 ssm, files, seqs, viewPanel);
111 List<String> commands = buildSetAttributeCommands(featureMap);
113 return commands.toArray(new String[commands.size()]);
118 * Helper method to build a map of
119 * { featureType, { feature value, AtomSpecModel } }
128 protected Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
129 StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
130 AlignmentViewPanel viewPanel)
132 Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
134 FeatureRenderer fr = viewPanel.getFeatureRenderer();
140 AlignViewportI viewport = viewPanel.getAlignViewport();
141 List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
144 * if alignment is showing features from complement, we also transfer
145 * these features to the corresponding mapped structure residues
147 boolean showLinkedFeatures = viewport.isShowComplementFeatures();
148 List<String> complementFeatures = new ArrayList<>();
149 FeatureRenderer complementRenderer = null;
150 if (showLinkedFeatures)
152 AlignViewportI comp = fr.getViewport().getCodingComplement();
155 complementRenderer = Desktop.getAlignFrameFor(comp)
156 .getFeatureRenderer();
157 complementFeatures = complementRenderer.getDisplayedFeatureTypes();
160 if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
165 AlignmentI alignment = viewPanel.getAlignment();
166 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
168 final int modelNumber = pdbfnum + getModelStartNo();
169 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
171 if (mapping == null || mapping.length < 1)
176 for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
178 for (int m = 0; m < mapping.length; m++)
180 final SequenceI seq = seqs[pdbfnum][seqNo];
181 int sp = alignment.findIndex(seq);
182 StructureMapping structureMapping = mapping[m];
183 if (structureMapping.getSequence() == seq && sp > -1)
186 * found a sequence with a mapping to a structure;
187 * now scan its features
189 if (!visibleFeatures.isEmpty())
191 scanSequenceFeatures(visibleFeatures, structureMapping, seq,
192 theMap, modelNumber);
194 if (showLinkedFeatures)
196 scanComplementFeatures(complementRenderer, structureMapping,
197 seq, theMap, modelNumber);
207 * Scans visible features in mapped positions of the CDS/peptide complement, and
208 * adds any found to the map of attribute values/structure positions
210 * @param complementRenderer
211 * @param structureMapping
216 protected static void scanComplementFeatures(
217 FeatureRenderer complementRenderer,
218 StructureMapping structureMapping, SequenceI seq,
219 Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
222 * for each sequence residue mapped to a structure position...
224 for (int seqPos : structureMapping.getMapping().keySet())
227 * find visible complementary features at mapped position(s)
229 MappedFeatures mf = complementRenderer
230 .findComplementFeaturesAtResidue(seq, seqPos);
233 for (SequenceFeature sf : mf.features)
235 String type = sf.getType();
238 * Don't copy features which originated from Chimera
240 if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
241 .equals(sf.getFeatureGroup()))
247 * record feature 'value' (score/description/type) as at the
248 * corresponding structure position
250 List<int[]> mappedRanges = structureMapping
251 .getPDBResNumRanges(seqPos, seqPos);
253 if (!mappedRanges.isEmpty())
255 String value = sf.getDescription();
256 if (value == null || value.length() == 0)
260 float score = sf.getScore();
261 if (score != 0f && !Float.isNaN(score))
263 value = Float.toString(score);
265 Map<Object, AtomSpecModel> featureValues = theMap.get(type);
266 if (featureValues == null)
268 featureValues = new HashMap<>();
269 theMap.put(type, featureValues);
271 for (int[] range : mappedRanges)
273 addAtomSpecRange(featureValues, value, modelNumber, range[0],
274 range[1], structureMapping.getChain());
283 * Inspect features on the sequence; for each feature that is visible, determine
284 * its mapped ranges in the structure (if any) according to the given mapping,
285 * and add them to the map.
287 * @param visibleFeatures
293 protected static void scanSequenceFeatures(List<String> visibleFeatures,
294 StructureMapping mapping, SequenceI seq,
295 Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
297 List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
298 visibleFeatures.toArray(new String[visibleFeatures.size()]));
299 for (SequenceFeature sf : sfs)
301 String type = sf.getType();
304 * Don't copy features which originated from Chimera
306 if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
307 .equals(sf.getFeatureGroup()))
312 List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
315 if (!mappedRanges.isEmpty())
317 String value = sf.getDescription();
318 if (value == null || value.length() == 0)
322 float score = sf.getScore();
323 if (score != 0f && !Float.isNaN(score))
325 value = Float.toString(score);
327 Map<Object, AtomSpecModel> featureValues = theMap.get(type);
328 if (featureValues == null)
330 featureValues = new HashMap<>();
331 theMap.put(type, featureValues);
333 for (int[] range : mappedRanges)
335 addAtomSpecRange(featureValues, value, modelNumber, range[0],
336 range[1], mapping.getChain());
343 * Traverse the map of features/values/models/chains/positions to construct a
344 * list of 'setattr' commands (one per distinct feature type and value).
346 * The format of each command is
349 * <blockquote> setattr r <featureName> " " #modelnumber:range.chain
350 * e.g. setattr r jv_chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
357 protected List<String> buildSetAttributeCommands(
358 Map<String, Map<Object, AtomSpecModel>> featureMap)
360 List<String> commands = new ArrayList<>();
361 for (String featureType : featureMap.keySet())
363 String attributeName = makeAttributeName(featureType);
366 * clear down existing attributes for this feature
368 // 'problem' - sets attribute to None on all residues - overkill?
369 // commands.add("~setattr r " + attributeName + " :*");
371 Map<Object, AtomSpecModel> values = featureMap.get(featureType);
372 for (Object value : values.keySet())
375 * for each distinct value recorded for this feature type,
376 * add a command to set the attribute on the mapped residues
377 * Put values in single quotes, encoding any embedded single quotes
379 AtomSpecModel atomSpecModel = values.get(value);
380 String featureValue = value.toString();
381 featureValue = featureValue.replaceAll("\\'", "'");
382 String cmd = getSetAttributeCommand(attributeName, featureValue,
392 * Returns a viewer command to set the given residue attribute value on
393 * residues specified by the AtomSpecModel, for example
396 * setatr res jv_chain 'primary' #1:12-34,48-55.B
399 * @param attributeName
400 * @param attributeValue
401 * @param atomSpecModel
404 protected String getSetAttributeCommand(String attributeName,
405 String attributeValue,
406 AtomSpecModel atomSpecModel)
408 StringBuilder sb = new StringBuilder(128);
409 sb.append("setattr res ").append(attributeName).append(" '")
410 .append(attributeValue).append("' ");
411 sb.append(getAtomSpec(atomSpecModel, false));
412 return sb.toString();
416 * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
417 * for a 'Jalview' namespace, and any non-alphanumeric character is converted
422 * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
424 protected static String makeAttributeName(String featureType)
426 StringBuilder sb = new StringBuilder();
427 if (featureType != null)
429 for (char c : featureType.toCharArray())
431 sb.append(Character.isLetterOrDigit(c) ? c : '_');
434 String attName = NAMESPACE_PREFIX + sb.toString();
437 * Chimera treats an attribute name ending in 'color' as colour-valued;
438 * Jalview doesn't, so prevent this by appending an underscore
440 if (attName.toUpperCase().endsWith("COLOR"))
449 public String colourByChain()
451 return CMD_COLOUR_BY_CHAIN;
455 public String colourByCharge()
457 return CMD_COLOUR_BY_CHARGE;
461 public String getResidueSpec(String residue)
463 return "::" + residue;
467 public String setBackgroundColour(Color col)
469 // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/set.html#bgcolor
470 return "set bgColor " + ColorUtils.toTkCode(col);
474 public String focusView()
476 // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/focus.html
481 public String showChains(List<String> toShow)
484 * Construct a chimera command like
486 * ~display #*;~ribbon #*;ribbon :.A,:.B
488 StringBuilder cmd = new StringBuilder(64);
489 boolean first = true;
490 for (String chain : toShow)
492 String[] tokens = chain.split(":");
493 if (tokens.length == 2)
495 String showChainCmd = tokens[0] + ":." + tokens[1];
500 cmd.append(showChainCmd);
506 * could append ";focus" to this command to resize the display to fill the
507 * window, but it looks more helpful not to (easier to relate chains to the
510 final String command = "~display #*; ~ribbon #*; ribbon :"
516 public String superposeStructures(AtomSpecModel spec, AtomSpecModel ref)
519 * Form Chimera match command to match spec to ref
521 * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA
524 * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
526 StringBuilder cmd = new StringBuilder();
527 String atomSpec = getAtomSpec(spec, true);
528 String refSpec = getAtomSpec(ref, true);
529 cmd.append("match ").append(atomSpec).append(" ").append(refSpec);
532 * show superposed residues as ribbon, others as chain
534 // fixme this should precede the loop over all alignments/structures
535 cmd.append(";~display all; chain @CA|P");
536 cmd.append("; ribbon ");
537 cmd.append(atomSpec).append("|").append(refSpec).append("; focus");
539 return cmd.toString();
543 public String openCommandFile(String path)
545 // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html
546 return "open cmd:" + path;
550 public String saveSession(String filepath)
552 // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html
553 return "save " + filepath;
557 * Returns the range(s) modelled by {@code atomSpec} formatted as a Chimera
558 * atomspec string, e.g.
561 * #0:15.A,28.A,54.A,70-72.A|#1:2.A,6.A,11.A,13-14.A
566 * <li>#0 is a model number</li>
567 * <li>15 or 70-72 is a residue number, or range of residue numbers</li>
568 * <li>.A is a chain identifier</li>
569 * <li>residue ranges are separated by comma</li>
570 * <li>atomspecs for distinct models are separated by | (or)</li>
578 * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
581 public String getAtomSpec(AtomSpecModel atomSpec, boolean alphaOnly)
583 StringBuilder sb = new StringBuilder(128);
584 boolean firstModel = true;
585 for (Integer model : atomSpec.getModels())
592 appendModel(sb, model, atomSpec, alphaOnly);
594 return sb.toString();
598 * A helper method to append an atomSpec string for atoms in the given model
605 protected void appendModel(StringBuilder sb, Integer model,
606 AtomSpecModel atomSpec, boolean alphaOnly)
608 sb.append("#").append(model).append(":");
610 boolean firstPositionForModel = true;
612 for (String chain : atomSpec.getChains(model))
614 chain = " ".equals(chain) ? chain : chain.trim();
616 List<int[]> rangeList = atomSpec.getRanges(model, chain);
619 * sort ranges into ascending start position order
621 Collections.sort(rangeList, IntRangeComparator.ASCENDING);
623 int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
624 int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
626 Iterator<int[]> iterator = rangeList.iterator();
627 while (iterator.hasNext())
629 int[] range = iterator.next();
630 if (range[0] <= end + 1)
633 * range overlaps or is contiguous with the last one
634 * - so just extend the end position, and carry on
635 * (unless this is the last in the list)
637 end = Math.max(end, range[1]);
642 * we have a break so append the last range
644 appendRange(sb, start, end, chain, firstPositionForModel, false);
645 firstPositionForModel = false;
652 * and append the last range
654 if (!rangeList.isEmpty())
656 appendRange(sb, start, end, chain, firstPositionForModel, false);
657 firstPositionForModel = false;
663 * restrict to alpha carbon, no alternative locations
664 * (needed to ensuring matching atom counts for superposition)
666 sb.append("@CA|P").append(NO_ALTLOCS);
671 public String showBackbone()
673 return "~display all;chain @CA|P";