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; import com.codigoparallevar.minicards.parts.connectors.ConnectorTypeInfo; import com.codigoparallevar.minicards.parts.connectors.RoundOutputConnector; 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; 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; import com.programaker.api.data.ProgramakerCustomBlock; import com.programaker.api.data.ProgramakerCustomBlockArgument; import com.programaker.api.data.ProgramakerCustomBlockSaveTo; import com.programaker.api.data.ProgramakerFunctionCallResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.UUID; public class ProgramakerCustomBlockPart implements Part { 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> inputConnectors = null; private List> 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; private Tuple2 saveToOutput; private RoundOutputConnector pulseOutput; public ProgramakerCustomBlockPart(String id, PartGrid grid, Tuple2 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 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() { 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"); final List> inputs = new LinkedList<>(); final List> outputs = new LinkedList<>(); RoundOutputConnector 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) { pulseOutput = new RoundOutputConnector(this, this._partGrid, 0, 0, IO_RADIUS); outputs.add(new Tuple2<>(new ConnectorTypeInfo(ConnectorTypeInfo.Type.PULSE), 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))); } } } Tuple2 saveToOutput = null; if (savedTo != null) { saveToOutput = new Tuple2<>(ConnectorTypeInfo.FromTypeName(savedTo.getType()), new RoundOutputConnector(this, this._partGrid, 0, 0, IO_RADIUS)); outputs.add(saveToOutput); } if (hasImplicitOutput) { outputs.add(new Tuple2<>(ConnectorTypeInfo.FromTypeName(_block.getBlock_result_type()), new RoundOutputConnector(this, this._partGrid, 0, 0, IO_RADIUS))); } // Tie everything inputConnectors = inputs; outputConnectors = outputs; this.saveToOutput = saveToOutput; this.pulseOutput = pulseOutput; this.updatePortPositions(); } private void updatePortPositions() { { // Update inputs int y = get_top(); int x = get_left() + IO_PADDING; for (Tuple2 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 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 getInputConnectors() { List result = new ArrayList<>(inputConnectors.size()); for (Tuple2 entry : inputConnectors) { result.add(entry.item2); } return result; } @Override public List getOutputConnectors() { List result = new ArrayList<>(outputConnectors.size()); for (Tuple2 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); serialized.put("on_string_output_connector", serializeConnectionEndpoints()); return serialized; } private JSONArray serializeConnectionEndpoints() { JSONArray serializedData = new JSONArray(); for (OutputConnector output : getOutputConnectors()) { JSONArray elements = new JSONArray(); for (Tuple2 endpoint : (List>) output.getConnectionEndpoints()) { elements.put(PartConnection.serializeToJson(endpoint.item1, endpoint.item2)); } serializedData.put(elements); } return serializedData; } public static Tuple2> 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 connections = new LinkedList<>(); 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 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) { // 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() { 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); }, ex -> { Log.e(LogTag, "Error executing function=" + this._block.getFunction_name() + "; Error=" + ex, ex); ProgramakerCustomBlockPart.this.freeBlock(token); })); } catch (Exception ex) { Log.e(LogTag, "Error executing function=" + this._block.getFunction_name() + "; Error=" + ex, ex); this.freeBlock(token); } } else { // An execution is in progress, so nothing is done } } private void runBlockOperation() { if (!this.active) { Log.w(LogTag, "Trying to run inactive block function=" + this._block.getFunction_name()); return; } ProgramakerApi api = this._partGrid.getApi(); List arguments = new LinkedList<>(); int index = -1; for (Tuple2 entry : inputConnectors) { index++; if (entry.item1.get_type() == ConnectorTypeInfo.Type.PULSE) { continue; } arguments.add(lastValues[index].toString()); // TODO: Do proper type formatting } ProgramakerFunctionCallResult result = api.callBlock(this._block, arguments); Log.d(LogTag, "Execution result="+result.getResult()); onExecutionCompleted(result); } private void onExecutionCompleted(ProgramakerFunctionCallResult result) { Tuple2 savedTo = this.saveToOutput; if (savedTo != null) { // TODO: Fix output typing // savedTo.item2.send((WireDataType) () -> result.getResult()); } RoundOutputConnector pulseOutput = this.pulseOutput; if (pulseOutput != null) { pulseOutput.send(new Signal()); } } 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) { 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 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 entry : inputConnectors) { if (entry.item2 == inputConnector) { return entry.item1; } } return null; } @Override public void resume() { this.active = true; } @Override public void pause() { this.active = false; } @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 } drawConnectors(canvas); drawWires(canvas, devMode); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 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 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 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); } } } 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() { this.active = false; for (InputConnector input : getInputConnectors()) { input.unlink(); } } }