Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
JalviewChimeraBinding | 55 | 204 | 82 |
1 | /* | |
2 | * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) | |
3 | * Copyright (C) $$Year-Rel$$ The Jalview Authors | |
4 | * | |
5 | * This file is part of Jalview. | |
6 | * | |
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. | |
11 | * | |
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. | |
16 | * | |
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. | |
20 | */ | |
21 | package jalview.ext.rbvi.chimera; | |
22 | ||
23 | import java.io.File; | |
24 | import java.io.FileOutputStream; | |
25 | import java.io.IOException; | |
26 | import java.io.PrintWriter; | |
27 | import java.net.BindException; | |
28 | import java.util.ArrayList; | |
29 | import java.util.Collections; | |
30 | import java.util.LinkedHashMap; | |
31 | import java.util.List; | |
32 | import java.util.Map; | |
33 | ||
34 | import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager; | |
35 | import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel; | |
36 | import ext.edu.ucsf.rbvi.strucviz2.StructureManager; | |
37 | import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType; | |
38 | import jalview.api.AlignmentViewPanel; | |
39 | import jalview.bin.Console; | |
40 | import jalview.datamodel.PDBEntry; | |
41 | import jalview.datamodel.SearchResultMatchI; | |
42 | import jalview.datamodel.SearchResultsI; | |
43 | import jalview.datamodel.SequenceFeature; | |
44 | import jalview.datamodel.SequenceI; | |
45 | import jalview.gui.StructureViewer.ViewerType; | |
46 | import jalview.httpserver.AbstractRequestHandler; | |
47 | import jalview.io.DataSourceType; | |
48 | import jalview.structure.AtomSpec; | |
49 | import jalview.structure.AtomSpecModel; | |
50 | import jalview.structure.StructureCommand; | |
51 | import jalview.structure.StructureCommandI; | |
52 | import jalview.structure.StructureSelectionManager; | |
53 | import jalview.structures.models.AAStructureBindingModel; | |
54 | ||
55 | public abstract class JalviewChimeraBinding extends AAStructureBindingModel | |
56 | { | |
57 | public static final String CHIMERA_SESSION_EXTENSION = ".py"; | |
58 | ||
59 | public static final String CHIMERA_FEATURE_GROUP = "Chimera"; | |
60 | ||
61 | /* | |
62 | * Object through which we talk to Chimera | |
63 | */ | |
64 | private ChimeraManager chimeraManager; | |
65 | ||
66 | /* | |
67 | * Object which listens to Chimera notifications | |
68 | */ | |
69 | private AbstractRequestHandler chimeraListener; | |
70 | ||
71 | /* | |
72 | * Map of ChimeraModel objects keyed by PDB full local file name | |
73 | */ | |
74 | protected Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<>(); | |
75 | ||
76 | String lastHighlightCommand; | |
77 | ||
78 | /** | |
79 | * Returns a model of the structure positions described by the Chimera format | |
80 | * atomspec | |
81 | * | |
82 | * @param atomSpec | |
83 | * @return | |
84 | */ | |
85 | 0 | protected AtomSpec parseAtomSpec(String atomSpec) |
86 | { | |
87 | 0 | return AtomSpec.fromChimeraAtomspec(atomSpec); |
88 | } | |
89 | ||
90 | /** | |
91 | * Open a PDB structure file in Chimera and set up mappings from Jalview. | |
92 | * | |
93 | * We check if the PDB model id is already loaded in Chimera, if so don't | |
94 | * reopen it. This is the case if Chimera has opened a saved session file. | |
95 | * | |
96 | * @param pe | |
97 | * @return | |
98 | */ | |
99 | 0 | public boolean openFile(PDBEntry pe) |
100 | { | |
101 | 0 | String file = pe.getFile(); |
102 | 0 | try |
103 | { | |
104 | 0 | List<ChimeraModel> modelsToMap = new ArrayList<>(); |
105 | 0 | List<ChimeraModel> oldList = chimeraManager.getModelList(); |
106 | 0 | boolean alreadyOpen = false; |
107 | ||
108 | /* | |
109 | * If Chimera already has this model, don't reopen it, but do remap it. | |
110 | */ | |
111 | 0 | for (ChimeraModel open : oldList) |
112 | { | |
113 | 0 | if (open.getModelName().equals(pe.getId())) |
114 | { | |
115 | 0 | alreadyOpen = true; |
116 | 0 | modelsToMap.add(open); |
117 | } | |
118 | } | |
119 | ||
120 | /* | |
121 | * If Chimera doesn't yet have this model, ask it to open it, and retrieve | |
122 | * the model name(s) added by Chimera. | |
123 | */ | |
124 | 0 | if (!alreadyOpen) |
125 | { | |
126 | 0 | chimeraManager.openModel(file, pe.getId(), ModelType.PDB_MODEL); |
127 | 0 | addChimeraModel(pe, modelsToMap); |
128 | } | |
129 | ||
130 | 0 | chimeraMaps.put(file, modelsToMap); |
131 | ||
132 | 0 | if (getSsm() != null) |
133 | { | |
134 | 0 | getSsm().addStructureViewerListener(this); |
135 | } | |
136 | 0 | return true; |
137 | } catch (Exception q) | |
138 | { | |
139 | 0 | log("Exception when trying to open model " + file + "\n" |
140 | + q.toString()); | |
141 | 0 | q.printStackTrace(); |
142 | } | |
143 | 0 | return false; |
144 | } | |
145 | ||
146 | /** | |
147 | * Adds the ChimeraModel corresponding to the given PDBEntry, based on model | |
148 | * name matching PDB id | |
149 | * | |
150 | * @param pe | |
151 | * @param modelsToMap | |
152 | */ | |
153 | 0 | protected void addChimeraModel(PDBEntry pe, |
154 | List<ChimeraModel> modelsToMap) | |
155 | { | |
156 | /* | |
157 | * Chimera: query for actual models and find the one with | |
158 | * matching model name - already set in viewer.openModel() | |
159 | */ | |
160 | 0 | List<ChimeraModel> newList = chimeraManager.getModelList(); |
161 | // JAL-1728 newList.removeAll(oldList) does not work | |
162 | 0 | for (ChimeraModel cm : newList) |
163 | { | |
164 | 0 | if (cm.getModelName().equals(pe.getId())) |
165 | { | |
166 | 0 | modelsToMap.add(cm); |
167 | } | |
168 | } | |
169 | } | |
170 | ||
171 | /** | |
172 | * Constructor | |
173 | * | |
174 | * @param ssm | |
175 | * @param pdbentry | |
176 | * @param sequenceIs | |
177 | * @param protocol | |
178 | */ | |
179 | 0 | public JalviewChimeraBinding(StructureSelectionManager ssm, |
180 | PDBEntry[] pdbentry, SequenceI[][] sequenceIs, | |
181 | DataSourceType protocol) | |
182 | { | |
183 | 0 | super(ssm, pdbentry, sequenceIs, protocol); |
184 | 0 | boolean chimeraX = ViewerType.CHIMERAX.equals(getViewerType()); |
185 | 0 | chimeraManager = chimeraX |
186 | ? new ChimeraXManager(new StructureManager(true)) | |
187 | : new ChimeraManager(new StructureManager(true)); | |
188 | 0 | setStructureCommands( |
189 | 0 | chimeraX ? new ChimeraXCommands() : new ChimeraCommands()); |
190 | } | |
191 | ||
192 | 0 | @Override |
193 | protected ViewerType getViewerType() | |
194 | { | |
195 | 0 | return ViewerType.CHIMERA; |
196 | } | |
197 | ||
198 | /** | |
199 | * Start a dedicated HttpServer to listen for Chimera notifications, and tell | |
200 | * it to start listening | |
201 | */ | |
202 | 0 | public void startChimeraListener() |
203 | { | |
204 | 0 | try |
205 | { | |
206 | 0 | chimeraListener = new ChimeraListener(this); |
207 | 0 | startListening(chimeraListener.getUri()); |
208 | } catch (BindException e) | |
209 | { | |
210 | 0 | jalview.bin.Console.errPrintln( |
211 | "Failed to start Chimera listener: " + e.getMessage()); | |
212 | } | |
213 | } | |
214 | ||
215 | /** | |
216 | * Close down the Jalview viewer and listener, and (optionally) the associated | |
217 | * Chimera window. | |
218 | */ | |
219 | 0 | @Override |
220 | public void closeViewer(boolean closeChimera) | |
221 | { | |
222 | 0 | super.closeViewer(closeChimera); |
223 | 0 | if (this.chimeraListener != null) |
224 | { | |
225 | 0 | chimeraListener.shutdown(); |
226 | 0 | chimeraListener = null; |
227 | } | |
228 | ||
229 | /* | |
230 | * the following call is added to avoid a stack trace error in Chimera | |
231 | * after "stop really" is sent; Chimera > 1.14 will not need it; see also | |
232 | * http://plato.cgl.ucsf.edu/trac/chimera/ticket/17597 | |
233 | */ | |
234 | 0 | if (closeChimera && (getViewerType() == ViewerType.CHIMERA)) |
235 | { | |
236 | 0 | chimeraManager.getChimeraProcess().destroy(); |
237 | } | |
238 | ||
239 | 0 | chimeraManager.clearOnChimeraExit(); |
240 | 0 | chimeraManager = null; |
241 | } | |
242 | ||
243 | /** | |
244 | * Helper method to construct model spec in Chimera format: | |
245 | * <ul> | |
246 | * <li>#0 (#1 etc) for a PDB file with no sub-models</li> | |
247 | * <li>#0.1 (#1.1 etc) for a PDB file with sub-models</li> | |
248 | * <ul> | |
249 | * Note for now we only ever choose the first of multiple models. This | |
250 | * corresponds to the hard-coded Jmol equivalent (compare {1.1}). Refactor in | |
251 | * future if there is a need to select specific sub-models. | |
252 | * | |
253 | * @param pdbfnum | |
254 | * @return | |
255 | */ | |
256 | 0 | protected String getModelSpec(int pdbfnum) |
257 | { | |
258 | 0 | if (pdbfnum < 0 || pdbfnum >= getPdbCount()) |
259 | { | |
260 | 0 | return "#" + pdbfnum; // temp hack for ChimeraX |
261 | } | |
262 | ||
263 | /* | |
264 | * For now, the test for having sub-models is whether multiple Chimera | |
265 | * models are mapped for the PDB file; the models are returned as a response | |
266 | * to the Chimera command 'list models type molecule', see | |
267 | * ChimeraManager.getModelList(). | |
268 | */ | |
269 | 0 | List<ChimeraModel> maps = chimeraMaps.get(getStructureFiles()[pdbfnum]); |
270 | 0 | boolean hasSubModels = maps != null && maps.size() > 1; |
271 | 0 | return "#" + String.valueOf(pdbfnum) + (hasSubModels ? ".1" : ""); |
272 | } | |
273 | ||
274 | /** | |
275 | * Launch Chimera, unless an instance linked to this object is already | |
276 | * running. Returns true if Chimera is successfully launched, or already | |
277 | * running, else false. | |
278 | * | |
279 | * @return | |
280 | */ | |
281 | 0 | public boolean launchChimera() |
282 | { | |
283 | 0 | if (chimeraManager.isChimeraLaunched()) |
284 | { | |
285 | 0 | return true; |
286 | } | |
287 | ||
288 | 0 | boolean launched = chimeraManager.launchChimera(getChimeraPaths()); |
289 | 0 | if (launched) |
290 | { | |
291 | 0 | startExternalViewerMonitor(chimeraManager.getChimeraProcess()); |
292 | } | |
293 | else | |
294 | { | |
295 | 0 | log("Failed to launch Chimera!"); |
296 | } | |
297 | 0 | return launched; |
298 | } | |
299 | ||
300 | /** | |
301 | * Returns a list of candidate paths to the Chimera program executable | |
302 | * | |
303 | * @return | |
304 | */ | |
305 | 0 | protected List<String> getChimeraPaths() |
306 | { | |
307 | 0 | return StructureManager.getChimeraPaths(false); |
308 | } | |
309 | ||
310 | /** | |
311 | * Answers true if the Chimera process is still running, false if ended or not | |
312 | * started. | |
313 | * | |
314 | * @return | |
315 | */ | |
316 | 0 | @Override |
317 | public boolean isViewerRunning() | |
318 | { | |
319 | 0 | return chimeraManager != null && chimeraManager.isChimeraLaunched(); |
320 | } | |
321 | ||
322 | /** | |
323 | * Send a command to Chimera, and optionally log and return any responses. | |
324 | * | |
325 | * @param command | |
326 | * @param getResponse | |
327 | */ | |
328 | 0 | @Override |
329 | public List<String> executeCommand(final StructureCommandI command, | |
330 | boolean getResponse) | |
331 | { | |
332 | 0 | if (chimeraManager == null || command == null) |
333 | { | |
334 | // ? thread running after viewer shut down | |
335 | 0 | return null; |
336 | } | |
337 | 0 | List<String> reply = null; |
338 | // trim command or it may never find a match in the replyLog!! | |
339 | 0 | String cmd = command.getCommand().trim(); |
340 | 0 | List<String> lastReply = chimeraManager.sendChimeraCommand(cmd, |
341 | getResponse); | |
342 | 0 | if (getResponse) |
343 | { | |
344 | 0 | reply = lastReply; |
345 | 0 | if (Console.isDebugEnabled()) |
346 | { | |
347 | 0 | Console.debug( |
348 | "Response from command ('" + cmd + "') was:\n" + lastReply); | |
349 | } | |
350 | } | |
351 | else | |
352 | { | |
353 | 0 | if (Console.isDebugEnabled()) |
354 | { | |
355 | 0 | Console.debug("Command executed: " + cmd); |
356 | } | |
357 | } | |
358 | ||
359 | 0 | return reply; |
360 | } | |
361 | ||
362 | 0 | @Override |
363 | public synchronized String[] getStructureFiles() | |
364 | { | |
365 | 0 | if (chimeraManager == null) |
366 | { | |
367 | 0 | return new String[0]; |
368 | } | |
369 | ||
370 | 0 | return chimeraMaps.keySet() |
371 | .toArray(modelFileNames = new String[chimeraMaps.size()]); | |
372 | } | |
373 | ||
374 | /** | |
375 | * Construct and send a command to highlight zero, one or more atoms. We do | |
376 | * this by sending an "rlabel" command to show the residue label at that | |
377 | * position. | |
378 | */ | |
379 | 0 | @Override |
380 | public void highlightAtoms(List<AtomSpec> atoms) | |
381 | { | |
382 | 0 | if (atoms == null || atoms.size() == 0) |
383 | { | |
384 | 0 | return; |
385 | } | |
386 | ||
387 | 0 | boolean forChimeraX = chimeraManager.isChimeraX(); |
388 | 0 | StringBuilder cmd = new StringBuilder(128); |
389 | 0 | boolean first = true; |
390 | 0 | boolean found = false; |
391 | ||
392 | 0 | for (AtomSpec atom : atoms) |
393 | { | |
394 | 0 | int pdbResNum = atom.getPdbResNum(); |
395 | 0 | String chain = atom.getChain(); |
396 | 0 | String pdbfile = atom.getPdbFile(); |
397 | 0 | List<ChimeraModel> cms = chimeraMaps.get(pdbfile); |
398 | 0 | if (cms != null && !cms.isEmpty()) |
399 | { | |
400 | 0 | if (first) |
401 | { | |
402 | 0 | cmd.append(forChimeraX ? "label #" : "rlabel #"); |
403 | } | |
404 | else | |
405 | { | |
406 | 0 | cmd.append(","); |
407 | } | |
408 | 0 | first = false; |
409 | 0 | if (forChimeraX) |
410 | { | |
411 | 0 | cmd.append(cms.get(0).getModelNumber()).append("/").append(chain) |
412 | .append(":").append(pdbResNum); | |
413 | } | |
414 | else | |
415 | { | |
416 | 0 | cmd.append(cms.get(0).getModelNumber()).append(":") |
417 | .append(pdbResNum); | |
418 | 0 | if (!chain.equals(" ") && !forChimeraX) |
419 | { | |
420 | 0 | cmd.append(".").append(chain); |
421 | } | |
422 | } | |
423 | 0 | found = true; |
424 | } | |
425 | } | |
426 | 0 | String command = cmd.toString(); |
427 | ||
428 | /* | |
429 | * avoid repeated commands for the same residue | |
430 | */ | |
431 | 0 | if (command.equals(lastHighlightCommand)) |
432 | { | |
433 | 0 | return; |
434 | } | |
435 | 0 | if (!found) |
436 | { | |
437 | // not a valid residue label command, so clear | |
438 | 0 | cmd.setLength(0); |
439 | } | |
440 | /* | |
441 | * prepend with command | |
442 | * to unshow the label for the previous residue | |
443 | */ | |
444 | 0 | if (lastHighlightCommand != null) |
445 | { | |
446 | 0 | cmd.insert(0, ";"); |
447 | 0 | cmd.insert(0, lastHighlightCommand); |
448 | 0 | cmd.insert(0, "~"); |
449 | ||
450 | } | |
451 | 0 | if (cmd.length() > 0) |
452 | { | |
453 | 0 | executeCommand(true, null, new StructureCommand(cmd.toString())); |
454 | } | |
455 | ||
456 | 0 | if (found) |
457 | { | |
458 | 0 | this.lastHighlightCommand = command; |
459 | } | |
460 | } | |
461 | ||
462 | /** | |
463 | * Query Chimera for its current selection, and highlight it on the alignment | |
464 | */ | |
465 | 0 | public void highlightChimeraSelection() |
466 | { | |
467 | /* | |
468 | * Ask Chimera for its current selection | |
469 | */ | |
470 | 0 | StructureCommandI command = getCommandGenerator().getSelectedResidues(); |
471 | ||
472 | 0 | Runnable action = new Runnable() |
473 | { | |
474 | 0 | @Override |
475 | public void run() | |
476 | { | |
477 | 0 | List<String> chimeraReply = executeCommand(command, true); |
478 | ||
479 | 0 | List<String> selectedResidues = new ArrayList<>(); |
480 | 0 | if (chimeraReply != null) |
481 | { | |
482 | /* | |
483 | * expect 0, 1 or more lines of the format either | |
484 | * Chimera: | |
485 | * residue id #0:43.A type GLY | |
486 | * ChimeraX: | |
487 | * residue id /A:89 name THR index 88 | |
488 | * We are only interested in the atomspec (third token of the reply) | |
489 | */ | |
490 | 0 | for (String inputLine : chimeraReply) |
491 | { | |
492 | 0 | String[] inputLineParts = inputLine.split("\\s+"); |
493 | 0 | if (inputLineParts.length >= 5) |
494 | { | |
495 | 0 | selectedResidues.add(inputLineParts[2]); |
496 | } | |
497 | } | |
498 | } | |
499 | ||
500 | /* | |
501 | * Parse model number, residue and chain for each selected position, | |
502 | * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain) | |
503 | */ | |
504 | 0 | List<AtomSpec> atomSpecs = convertStructureResiduesToAlignment( |
505 | selectedResidues); | |
506 | ||
507 | /* | |
508 | * Broadcast the selection (which may be empty, if the user just cleared all | |
509 | * selections) | |
510 | */ | |
511 | 0 | getSsm().mouseOverStructure(atomSpecs); |
512 | ||
513 | } | |
514 | }; | |
515 | 0 | new Thread(action).start(); |
516 | } | |
517 | ||
518 | /** | |
519 | * Converts a list of Chimera(X) atomspecs to a list of AtomSpec representing | |
520 | * the corresponding residues (if any) in Jalview | |
521 | * | |
522 | * @param structureSelection | |
523 | * @return | |
524 | */ | |
525 | 0 | protected List<AtomSpec> convertStructureResiduesToAlignment( |
526 | List<String> structureSelection) | |
527 | { | |
528 | 0 | List<AtomSpec> atomSpecs = new ArrayList<>(); |
529 | 0 | for (String atomSpec : structureSelection) |
530 | { | |
531 | 0 | try |
532 | { | |
533 | 0 | AtomSpec spec = parseAtomSpec(atomSpec); |
534 | 0 | String pdbfilename = getPdbFileForModel(spec.getModelNumber()); |
535 | 0 | spec.setPdbFile(pdbfilename); |
536 | 0 | atomSpecs.add(spec); |
537 | } catch (IllegalArgumentException e) | |
538 | { | |
539 | 0 | Console.error("Failed to parse atomspec: " + atomSpec); |
540 | } | |
541 | } | |
542 | 0 | return atomSpecs; |
543 | } | |
544 | ||
545 | /** | |
546 | * @param modelId | |
547 | * @return | |
548 | */ | |
549 | 0 | protected String getPdbFileForModel(int modelId) |
550 | { | |
551 | /* | |
552 | * Work out the pdbfilename from the model number | |
553 | */ | |
554 | 0 | String pdbfilename = modelFileNames[0]; |
555 | 0 | findfileloop: for (String pdbfile : this.chimeraMaps.keySet()) |
556 | { | |
557 | 0 | for (ChimeraModel cm : chimeraMaps.get(pdbfile)) |
558 | { | |
559 | 0 | if (cm.getModelNumber() == modelId) |
560 | { | |
561 | 0 | pdbfilename = pdbfile; |
562 | 0 | break findfileloop; |
563 | } | |
564 | } | |
565 | } | |
566 | 0 | return pdbfilename; |
567 | } | |
568 | ||
569 | 0 | private void log(String message) |
570 | { | |
571 | 0 | jalview.bin.Console.errPrintln("## Chimera log: " + message); |
572 | } | |
573 | ||
574 | /** | |
575 | * Constructs and send commands to Chimera to set attributes on residues for | |
576 | * features visible in Jalview. | |
577 | * <p> | |
578 | * The syntax is: setattr r <attName> <attValue> <atomSpec> | |
579 | * <p> | |
580 | * For example: setattr r jv_chain "Ferredoxin-1, Chloroplastic" #0:94.A | |
581 | * | |
582 | * @param avp | |
583 | * @return | |
584 | */ | |
585 | 0 | public int sendFeaturesToViewer(AlignmentViewPanel avp) |
586 | { | |
587 | // TODO refactor as required to pull up to an interface | |
588 | ||
589 | 0 | Map<String, Map<Object, AtomSpecModel>> featureValues = buildFeaturesMap( |
590 | avp); | |
591 | 0 | List<StructureCommandI> commands = getCommandGenerator() |
592 | .setAttributes(featureValues); | |
593 | 0 | if (commands.size() > 10) |
594 | { | |
595 | 0 | sendCommandsByFile(commands); |
596 | } | |
597 | else | |
598 | { | |
599 | 0 | executeCommands(commands, false, null); |
600 | } | |
601 | 0 | return commands.size(); |
602 | } | |
603 | ||
604 | /** | |
605 | * Write commands to a temporary file, and send a command to Chimera to open | |
606 | * the file as a commands script. For use when sending a large number of | |
607 | * separate commands would overload the REST interface mechanism. | |
608 | * | |
609 | * @param commands | |
610 | */ | |
611 | 0 | protected void sendCommandsByFile(List<StructureCommandI> commands) |
612 | { | |
613 | 0 | try |
614 | { | |
615 | 0 | File tmp = File.createTempFile("chim", getCommandFileExtension()); |
616 | 0 | tmp.deleteOnExit(); |
617 | 0 | PrintWriter out = new PrintWriter(new FileOutputStream(tmp)); |
618 | 0 | for (StructureCommandI command : commands) |
619 | { | |
620 | 0 | out.println(command.getCommand()); |
621 | } | |
622 | 0 | out.flush(); |
623 | 0 | out.close(); |
624 | 0 | String path = tmp.getAbsolutePath(); |
625 | 0 | StructureCommandI command = getCommandGenerator() |
626 | .openCommandFile(path); | |
627 | 0 | executeCommand(false, null, command); |
628 | } catch (IOException e) | |
629 | { | |
630 | 0 | jalview.bin.Console.errPrintln( |
631 | "Sending commands to Chimera via file failed with " | |
632 | + e.getMessage()); | |
633 | } | |
634 | } | |
635 | ||
636 | /** | |
637 | * Returns the file extension required for a file of commands to be read by | |
638 | * the structure viewer | |
639 | * | |
640 | * @return | |
641 | */ | |
642 | 0 | protected String getCommandFileExtension() |
643 | { | |
644 | 0 | return ".com"; |
645 | } | |
646 | ||
647 | /** | |
648 | * Create features in Jalview for the given attribute name and structure | |
649 | * residues. | |
650 | * | |
651 | * <pre> | |
652 | * The residue list should be 0, 1 or more reply lines of the format: | |
653 | * residue id #0:5.A isHelix -155.000836316 index 5 | |
654 | * or | |
655 | * residue id #0:6.A isHelix None | |
656 | * </pre> | |
657 | * | |
658 | * @param attName | |
659 | * @param residues | |
660 | * @return the number of features added | |
661 | */ | |
662 | 0 | protected int createFeaturesForAttributes(String attName, |
663 | List<String> residues) | |
664 | { | |
665 | 0 | int featuresAdded = 0; |
666 | 0 | String featureGroup = getViewerFeatureGroup(); |
667 | ||
668 | 0 | for (String residue : residues) |
669 | { | |
670 | 0 | AtomSpec spec = null; |
671 | 0 | String[] tokens = residue.split(" "); |
672 | 0 | if (tokens.length < 5) |
673 | { | |
674 | 0 | continue; |
675 | } | |
676 | 0 | String atomSpec = tokens[2]; |
677 | 0 | String attValue = tokens[4]; |
678 | ||
679 | /* | |
680 | * ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix) | |
681 | */ | |
682 | 0 | if ("None".equalsIgnoreCase(attValue) |
683 | || "False".equalsIgnoreCase(attValue)) | |
684 | { | |
685 | 0 | continue; |
686 | } | |
687 | ||
688 | 0 | try |
689 | { | |
690 | 0 | spec = parseAtomSpec(atomSpec); |
691 | } catch (IllegalArgumentException e) | |
692 | { | |
693 | 0 | Console.error("Problem parsing atomspec " + atomSpec); |
694 | 0 | continue; |
695 | } | |
696 | ||
697 | 0 | String chainId = spec.getChain(); |
698 | 0 | String description = attValue; |
699 | 0 | float score = Float.NaN; |
700 | 0 | try |
701 | { | |
702 | 0 | score = Float.valueOf(attValue); |
703 | 0 | description = chainId; |
704 | } catch (NumberFormatException e) | |
705 | { | |
706 | // was not a float value | |
707 | } | |
708 | ||
709 | 0 | String pdbFile = getPdbFileForModel(spec.getModelNumber()); |
710 | 0 | spec.setPdbFile(pdbFile); |
711 | ||
712 | 0 | List<AtomSpec> atoms = Collections.singletonList(spec); |
713 | ||
714 | /* | |
715 | * locate the mapped position in the alignment (if any) | |
716 | */ | |
717 | 0 | SearchResultsI sr = getSsm() |
718 | .findAlignmentPositionsForStructurePositions(atoms); | |
719 | ||
720 | /* | |
721 | * expect one matched alignment position, or none | |
722 | * (if the structure position is not mapped) | |
723 | */ | |
724 | 0 | for (SearchResultMatchI m : sr.getResults()) |
725 | { | |
726 | 0 | SequenceI seq = m.getSequence(); |
727 | 0 | int start = m.getStart(); |
728 | 0 | int end = m.getEnd(); |
729 | 0 | SequenceFeature sf = new SequenceFeature(attName, description, |
730 | start, end, score, featureGroup); | |
731 | // todo: should SequenceFeature have an explicit property for chain? | |
732 | // note: repeating the action shouldn't duplicate features | |
733 | 0 | if (seq.addSequenceFeature(sf)) |
734 | { | |
735 | 0 | featuresAdded++; |
736 | } | |
737 | } | |
738 | } | |
739 | 0 | return featuresAdded; |
740 | } | |
741 | ||
742 | /** | |
743 | * Answers the feature group name to apply to features created in Jalview from | |
744 | * Chimera attributes | |
745 | * | |
746 | * @return | |
747 | */ | |
748 | 0 | protected String getViewerFeatureGroup() |
749 | { | |
750 | // todo pull up to interface | |
751 | 0 | return CHIMERA_FEATURE_GROUP; |
752 | } | |
753 | ||
754 | 0 | @Override |
755 | public String getModelIdForFile(String pdbFile) | |
756 | { | |
757 | 0 | List<ChimeraModel> foundModels = chimeraMaps.get(pdbFile); |
758 | 0 | if (foundModels != null && !foundModels.isEmpty()) |
759 | { | |
760 | 0 | return String.valueOf(foundModels.get(0).getModelNumber()); |
761 | } | |
762 | 0 | return ""; |
763 | } | |
764 | ||
765 | /** | |
766 | * Answers a (possibly empty) list of attribute names in Chimera[X], excluding | |
767 | * any which were added from Jalview | |
768 | * | |
769 | * @return | |
770 | */ | |
771 | 0 | public List<String> getChimeraAttributes() |
772 | { | |
773 | 0 | List<String> attributes = new ArrayList<>(); |
774 | 0 | StructureCommandI command = getCommandGenerator() |
775 | .listResidueAttributes(); | |
776 | 0 | final List<String> reply = executeCommand(command, true); |
777 | 0 | if (reply != null) |
778 | { | |
779 | 0 | for (String inputLine : reply) |
780 | { | |
781 | 0 | String[] lineParts = inputLine.split("\\s"); |
782 | 0 | if (lineParts.length == 2 && lineParts[0].equals("resattr")) |
783 | { | |
784 | 0 | String attName = lineParts[1]; |
785 | /* | |
786 | * exclude attributes added from Jalview | |
787 | */ | |
788 | 0 | if (!attName.startsWith(ChimeraCommands.NAMESPACE_PREFIX)) |
789 | { | |
790 | 0 | attributes.add(attName); |
791 | } | |
792 | } | |
793 | } | |
794 | } | |
795 | 0 | return attributes; |
796 | } | |
797 | ||
798 | /** | |
799 | * Returns the file extension to use for a saved viewer session file (.py) | |
800 | * | |
801 | * @return | |
802 | */ | |
803 | 0 | @Override |
804 | public String getSessionFileExtension() | |
805 | { | |
806 | 0 | return CHIMERA_SESSION_EXTENSION; |
807 | } | |
808 | ||
809 | 0 | @Override |
810 | public String getHelpURL() | |
811 | { | |
812 | 0 | return "https://www.cgl.ucsf.edu/chimera/docs/UsersGuide"; |
813 | } | |
814 | } |