0
2

I am trying to implement a curved edge and realize I am out of depth. In short, I don't understand how the label position relates to the bounds and way points of the edge geometry.

The way I want the edge to work is like a (normal) straight line, only that the edge curves through the label. When the label is moved, the curve is adjusted so that it still passes through the label.

After a couple of different attempts, I have ended up with the following solution:

I have created a subclass of mxEdgeHandler, which overrides moveLabelTo(). This method does what the original version does and also makes sure that the geometry of the edgeState contains a point where the label was dropped.

@Override
protected void moveLabelTo(mxCellState edgeState, double x, double y)
{
    mxGraph graph = graphComponent.getGraph();
    mxIGraphModel model = graph.getModel();
    mxGeometry geometry = model.getGeometry(state.getCell());

    if (geometry != null)
    {
        geometry = (mxGeometry) geometry.clone();
        // Resets the relative location stored inside the geometry
        mxPoint pt = graph.getView().getRelativePoint(edgeState, x, y);
        geometry.setX(pt.getX());
        geometry.setY(pt.getY())
        // Resets the offset inside the geometry to find the offset
        // from the resulting point
        double scale = graph.getView().getScale();
        geometry.setOffset(new mxPoint(0, 0));
        pt = graph.getView().getPoint(edgeState, geometry);
        geometry.setOffset(new mxPoint(Math.round((x - pt.getX()) / scale),
            Math.round((y - pt.getY()) / scale)));

        /** This is my stuff */
        mxPoint labelPoint = new mxPoint( x, y );
        List<mxPoint> points = geometry.getPoints();

        if( points == null )
        {
            points = new ArrayList<mxPoint>();
            points.add( labelPoint );
            geometry.setPoints(points);
        }
        else
        {
            points.set( 0, labelPoint );
        }

        model.setGeometry(edgeState.getCell(), geometry);
    }
}

I have created a subclass of mxConnectorShape. The paintShape() method checks if the edge has more than two points, and then draws a curve through them. If only two points are available, the super class draws the shape.

Initially I have an edge, containing only two points:

alt text

I have added code to draw all the way points of the edge to be sure everything is ok. Selecting and moving the label of the edge, I end up with this:

alt text

The new way point is where the label was dropped. The label positions itself a bit away from the way point.

Moving the way point again:

alt text

Moving one of the vertices

alt text

Moving the label:

alt text

What dogs me is that the label positioning is so erratic. Also that when the label is moved and dropped, it does not end up where the mouse cursor is.

asked 01 Mar '12, 07:25

Earthumb's gravatar image

Earthumb
2616
accept rate: 16%

edited 07 May '12, 17:09

David's gravatar image

David
4.9k21831


I would probably override mxGraphView.getPoint as follows:

mxGraph graph = new mxGraph()
{
  protected mxGraphView createGraphView()
  {
    return new mxGraphView(this)
    {
      public mxPoint getPoint(mxCellState state,
          mxGeometry geometry)
      {
        double x = state.getCenterX();
        double y = state.getCenterY();
        if (state.getAbsolutePointCount() == 3)
        {
          mxPoint mid = state.getAbsolutePoint(1);
          x = mid.getX();
          y = mid.getY();
        }

        return new mxPoint(x, y);
      }
    };
  }
};
link

answered 01 Mar '12, 21:46

Gaudenz's gravatar image

Gaudenz
80.1k1310
accept rate: 39%

edited 07 May '12, 17:13

David's gravatar image

David
4.9k21831

Fantastic, it worked. But why? What does getPoint do?

