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.SequenceFeature;
30 import jalview.datamodel.SequenceI;
31 import jalview.renderer.seqfeatures.FeatureColourFinder;
32 import jalview.structure.StructureMapping;
33 import jalview.structure.StructureMappingcommandSet;
34 import jalview.structure.StructureSelectionManager;
35 import jalview.util.ColorUtils;
36 import jalview.util.Comparison;
38 import java.awt.Color;
39 import java.util.ArrayList;
40 import java.util.HashMap;
41 import java.util.LinkedHashMap;
42 import java.util.List;
46 * Routines for generating Chimera commands for Jalview/Chimera binding
51 public class ChimeraCommands
53 public static final String NAMESPACE_PREFIX = "jv_";
55 private static final String COLOR_GRAY_HEX = "color "
56 + ColorUtils.toTkCode(Color.GRAY);
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 sb.append(COLOR_GRAY_HEX);
113 for (Object key : colourMap.keySet())
115 Color colour = (Color) key;
116 String colourCode = ColorUtils.toTkCode(colour);
118 sb.append("color ").append(colourCode).append(" ");
119 final AtomSpecModel colourData = colourMap.get(colour);
120 sb.append(colourData.getAtomSpec());
122 commands.add(sb.toString());
127 * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and
128 * builds a Chimera format atom spec
130 * @param modelAndChainRanges
132 protected static String getAtomSpec(
133 Map<Integer, Map<String, List<int[]>>> modelAndChainRanges)
135 StringBuilder sb = new StringBuilder(128);
136 boolean firstModelForColour = true;
137 for (Integer model : modelAndChainRanges.keySet())
139 boolean firstPositionForModel = true;
140 if (!firstModelForColour)
144 firstModelForColour = false;
145 sb.append("#").append(model).append(":");
147 final Map<String, List<int[]>> modelData = modelAndChainRanges
149 for (String chain : modelData.keySet())
151 boolean hasChain = !"".equals(chain.trim());
152 for (int[] range : modelData.get(chain))
154 if (!firstPositionForModel)
158 if (range[0] == range[1])
164 sb.append(range[0]).append("-").append(range[1]);
168 sb.append(".").append(chain);
170 firstPositionForModel = false;
174 return sb.toString();
179 * Build a data structure which records contiguous subsequences for each colour.
180 * From this we can easily generate the Chimera command for colour by sequence.
184 * list of start/end ranges
185 * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
188 protected static Map<Object, AtomSpecModel> buildColoursMap(
189 StructureSelectionManager ssm, String[] files,
190 SequenceI[][] sequence, SequenceRenderer sr,
191 AlignmentViewPanel viewPanel)
193 FeatureRenderer fr = viewPanel.getFeatureRenderer();
194 FeatureColourFinder finder = new FeatureColourFinder(fr);
195 AlignViewportI viewport = viewPanel.getAlignViewport();
196 HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
197 AlignmentI al = viewport.getAlignment();
198 Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
199 Color lastColour = null;
201 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
203 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
205 if (mapping == null || mapping.length < 1)
210 int startPos = -1, lastPos = -1;
211 String lastChain = "";
212 for (int s = 0; s < sequence[pdbfnum].length; s++)
214 for (int sp, m = 0; m < mapping.length; m++)
216 final SequenceI seq = sequence[pdbfnum][s];
217 if (mapping[m].getSequence() == seq
218 && (sp = al.findIndex(seq)) > -1)
220 SequenceI asp = al.getSequenceAt(sp);
221 for (int r = 0; r < asp.getLength(); r++)
223 // no mapping to gaps in sequence
224 if (Comparison.isGap(asp.getCharAt(r)))
228 int pos = mapping[m].getPDBResNum(asp.findPosition(r));
230 if (pos < 1 || pos == lastPos)
235 Color colour = sr.getResidueColour(seq, r, finder);
238 * hidden regions are shown gray
239 * todo: iterate over visible columns only
241 if (!cs.isVisible(r))
243 continue; // colour = Color.GRAY;
246 final String chain = mapping[m].getChain();
249 * Just keep incrementing the end position for this colour range
250 * _unless_ colour, PDB model or chain has changed, or there is a
251 * gap in the mapped residue sequence
253 final boolean newColour = !colour.equals(lastColour);
254 final boolean nonContig = lastPos + 1 != pos;
255 final boolean newChain = !chain.equals(lastChain);
256 if (newColour || nonContig || newChain)
260 addColourRange(colourMap, lastColour, pdbfnum, startPos,
269 // final colour range
270 if (lastColour != null)
272 addColourRange(colourMap, lastColour, pdbfnum, startPos,
284 * Helper method to add one contiguous colour range to the colour map.
293 protected static void addColourRange(Map<Object, AtomSpecModel> map,
294 Object key, int model, int startPos, int endPos, String chain)
297 * Get/initialize map of data for the colour
299 AtomSpecModel atomSpec = map.get(key);
300 if (atomSpec == null)
302 atomSpec = new AtomSpecModel();
303 map.put(key, atomSpec);
306 atomSpec.addRange(model, startPos, endPos, chain);
310 * Constructs and returns Chimera commands to set attributes on residues
311 * corresponding to features in Jalview. Attribute names are the Jalview
312 * feature type, with a "jv_" prefix.
320 public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
321 StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
322 AlignmentViewPanel viewPanel)
324 Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
325 ssm, files, seqs, viewPanel);
327 List<String> commands = buildSetAttributeCommands(featureMap);
329 StructureMappingcommandSet cs = new StructureMappingcommandSet(
330 ChimeraCommands.class, null,
331 commands.toArray(new String[commands.size()]));
338 * Helper method to build a map of
339 * { featureType, { feature value, AtomSpecModel } }
348 protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
349 StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
350 AlignmentViewPanel viewPanel)
352 Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
354 FeatureRenderer fr = viewPanel.getFeatureRenderer();
360 List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
361 if (visibleFeatures.isEmpty())
366 AlignmentI alignment = viewPanel.getAlignment();
367 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
369 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
371 if (mapping == null || mapping.length < 1)
376 for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
378 for (int m = 0; m < mapping.length; m++)
380 final SequenceI seq = seqs[pdbfnum][seqNo];
381 int sp = alignment.findIndex(seq);
382 if (mapping[m].getSequence() == seq && sp > -1)
385 * found a sequence with a mapping to a structure;
386 * now scan its features
388 SequenceI asp = alignment.getSequenceAt(sp);
390 scanSequenceFeatures(visibleFeatures, mapping[m], asp, theMap,
400 * Inspect features on the sequence; for each feature that is visible,
401 * determine its mapped ranges in the structure (if any) according to the
402 * given mapping, and add them to the map
404 * @param visibleFeatures
410 protected static void scanSequenceFeatures(List<String> visibleFeatures,
411 StructureMapping mapping, SequenceI seq,
412 Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
414 List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
415 visibleFeatures.toArray(new String[visibleFeatures.size()]));
416 for (SequenceFeature sf : sfs)
418 String type = sf.getType();
421 * Only copy visible features, don't copy any which originated
422 * from Chimera, and suppress uninteresting ones (e.g. RESNUM)
424 boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
425 .equals(sf.getFeatureGroup());
430 List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
433 if (!mappedRanges.isEmpty())
435 String value = sf.getDescription();
436 if (value == null || value.length() == 0)
440 float score = sf.getScore();
441 if (score != 0f && !Float.isNaN(score))
443 value = Float.toString(score);
445 Map<Object, AtomSpecModel> featureValues = theMap.get(type);
446 if (featureValues == null)
448 featureValues = new HashMap<>();
449 theMap.put(type, featureValues);
451 for (int[] range : mappedRanges)
453 addColourRange(featureValues, value, modelNumber, range[0],
454 range[1], mapping.getChain());
461 * Traverse the map of features/values/models/chains/positions to construct a
462 * list of 'setattr' commands (one per distinct feature type and value).
464 * The format of each command is
467 * <blockquote> setattr r <featureName> " " #modelnumber:range.chain
468 * e.g. setattr r jv:chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
475 protected static List<String> buildSetAttributeCommands(
476 Map<String, Map<Object, AtomSpecModel>> featureMap)
478 List<String> commands = new ArrayList<>();
479 for (String featureType : featureMap.keySet())
481 String attributeName = makeAttributeName(featureType);
484 * clear down existing attributes for this feature
486 // 'problem' - sets attribute to None on all residues - overkill?
487 // commands.add("~setattr r " + attributeName + " :*");
489 Map<Object, AtomSpecModel> values = featureMap.get(featureType);
490 for (Object value : values.keySet())
493 * for each distinct value recorded for this feature type,
494 * add a command to set the attribute on the mapped residues
495 * Put values in single quotes, encoding any embedded single quotes
497 StringBuilder sb = new StringBuilder(128);
498 String featureValue = value.toString();
499 featureValue = featureValue.replaceAll("\\'", "'");
500 sb.append("setattr r ").append(attributeName).append(" '")
501 .append(featureValue).append("' ");
502 sb.append(values.get(value).getAtomSpec());
503 commands.add(sb.toString());
511 * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
512 * for a 'Jalview' namespace, and any non-alphanumeric character is converted
519 * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
522 protected static String makeAttributeName(String featureType)
524 StringBuilder sb = new StringBuilder();
525 if (featureType != null)
527 for (char c : featureType.toCharArray())
529 sb.append(Character.isLetterOrDigit(c) ? c : '_');
532 String attName = NAMESPACE_PREFIX + sb.toString();
535 * Chimera treats an attribute name ending in 'color' as colour-valued;
536 * Jalview doesn't, so prevent this by appending an underscore
538 if (attName.toUpperCase().endsWith("COLOR"))