for (ChimeraModel chimeraModel : modelList)
{
// get model color
- Color modelColor = getModelColor(chimeraModel);
+ Color modelColor = isChimeraX() ? null : getModelColor(chimeraModel);
if (modelColor != null)
{
chimeraModel.setModelColor(modelColor);
// chimeraSend("repr stick "+newModel.toSpec());
// Create the information we need for the navigator
- if (type != ModelType.SMILES)
+ if (type != ModelType.SMILES && !isChimeraX())
{
addResidues(chimeraModel);
}
public List<ChimeraModel> getModelList()
{
List<ChimeraModel> modelList = new ArrayList<>();
- List<String> list = sendChimeraCommand("list models type molecule",
- true);
- if (list != null)
- {
- for (String modelLine : list)
- {
- ChimeraModel chimeraModel = new ChimeraModel(modelLine);
- modelList.add(chimeraModel);
- }
- }
- return modelList;
+ modelList.add(new ChimeraModel("4zhp", ModelType.PDB_MODEL, 1, 0));
+ return modelList; // ChimeraX doesn't have 'list models' command
+ // List<String> list = sendChimeraCommand("list models type molecule",
+ // true);
+ // if (list != null)
+ // {
+ // for (String modelLine : list)
+ // {
+ // ChimeraModel chimeraModel = new ChimeraModel(modelLine);
+ // modelList.add(chimeraModel);
+ // }
+ // }
+ // return modelList;
}
/**
{
// ensure symbolic links are resolved
chimeraPath = Paths.get(chimeraPath).toRealPath().toString();
+ isChimeraX = chimeraPath.toLowerCase().contains("chimerax");
File path = new File(chimeraPath);
// uncomment the next line to simulate Chimera not installed
// path = new File(chimeraPath + "x");
args.add(chimeraPath);
// shows Chimera output window but suppresses REST responses:
// args.add("--debug");
- args.add("--start");
- args.add("RESTServer");
+ if (isChimeraX())
+ {
+ args.add("--cmd");
+ args.add("remote rest start");
+ }
+ else
+ {
+ args.add("--start");
+ args.add("RESTServer");
+ }
ProcessBuilder pb = new ProcessBuilder(args);
chimera = pb.start();
error = "";
{
responses.append("\n" + response);
// expect: REST server on host 127.0.0.1 port port_number
+ // ChimeraX is the same except "REST server started on host..."
if (response.startsWith("REST server"))
{
String[] tokens = response.split(" ");
- if (tokens.length == 7 && "port".equals(tokens[5]))
+ for (int i = 0; i < tokens.length - 1; i++)
{
- port = Integer.parseInt(tokens[6]);
- break;
+ if ("port".equals(tokens[i]))
+ {
+ port = Integer.parseInt(tokens[i + 1]);
+ break;
+ }
}
}
+ if (port > 0)
+ {
+ break; // hack for hanging readLine()
+ }
response = lineReader.readLine();
}
} catch (Exception e)
private volatile boolean busy = false;
+ private boolean isChimeraX;
+
/**
* Send a command to Chimera.
*
*/
public List<String> sendChimeraCommand(String command, boolean reply)
{
- // System.out.println("chimeradebug>> " + command);
+ System.out.println("chimeradebug>> " + command);
if (!isChimeraLaunched() || command == null
|| "".equals(command.trim()))
{
{
String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
List<NameValuePair> commands = new ArrayList<>(1);
- commands.add(new BasicNameValuePair("command", command));
+ String encoded = command.replace(" ", "+").replace("#", "%23")
+ .replace("|", "%7C").replace(";", "%3B");
+ commands.add(new BasicNameValuePair("command", encoded));
List<String> reply = new ArrayList<>();
BufferedReader response = null;
try
{
- response = HttpClientUtils.doHttpUrlPost(restUrl, commands, CONNECTION_TIMEOUT_MS,
- REST_REPLY_TIMEOUT_MS);
+ if (isChimeraX())
+ {
+ response = HttpClientUtils.doHttpGet(restUrl, commands,
+ CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS);
+ }
+ else
+ {
+ response = HttpClientUtils.doHttpUrlPost(restUrl, commands,
+ CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS);
+ }
String line = "";
while ((line = response.readLine()) != null)
{
{
return chimera;
}
+
+ public boolean isChimeraX()
+ {
+ return isChimeraX;
+ }
}
*/
public AtomSpecModel()
{
- atomSpec = new TreeMap<Integer, Map<String, List<int[]>>>();
+ atomSpec = new TreeMap<>();
}
/**
Map<String, List<int[]>> modelData = atomSpec.get(model);
if (modelData == null)
{
- atomSpec.put(model, modelData = new TreeMap<String, List<int[]>>());
+ atomSpec.put(model, modelData = new TreeMap<>());
}
/*
List<int[]> chainData = modelData.get(chain);
if (chainData == null)
{
- chainData = new ArrayList<int[]>();
+ chainData = new ArrayList<>();
modelData.put(chain, chainData);
}
/*
* we have a break so append the last range
*/
- appendRange(sb, start, end, chain, firstPositionForModel);
+ appendRange(sb, start, end, chain, firstPositionForModel,
+ false);
firstPositionForModel = false;
start = range[0];
end = range[1];
*/
if (!rangeList.isEmpty())
{
- appendRange(sb, start, end, chain, firstPositionForModel);
+ appendRange(sb, start, end, chain, firstPositionForModel, false);
firstPositionForModel = false;
}
}
* @param firstPositionForModel
*/
protected void appendRange(StringBuilder sb, int start, int end,
- String chain, boolean firstPositionForModel)
+ String chain, boolean firstPositionForModel, boolean isChimeraX)
{
if (!firstPositionForModel)
{
sb.append(start).append("-").append(end);
}
- sb.append(".");
- if (!" ".equals(chain)) {
- sb.append(chain);
+ if (!isChimeraX)
+ {
+ sb.append(".");
+ if (!" ".equals(chain))
+ {
+ sb.append(chain);
+ }
}
}
+
+ /**
+ * Returns the range(s) formatted as a ChimeraX atomspec, for example
+ * <p>
+ * #1/A:2-20,30-40/B:10-20|#2/A:12-30
+ *
+ * @return
+ */
+ public String getAtomSpecX()
+ {
+ StringBuilder sb = new StringBuilder(128);
+ boolean firstModel = true;
+ for (Integer model : atomSpec.keySet())
+ {
+ if (!firstModel)
+ {
+ sb.append("|");
+ }
+ firstModel = false;
+ sb.append("#").append(model);
+
+ final Map<String, List<int[]>> modelData = atomSpec.get(model);
+
+ for (String chain : modelData.keySet())
+ {
+ boolean firstPositionForChain = true;
+ chain = " ".equals(chain) ? chain : chain.trim();
+ sb.append("/").append(chain).append(":");
+ List<int[]> rangeList = modelData.get(chain);
+
+ /*
+ * sort ranges into ascending start position order
+ */
+ Collections.sort(rangeList, IntRangeComparator.ASCENDING);
+
+ int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
+ int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
+
+ Iterator<int[]> iterator = rangeList.iterator();
+ while (iterator.hasNext())
+ {
+ int[] range = iterator.next();
+ if (range[0] <= end + 1)
+ {
+ /*
+ * range overlaps or is contiguous with the last one
+ * - so just extend the end position, and carry on
+ * (unless this is the last in the list)
+ */
+ end = Math.max(end, range[1]);
+ }
+ else
+ {
+ /*
+ * we have a break so append the last range
+ */
+ appendRange(sb, start, end, chain, firstPositionForChain, true);
+ start = range[0];
+ end = range[1];
+ firstPositionForChain = false;
+ }
+ }
+
+ /*
+ * and append the last range
+ */
+ if (!rangeList.isEmpty())
+ {
+ appendRange(sb, start, end, chain, firstPositionForChain, true);
+ }
+ firstPositionForChain = false;
+ }
+ }
+ return sb.toString();
+ }
}
* @param sr
* @param fr
* @param viewPanel
+ * @param isChimeraX
* @return
*/
public static StructureMappingcommandSet[] getColourBySequenceCommand(
StructureSelectionManager ssm, String[] files,
SequenceI[][] sequence, SequenceRenderer sr,
- AlignmentViewPanel viewPanel)
+ AlignmentViewPanel viewPanel, boolean isChimeraX)
{
Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, files,
- sequence, sr, viewPanel);
+ sequence, sr, viewPanel, isChimeraX);
- List<String> colourCommands = buildColourCommands(colourMap);
+ List<String> colourCommands = buildColourCommands(colourMap,
+ isChimeraX);
StructureMappingcommandSet cs = new StructureMappingcommandSet(
ChimeraCommands.class, null,
* </pre>
*
* @param colourMap
+ * @param isChimeraX
* @return
*/
protected static List<String> buildColourCommands(
- Map<Object, AtomSpecModel> colourMap)
+ Map<Object, AtomSpecModel> colourMap, boolean isChimeraX)
{
/*
* This version concatenates all commands into a single String (semi-colon
{
sb.append("; ");
}
- sb.append("color ").append(colourCode).append(" ");
+ sb.append("color ");
firstColour = false;
final AtomSpecModel colourData = colourMap.get(colour);
- sb.append(colourData.getAtomSpec());
+ if (isChimeraX)
+ {
+ sb.append(colourData.getAtomSpecX()).append(" ").append(colourCode);
+ }
+ else
+ {
+ sb.append(colourCode).append(" ").append(colourData.getAtomSpec());
+ }
}
commands.add(sb.toString());
return commands;
* list of start/end ranges
* Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
* </pre>
+ *
+ * @param ssm
+ * @param files
+ * @param sequence
+ * @param sr
+ * @param viewPanel
+ * @param isChimeraX
+ * @return
*/
protected static Map<Object, AtomSpecModel> buildColoursMap(
StructureSelectionManager ssm, String[] files,
SequenceI[][] sequence, SequenceRenderer sr,
- AlignmentViewPanel viewPanel)
+ AlignmentViewPanel viewPanel, boolean isChimeraX)
{
FeatureRenderer fr = viewPanel.getFeatureRenderer();
FeatureColourFinder finder = new FeatureColourFinder(fr);
for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
{
+ final int modelNumber = pdbfnum + (isChimeraX ? 1 : 0);
StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
if (mapping == null || mapping.length < 1)
{
if (startPos != -1)
{
- addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
- lastPos, lastChain);
+ addAtomSpecRange(colourMap, lastColour, modelNumber,
+ startPos, lastPos, lastChain);
}
startPos = pos;
}
// final colour range
if (lastColour != null)
{
- addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
+ addAtomSpecRange(colourMap, lastColour, modelNumber, startPos,
lastPos, lastChain);
}
// break;
/**
* Constructs and returns Chimera commands to set attributes on residues
- * corresponding to features in Jalview. Attribute names are the Jalview
- * feature type, with a "jv_" prefix.
+ * corresponding to features in Jalview. Attribute names are the Jalview feature
+ * type, with a "jv_" prefix.
*
* @param ssm
* @param files
* @param seqs
* @param viewPanel
+ * @param isChimeraX
* @return
*/
public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
- AlignmentViewPanel viewPanel)
+ AlignmentViewPanel viewPanel, boolean isChimeraX)
{
Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
ssm, files, seqs, viewPanel);
- List<String> commands = buildSetAttributeCommands(featureMap);
+ List<String> commands = buildSetAttributeCommands(featureMap,
+ isChimeraX);
StructureMappingcommandSet cs = new StructureMappingcommandSet(
ChimeraCommands.class, null,
* </pre>
*
* @param featureMap
+ * @param isChimeraX
* @return
*/
protected static List<String> buildSetAttributeCommands(
- Map<String, Map<Object, AtomSpecModel>> featureMap)
+ Map<String, Map<Object, AtomSpecModel>> featureMap,
+ boolean isChimeraX)
{
List<String> commands = new ArrayList<>();
for (String featureType : featureMap.keySet())
featureValue = featureValue.replaceAll("\\'", "'");
sb.append("setattr r ").append(attributeName).append(" '")
.append(featureValue).append("' ");
- sb.append(values.get(value).getAtomSpec());
+ AtomSpecModel atomSpecModel = values.get(value);
+ sb.append(isChimeraX ? atomSpecModel.getAtomSpecX()
+ : atomSpecModel.getAtomSpec());
commands.add(sb.toString());
}
}
private static final String ALPHACARBON = "CA";
- private List<String> chainNames = new ArrayList<String>();
+ private List<String> chainNames = new ArrayList<>();
- private Hashtable<String, String> chainFile = new Hashtable<String, String>();
+ private Hashtable<String, String> chainFile = new Hashtable<>();
/*
* Object through which we talk to Chimera
/*
* Map of ChimeraModel objects keyed by PDB full local file name
*/
- private Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<String, List<ChimeraModel>>();
+ private Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<>();
String lastHighlightCommand;
/**
* Open a PDB structure file in Chimera and set up mappings from Jalview.
*
- * We check if the PDB model id is already loaded in Chimera, if so don't
- * reopen it. This is the case if Chimera has opened a saved session file.
+ * We check if the PDB model id is already loaded in Chimera, if so don't reopen
+ * it. This is the case if Chimera has opened a saved session file.
*
* @param pe
* @return
String file = pe.getFile();
try
{
- List<ChimeraModel> modelsToMap = new ArrayList<ChimeraModel>();
- List<ChimeraModel> oldList = viewer.getModelList();
+ List<ChimeraModel> modelsToMap = new ArrayList<>();
+ List<ChimeraModel> oldList = viewer.isChimeraX() ? new ArrayList<>()
+ : viewer.getModelList();
boolean alreadyOpen = false;
/*
if (!alreadyOpen)
{
viewer.openModel(file, pe.getId(), ModelType.PDB_MODEL);
- List<ChimeraModel> newList = viewer.getModelList();
- // JAL-1728 newList.removeAll(oldList) does not work
- for (ChimeraModel cm : newList)
+ if (viewer.isChimeraX())
{
- if (cm.getModelName().equals(pe.getId()))
+ /*
+ * ChimeraX hack: force chimera model name to pdbId
+ */
+ int modelNumber = chimeraMaps.size() + 1;
+ String command = "setattr #" + modelNumber + " models name "
+ + pe.getId();
+ sendChimeraCommand(command, false);
+ modelsToMap.add(new ChimeraModel(pe.getId(), ModelType.PDB_MODEL,
+ modelNumber, 0));
+ }
+ else
+ {
+ /*
+ * Chimera: query for actual models and find the one with
+ * matching model name - set in viewer.openModel()
+ */
+ List<ChimeraModel> newList = viewer.getModelList();
+ // JAL-1728 newList.removeAll(oldList) does not work
+ for (ChimeraModel cm : newList)
{
- modelsToMap.add(cm);
+ if (cm.getModelName().equals(pe.getId()))
+ {
+ modelsToMap.add(cm);
+ }
}
}
}
}
/**
- * Starts a thread that waits for the Chimera process to finish, so that we
- * can then close the associated resources. This avoids leaving orphaned
- * Chimera viewer panels in Jalview if the user closes Chimera.
+ * Starts a thread that waits for the Chimera process to finish, so that we can
+ * then close the associated resources. This avoids leaving orphaned Chimera
+ * viewer panels in Jalview if the user closes Chimera.
*/
protected void startChimeraProcessMonitor()
{
}
/**
- * Start a dedicated HttpServer to listen for Chimera notifications, and tell
- * it to start listening
+ * Start a dedicated HttpServer to listen for Chimera notifications, and tell it
+ * to start listening
*/
public void startChimeraListener()
{
String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel)
{
return ChimeraCommands.getColourBySequenceCommand(getSsm(), files,
- getSequence(), sr, viewPanel);
+ getSequence(), sr, viewPanel, viewer.isChimeraX());
}
/**
}
/**
- * Construct and send a command to highlight zero, one or more atoms. We do
- * this by sending an "rlabel" command to show the residue label at that
- * position.
+ * Construct and send a command to highlight zero, one or more atoms. We do this
+ * by sending an "rlabel" command to show the residue label at that position.
*/
@Override
public void highlightAtoms(List<AtomSpec> atoms)
protected List<AtomSpec> convertStructureResiduesToAlignment(
List<String> structureSelection)
{
- List<AtomSpec> atomSpecs = new ArrayList<AtomSpec>();
+ List<AtomSpec> atomSpecs = new ArrayList<>();
for (String atomSpec : structureSelection)
{
try
StructureMappingcommandSet commandSet = ChimeraCommands
.getSetAttributeCommandsForFeatures(getSsm(), files,
- getSequence(), avp);
+ getSequence(), avp, viewer.isChimeraX());
String[] commands = commandSet.commands;
if (commands.length > 10)
{
}
/**
- * Write commands to a temporary file, and send a command to Chimera to open
- * the file as a commands script. For use when sending a large number of
- * separate commands would overload the REST interface mechanism.
+ * Write commands to a temporary file, and send a command to Chimera to open the
+ * file as a commands script. For use when sending a large number of separate
+ * commands would overload the REST interface mechanism.
*
* @param commands
*/
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;
return null;
}
}
+
+ /**
+ * do an HTTP GET with URL-Encoded parameters passed in the Query string
+ *
+ * @param url
+ * @param vals
+ * @return Reader containing content, if any, or null if no entity returned.
+ * @throws IOException
+ * @throws ClientProtocolException
+ * @throws Exception
+ */
+ public static BufferedReader doHttpGet(String url,
+ List<NameValuePair> vals, int connectionTimeoutMs,
+ int readTimeoutMs) throws ClientProtocolException, IOException
+ {
+ // todo use HttpClient 4.3 or later and class RequestConfig
+ HttpParams params = new BasicHttpParams();
+ params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION,
+ HttpVersion.HTTP_1_1);
+ if (connectionTimeoutMs > 0)
+ {
+ HttpConnectionParams.setConnectionTimeout(params,
+ connectionTimeoutMs);
+ }
+ if (readTimeoutMs > 0)
+ {
+ HttpConnectionParams.setSoTimeout(params, readTimeoutMs);
+ }
+ boolean first = true;
+ for (NameValuePair param : vals)
+ {
+ if (first)
+ {
+ url += "?";
+ }
+ else
+ {
+ url += "&";
+ }
+ url += param.getName();
+ url += "=";
+ url += param.getValue();
+ }
+ HttpClient httpclient = new DefaultHttpClient(params);
+ HttpGet httpGet = new HttpGet(url);
+ // UrlEncodedFormEntity ue = new UrlEncodedFormEntity(vals, "UTF-8");
+ // httpGet.setEntity(ue);
+ HttpResponse response = httpclient.execute(httpGet);
+ HttpEntity resEntity = response.getEntity();
+
+ if (resEntity != null)
+ {
+ BufferedReader r = new BufferedReader(
+ new InputStreamReader(resEntity.getContent()));
+ return r;
+ }
+ else
+ {
+ return null;
+ }
+ }
}
public void testBuildColourCommands()
{
- Map<Object, AtomSpecModel> map = new LinkedHashMap<Object, AtomSpecModel>();
+ Map<Object, AtomSpecModel> map = new LinkedHashMap<>();
ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 2, 5, "A");
ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 7, 7, "B");
ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 9, 23, "A");
// Colours should appear in the Chimera command in the order in which
// they were added; within colour, by model, by chain, ranges in start order
- String command = ChimeraCommands.buildColourCommands(map).get(0);
+ String command = ChimeraCommands.buildColourCommands(map, false).get(0);
assertEquals(
command,
"color #0000ff #0:2-5.A,9-23.A,7.B|#1:1.A,4-7.B; color #ffff00 #1:3-5.A,8.A; color #ff0000 #0:3-9.A");
/*
* make a map of { featureType, {featureValue, {residue range specification } } }
*/
- Map<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<String, Map<Object, AtomSpecModel>>();
- Map<Object, AtomSpecModel> featureValues = new HashMap<Object, AtomSpecModel>();
+ Map<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<>();
+ Map<Object, AtomSpecModel> featureValues = new HashMap<>();
/*
* start with just one feature/value...
ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 8, 20, "A");
List<String> commands = ChimeraCommands
- .buildSetAttributeCommands(featuresMap);
+ .buildSetAttributeCommands(featuresMap, false);
assertEquals(1, commands.size());
/*
ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 3, 9, "A");
// same feature value, contiguous range
ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "A");
- commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
+ commands = ChimeraCommands.buildSetAttributeCommands(featuresMap,
+ false);
assertEquals(1, commands.size());
assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:3-25.A");
ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "B");
// same feature value and chain, different model
ChimeraCommands.addAtomSpecRange(featureValues, "X", 1, 26, 30, "A");
- commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
+ commands = ChimeraCommands.buildSetAttributeCommands(featuresMap,
+ false);
assertEquals(1, commands.size());
assertEquals(commands.get(0),
"setattr r jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A");
// same feature, different value
ChimeraCommands.addAtomSpecRange(featureValues, "Y", 0, 40, 50, "A");
- commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
+ commands = ChimeraCommands.buildSetAttributeCommands(featuresMap,
+ false);
assertEquals(2, commands.size());
// commands are ordered by feature type but not by value
// so use contains to test for the expected command:
"A");
// feature names are sanitised to change non-alphanumeric to underscore
// feature values are sanitised to encode single quote characters
- commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
+ commands = ChimeraCommands.buildSetAttributeCommands(featuresMap,
+ false);
assertTrue(commands
.contains("setattr r jv_side_chain_binding_ '<html>metal <a href=\"http:a.b.c/x\"> 'ion!' #0:7-15.A"));
}
/*
* map residues 1-10 to residues 21-30 (atoms 105-150) in structures
*/
- HashMap<Integer, int[]> map = new HashMap<Integer, int[]>();
+ HashMap<Integer, int[]> map = new HashMap<>();
for (int pos = 1; pos <= seq1.getLength(); pos++)
{
map.put(pos, new int[] { 20 + pos, 5 * (20 + pos) });
ssm.addStructureMapping(sm2);
StructureMappingcommandSet[] commands = ChimeraCommands
- .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel);
+ .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel,
+ false);
assertEquals(1, commands.length);
assertEquals(1, commands[0].commands.length);
String theCommand = commands[0].commands[0];