package nl.utwente.viskell.ui.components; import java.util.Map; import java.util.Optional; import java.util.Set; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.geometry.Point2D; import javafx.scene.paint.Color; import javafx.scene.shape.CubicCurve; import javafx.scene.transform.Transform; import nl.utwente.viskell.haskell.expr.LetExpression; import nl.utwente.viskell.haskell.type.*; import nl.utwente.viskell.ui.BlockContainer; import nl.utwente.viskell.ui.ComponentLoader; import nl.utwente.viskell.ui.serialize.Bundleable; import com.google.common.collect.ImmutableMap; /** * This is a Connection that represents a flow between an {@link InputAnchor} * and {@link OutputAnchor}. Both anchors are stored referenced respectively as * startAnchor and endAnchor {@link Optional} within this class. * Visually a connection is represented as a cubic Bezier curve. * * Connection is also a changeListener for a Transform, in order to be able to * update the Line's position when the anchor's positions change. */ public class Connection extends CubicCurve implements ChangeListener<Transform>, Bundleable, ComponentLoader { /** * Control offset for this bezier curve of this line. * It determines how a far a line attempts to goes straight from its end points. */ public static final double BEZIER_CONTROL_OFFSET = 150f; /** * Labels for serialization to and from JSON */ private static final String SOURCE_LABEL = "from"; private static final String SINK_LABEL = "to"; /** Starting point of this Line that can be Anchored onto other objects. */ private final OutputAnchor startAnchor; /** Ending point of this Line that can be Anchored onto other objects. */ private final InputAnchor endAnchor; /** Whether this connection produced an error in the latest type unification. */ private boolean errorState; /** Whether this connection is impossible due to scope restrictions */ private boolean scopeError; /** * Construct a new Connection. * @param source The OutputAnchor this connection comes from * @param sink The InputAnchor this connection goes to */ public Connection(OutputAnchor source, InputAnchor sink) { this.setMouseTransparent(true); this.setFill(null); this.startAnchor = source; this.endAnchor = sink; this.errorState = false; this.scopeError = false; source.getPane().addConnection(this); this.invalidateAnchorPositions(); this.startAnchor.addConnection(this); this.startAnchor.localToSceneTransformProperty().addListener(this); this.endAnchor.setConnection(this); this.endAnchor.localToSceneTransformProperty().addListener(this); // typecheck the new connection to mark potential errors at the best location try { TypeChecker.unify("new connection", this.startAnchor.getType(Optional.of(this)), this.endAnchor.getType()); } catch (HaskellTypeError e) { this.endAnchor.setErrorState(true); this.errorState = true; } } /** * @return the output anchor of this connection. */ public OutputAnchor getStartAnchor() { return this.startAnchor; } /** * @return the input anchor of this connection. */ public InputAnchor getEndAnchor() { return this.endAnchor; } /** * Handles the upward connections changes through an connection. * Also perform typechecking for this connection. * @param finalPhase whether the change propagation is in the second (final) phase. */ public void handleConnectionChangesUpwards(boolean finalPhase) { // first make sure the output anchor block and types are fresh if (!finalPhase) { this.startAnchor.prepareConnectionChanges(); } // for connections in error state typechecking is delayed to the final phase to keep error locations stable if (finalPhase == this.errorState) { try { // first a trial unification on a copy of the types to minimize error propagation TypeScope scope = new TypeScope(); TypeChecker.unify("trial connection", this.startAnchor.getType(Optional.of(this)).getFresh(scope), this.endAnchor.getType().getFresh(scope)); // unify the actual types TypeChecker.unify("connection", this.startAnchor.getType(Optional.of(this)), this.endAnchor.getType()); this.endAnchor.setErrorState(false); this.errorState = false; } catch (HaskellTypeError e) { this.endAnchor.setErrorState(true); this.errorState = true; } } // continue with propagating connections changes in the output anchor block this.startAnchor.handleConnectionChanges(finalPhase); } /** * Removes this Connection, disconnecting its anchors and removing this Connection from the pane it is on. */ public final void remove() { this.startAnchor.localToSceneTransformProperty().removeListener(this); this.endAnchor.localToSceneTransformProperty().removeListener(this); this.startAnchor.dropConnection(this); this.endAnchor.removeConnections(); this.startAnchor.getPane().removeConnection(this); // propagate the connection changes of both anchors simultaneously in two phases to avoid duplicate work this.startAnchor.handleConnectionChanges(false); this.endAnchor.handleConnectionChanges(false); this.startAnchor.handleConnectionChanges(true); this.endAnchor.handleConnectionChanges(true); } @Override public final void changed(ObservableValue<? extends Transform> observable, Transform oldValue, Transform newValue) { this.invalidateAnchorPositions(); } /** Update the UI positions of both start and end anchors. */ private void invalidateAnchorPositions() { this.setStartPosition(this.startAnchor.getAttachmentPoint()); this.setEndPosition(this.endAnchor.getAttachmentPoint()); } @Override public String toString() { return "Connection connecting \n(out) " + startAnchor + " to\n(in) " + endAnchor; } @Override public Map<String, Object> toBundle() { ImmutableMap.Builder<String, Object> bundle = ImmutableMap.builder(); bundle.put(SOURCE_LABEL, this.startAnchor.toBundle()); bundle.put(SINK_LABEL, this.endAnchor.toBundle()); return bundle.build(); } public static void fromBundle(Map<String,Object> connectionBundle, Map<Integer, Block> blockLookupTable) { Map<String,Object> source = (Map<String,Object>)connectionBundle.get(SOURCE_LABEL); Integer sourceId = ((Double)source.get(ConnectionAnchor.BLOCK_LABEL)).intValue(); Block sourceBlock = blockLookupTable.get(sourceId); OutputAnchor sourceAnchor = sourceBlock.getAllOutputs().get(0); Map<String,Object> sink = (Map<String,Object>)connectionBundle.get(SINK_LABEL); Integer sinkId = ((Double)sink.get(ConnectionAnchor.BLOCK_LABEL)).intValue(); Integer sinkAnchorNumber = ((Double)sink.get(ConnectionAnchor.ANCHOR_LABEL)).intValue(); Block sinkBlock = blockLookupTable.get(sinkId); InputAnchor sinkAnchor = sinkBlock.getAllInputs().get(sinkAnchorNumber); Connection connection = new Connection(sourceAnchor, sinkAnchor); connection.invalidateVisualState(); sinkBlock.invalidateVisualState(); } /** * Sets the start coordinates for this Connection. * @param point Coordinates local to this Line's parent. */ private void setStartPosition(Point2D point) { this.setStartX(point.getX()); this.setStartY(point.getY()); updateBezierControlPoints(this); } /** * Sets the end coordinates for this Connection. * @param point coordinates local to this Line's parent. */ private void setEndPosition(Point2D point) { this.setEndX(point.getX()); this.setEndY(point.getY()); updateBezierControlPoints(this); } /** Returns the current bezier offset based on the current start and end positions. */ private static double getBezierYOffset(CubicCurve wire) { double distX = Math.abs(wire.getEndX() - wire.getStartX())/3; double diffY = wire.getEndY() - wire.getStartY(); double distY = diffY > 0 ? diffY/2 : Math.max(0, -diffY-10); if (distY < BEZIER_CONTROL_OFFSET) { if (distX < BEZIER_CONTROL_OFFSET) { // short lines are extra flexible return Math.max(1, Math.max(distX, distY)); } else { return BEZIER_CONTROL_OFFSET; } } else { return Math.cbrt(distY / BEZIER_CONTROL_OFFSET) * BEZIER_CONTROL_OFFSET; } } /** Updates the Bezier offset (curviness) according to the current start and end positions. */ protected static void updateBezierControlPoints(CubicCurve wire) { double yOffset = getBezierYOffset(wire); wire.setControlX1(wire.getStartX()); wire.setControlY1(wire.getStartY() + yOffset); wire.setControlX2(wire.getEndX()); wire.setControlY2(wire.getEndY() - yOffset); } protected static double lengthSquared(CubicCurve wire) { double diffX = wire.getStartX() - wire.getEndX(); double diffY = wire.getStartY() - wire.getEndY(); return diffX*diffX + diffY*diffY; } /** * Extends the expression graph to include all subexpression required * @param exprGraph the let expression representing the current expression graph * @param container the container to which this expression graph is constrained * @param outsideAnchors a mutable set of required OutputAnchors from a surrounding container */ protected void extendExprGraph(LetExpression exprGraph, BlockContainer container, Set<OutputAnchor> outsideAnchors) { OutputAnchor anchor = this.getStartAnchor(); if (container == anchor.getContainer()) anchor.extendExprGraph(exprGraph, container, outsideAnchors); else outsideAnchors.add(anchor); } public void invalidateVisualState() { this.scopeError = !this.endAnchor.getContainer().isContainedWithin(this.startAnchor.getContainer()); if (this.errorState) { this.setStroke(Color.RED); this.getStrokeDashArray().clear(); this.setStrokeWidth(3); } else if (this.scopeError) { this.setStroke(Color.RED); this.setStrokeWidth(3); if (this.getStrokeDashArray().isEmpty()) { this.getStrokeDashArray().addAll(10.0, 10.0); } } else { this.setStroke(Color.BLACK); this.getStrokeDashArray().clear(); this.setStrokeWidth(calculateTypeWidth(this.endAnchor.getType())); } } private static int calculateTypeWidth(Type wireType) { Type type = wireType.getConcrete(); int fcount = 0; while (type instanceof FunType) { fcount++; type = ((FunType)type).getResult(); } if (fcount > 0) { return 4 + 2*fcount; } int arity = 0; while (type instanceof TypeApp) { arity++; type = ((TypeApp)type).getTypeFun(); } if (type instanceof ListTypeCon) { return 5; } return 3 + arity; } public boolean hasTypeError() { return this.errorState; } public boolean hasScopeError() { return this.scopeError; } }