mini-cards/app/src/main/java/com/codigoparallevar/minicards/parts/ProgramakerCustomBlockPart.java

724 lines
24 KiB
Java
Raw Normal View History

package com.codigoparallevar.minicards.parts;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.util.Log;
import com.codigoparallevar.minicards.ScrolledCanvas;
import com.codigoparallevar.minicards.parts.connectors.AnyRoundInputConnector;
2020-05-29 11:32:05 +00:00
import com.codigoparallevar.minicards.parts.connectors.AnyRoundOutputConnector;
import com.codigoparallevar.minicards.parts.connectors.ConnectorTypeInfo;
import com.codigoparallevar.minicards.parts.connectors.RoundOutputConnector;
2020-05-29 11:32:05 +00:00
import com.codigoparallevar.minicards.parts.connectors.SignalRoundOutputConnector;
import com.codigoparallevar.minicards.types.Moveable;
import com.codigoparallevar.minicards.types.Part;
import com.codigoparallevar.minicards.types.PartConnection;
import com.codigoparallevar.minicards.types.PartGrid;
import com.codigoparallevar.minicards.types.connectors.input.InputConnector;
import com.codigoparallevar.minicards.types.connectors.output.OutputConnector;
import com.codigoparallevar.minicards.types.functional.Tuple2;
2020-05-29 11:32:05 +00:00
import com.codigoparallevar.minicards.types.wireData.AnySignal;
2020-05-26 09:56:49 +00:00
import com.codigoparallevar.minicards.types.wireData.Signal;
import com.codigoparallevar.minicards.types.wireData.WireDataType;
import com.codigoparallevar.minicards.ui_helpers.DoAsync;
import com.programaker.api.ProgramakerApi;
2020-05-29 10:38:22 +00:00
import com.programaker.api.ProgramakerSignalListener;
import com.programaker.api.data.ProgramakerCustomBlock;
import com.programaker.api.data.ProgramakerCustomBlockArgument;
import com.programaker.api.data.ProgramakerCustomBlockSaveTo;
import com.programaker.api.data.ProgramakerFunctionCallResult;
2020-05-29 10:38:22 +00:00
import org.jetbrains.annotations.NotNull;
2020-05-26 09:56:49 +00:00
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
2020-05-29 10:38:22 +00:00
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
2020-05-29 10:38:22 +00:00
public class ProgramakerCustomBlockPart implements Part, ProgramakerSignalListener {
private static final int WIDTH_PADDING = 50;
private static final int HEIGHT_PADDING = 50;
private static final int IO_RADIUS = 50;
private static final int IO_PADDING = 20;
private static final String LogTag = "PM Custom block part";
private List<Tuple2<ConnectorTypeInfo, AnyRoundInputConnector>> inputConnectors = null;
private List<Tuple2<ConnectorTypeInfo, RoundOutputConnector>> outputConnectors = null;
private final PartGrid _partGrid;
private final String _id;
private final ProgramakerCustomBlock _block;
private int _left;
private int _top;
private int width = 100;
private int height = 100;
private String token = null;
private Object[] lastValues;
private boolean active = true;
2020-05-29 11:32:05 +00:00
private Tuple2<ConnectorTypeInfo, AnyRoundOutputConnector> saveToOutput;
private SignalRoundOutputConnector pulseOutput;
public ProgramakerCustomBlockPart(String id, PartGrid grid, Tuple2<Integer, Integer> center, ProgramakerCustomBlock block) {
this._id = id;
this._partGrid = grid;
this._block = block;
this.updateWidthHeight();
this._left = center.item1 - width / 2;
this._top = center.item2 - height / 2;
this.initialize();
}
private ProgramakerCustomBlockPart(String id, PartGrid grid, int left, int top, ProgramakerCustomBlock block) {
this._id = id;
this._partGrid = grid;
this._block = block;
this.updateWidthHeight();
this._left = left;
this._top = top;
this.initialize();
}
public ProgramakerCustomBlockPart(PartGrid grid, Tuple2<Integer, Integer> center, ProgramakerCustomBlock block) {
this(UUID.randomUUID().toString(), grid, center, block);
}
@Override
public int get_left() {
return _left;
}
@Override
public int get_right() {
return _left + width;
}
@Override
public int get_top() {
return _top;
}
@Override
public int get_bottom() {
return _top + height;
}
private Paint getTextPaint() {
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(Color.BLACK);
p.setTextSize(50);
return p;
}
private void updateWidthHeight() {
Paint p = getTextPaint();
String message = getMessage();
Rect bounds = new Rect();
p.getTextBounds(message, 0, message.length(), bounds);
this.height = bounds.height() + HEIGHT_PADDING * 2;
this.width = bounds.width() + WIDTH_PADDING * 2;
}
private void initialize() {
this.updatePorts();
lastValues = new Object[this.inputConnectors.size()];
}
private void updatePorts() {
2020-05-26 09:56:49 +00:00
final String type = this._block.getBlock_type() == null ? "" : this._block.getBlock_type();
final boolean has_pulse_input = type.equals("operation");
final boolean has_pulse_output = has_pulse_input || type.equals("trigger");
final boolean hasImplicitOutput = type.equals("getter");
2020-05-26 09:56:49 +00:00
final List<Tuple2<ConnectorTypeInfo, AnyRoundInputConnector>> inputs = new LinkedList<>();
final List<Tuple2<ConnectorTypeInfo, RoundOutputConnector>> outputs = new LinkedList<>();
2020-05-29 11:32:05 +00:00
SignalRoundOutputConnector pulseOutput = null;
// Add pulses
if (has_pulse_input) {
inputs.add(new Tuple2<>(new ConnectorTypeInfo(ConnectorTypeInfo.Type.PULSE),
new AnyRoundInputConnector(this, 0, 0,
IO_RADIUS))
);
}
if (has_pulse_output) {
2020-05-29 11:32:05 +00:00
pulseOutput = new SignalRoundOutputConnector(this, this._partGrid, 0, 0,
2020-05-26 09:56:49 +00:00
IO_RADIUS);
outputs.add(new Tuple2<>(new ConnectorTypeInfo(ConnectorTypeInfo.Type.PULSE),
2020-05-26 09:56:49 +00:00
pulseOutput)
);
}
// Add block IO
ProgramakerCustomBlockArgument savedTo = null;
if (_block.getArguments() != null) {
ProgramakerCustomBlockSaveTo saveTo = _block.getSave_to();
int skip = -1;
if (saveTo != null) {
if (saveTo.getType() != null
&& saveTo.getType().equals("argument")) {
skip = saveTo.getIndex();
}
else {
Log.w(LogTag, "Unknown save-to type=" + saveTo.getType());
}
}
int index = -1;
for (ProgramakerCustomBlockArgument arg : _block.getArguments()) {
index++;
if (skip == index) {
savedTo = arg;
}
else {
inputs.add(new Tuple2<>(ConnectorTypeInfo.FromTypeName(arg.getType()),
new AnyRoundInputConnector(this, 0, 0, IO_RADIUS)));
}
}
}
2020-05-29 11:32:05 +00:00
Tuple2<ConnectorTypeInfo, AnyRoundOutputConnector> saveToOutput = null;
2020-05-26 09:56:49 +00:00
if (savedTo != null) {
2020-05-26 09:56:49 +00:00
saveToOutput = new Tuple2<>(ConnectorTypeInfo.FromTypeName(savedTo.getType()),
2020-05-29 11:32:05 +00:00
new AnyRoundOutputConnector(this, this._partGrid, 0, 0, IO_RADIUS));
outputs.add(new Tuple2<>(saveToOutput.item1, saveToOutput.item2));
}
if (hasImplicitOutput) {
outputs.add(new Tuple2<>(ConnectorTypeInfo.FromTypeName(_block.getBlock_result_type()),
2020-05-29 11:32:05 +00:00
new AnyRoundOutputConnector(this, this._partGrid, 0, 0, IO_RADIUS)));
}
// Tie everything
inputConnectors = inputs;
outputConnectors = outputs;
2020-05-26 09:56:49 +00:00
this.saveToOutput = saveToOutput;
this.pulseOutput = pulseOutput;
this.updatePortPositions();
}
2020-05-29 10:38:22 +00:00
private void updatePortPositions() {
{
// Update inputs
int y = get_top();
int x = get_left() + IO_PADDING;
for (Tuple2<ConnectorTypeInfo, AnyRoundInputConnector> entry : inputConnectors) {
InputConnector input = entry.item2;
int new_x = x + IO_PADDING + IO_RADIUS / 2;
input.updatePosition(new_x, y);
x = x + IO_RADIUS * 2 + IO_PADDING * 2;
}
}
{
// Update outputs
int y = get_bottom();
int x = get_left() + IO_PADDING;
for (Tuple2<ConnectorTypeInfo, RoundOutputConnector> entry : outputConnectors) {
OutputConnector output = entry.item2;
int new_x = x + IO_PADDING + IO_RADIUS / 2;
output.updatePosition(new_x, y);
x = x + IO_RADIUS * 2 + IO_PADDING * 2;
}
}
}
private String getMessage() {
return this._block.getMessage();
}
@Override
public void touched() {
Log.i(LogTag, "Part touched (block_fun=" + this._block.getFunction_name() + ")");
}
@Override
public List<InputConnector> getInputConnectors() {
List<InputConnector> result = new ArrayList<>(inputConnectors.size());
for (Tuple2<ConnectorTypeInfo, AnyRoundInputConnector> entry : inputConnectors) {
result.add(entry.item2);
}
return result;
}
@Override
public List<OutputConnector> getOutputConnectors() {
List<OutputConnector> result = new ArrayList<>(outputConnectors.size());
for (Tuple2<ConnectorTypeInfo, RoundOutputConnector> entry : outputConnectors) {
result.add(entry.item2);
}
return result;
}
@Override
public JSONObject serialize() throws JSONException {
JSONObject serialized = new JSONObject();
serialized.put("id", _id);
serialized.put("left", _left);
serialized.put("top", _top);
JSONObject blockData = _block.serialize();
serialized.put("block", blockData);
2020-05-26 09:56:49 +00:00
serialized.put("on_string_output_connector", serializeConnectionEndpoints());
return serialized;
}
2020-05-26 09:56:49 +00:00
private JSONArray serializeConnectionEndpoints() {
JSONArray serializedData = new JSONArray();
for (OutputConnector output : getOutputConnectors()) {
JSONArray elements = new JSONArray();
for (Tuple2<String, String> endpoint : (List<Tuple2<String, String>>) output.getConnectionEndpoints()) {
elements.put(PartConnection.serializeToJson(endpoint.item1, endpoint.item2));
}
serializedData.put(elements);
}
return serializedData;
}
public static Tuple2<Part, List<PartConnection>> deserialize(PartGrid grid, JSONObject data) throws JSONException {
String id = data.getString("id");
int left = data.getInt("left");
int top = data.getInt("top");
JSONObject el = data.getJSONObject("block");
ProgramakerCustomBlock block = ProgramakerCustomBlock.deserialize(el);
ProgramakerCustomBlockPart part = new ProgramakerCustomBlockPart(id, grid, left, top, block);
List<PartConnection> connections = new LinkedList<>();
2020-05-26 09:56:49 +00:00
JSONArray allConnectorOuts = data.optJSONArray("on_string_output_connector");
if (allConnectorOuts == null) {
allConnectorOuts = new JSONArray();
}
for (int i = 0; i < allConnectorOuts.length(); i++) {
JSONArray connectorOuts = allConnectorOuts.getJSONArray(i);
Tuple2<ConnectorTypeInfo, RoundOutputConnector> connector = part.outputConnectors.get(i);
for (int j = 0; j < connectorOuts.length(); j++) {
connections.add(PartConnection.deserialize(
connector.item2, connectorOuts.getJSONObject(j)));
}
}
return new Tuple2<>(part, connections);
}
@Override
public void send(InputConnector inputConnector, WireDataType signal) {
2020-05-26 09:56:49 +00:00
// Log.i(LogTag, "Received signal from Input="+inputConnector);
ConnectorTypeInfo info = getConnectorInfo(inputConnector);
if (info == null) {
Log.e(LogTag, "Unknown connector" + inputConnector);
return;
}
if (info.get_type() == ConnectorTypeInfo.Type.PULSE) {
wrappedRunOperation();
}
else {
int index = getConnectorIndex(inputConnector);
if (index < 0) {
return;
}
this.lastValues[index] = signal.get();
}
}
private void wrappedRunOperation() {
2020-05-26 09:56:49 +00:00
Log.d(LogTag, "Running block function=" + this._block.getFunction_name());
String token = this.prepareStart();
if (token != null) {
try {
new DoAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
new Tuple2<>(() -> {
ProgramakerCustomBlockPart.this.runBlockOperation();
ProgramakerCustomBlockPart.this.freeBlock(token);
2020-05-26 15:43:19 +00:00
}, ex -> {
Log.w(LogTag, "Error executing function=" + this._block.getFunction_name()
2020-05-26 15:43:19 +00:00
+ "; Error=" + ex, ex);
ProgramakerCustomBlockPart.this.freeBlock(token);
}));
}
catch (Exception ex) {
Log.e(LogTag, "Error executing function=" + this._block.getFunction_name()
2020-05-26 15:43:19 +00:00
+ "; Error=" + ex, ex);
this.freeBlock(token);
}
} else {
// An execution is in progress, so nothing is done
}
}
private ProgramakerFunctionCallResult runBlockOperation() {
if (!this.active) {
Log.w(LogTag, "Trying to run inactive block function=" + this._block.getFunction_name());
return null;
}
ProgramakerApi api = this._partGrid.getApi();
List<String> arguments = new LinkedList<>();
int index = -1;
for (Tuple2<ConnectorTypeInfo, AnyRoundInputConnector> entry : inputConnectors) {
index++;
if (entry.item1.get_type() == ConnectorTypeInfo.Type.PULSE) {
continue;
}
Object queriedValue = entry.item2.query(lastValues[index]);
if (queriedValue != null) {
lastValues[index] = queriedValue;
}
if (lastValues[index] == null) {
arguments.add(null); // TODO: Get default value from block definition
}
else {
arguments.add(lastValues[index].toString()); // TODO: Do proper type formatting
}
}
ProgramakerFunctionCallResult result = api.callBlock(this._block, arguments);
Log.i(LogTag, "Execution result="+result.getResult());
2020-05-26 09:56:49 +00:00
onExecutionCompleted(result);
return result;
2020-05-26 09:56:49 +00:00
}
private void onExecutionCompleted(ProgramakerFunctionCallResult result) {
2020-05-29 11:32:05 +00:00
Tuple2<ConnectorTypeInfo, AnyRoundOutputConnector> savedTo = this.saveToOutput;
2020-05-26 09:56:49 +00:00
if (savedTo != null) {
2020-05-29 11:32:05 +00:00
Object value = null;
if (result != null) {
result.getResult();
}
savedTo.item2.send(new AnySignal(value));
2020-05-26 09:56:49 +00:00
}
2020-05-29 11:32:05 +00:00
SignalRoundOutputConnector pulseOutput = this.pulseOutput;
2020-05-26 09:56:49 +00:00
if (pulseOutput != null) {
pulseOutput.send(new Signal());
}
// Notify screen that there's updates
this._partGrid.update();
}
private void freeBlock(String token) {
tryUpdateBlock(token, null);
}
private String prepareStart() {
String token = UUID.randomUUID().toString();
if (tryUpdateBlock(null, token)) {
return token;
}
return null;
}
private synchronized boolean tryUpdateBlock(String oldVal, String newVal) {
if (this.token == oldVal) {
this.token = newVal;
return true;
}
return false;
}
@Override
public String get_id() {
return _id;
}
@Override
public InputConnector getConnectorWithId(String inputConnectorId) {
if (inputConnectorId.startsWith("index=")) {
String[] chunks = inputConnectorId.split("=");
if (chunks.length > 1) {
Integer index = null;
try {
index = Integer.parseInt(chunks[1]);
}
catch (NumberFormatException ex) {
2020-05-26 15:43:19 +00:00
Log.e(LogTag, "Error parsing connector id="+inputConnectorId, ex);
}
if (index != null && index < inputConnectors.size()) {
return inputConnectors.get(index).item2;
}
}
}
return null;
}
private int getConnectorIndex(InputConnector inputConnector) {
int index = 0;
for (Tuple2<ConnectorTypeInfo, AnyRoundInputConnector> entry : inputConnectors) {
if (entry.item2 == inputConnector) {
return index;
}
index++;
}
return -1;
}
@Override
public String getConnectorId(InputConnector inputConnector) {
int index = getConnectorIndex(inputConnector);
if (index < 0 ) {
return null;
}
return "index=" + index;
}
public ConnectorTypeInfo getConnectorInfo(InputConnector inputConnector) {
for (Tuple2<ConnectorTypeInfo, AnyRoundInputConnector> entry : inputConnectors) {
if (entry.item2 == inputConnector) {
return entry.item1;
}
}
return null;
}
@Override
public void resume() {
this.active = true;
2020-05-29 10:38:22 +00:00
String type = _block.getBlock_type();
if (type != null && (type.equals("trigger"))) {
// Listen to signal
ProgramakerApi api = _partGrid.getApi();
if (api == null) {
Log.e(LogTag, "Cannot listen to API (API not found)");
return;
}
new DoAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
new Tuple2<>(
() -> {
_partGrid.getListenerManager().registerSignalListener(
_block.getBridge_id(), _block.getKey(),
this);
},
ex -> {
Log.e(LogTag, "Error establishing connection to monitor", ex);
}
));
}
}
@Override
public void onNewSignal(@NotNull String bridgeId, @NotNull String key, @NotNull HashMap<?, ?> signal) {
// Propagate signal
// Stream object on save_to, then trigger pulse
Object content = signal.get("content");
if (this.saveToOutput != null) {
2020-05-29 11:32:05 +00:00
this.saveToOutput.item2.send(new AnySignal(content));
2020-05-29 10:38:22 +00:00
}
if (this.pulseOutput != null) {
this.pulseOutput.send(new Signal());
}
_partGrid.update();
}
@Override
public void pause() {
this.active = false;
2020-05-29 10:38:22 +00:00
String type = _block.getBlock_type();
if (type != null && (type.equals("trigger"))) {
// Release listening of signal
ProgramakerApi api = _partGrid.getApi();
if (api == null) {
return;
}
new DoAsync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
new Tuple2<>(
() -> {
_partGrid.getListenerManager().unregisterSignalListener(this);
},
ex -> {
Log.e(LogTag, "Error disconnecting from monitor", ex);
}
));
}
}
@Override
public void draw(ScrolledCanvas canvas, boolean devMode) {
updateWidthHeight(); // TODO: Remove after the calculations have stabilized
if (!devMode) {
return; // Logic block, don't show on user-mode
}
boolean operationInProgress = this.token != null;
drawConnectors(canvas);
2020-05-26 09:56:49 +00:00
drawWires(canvas, devMode);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
if (operationInProgress) {
paint.setColor(Color.YELLOW);
}
else {
paint.setColor(Color.WHITE);
}
canvas.drawRect(
new Rect(get_left(), get_top(),
get_right(), get_bottom()),
paint);
Paint textPaint = getTextPaint();
canvas.drawText(getMessage(),
get_left() + WIDTH_PADDING,
get_bottom() - HEIGHT_PADDING,
textPaint);
}
private void drawConnectors(ScrolledCanvas canvas) {
if (inputConnectors != null) {
for (Tuple2<ConnectorTypeInfo, AnyRoundInputConnector> entry : inputConnectors) {
Paint outerInputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
outerInputConnectorPaint.setColor(entry.item1.getOuterColor());
canvas.drawCircle(
entry.item2.getX(),
entry.item2.getY(),
entry.item2.getRadius(),
outerInputConnectorPaint);
Paint innerInputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
innerInputConnectorPaint.setColor(entry.item1.getInnerColor());
canvas.drawCircle(
entry.item2.getX(),
entry.item2.getY(),
entry.item2.getRadius() / 2,
innerInputConnectorPaint);
}
}
if (outputConnectors != null) {
for (Tuple2<ConnectorTypeInfo, RoundOutputConnector> entry : outputConnectors) {
Paint outerOutputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
outerOutputConnectorPaint.setColor(entry.item1.getOuterColor());
canvas.drawCircle(
entry.item2.getX(),
entry.item2.getY(),
entry.item2.getRadius(),
outerOutputConnectorPaint);
Paint innerOutputConnectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
innerOutputConnectorPaint.setColor(entry.item1.getInnerColor());
canvas.drawCircle(
entry.item2.getX(),
entry.item2.getY(),
entry.item2.getRadius() / 2,
innerOutputConnectorPaint);
}
}
}
2020-05-26 09:56:49 +00:00
private void drawWires(ScrolledCanvas canvas, boolean devMode) {
for (OutputConnector outputConnector : getOutputConnectors()){
outputConnector.drawWires(canvas, devMode);
}
}
@Override
public void moveEnd(int x, int y) {
_left = x - width / 2;
_top = y - height / 2;
this.updatePortPositions();
}
@Override
public void drop(int x, int y) {
moveEnd(x, y);
}
@Override
public boolean containsPoint(int x, int y) {
return ((x >= this.get_left()) && (x <= this.get_right())
&& (y >= this.get_top()) && (y <= this.get_bottom()));
}
@Override
public Moveable getMoveable() {
return this;
}
@Override
public void unlink() {
2020-05-29 10:38:22 +00:00
pause();
for (InputConnector input : getInputConnectors()) {
input.unlink();
}
}
@Override
public Object query(Object lastValue) {
String blockType = _block.getBlock_type();
if ((blockType == null) || (!blockType.equals("getter"))) {
// Only relevant for getters
return null;
}
String token = this.prepareStart();
if (token != null) {
try {
_partGrid.update();
ProgramakerFunctionCallResult result = runBlockOperation();
if (result != null) {
return result.getResult();
}
}
finally {
this.freeBlock(token);
}
}
return null;
}
}