workcraft/StgPlugin/src/org/workcraft/plugins/stg/tools/StgSimulationTool.java
package org.workcraft.plugins.stg.tools;
import org.workcraft.Framework;
import org.workcraft.dom.hierarchy.NamespaceHelper;
import org.workcraft.dom.math.MathNode;
import org.workcraft.dom.visual.SizeHelper;
import org.workcraft.dom.visual.VisualComponent;
import org.workcraft.dom.visual.VisualModel;
import org.workcraft.dom.visual.connections.VisualConnection;
import org.workcraft.gui.controls.FlatHeaderRenderer;
import org.workcraft.gui.properties.BooleanCellEditor;
import org.workcraft.gui.properties.BooleanCellRenderer;
import org.workcraft.gui.properties.ColorCellEditor;
import org.workcraft.gui.properties.ColorCellRenderer;
import org.workcraft.gui.tools.Decoration;
import org.workcraft.gui.tools.GraphEditor;
import org.workcraft.plugins.builtin.settings.SignalCommonSettings;
import org.workcraft.plugins.builtin.settings.SimulationDecorationSettings;
import org.workcraft.plugins.dtd.DtdDescriptor;
import org.workcraft.plugins.dtd.VisualDtd;
import org.workcraft.plugins.petri.Transition;
import org.workcraft.plugins.petri.VisualPlace;
import org.workcraft.plugins.petri.VisualTransition;
import org.workcraft.plugins.petri.tools.PetriSimulationTool;
import org.workcraft.plugins.petri.tools.PlaceDecoration;
import org.workcraft.plugins.stg.*;
import org.workcraft.plugins.stg.converters.StgToDtdConverter;
import org.workcraft.plugins.stg.converters.StgToStgConverter;
import org.workcraft.plugins.stg.utils.LabelParser;
import org.workcraft.plugins.stg.utils.StgUtils;
import org.workcraft.shared.ColorGenerator;
import org.workcraft.traces.Trace;
import org.workcraft.types.Pair;
import org.workcraft.types.Triple;
import org.workcraft.utils.*;
import org.workcraft.workspace.ModelEntry;
import org.workcraft.workspace.WorkspaceEntry;
import javax.activation.ActivationDataFlavor;
import javax.activation.DataHandler;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TableModelEvent;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DragSource;
import java.util.List;
import java.util.*;
public class StgSimulationTool extends PetriSimulationTool {
private static final int COLUMN_SIGNAL = 0;
private static final int COLUMN_STATE = 1;
private static final int COLUMN_VISIBLE = 2;
private static final int COLUMN_COLOR = 3;
private static final String GRAY_CODE = ColorUtils.getHexRGB(Color.GRAY);
protected Map<String, SignalData> signalDataMap = new HashMap<>();
protected List<String> signals = new LinkedList<>();
protected JTable stateTable;
private JPanel panel;
private Map<String, Boolean> initialSignalState = new HashMap<>();
private StgToStgConverter converter;
public StgSimulationTool() {
super(true);
}
public static final class SignalData {
public final String name;
public final Signal.Type type;
public Signal.State value = Signal.State.UNDEFINED;
public boolean excited = false;
public Boolean visible;
public Color color = Color.BLACK;
public SignalData(String name, Signal.Type type) {
this.name = name;
this.type = type;
visible = type != Signal.Type.INTERNAL;
}
}
private final class StateTable extends JTable {
StateTable(StateTableModel model) {
super(model);
getTableHeader().setDefaultRenderer(new FlatHeaderRenderer());
getTableHeader().setReorderingAllowed(false);
setDragEnabled(true);
setDropMode(DropMode.INSERT_ROWS);
setTransferHandler(new StateTableRowTransferHandler(this));
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
setRowHeight(SizeHelper.getComponentHeightFromFont(this.getFont()));
setDefaultRenderer(SignalData.class, new SignalDataRenderer());
setDefaultRenderer(Boolean.class, new BooleanCellRenderer());
setDefaultEditor(Boolean.class, new BooleanCellEditor());
setDefaultRenderer(Color.class, new ColorCellRenderer());
setDefaultEditor(Color.class, new ColorCellEditor());
}
@Override
public void editingStopped(ChangeEvent e) {
TableCellEditor cellEditor = getCellEditor();
String signalName = signals.get(editingRow);
if ((cellEditor != null) && (signalName != null)) {
SignalData signalData = signalDataMap.get(signalName);
Object value = cellEditor.getCellEditorValue();
if ((signalData != null) && (value != null)) {
switch (editingColumn) {
case COLUMN_VISIBLE:
signalData.visible = (Boolean) value;
break;
case COLUMN_COLOR:
signalData.color = (Color) value;
break;
}
setValueAt(value, editingRow, editingColumn);
removeEditor();
}
}
}
}
private final class StateTableModel extends AbstractTableModel {
@Override
public int getColumnCount() {
return 4;
}
@Override
public String getColumnName(int column) {
switch (column) {
case COLUMN_SIGNAL:
return "Signal";
case COLUMN_STATE:
return "State";
case COLUMN_VISIBLE:
return "Visible";
case COLUMN_COLOR:
return "Color";
default:
return null;
}
}
@Override
public Class<?> getColumnClass(int col) {
switch (col) {
case COLUMN_SIGNAL:
case COLUMN_STATE:
return SignalData.class;
case COLUMN_VISIBLE:
return Boolean.class;
case COLUMN_COLOR:
return Color.class;
default:
return null;
}
}
@Override
public int getRowCount() {
return (signalDataMap == null) ? 0 : signalDataMap.size();
}
@Override
public Object getValueAt(int row, int col) {
if (row < signalDataMap.size()) {
String signalName = signals.get(row);
SignalData signalData = signalDataMap.get(signalName);
if (signalData != null) {
switch (col) {
case COLUMN_SIGNAL:
case COLUMN_STATE:
return signalData;
case COLUMN_VISIBLE:
return signalData.visible;
case COLUMN_COLOR:
return signalData.color;
default:
return null;
}
}
}
return null;
}
@Override
public boolean isCellEditable(int row, int col) {
switch (col) {
case COLUMN_VISIBLE:
case COLUMN_COLOR:
return true;
default:
return false;
}
}
public void reorderRows(int from, int to) {
if ((from >= 0) && (from < signals.size()) && (to >= 0) && (to < signals.size()) && (from != to)) {
String name = signals.remove(from);
signals.add(to, name);
fireTableDataChanged();
}
}
}
private final class SignalDataRenderer implements TableCellRenderer {
private final JLabel label = new JLabel() {
@Override
public void paint(Graphics g) {
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
super.paint(g);
}
};
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int col) {
label.setText("");
label.setBorder(GuiUtils.getTableCellBorder());
label.setForeground(table.getForeground());
label.setBackground(table.getBackground());
label.setFont(table.getFont().deriveFont(Font.PLAIN));
if (isActivated() && (value instanceof SignalData)) {
SignalData signalData = (SignalData) value;
switch (col) {
case COLUMN_SIGNAL:
label.setText(signalData.name);
label.setForeground(getTypeColor(signalData.type));
break;
case COLUMN_STATE:
label.setText(signalData.value.toString());
if (signalData.excited) {
label.setFont(table.getFont().deriveFont(Font.BOLD));
}
break;
default:
break;
}
}
boolean fits = GuiUtils.getLabelTextWidth(label) < GuiUtils.getTableColumnTextWidth(table, col);
label.setToolTipText(fits ? null : label.getText());
return label;
}
}
public class StateTableRowTransferHandler extends TransferHandler {
private final DataFlavor localObjectFlavor = new ActivationDataFlavor(Integer.class,
"Integer Row Index");
private final JTable table;
public StateTableRowTransferHandler(JTable table) {
this.table = table;
}
@Override
protected Transferable createTransferable(JComponent c) {
return new DataHandler(table.getSelectedRow(), localObjectFlavor.getMimeType());
}
@Override
public boolean canImport(TransferHandler.TransferSupport info) {
boolean result = (info.getComponent() == table) && info.isDrop() && info.isDataFlavorSupported(localObjectFlavor);
table.setCursor(result ? DragSource.DefaultMoveDrop : DragSource.DefaultMoveNoDrop);
return result;
}
@Override
public int getSourceActions(JComponent c) {
return TransferHandler.COPY_OR_MOVE;
}
@Override
public boolean importData(TransferHandler.TransferSupport info) {
JTable target = (JTable) info.getComponent();
JTable.DropLocation dl = (JTable.DropLocation) info.getDropLocation();
int toRow = dl.getRow();
int lastRow = table.getModel().getRowCount();
if ((toRow < 0) || (toRow > lastRow)) {
toRow = lastRow;
}
target.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
try {
int fromRow = (Integer) info.getTransferable().getTransferData(localObjectFlavor);
if (toRow > fromRow) {
toRow--;
}
if ((fromRow != -1) && (fromRow != toRow)) {
StateTableModel stateTableModel = (StateTableModel) table.getModel();
stateTableModel.reorderRows(fromRow, toRow);
target.getSelectionModel().addSelectionInterval(toRow, toRow);
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@Override
protected void exportDone(JComponent c, Transferable t, int act) {
if ((act == TransferHandler.MOVE) || (act == TransferHandler.NONE)) {
table.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
}
private final class TraceTableCellRendererImplementation implements TableCellRenderer {
private final JLabel label = new JLabel() {
@Override
public void paint(Graphics g) {
g.setColor(getBackground());
g.fillRect(0, 0, getWidth() - 1, getHeight() - 1);
super.paint(g);
}
};
private boolean isActive(int row, int column) {
if (column == 0) {
if (!mainTrace.isEmpty() && branchTrace.isEmpty()) {
return row == mainTrace.getPosition();
}
} else {
int absoluteBranchSize = mainTrace.getPosition() + branchTrace.size();
int absoluteBranchPosition = mainTrace.getPosition() + branchTrace.getPosition();
if (!branchTrace.isEmpty() && (row >= mainTrace.getPosition()) && (row < absoluteBranchSize)) {
return row == absoluteBranchPosition;
}
}
return false;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
JLabel result = null;
label.setBorder(GuiUtils.getTableCellBorder());
if (isActivated() && (value instanceof String)) {
String text = (String) value;
Pair<String, String> pair = TraceUtils.splitLoopDecoration(text);
String prefix = pair.getFirst();
String ref = pair.getSecond();
Triple<String, SignalTransition.Direction, Integer> r = LabelParser.parseSignalTransition(ref);
String signalName = r == null ? null : r.getFirst();
Signal.Type signalType = getUnderlyingModel().getSignalType(signalName);
String colorCode = ColorUtils.getHexRGB(getTypeColor(signalType));
label.setText("<html><span style='color: " + GRAY_CODE + "'>" + prefix + "</span>" +
"<span style='color: " + colorCode + "'>" + ref + "</span></html>");
if (isActive(row, column)) {
label.setBackground(table.getSelectionBackground());
} else {
label.setBackground(table.getBackground());
}
result = label;
}
return result;
}
}
@Override
public void generateUnderlyingModel(WorkspaceEntry we) {
converter = new StgToStgConverter(WorkspaceUtils.getAs(we, VisualStg.class));
}
@Override
public Stg getUnderlyingModel() {
return converter.getDstModel().getMathModel();
}
@Override
public VisualModel getUnderlyingVisualModel() {
return converter.getDstModel();
}
@Override
public JPanel getControlsPanel(GraphEditor editor) {
if (panel == null) {
panel = super.getControlsPanel(editor);
stateTable = new StateTable(new StateTableModel());
statePane.setViewportView(stateTable);
traceTable.setDefaultRenderer(Object.class, new TraceTableCellRendererImplementation());
}
return panel;
}
@Override
public void updateState(GraphEditor editor) {
super.updateState(editor);
updateSignalState();
stateTable.tableChanged(new TableModelEvent(stateTable.getModel()));
}
public void updateSignalState() {
initialiseSignalState();
ArrayList<String> combinedTrace = new ArrayList<>();
if (!mainTrace.isEmpty()) {
combinedTrace.addAll(mainTrace.subList(0, mainTrace.getPosition()));
}
if (!branchTrace.isEmpty()) {
combinedTrace.addAll(branchTrace.subList(0, branchTrace.getPosition()));
}
for (String ref : combinedTrace) {
MathNode node = getUnderlyingNode(ref);
if (node instanceof SignalTransition) {
SignalTransition transition = (SignalTransition) node;
String signalReference = getUnderlyingModel().getSignalReference(transition);
SignalData signalState = signalDataMap.get(signalReference);
if (signalState != null) {
switch (transition.getDirection()) {
case MINUS:
signalState.value = Signal.State.LOW;
break;
case PLUS:
signalState.value = Signal.State.HIGH;
break;
case TOGGLE:
signalState.value = signalState.value.toggle();
break;
default:
break;
}
}
}
}
for (SignalTransition transition : getUnderlyingModel().getSignalTransitions()) {
String signalReference = getUnderlyingModel().getSignalReference(transition);
SignalData signalData = signalDataMap.get(signalReference);
if (signalData != null) {
signalData.excited |= isEnabledUnderlyingNode(transition);
}
}
}
public void initialiseSignalState() {
for (String signal : signalDataMap.keySet()) {
SignalData signalData = signalDataMap.get(signal);
Boolean signalState = (initialSignalState == null) ? null : initialSignalState.get(signal);
if (signalState == null) {
signalData.value = Signal.State.UNDEFINED;
} else {
signalData.value = signalState ? Signal.State.HIGH : Signal.State.LOW;
}
signalData.excited = false;
}
}
@Override
public void activated(GraphEditor editor) {
super.activated(editor);
initialSignalState = getInitialState();
initialiseStateMap();
setStatePaneVisibility(true);
}
private Map<String, Boolean> getInitialState() {
Stg stg = getUnderlyingModel();
Map<String, Boolean> result = StgUtils.guessInitialStateFromSignalPlaces(stg);
Set<String> signalRefs = stg.getSignalReferences();
if (result.size() < signalRefs.size()) {
result = StgUtils.getInitialState(stg, 500);
}
return result;
}
@Override
public void generateTraceGraph(GraphEditor editor) {
Trace trace = getCombinedTrace();
if (trace.isEmpty()) {
DialogUtils.showWarning("Cannot generate a timing diagram for an empty trace.");
} else {
Stg stg = getUnderlyingModel();
LinkedList<Pair<String, Color>> visibleSignals = getVisibleSignals();
StgToDtdConverter converter = new StgToDtdConverter(stg, trace, visibleSignals);
VisualDtd dtd = converter.getVisualDtd();
ModelEntry me = new ModelEntry(new DtdDescriptor(), dtd);
Framework framework = Framework.getInstance();
framework.createWork(me, editor.getWorkspaceEntry().getFileName());
}
}
private LinkedList<Pair<String, Color>> getVisibleSignals() {
LinkedList<Pair<String, Color>> result = new LinkedList<>();
for (String signalRef : signals) {
SignalData signalData = signalDataMap.get(signalRef);
if ((signalData != null) && signalData.visible) {
result.add(Pair.of(signalData.name, signalData.color));
}
}
return result;
}
@Override
public String getTraceLabelByReference(String ref) {
String result = ref;
if (ref != null) {
String name = NamespaceHelper.getReferenceName(ref);
Pair<String, Integer> instancedTransition = LabelParser.parseInstancedTransition(name);
if (instancedTransition != null) {
String parentRef = NamespaceHelper.getParentReference(ref);
result = NamespaceHelper.getReference(parentRef, instancedTransition.getFirst());
}
}
return result;
}
private void initialiseStateMap() {
Stg stg = getUnderlyingModel();
HashMap<String, SignalData> newStateMap = new HashMap<>();
List<String> allSignals = new LinkedList<>();
for (Signal.Type type : Signal.Type.values()) {
List<String> typedSignals = SortUtils.getSortedNatural(stg.getSignalReferences(type));
allSignals.addAll(typedSignals);
for (String signal : typedSignals) {
SignalData signalData = signalDataMap.getOrDefault(signal, new SignalData(signal, type));
newStateMap.put(signal, signalData);
}
}
signalDataMap = newStateMap;
// Preserve "old" and append "new" items of allSignals to signals list.
signals.retainAll(allSignals);
allSignals.removeAll(signals);
signals.addAll(allSignals);
updateSignalState();
}
private Color getTypeColor(Signal.Type type) {
if (type == null) {
return SignalCommonSettings.getDummyColor();
}
switch (type) {
case INPUT: return SignalCommonSettings.getInputColor();
case OUTPUT: return SignalCommonSettings.getOutputColor();
case INTERNAL: return SignalCommonSettings.getInternalColor();
default: return SignalCommonSettings.getDummyColor();
}
}
@Override
public void coloriseTokens(Transition t) {
VisualModel model = getUnderlyingVisualModel();
VisualStg stg = (model instanceof VisualStg) ? (VisualStg) model : null;
if (stg == null) {
return;
}
VisualTransition vt = stg.getVisualComponent(t, VisualTransition.class);
if (vt == null) {
return;
}
Color tokenColor = Color.BLACK;
ColorGenerator tokenColorGenerator = vt.getTokenColorGenerator();
if (tokenColorGenerator != null) {
// Generate token colour
tokenColor = tokenColorGenerator.updateColor();
} else {
// Combine preset token colours
for (VisualConnection vc : stg.getConnections(vt)) {
if ((vc.getSecond() == vt) && vc.isTokenColorPropagator()) {
if (vc.getFirst() instanceof VisualPlace) {
VisualPlace vp = (VisualPlace) vc.getFirst();
tokenColor = ColorUtils.colorise(tokenColor, vp.getTokenColor());
} else if (vc instanceof VisualImplicitPlaceArc) {
VisualImplicitPlaceArc vipa = (VisualImplicitPlaceArc) vc;
tokenColor = ColorUtils.colorise(tokenColor, vipa.getTokenColor());
}
}
}
}
// Propagate the colour to postset tokens
for (VisualConnection vc : stg.getConnections(vt)) {
if ((vc.getFirst() == vt) && vc.isTokenColorPropagator()) {
if (vc.getSecond() instanceof VisualPlace) {
VisualPlace vp = (VisualPlace) vc.getSecond();
vp.setTokenColor(tokenColor);
} else if (vc instanceof VisualImplicitPlaceArc) {
VisualImplicitPlaceArc vipa = (VisualImplicitPlaceArc) vc;
vipa.setTokenColor(tokenColor);
}
}
}
}
@Override
public Decoration getConnectionDecoration(VisualModel model, VisualConnection connection) {
VisualImplicitPlaceArc underlyingVisualImplicitArc = getUnderlyingVisualImplicitArc(model, connection);
if (underlyingVisualImplicitArc == null) {
return super.getConnectionDecoration(model, connection);
}
StgPlace underlyingPlace = underlyingVisualImplicitArc.getImplicitPlace();
return new PlaceDecoration() {
@Override
public Color getColorisation() {
return underlyingPlace.isImplicit() & (underlyingPlace.getTokens() > 0)
? SimulationDecorationSettings.getExcitedComponentColor() : null;
}
@Override
public int getTokens() {
return underlyingPlace.getTokens();
}
@Override
public Color getTokenColor() {
return underlyingVisualImplicitArc.getTokenColor();
}
};
}
private VisualImplicitPlaceArc getUnderlyingVisualImplicitArc(VisualModel model, VisualConnection connection) {
VisualModel underlyingVisualModel = getUnderlyingVisualModel();
if ((connection instanceof VisualImplicitPlaceArc) && (underlyingVisualModel instanceof VisualStg)) {
String firstRef = model.getMathReference(connection.getFirst());
MathNode firstUnderlyingNode = getUnderlyingNode(firstRef);
VisualComponent firstVisualComponent = underlyingVisualModel.getVisualComponent(firstUnderlyingNode, VisualComponent.class);
String secondRef = model.getMathReference(connection.getSecond());
MathNode secondUnderlyingNode = getUnderlyingNode(secondRef);
VisualComponent secondVisualComponent = underlyingVisualModel.getVisualComponent(secondUnderlyingNode, VisualComponent.class);
VisualConnection result = underlyingVisualModel.getConnection(firstVisualComponent, secondVisualComponent);
if (result instanceof VisualImplicitPlaceArc) {
return (VisualImplicitPlaceArc) result;
}
}
return null;
}
}