package aima.gui.fx.framework;
import java.util.List;
import java.util.Optional;
import aima.core.util.CancelableThread;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.input.MouseButton;
/**
* Controller class for simulation panes. It is responsible for maintaining the
* current parameter settings, the simulation state, the current simulation
* thread, and for handling events (parameter change events and simulation
* button events). If not otherwise stated, methods must be called from the FX
* Application Thread.
*
* @author Ruediger Lunde
*/
public class SimulationPaneCtrl {
public enum State {
READY, RUNNING, FINISHED, PAUSED, CANCELED
}
public final static String PARAM_SIM_SPEED = "simSpeed";
private Button simBtn;
private Label statusLabel;
private List<Parameter> params;
private List<ComboBox<String>> paramCombos;
private Runnable initMethod;
private Runnable simMethod;
private ObjectProperty<State> state = new SimpleObjectProperty<>();
private Optional<CancelableThread> simThread = Optional.empty();
/** Should only be called by the SimulationPaneBuilder. */
public SimulationPaneCtrl(List<Parameter> params, List<ComboBox<String>> paramCombos, Runnable initMethod,
Runnable simMethod, Button simBtn, Label statusLabel) {
this.params = params;
this.paramCombos = paramCombos;
this.initMethod = initMethod;
this.simMethod = simMethod;
this.simBtn = simBtn;
this.statusLabel = statusLabel;
ChangeListener<String> listener = (obs, o, n) -> onParamChanged();
for (ComboBox<String> combo : paramCombos)
// allow simSpeed adjustments during simulation (without
// re-initialization)
if (!combo.getId().equals(PARAM_SIM_SPEED))
combo.getSelectionModel().selectedItemProperty().addListener(listener);
simBtn.setOnAction(ev -> onSimButtonAction());
simBtn.setOnMouseClicked(ev -> {
if (ev.getButton() == MouseButton.SECONDARY)
setParamValue(PARAM_SIM_SPEED, Integer.MAX_VALUE);
});
updateParamVisibility();
state.addListener((obs, o, n) -> onStateChanged());
setState(State.READY);
}
public Optional<Parameter> getParam(String paramName) {
return Parameter.find(params, paramName);
}
public int getParamValueIndex(String paramName) {
int valIdx = -1;
int paramIdx = Parameter.indexOf(params, paramName);
if (paramIdx != -1)
valIdx = paramCombos.get(paramIdx).getSelectionModel().getSelectedIndex();
if (valIdx == -1)
throw new IllegalStateException("No selected value for parameter " + paramName);
return valIdx;
}
public Object getParamValue(String paramName) {
int valIdx = getParamValueIndex(paramName);
return Parameter.find(params, paramName).get().getValues().get(valIdx);
}
public int getParamAsInt(String paramName) {
return (Integer) getParamValue(paramName);
}
public double getParamAsDouble(String paramName) {
return (Double) getParamValue(paramName);
}
public boolean isParamVisible(String paramName) {
int paramIdx = Parameter.indexOf(params, paramName);
return paramCombos.get(paramIdx).isVisible();
}
/** Call this only for parameters which do not depend on other parameters! */
public void setParamDisable(String paramName, boolean value) {
int paramIdx = Parameter.indexOf(params, paramName);
paramCombos.get(paramIdx).setDisable(value);
}
public void setParam(String paramName, int valueIdx) {
int idx = Parameter.indexOf(params, paramName);
if (idx != -1)
paramCombos.get(idx).getSelectionModel().select(valueIdx);
else
throw new IllegalStateException("Parameter " + paramName + " not found.");
}
public void setParamValue(String paramName, Object value) {
int pIdx = Parameter.indexOf(params, paramName);
if (pIdx != -1) {
List<Object> values = params.get(pIdx).getValues();
for (int i = 0; i < values.size(); i++)
if (values.get(i).equals(value)) {
paramCombos.get(pIdx).getSelectionModel().select(i);
break;
}
} else
throw new IllegalStateException("Parameter " + paramName + " not found.");
}
/** Sets status label text (can be called from any thread). */
public void setStatus(final String text) {
if (Platform.isFxApplicationThread())
statusLabel.setText(text);
else
Platform.runLater(() -> statusLabel.setText(text));
}
/**
* Sleeps as specified in simulation speed parameter. If the value is
* <code>Integer.MAX_VALUE</code> the simulation state is set to paused.
* Caller is typically not the FX Application Thread.
*/
public void waitAfterStep() {
try {
int msec = getParamAsInt(PARAM_SIM_SPEED);
if (msec == Integer.MAX_VALUE)
setState(State.PAUSED);
Thread.sleep(msec);
} catch (InterruptedException e) {
// nothing to do here.
}
}
/**
* Tries to stop simulation. This will only have an effect, if all loops in
* the running simulation method check {@link CancelableThread#isCanceled()}
* in every time-consuming loop.
*/
public void cancelSimulation() {
if (simThread.isPresent() && simThread.get().isAlive()) {
simThread.get().cancel();
setState(State.CANCELED);
if (state.get() == State.PAUSED)
simThread.get().interrupt();
}
}
/** Can be called from FX application threads and other threads as well. */
private void setState(State newState) {
if (Platform.isFxApplicationThread())
state.set(newState);
else
Platform.runLater(() -> state.set(newState));
}
private void onParamChanged() {
cancelSimulation();
setStatus("");
updateParamVisibility();
initMethod.run();
setState(State.READY);
}
private void onStateChanged() {
if (state.get() == State.READY)
simBtn.setText("Start");
else if (state.get() == State.RUNNING)
simBtn.setText("Cancel");
else if (state.get() == State.PAUSED)
simBtn.setText("Continue");
else if (state.get() == State.CANCELED)
simBtn.setText("Stop");
else if (state.get() == State.FINISHED)
simBtn.setText("Init");
boolean disable = state.get() != State.READY && state.get() != State.FINISHED;
for (ComboBox<String> combo : paramCombos)
if (!combo.getId().equals(PARAM_SIM_SPEED))
combo.setDisable(disable);
}
@SuppressWarnings("deprecation")
private void onSimButtonAction() {
if (state.get() == State.FINISHED) {
onParamChanged();
} else if (!simThread.isPresent() || !simThread.get().isAlive()) {
simThread = Optional.of(new CancelableThread(this::runSimulation));
simThread.get().setDaemon(true);
simThread.get().start();
} else if (state.get() == State.PAUSED) {
simThread.get().interrupt();
setState(State.RUNNING);
} else if (simThread.get().isCanceled()) {
simThread.get().stop();
setState(State.READY);
} else {
cancelSimulation();
}
}
private void runSimulation() {
try {
setState(State.RUNNING);
simMethod.run();
} catch (Exception e) {
setStatus("Sorry, something went wrong during simulation: " + e.getClass().getSimpleName());
e.printStackTrace();
} catch (Error e) {
setStatus("Sorry, something went totally wrong during simulation: " + e.getClass().getSimpleName());
e.printStackTrace();
}
setState(State.FINISHED);
}
private void updateParamVisibility() {
for (int i = 0; i < params.size(); i++) {
String depParam = params.get(i).getDependencyParameter();
if (depParam != null) {
Parameter para = params.get(i);
ComboBox<String> combo = paramCombos.get(i);
combo.setVisible(para.getDependencyValues().contains(getParamValue(depParam)));
}
}
}
}