(02 Mar '12, 01:33) Earthumb

It returns the position of the label (given as an mxGeometry) on an edge (given as an mxCellState). Feel free to read the source code, it is included in the distribution.

link

answered 02 Mar '12, 01:40

Gaudenz's gravatar image

Gaudenz
80.1k1310
accept rate: 39%

I have been reading the source code until my eyes are bleeding, missed the javadoc here. Sorry about that.

(02 Mar '12, 02:25) Earthumb

Is the approach I chose a good one? Is there an approach which would involve less subclassing?

I have seen others asking for curved edges and would gladly share my code if it could be made less hardwired.

(02 Mar '12, 02:31) Earthumb

The edges I have will sometimes extend outside of the box made up of the control points and the label. This causes problems with the repaint dirty-rect. Is there a way to extend the bounds of the edge with the path describing the curve?

link

answered 02 Mar '12, 02:35

Earthumb's gravatar image

Earthumb
2616
accept rate: 16%

edited 07 May '12, 17:13

David's gravatar image

David
4.9k21831

I think the approach is a good one, the problem with the subclassing is that the architecture is based on a JavaScript implementation, which requires less subclassing to override stuff.

To update the diry-rect for the edge, override mxGraphView.updateBoundingBox (and use mxCellState.get/setBoundingBox).

(02 Mar '12, 02:37) Gaudenz

Set the edge style to:

shape=curve

to use the in-built curve. That also supports the label being drawn along the curve.

link

answered 02 Mar '12, 03:07

David's gravatar image

David
4.9k21831
accept rate: 47%

edited 07 May '12, 17:10

Hahaha, that is so mean. I've spent days trying to get this to work. Well, I learnt plenty about the jGraph internals.

\n

The (existing) curve shape has some problems, the bounding box does not encompass the full shape and the end arrows are a bit twisted.

\n

Here is my version, in case anybody is interested. It requires an override of mxGraphView (as suggested by Gaudenz) to work properly.

\n
class CurveGraphView extends mxGraphView {\npublic CurveGraphView(mxGraph graph) {\n    super(graph);\n}\n
\n

/* Only override this if you want the label to automatically position itself on the control point /\n @Override\n public mxPoint getPoint(mxCellState state, mxGeometry geometry) {\n double x = state.getCenterX();\n double y = state.getCenterY();

\n
    if (state.getAbsolutePointCount() == 3)\n    {\n        mxPoint mid = state.getAbsolutePoint(1);\n        x = mid.getX();\n        y = mid.getY();\n    }\n\n    return new mxPoint(x, y);\n}\n
\n

/* Makes sure that the full path of the curve is included in the bounding box /\n @Override\n public mxRectangle updateBoundingBox(mxCellState state)\n {\n mxRectangle bounds = super.updateBoundingBox(state);

\n
    Object style = state.getStyle().get( "edgeStyle" );\n\n    List<mxPoint> points = state.getAbsolutePoints();\n\n    if( CurvedEdgeStyle.KEY.equals( style ) && points != null && points.size() == 3 ) {\n        Rectangle pathBounds = CurvedShape.createPath(state.getAbsolutePoints()).getBounds();\n\n        Rectangle union = bounds.getRectangle().union( pathBounds );\n\n        bounds = new mxRectangle(union);\n        state.setBoundingBox(bounds);\n    }\n\n    return bounds;\n}\n
\n

}

\n

class CurvedShape extends mxConnectorShape {\n public static final String KEY = "curvedEdge";\n private GeneralPath path;

\n
@Override\npublic void paintShape(mxGraphics2DCanvas canvas, mxCellState state)\n{\n    List<mxPoint> abs = state.getAbsolutePoints();\n    int n = state.getAbsolutePointCount();\n\n    if( n < 3 ) {\n        super.paintShape(canvas, state);\n    } else if (configureGraphics(canvas, state, false) ) {\n        Graphics2D g = canvas.getGraphics();\n        path = createPath( abs );\n\n        g.draw( path );\n\n        paintMarker(canvas, state, false);\n        paintMarker(canvas, state, true);\n    }\n}\n
\n

/* Code borrowed from here: http://www.codeproject.com/Articles/31859/Draw-a-Smooth-Curve-through-a-Set-of-2D-Points-wit /\n public static GeneralPath createPath(List<mxpoint> abs) {\n mxPoint[] knots = abs.toArray( new mxPoint[abs.size()]);

\n
    int n = knots.length - 1;\n    mxPoint[] firstControlPoints = new mxPoint[n];\n    mxPoint[] secondControlPoints = new mxPoint[n];\n\n    // Calculate first Bezier control points\n    // Right hand side vector\n    double[] rhs = new double[n];\n\n    // Set right hand side X values\n    for (int i = 1; i < n - 1; ++i) {\n        rhs[i] = 4 * knots[i].getX() + 2 * knots[i + 1].getX();\n    }\n    rhs[0] = knots[0].getX() + 2 * knots[1].getX();\n    rhs[n - 1] = (8 * knots[n - 1].getX() + knots[n].getX()) / 2.0;\n    // Get first control points X-values\n    double[] x = getFirstControlPoints(rhs);\n\n    // Set right hand side Y values\n    for (int i = 1; i < n - 1; ++i) {\n        rhs[i] = 4 * knots[i].getY() + 2 * knots[i + 1].getY();\n    }\n    rhs[0] = knots[0].getY() + 2 * knots[1].getY();\n    rhs[n - 1] = (8 * knots[n - 1].getY() + knots[n].getY()) / 2.0;\n    // Get first control points Y-values\n    double[] y = getFirstControlPoints(rhs);\n\n    // Fill output arrays.\n    for (int i = 0; i < n; ++i)\n    {\n        // First control point\n        firstControlPoints[i] = new mxPoint(x[i], y[i]);\n        // Second control point\n        if (i < n - 1) {\n            secondControlPoints[i] = new mxPoint(2 * knots\n                [i + 1].getX() - x[i + 1], 2 *\n                knots[i + 1].getY() - y[i + 1]);\n        } else {\n            secondControlPoints[i] = new mxPoint((knots\n                [n].getX() + x[n - 1]) / 2,\n                (knots[n].getY() + y[n - 1]) / 2);\n        }\n    }\n\n    GeneralPath path = new GeneralPath();\n    path.moveTo(knots[0].getX(), knots[0].getY());\n    for( int i=1; i<n+1; i++ ) {\n        path.curveTo(\n            firstControlPoints[i-1].getX(), firstControlPoints[i-1].getY(),\n            secondControlPoints[i-1].getX(), secondControlPoints[i-1].getY(),\n            knots[i].getX(), knots[i].getY() );\n    }\n\n    return path;\n}\n\n/// <summary>\n/// Solves a tridiagonal system for one of coordinates (x or y)\n/// of first Bezier control points.\n/// </summary>\n/// <param name="rhs">Right hand side vector.</param>\n/// <returns>Solution vector.</returns>\nprivate static double[] getFirstControlPoints(double[] rhs)\n{\n    int n = rhs.length;\n    double[] x = new double[n]; // Solution vector.\n    double[] tmp = new double[n]; // Temp workspace.\n\n    double b = 2.0;\n    x[0] = rhs[0] / b;\n    for (int i = 1; i < n; i++) // Decomposition and forward substitution.\n    {\n        tmp[i] = 1 / b;\n        b = (i < n - 1 ? 4.0 : 3.5) - tmp[i];\n        x[i] = (rhs[i] - x[i - 1]) / b;\n    }\n    for (int i = 1; i < n; i++) {\n        x[n - i - 1] -= tmp[n - i] * x[n - i]; // Backsubstitution.\n    }\n\n    return x;\n}\n\[email protected]\nprotected mxLine getMarkerVector(List<mxPoint> points, boolean source, double markerSize) {\n    if( path == null || points.size() < 3 ) {\n        return super.getMarkerVector(points, source, markerSize);\n    }\n    double coords[] = new double[6];\n\n    double x0=0;\n    double y0=0;\n    double x1=0;\n    double y1=0;\n\n    PathIterator p = path.getPathIterator(null, 2.0);\n\n    if( source ) {\n        p.currentSegment(coords);\n        x1 = coords[0];\n        y1 = coords[1];\n        p.next();\n        p.currentSegment(coords);\n        x0 = coords[0];\n        y0 = coords[1];\n    } else {\n        while (!p.isDone()) {\n            p.currentSegment(coords);\n            x0 = x1;\n            y0 = y1;\n            x1 = coords[0];\n            y1 = coords[1];\n            p.next();\n        }\n    }\n\n    return new mxLine(x0,y0,new mxPoint(x1,y1));\n}\n
\n

}

\n

class CurvedEdgeStyle implements mxEdgeStyle.mxEdgeStyleFunction {\n public static final String KEY = "curvedEdgeStyle";

\n
@Override\npublic void apply(mxCellState state, mxCellState source, mxCellState target, List<mxPoint> points, List<mxPoint> result) {\n    mxPoint pt = (points != null && points.size() > 0) ? points.get(0): null;\n\n    if (source != null && target != null)\n    {\n        if( pt != null ) {\n            result.add( pt );\n        } else {\n            double x = (target.getCenterX() + source.getCenterX())/2;\n            double y = (target.getCenterY() + source.getCenterY())/2;\n\n            mxPoint point = new mxPoint( x, y );\n\n            result.add( point );\n        }\n    }\n}\n
\n

}

\n

public class HelloCurve extends JFrame\n{\n public HelloCurve()\n {\n super("Hello, Curve!");

\n
    mxGraphics2DCanvas.putShape(CurvedShape.KEY, new CurvedShape());\n    mxStyleRegistry.putValue(CurvedEdgeStyle.KEY, new CurvedEdgeStyle() );\n\n    mxGraph graph = new mxGraph() {\n        @Override\n        protected mxGraphView createGraphView() {\n            return new CurveGraphView(this);\n        }\n    };\n\n    graph.getStylesheet().getDefaultEdgeStyle().put( mxConstants.STYLE_SHAPE, CurvedShape.KEY );\n    graph.getStylesheet().getDefaultEdgeStyle().put( mxConstants.STYLE_EDGE, CurvedEdgeStyle.KEY );\n    graph.getStylesheet().getDefaultEdgeStyle().put( mxConstants.STYLE_STARTARROW, mxConstants.ARROW_CLASSIC );\n\n    graph.setMultigraph(true);\n    graph.setAllowDanglingEdges(false);\n    graph.setAllowLoops(false);\n    graph.setAutoSizeCells(true);\n\n// I want the label to always stay on top of the control point \n    graph.setEdgeLabelsMovable(false);\n\n    Object parent = graph.getDefaultParent();\n\n    graph.getModel().beginUpdate();\n    try\n    {\n        Object v1 = graph.insertVertex(parent, null, "Hello", 20, 20, 80,\n            30);\n        Object v2 = graph.insertVertex(parent, null, "wicked", 240, 150,\n            80, 30,"shape=rhombus;perimeter=rhombusPerimeter;");\n\n        Object v3 = graph.insertVertex(parent, null, "Curve", 20, 150,\n            80, 30, "shape=ellipse;perimeter=ellipsePerimeter;");\n\n        graph.insertEdge(parent, null, "Edge", v1, v2, "shape=curvedEdge;edgeStyle=curvedEdgeStyle");\n    }\n    finally\n    {\n        graph.getModel().endUpdate();\n    }\n\n    mxGraphComponent graphComponent = new mxGraphComponent(graph);\n    getContentPane().add(graphComponent);\n}\n\npublic static void main(String[] args)\n{\n    HelloCurve frame = new HelloCurve();\n    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);\n    frame.setSize(400, 320);\n    frame.setVisible(true);\n}\n
\n

}

\n

Thanks for all your help though, much appreciated.

link

answered 02 Mar '12, 06:19

Earthumb's gravatar image

Earthumb
2616
accept rate: 16%

Earthumb, how did you style edge labels so they have rounded border?

link

answered 07 May '12, 05:47

bab%C4%8Da's gravatar image

babča
2316
accept rate: 0%

Your answer
toggle preview

Follow this question

By Email:

Once you sign in you will be able to subscribe for any updates here

By RSS:

Answers

Answers and Comments

Markdown Basics

  • *italic* or _italic_
  • **bold** or __bold__
  • link:[text](http://url.com/ "title")
  • image?![alt text](/path/img.jpg "title")
  • numbered list: 1. Foo 2. Bar
  • to add a line break simply add two spaces to where you would like the new line to be.
  • basic HTML tags are also supported

Tags:

×100
×29
×9
×1
×1

Asked: 01 Mar '12, 07:25

Seen: 4,343 times

Last updated: 12 Jul '15, 07:16