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.structures.models.AAStructureBindingModel;
36 import jalview.util.ColorUtils;
37 import jalview.util.Comparison;
38 import jalview.util.IntRangeComparator;
40 import java.awt.Color;
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.HashMap;
44 import java.util.Iterator;
45 import java.util.LinkedHashMap;
46 import java.util.List;
50 * Routines for generating Chimera commands for Jalview/Chimera binding
55 public class ChimeraCommands
57 public static final String NAMESPACE_PREFIX = "jv_";
60 * colour for residues shown in structure but hidden in alignment
62 private static final String COLOR_GRAY_HEX = "color "
63 + ColorUtils.toTkCode(Color.GRAY);
66 * Constructs Chimera commands to colour residues as per the Jalview alignment
73 public static StructureMappingcommandSet[] getColourBySequenceCommand(
74 String[] files, AlignmentViewPanel viewPanel,
75 AAStructureBindingModel binding)
77 StructureSelectionManager ssm = binding.getSsm();
78 SequenceRenderer sr = binding.getSequenceRenderer(viewPanel);
79 SequenceI[][] sequence = binding.getSequence();
80 boolean hideHiddenRegions = binding.isShowAlignmentOnly()
81 && binding.isHideHiddenRegions();
83 Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, files,
84 sequence, sr, hideHiddenRegions, viewPanel);
86 List<String> colourCommands = buildColourCommands(colourMap, binding);
88 StructureMappingcommandSet cs = new StructureMappingcommandSet(
89 ChimeraCommands.class, null,
90 colourCommands.toArray(new String[colourCommands.size()]));
92 return new StructureMappingcommandSet[] { cs };
96 * Traverse the map of colours/models/chains/positions to construct a list of
97 * 'color' commands (one per distinct colour used). The format of each command
102 * color colorname #modelnumber:range.chain
103 * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
111 protected static List<String> buildColourCommands(
112 Map<Object, AtomSpecModel> colourMap,
113 AAStructureBindingModel binding)
116 * This version concatenates all commands into a single String (semi-colon
117 * delimited). If length limit issues arise, refactor to return one color
118 * command per colour.
120 List<String> commands = new ArrayList<>();
121 StringBuilder sb = new StringBuilder(256);
122 sb.append(COLOR_GRAY_HEX);
124 for (Object key : colourMap.keySet())
126 Color colour = (Color) key;
127 String colourCode = ColorUtils.toTkCode(colour);
129 sb.append("color ").append(colourCode).append(" ");
130 final AtomSpecModel colourData = colourMap.get(colour);
131 sb.append(getAtomSpec(colourData, binding));
133 commands.add(sb.toString());
138 * Build a data structure which records contiguous subsequences for each colour.
139 * From this we can easily generate the Chimera command for colour by sequence.
145 * list of start/end ranges
148 * Ordering is by order of addition (for colours and positions), natural
149 * ordering (for models and chains)
155 * @param hideHiddenRegions
159 protected static Map<Object, AtomSpecModel> buildColoursMap(
160 StructureSelectionManager ssm, String[] files,
161 SequenceI[][] sequence, SequenceRenderer sr,
162 boolean hideHiddenRegions, AlignmentViewPanel viewPanel)
164 FeatureRenderer fr = viewPanel.getFeatureRenderer();
165 FeatureColourFinder finder = new FeatureColourFinder(fr);
166 AlignViewportI viewport = viewPanel.getAlignViewport();
167 HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
168 AlignmentI al = viewport.getAlignment();
169 Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
170 Color lastColour = null;
172 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
174 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
176 if (mapping == null || mapping.length < 1)
181 int startPos = -1, lastPos = -1;
182 String lastChain = "";
183 for (int s = 0; s < sequence[pdbfnum].length; s++)
185 for (int sp, m = 0; m < mapping.length; m++)
187 final SequenceI seq = sequence[pdbfnum][s];
188 if (mapping[m].getSequence() == seq
189 && (sp = al.findIndex(seq)) > -1)
191 SequenceI asp = al.getSequenceAt(sp);
192 for (int r = 0; r < asp.getLength(); r++)
194 // no mapping to gaps in sequence
195 if (Comparison.isGap(asp.getCharAt(r)))
199 int pos = mapping[m].getPDBResNum(asp.findPosition(r));
201 if (pos < 1 || pos == lastPos)
206 Color colour = sr.getResidueColour(seq, r, finder);
209 * hidden regions are shown gray or, optionally, ignored
211 if (!cs.isVisible(r))
213 if (hideHiddenRegions)
223 final String chain = mapping[m].getChain();
226 * Just keep incrementing the end position for this colour range
227 * _unless_ colour, PDB model or chain has changed, or there is a
228 * gap in the mapped residue sequence
230 final boolean newColour = !colour.equals(lastColour);
231 final boolean nonContig = lastPos + 1 != pos;
232 final boolean newChain = !chain.equals(lastChain);
233 if (newColour || nonContig || newChain)
237 addColourRange(colourMap, lastColour, pdbfnum, startPos,
246 // final colour range
247 if (lastColour != null)
249 addColourRange(colourMap, lastColour, pdbfnum, startPos,
261 * Helper method to add one contiguous colour range to the colour map.
270 protected static void addColourRange(Map<Object, AtomSpecModel> map,
271 Object key, int model, int startPos, int endPos, String chain)
274 * Get/initialize map of data for the colour
276 AtomSpecModel atomSpec = map.get(key);
277 if (atomSpec == null)
279 atomSpec = new AtomSpecModel();
280 map.put(key, atomSpec);
283 atomSpec.addRange(model, startPos, endPos, chain);
287 * Constructs and returns Chimera commands to set attributes on residues
288 * corresponding to features in Jalview. Attribute names are the Jalview feature
289 * type, with a "jv_" prefix.
298 public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
299 AlignmentViewPanel viewPanel, AAStructureBindingModel binding)
301 StructureSelectionManager ssm = binding.getSsm();
302 String[] files = binding.getStructureFiles();
303 SequenceI[][] seqs = binding.getSequence();
305 Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
306 ssm, files, seqs, viewPanel);
308 List<String> commands = buildSetAttributeCommands(featureMap, binding);
310 StructureMappingcommandSet cs = new StructureMappingcommandSet(
311 ChimeraCommands.class, null,
312 commands.toArray(new String[commands.size()]));
319 * Helper method to build a map of
320 * { featureType, { feature value, AtomSpecModel } }
329 protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
330 StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
331 AlignmentViewPanel viewPanel)
333 Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
335 FeatureRenderer fr = viewPanel.getFeatureRenderer();
341 List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
342 if (visibleFeatures.isEmpty())
347 AlignmentI alignment = viewPanel.getAlignment();
348 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
350 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
352 if (mapping == null || mapping.length < 1)
357 for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
359 for (int m = 0; m < mapping.length; m++)
361 final SequenceI seq = seqs[pdbfnum][seqNo];
362 int sp = alignment.findIndex(seq);
363 if (mapping[m].getSequence() == seq && sp > -1)
366 * found a sequence with a mapping to a structure;
367 * now scan its features
369 SequenceI asp = alignment.getSequenceAt(sp);
371 scanSequenceFeatures(visibleFeatures, mapping[m], asp, theMap,
381 * Inspect features on the sequence; for each feature that is visible,
382 * determine its mapped ranges in the structure (if any) according to the
383 * given mapping, and add them to the map
385 * @param visibleFeatures
391 protected static void scanSequenceFeatures(List<String> visibleFeatures,
392 StructureMapping mapping, SequenceI seq,
393 Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
395 List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
396 visibleFeatures.toArray(new String[visibleFeatures.size()]));
397 for (SequenceFeature sf : sfs)
399 String type = sf.getType();
402 * Only copy visible features, don't copy any which originated
403 * from Chimera, and suppress uninteresting ones (e.g. RESNUM)
405 boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
406 .equals(sf.getFeatureGroup());
411 List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
414 if (!mappedRanges.isEmpty())
416 String value = sf.getDescription();
417 if (value == null || value.length() == 0)
421 float score = sf.getScore();
422 if (score != 0f && !Float.isNaN(score))
424 value = Float.toString(score);
426 Map<Object, AtomSpecModel> featureValues = theMap.get(type);
427 if (featureValues == null)
429 featureValues = new HashMap<>();
430 theMap.put(type, featureValues);
432 for (int[] range : mappedRanges)
434 addColourRange(featureValues, value, modelNumber, range[0],
435 range[1], mapping.getChain());
442 * Traverse the map of features/values/models/chains/positions to construct a
443 * list of 'setattr' commands (one per distinct feature type and value).
445 * The format of each command is
448 * <blockquote> setattr r <featureName> " " #modelnumber:range.chain
449 * e.g. setattr r jv:chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
457 protected static List<String> buildSetAttributeCommands(
458 Map<String, Map<Object, AtomSpecModel>> featureMap,
459 AAStructureBindingModel binding)
461 List<String> commands = new ArrayList<>();
462 for (String featureType : featureMap.keySet())
464 String attributeName = makeAttributeName(featureType);
467 * clear down existing attributes for this feature
469 // 'problem' - sets attribute to None on all residues - overkill?
470 // commands.add("~setattr r " + attributeName + " :*");
472 Map<Object, AtomSpecModel> values = featureMap.get(featureType);
473 for (Object value : values.keySet())
476 * for each distinct value recorded for this feature type,
477 * add a command to set the attribute on the mapped residues
478 * Put values in single quotes, encoding any embedded single quotes
480 StringBuilder sb = new StringBuilder(128);
481 String featureValue = value.toString();
482 featureValue = featureValue.replaceAll("\\'", "'");
483 sb.append("setattr r ").append(attributeName).append(" '")
484 .append(featureValue).append("' ");
485 sb.append(getAtomSpec(values.get(value), binding));
486 commands.add(sb.toString());
494 * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
495 * for a 'Jalview' namespace, and any non-alphanumeric character is converted
502 * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
505 protected static String makeAttributeName(String featureType)
507 StringBuilder sb = new StringBuilder();
508 if (featureType != null)
510 for (char c : featureType.toCharArray())
512 sb.append(Character.isLetterOrDigit(c) ? c : '_');
515 String attName = NAMESPACE_PREFIX + sb.toString();
518 * Chimera treats an attribute name ending in 'color' as colour-valued;
519 * Jalview doesn't, so prevent this by appending an underscore
521 if (attName.toUpperCase().endsWith("COLOR"))
530 * Returns the range(s) formatted as a Chimera atomspec
534 public static String getAtomSpec(AtomSpecModel atomSpec,
535 AAStructureBindingModel binding)
537 StringBuilder sb = new StringBuilder(128);
538 boolean firstModel = true;
539 for (Integer model : atomSpec.getModels())
546 // todo use JalviewChimeraBinding.getModelSpec(model)
547 // which means this cannot be static
548 sb.append(binding.getModelSpec(model)).append(":");
550 boolean firstPositionForModel = true;
552 for (String chain : atomSpec.getChains(model))
554 chain = " ".equals(chain) ? chain : chain.trim();
556 List<int[]> rangeList = atomSpec.getRanges(model, chain);
559 * sort ranges into ascending start position order
561 Collections.sort(rangeList, IntRangeComparator.ASCENDING);
563 int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
564 int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
566 Iterator<int[]> iterator = rangeList.iterator();
567 while (iterator.hasNext())
569 int[] range = iterator.next();
570 if (range[0] <= end + 1)
573 * range overlaps or is contiguous with the last one
574 * - so just extend the end position, and carry on
575 * (unless this is the last in the list)
577 end = Math.max(end, range[1]);
582 * we have a break so append the last range
584 appendRange(sb, start, end, chain, firstPositionForModel);
585 firstPositionForModel = false;
592 * and append the last range
594 if (!rangeList.isEmpty())
596 appendRange(sb, start, end, chain, firstPositionForModel);
597 firstPositionForModel = false;
601 return sb.toString();
605 * A helper method that appends one start-end range to a Chimera atomspec
611 * @param firstPositionForModel
613 static void appendRange(StringBuilder sb, int start, int end,
614 String chain, boolean firstPositionForModel)
616 if (!firstPositionForModel)
626 sb.append(start).append("-").append(end);
630 if (!" ".equals(chain))