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
72 public static String[] getColourBySequenceCommand(
73 Map<Object, AtomSpecModel> colourMap,
74 AAStructureBindingModel binding)
76 List<String> colourCommands = buildColourCommands(colourMap, binding);
78 return colourCommands.toArray(new String[colourCommands.size()]);
82 * Traverse the map of colours/models/chains/positions to construct a list of
83 * 'color' commands (one per distinct colour used). The format of each command
88 * color colorname #modelnumber:range.chain
89 * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
97 protected static List<String> buildColourCommands(
98 Map<Object, AtomSpecModel> colourMap,
99 AAStructureBindingModel binding)
102 * This version concatenates all commands into a single String (semi-colon
103 * delimited). If length limit issues arise, refactor to return one color
104 * command per colour.
106 List<String> commands = new ArrayList<>();
107 StringBuilder sb = new StringBuilder(256);
108 sb.append(COLOR_GRAY_HEX);
110 for (Object key : colourMap.keySet())
112 Color colour = (Color) key;
113 String colourCode = ColorUtils.toTkCode(colour);
115 sb.append("color ").append(colourCode).append(" ");
116 final AtomSpecModel colourData = colourMap.get(colour);
117 sb.append(getAtomSpec(colourData, binding));
119 commands.add(sb.toString());
124 * Build a data structure which records contiguous subsequences for each colour.
125 * From this we can easily generate the Chimera command for colour by sequence.
131 * list of start/end ranges
134 * Ordering is by order of addition (for colours and positions), natural
135 * ordering (for models and chains)
141 * @param hideHiddenRegions
145 protected static Map<Object, AtomSpecModel> buildColoursMap(
146 StructureSelectionManager ssm, String[] files,
147 SequenceI[][] sequence, SequenceRenderer sr,
148 boolean hideHiddenRegions, AlignmentViewPanel viewPanel)
150 FeatureRenderer fr = viewPanel.getFeatureRenderer();
151 FeatureColourFinder finder = new FeatureColourFinder(fr);
152 AlignViewportI viewport = viewPanel.getAlignViewport();
153 HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
154 AlignmentI al = viewport.getAlignment();
155 Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
156 Color lastColour = null;
158 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
160 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
162 if (mapping == null || mapping.length < 1)
167 int startPos = -1, lastPos = -1;
168 String lastChain = "";
169 for (int s = 0; s < sequence[pdbfnum].length; s++)
171 for (int sp, m = 0; m < mapping.length; m++)
173 final SequenceI seq = sequence[pdbfnum][s];
174 if (mapping[m].getSequence() == seq
175 && (sp = al.findIndex(seq)) > -1)
177 SequenceI asp = al.getSequenceAt(sp);
178 for (int r = 0; r < asp.getLength(); r++)
180 // no mapping to gaps in sequence
181 if (Comparison.isGap(asp.getCharAt(r)))
185 int pos = mapping[m].getPDBResNum(asp.findPosition(r));
187 if (pos < 1 || pos == lastPos)
192 Color colour = sr.getResidueColour(seq, r, finder);
195 * hidden regions are shown gray or, optionally, ignored
197 if (!cs.isVisible(r))
199 if (hideHiddenRegions)
209 final String chain = mapping[m].getChain();
212 * Just keep incrementing the end position for this colour range
213 * _unless_ colour, PDB model or chain has changed, or there is a
214 * gap in the mapped residue sequence
216 final boolean newColour = !colour.equals(lastColour);
217 final boolean nonContig = lastPos + 1 != pos;
218 final boolean newChain = !chain.equals(lastChain);
219 if (newColour || nonContig || newChain)
223 addMapRange(colourMap, lastColour, pdbfnum, startPos,
232 // final colour range
233 if (lastColour != null)
235 addMapRange(colourMap, lastColour, pdbfnum, startPos,
247 * Helper method to add one contiguous range to the map, for a given value key
248 * (e.g. colour or feature type), structure model number, and chain
257 public static void addMapRange(Map<Object, AtomSpecModel> map,
258 Object key, int model, int startPos, int endPos, String chain)
261 * Get/initialize map of data for the colour
263 AtomSpecModel atomSpec = map.get(key);
264 if (atomSpec == null)
266 atomSpec = new AtomSpecModel();
267 map.put(key, atomSpec);
270 atomSpec.addRange(model, startPos, endPos, chain);
274 * Constructs and returns Chimera commands to set attributes on residues
275 * corresponding to features in Jalview. Attribute names are the Jalview feature
276 * type, with a "jv_" prefix.
285 public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
286 AlignmentViewPanel viewPanel, AAStructureBindingModel binding)
288 StructureSelectionManager ssm = binding.getSsm();
289 String[] files = binding.getStructureFiles();
290 SequenceI[][] seqs = binding.getSequence();
292 Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
293 ssm, files, seqs, viewPanel);
295 List<String> commands = buildSetAttributeCommands(featureMap, binding);
297 StructureMappingcommandSet cs = new StructureMappingcommandSet(
298 ChimeraCommands.class, null,
299 commands.toArray(new String[commands.size()]));
306 * Helper method to build a map of
307 * { featureType, { feature value, AtomSpecModel } }
316 protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
317 StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
318 AlignmentViewPanel viewPanel)
320 Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
322 FeatureRenderer fr = viewPanel.getFeatureRenderer();
328 List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
329 if (visibleFeatures.isEmpty())
334 AlignmentI alignment = viewPanel.getAlignment();
335 for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
337 StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
339 if (mapping == null || mapping.length < 1)
344 for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
346 for (int m = 0; m < mapping.length; m++)
348 final SequenceI seq = seqs[pdbfnum][seqNo];
349 int sp = alignment.findIndex(seq);
350 if (mapping[m].getSequence() == seq && sp > -1)
353 * found a sequence with a mapping to a structure;
354 * now scan its features
356 SequenceI asp = alignment.getSequenceAt(sp);
358 scanSequenceFeatures(visibleFeatures, mapping[m], asp, theMap,
368 * Inspect features on the sequence; for each feature that is visible,
369 * determine its mapped ranges in the structure (if any) according to the
370 * given mapping, and add them to the map
372 * @param visibleFeatures
378 protected static void scanSequenceFeatures(List<String> visibleFeatures,
379 StructureMapping mapping, SequenceI seq,
380 Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
382 List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
383 visibleFeatures.toArray(new String[visibleFeatures.size()]));
384 for (SequenceFeature sf : sfs)
386 String type = sf.getType();
389 * Only copy visible features, don't copy any which originated
390 * from Chimera, and suppress uninteresting ones (e.g. RESNUM)
392 boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
393 .equals(sf.getFeatureGroup());
398 List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
401 if (!mappedRanges.isEmpty())
403 String value = sf.getDescription();
404 if (value == null || value.length() == 0)
408 float score = sf.getScore();
409 if (score != 0f && !Float.isNaN(score))
411 value = Float.toString(score);
413 Map<Object, AtomSpecModel> featureValues = theMap.get(type);
414 if (featureValues == null)
416 featureValues = new HashMap<>();
417 theMap.put(type, featureValues);
419 for (int[] range : mappedRanges)
421 addMapRange(featureValues, value, modelNumber, range[0],
422 range[1], mapping.getChain());
429 * Traverse the map of features/values/models/chains/positions to construct a
430 * list of 'setattr' commands (one per distinct feature type and value).
432 * The format of each command is
435 * <blockquote> setattr r <featureName> " " #modelnumber:range.chain
436 * e.g. setattr r jv:chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
444 protected static List<String> buildSetAttributeCommands(
445 Map<String, Map<Object, AtomSpecModel>> featureMap,
446 AAStructureBindingModel binding)
448 List<String> commands = new ArrayList<>();
449 for (String featureType : featureMap.keySet())
451 String attributeName = makeAttributeName(featureType);
454 * clear down existing attributes for this feature
456 // 'problem' - sets attribute to None on all residues - overkill?
457 // commands.add("~setattr r " + attributeName + " :*");
459 Map<Object, AtomSpecModel> values = featureMap.get(featureType);
460 for (Object value : values.keySet())
463 * for each distinct value recorded for this feature type,
464 * add a command to set the attribute on the mapped residues
465 * Put values in single quotes, encoding any embedded single quotes
467 StringBuilder sb = new StringBuilder(128);
468 String featureValue = value.toString();
469 featureValue = featureValue.replaceAll("\\'", "'");
470 sb.append("setattr r ").append(attributeName).append(" '")
471 .append(featureValue).append("' ");
472 sb.append(getAtomSpec(values.get(value), binding));
473 commands.add(sb.toString());
481 * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
482 * for a 'Jalview' namespace, and any non-alphanumeric character is converted
489 * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
492 protected static String makeAttributeName(String featureType)
494 StringBuilder sb = new StringBuilder();
495 if (featureType != null)
497 for (char c : featureType.toCharArray())
499 sb.append(Character.isLetterOrDigit(c) ? c : '_');
502 String attName = NAMESPACE_PREFIX + sb.toString();
505 * Chimera treats an attribute name ending in 'color' as colour-valued;
506 * Jalview doesn't, so prevent this by appending an underscore
508 if (attName.toUpperCase().endsWith("COLOR"))
517 * Returns the range(s) formatted as a Chimera atomspec
521 public static String getAtomSpec(AtomSpecModel atomSpec,
522 AAStructureBindingModel binding)
524 StringBuilder sb = new StringBuilder(128);
525 boolean firstModel = true;
526 for (Integer model : atomSpec.getModels())
533 // todo use JalviewChimeraBinding.getModelSpec(model)
534 // which means this cannot be static
535 sb.append(binding.getModelSpec(model)).append(":");
537 boolean firstPositionForModel = true;
539 for (String chain : atomSpec.getChains(model))
541 chain = " ".equals(chain) ? chain : chain.trim();
543 List<int[]> rangeList = atomSpec.getRanges(model, chain);
546 * sort ranges into ascending start position order
548 Collections.sort(rangeList, IntRangeComparator.ASCENDING);
550 int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
551 int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
553 Iterator<int[]> iterator = rangeList.iterator();
554 while (iterator.hasNext())
556 int[] range = iterator.next();
557 if (range[0] <= end + 1)
560 * range overlaps or is contiguous with the last one
561 * - so just extend the end position, and carry on
562 * (unless this is the last in the list)
564 end = Math.max(end, range[1]);
569 * we have a break so append the last range
571 appendRange(sb, start, end, chain, firstPositionForModel);
572 firstPositionForModel = false;
579 * and append the last range
581 if (!rangeList.isEmpty())
583 appendRange(sb, start, end, chain, firstPositionForModel);
584 firstPositionForModel = false;
588 return sb.toString();
592 * A helper method that appends one start-end range to a Chimera atomspec
598 * @param firstPositionForModel
600 static void appendRange(StringBuilder sb, int start, int end,
601 String chain, boolean firstPositionForModel)
603 if (!firstPositionForModel)
613 sb.append(start).append("-").append(end);
617 if (!" ".equals(chain))