2

I'm drawing two shapes (circles) in a JPanel and I need to connect them with a line. I was doing this by just getting the middle point of the circle and connecting each other, easy.

The problem is that now I need to make single-direction lines, which has an "arrow" at the end, to point out which direction the line goes. So now I can't use the middle point of the circle because I need to connect each other from border to border, so the "arrow' can appear correctly.

On my last try that was the result, nothing good:

PS: In the screenshot I'm not filling the circles just to see the exact position of the line, but normally I would fill it.

I'm having trouble to calculate the exact position of the border I need to start/end my line. Anyone has any idea on how to do this?

EDIT: The circles are movable, they could be in any position, so the line should work in any case.

Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
Deeh
  • 307
  • 1
  • 6
  • 17
  • By "border to border" do you mean you want the line segment to be tangent to each circle? If so, there are (usually) two such line segments with the circles on the same side of the line and two more with the circles on opposites sides of the line. Which of those line segments do you want? Also, will the two circles have the same size, as in your screenshot, or could they be more general? – Rory Daulton Nov 18 '17 at 19:48
  • So, you know the centre position of the circles, you know their radius. From this you can determine their offset position (x/y of the top left corder). What you need to do is calculate the angle between the two circles, from this you can then calculate the point on the circumference of the circles that a line will intersect – MadProgrammer Nov 18 '17 at 20:36

4 Answers4

5

Okay, so basically, we can break down the problem to basic issues:

  1. Get the angle between the two circles
  2. Draw a line from circumference of one circle to another along this angle

Both these issues aren't hard to solve (and any time spent searching the internet would provide solutions - because that's where I got them from ;))

So, the angle between two points could be calculated using something like...

protected double angleBetween(Point2D from, Point2D to) {
    double x = from.getX();
    double y = from.getY();

    // This is the difference between the anchor point
    // and the mouse.  Its important that this is done
    // within the local coordinate space of the component,
    // this means either the MouseMotionListener needs to
    // be registered to the component itself (preferably)
    // or the mouse coordinates need to be converted into
    // local coordinate space
    double deltaX = to.getX() - x;
    double deltaY = to.getY() - y;

    // Calculate the angle...
    // This is our "0" or start angle..
    double rotation = -Math.atan2(deltaX, deltaY);
    rotation = Math.toRadians(Math.toDegrees(rotation) + 180);

    return rotation;
}

And the point on a circle can be calculated using something like...

protected Point2D getPointOnCircle(Point2D center, double radians, double radius) {

    double x = center.getX();
    double y = center.getY();

    radians = radians - Math.toRadians(90.0); // 0 becomes the top
    // Calculate the outter point of the line
    double xPosy = Math.round((float) (x + Math.cos(radians) * radius));
    double yPosy = Math.round((float) (y + Math.sin(radians) * radius));

    return new Point2D.Double(xPosy, yPosy);

}

Just beware, there's some internal modifications of the results to allow for the difference between the mathematical solution and the way that the Graphics API draws circles

Okay, so big deal you say, how does that help me? Well, I great deal actually.

You'd calculate the angle between the to circles (both to and from, you might be able to simple inverse one angle, but I have the calculation available so I used it). From that, you can calculate the point on each circle where the line will intersect and then you simply need to draw it, something like...

double from = angleBetween(circle1, circle2);
double to = angleBetween(circle2, circle1);

Point2D pointFrom = getPointOnCircle(circle1, from);
Point2D pointTo = getPointOnCircle(circle2, to);

Line2D line = new Line2D.Double(pointFrom, pointTo);
g2d.draw(line);

Runnable Example

Because I've distilled much of the calculations down to communalised properties, I've provided my test code as a runnable example. All the calculations are based on dynamic values, nothing is really hard coded. For example, you can change the size and positions of the circles and the calculations should continue to work...

Example

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private Ellipse2D circle1;
        private Ellipse2D circle2;

        private Point2D drawTo;

        public TestPane() {
            circle1 = new Ellipse2D.Double(10, 10, 40, 40);
            circle2 = new Ellipse2D.Double(100, 150, 40, 40);

            //addMouseMotionListener(new MouseAdapter() {
            //  @Override
            //  public void mouseMoved(MouseEvent e) {
            //      drawTo = new Point2D.Double(e.getPoint().x, e.getPoint().y);
            //      repaint();
            //  }
            //});
        }

        protected Point2D center(Rectangle2D bounds) {
            return new Point2D.Double(bounds.getCenterX(), bounds.getCenterY());
        }

        protected double angleBetween(Shape from, Shape to) {
            return angleBetween(center(from.getBounds2D()), center(to.getBounds2D()));
        }

        protected double angleBetween(Point2D from, Point2D to) {
            double x = from.getX();
            double y = from.getY();

            // This is the difference between the anchor point
            // and the mouse.  Its important that this is done
            // within the local coordinate space of the component,
            // this means either the MouseMotionListener needs to
            // be registered to the component itself (preferably)
            // or the mouse coordinates need to be converted into
            // local coordinate space
            double deltaX = to.getX() - x;
            double deltaY = to.getY() - y;

            // Calculate the angle...
            // This is our "0" or start angle..
            double rotation = -Math.atan2(deltaX, deltaY);
            rotation = Math.toRadians(Math.toDegrees(rotation) + 180);

            return rotation;
        }

        protected Point2D getPointOnCircle(Shape shape, double radians) {
            Rectangle2D bounds = shape.getBounds();
//          Point2D point = new Point2D.Double(bounds.getX(), bounds.getY());
            Point2D point = center(bounds);
            return getPointOnCircle(point, radians, Math.max(bounds.getWidth(), bounds.getHeight()) / 2d);
        }

        protected Point2D getPointOnCircle(Point2D center, double radians, double radius) {

            double x = center.getX();
            double y = center.getY();

            radians = radians - Math.toRadians(90.0); // 0 becomes th?e top
            // Calculate the outter point of the line
            double xPosy = Math.round((float) (x + Math.cos(radians) * radius));
            double yPosy = Math.round((float) (y + Math.sin(radians) * radius));

            return new Point2D.Double(xPosy, yPosy);

        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.draw(circle1);
            g2d.draw(circle2);

            // This was used for testing, it will draw a line from circle1 to the
            // drawTo point, which, if enabled, is the last known position of the
            // mouse
            //if (drawTo != null) {
            //  Point2D pointFrom = center(circle1.getBounds2D());
            //  g2d.setColor(Color.RED);
            //  g2d.draw(new Line2D.Double(drawTo, pointFrom));
            //
            //  double from = angleBetween(pointFrom, drawTo);
            //  System.out.println(NumberFormat.getNumberInstance().format(Math.toDegrees(from)));
            //
            //  Point2D poc = getPointOnCircle(circle1, from);
            //  g2d.setColor(Color.BLUE);
            //  g2d.draw(new Line2D.Double(poc, drawTo));
            //}

            double from = angleBetween(circle1, circle2);
            double to = angleBetween(circle2, circle1);

            Point2D pointFrom = getPointOnCircle(circle1, from);
            Point2D pointTo = getPointOnCircle(circle2, to);

            g2d.setColor(Color.RED);
            Line2D line = new Line2D.Double(pointFrom, pointTo);
            g2d.draw(line);
            g2d.dispose();
        }

    }

}

Arrow head

The intention is to treat the arrow head as a separate entity. The reason is because it's just simpler that way, you also get a more consistent result regardless of the distance between the objects.

So, to start with, I define a new Shape...

public class ArrowHead extends Path2D.Double {

    public ArrowHead() {
        int size = 10;
        moveTo(0, size);
        lineTo(size / 2, 0);
        lineTo(size, size);
    }
    
}

Pretty simple really. It just creates two lines, which point up, meeting in the middle of the available space.

Then in the paintComponent method, we perform some AffineTransform magic using the available information we already have, namely

  • The point on our target circles circumference
  • The angle to our target circle

And transform the ArrowHead shape...

g2d.setColor(Color.MAGENTA);
ArrowHead arrowHead = new ArrowHead();
AffineTransform at = AffineTransform.getTranslateInstance(
                pointTo.getX() - (arrowHead.getBounds2D().getWidth() / 2d), 
                pointTo.getY());
at.rotate(from, arrowHead.getBounds2D().getCenterX(), 0);
arrowHead.transform(at);
g2d.draw(arrowHead);

Pointy

Now, because I'm crazy, I also tested the code by drawing an arrow pointing at our source circle, just to prove that the calculations would work...

// This just proofs that the previous calculations weren't a fluke
// and that the arrow can be painted pointing to the source object as well
g2d.setColor(Color.GREEN);
arrowHead = new ArrowHead();
at = AffineTransform.getTranslateInstance(
                pointFrom.getX() - (arrowHead.getBounds2D().getWidth() / 2d), 
                pointFrom.getY());
at.rotate(to, arrowHead.getBounds2D().getCenterX(), 0);
arrowHead.transform(at);
g2d.draw(arrowHead);
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • There is no need to work with angle and with trigonometric functions. – MBo Nov 19 '17 at 09:36
  • One question, my circle is extending an Ellipse2D.Float, and its coordinates are based on the top left corner of the shape. Your methods consider the center of the shape or the top left corner? – Deeh Nov 19 '17 at 19:09
  • Ok, I discovered it myself, it wasn't working until I started sending the center position of the circle, then it is working fine. I'll try to implement the arrowhead now. – Deeh Nov 19 '17 at 19:11
  • Perfect solution, thank you for your time and effort! – Deeh Nov 19 '17 at 19:32
  • Yep, I’m using a Ellipse as well and using the centre position for many of the calculations - the example has a number of helper methods to calculated various portions of the data – MadProgrammer Nov 19 '17 at 19:33
  • @Deeh I don't know about perfect, but it should give you jumping off point to work from – MadProgrammer Nov 19 '17 at 20:03
1

Let the first circle center coordinates are AX, AY, radius AR, and BX, BY, BR for the second circle.

Difference vector

D = (DX, DY)  = (BX - AX, BY - AY)

Normalized

d = (dx, dy) = (DX / Length(D), DY / Length(D))

Start point of arrow

S = (sx, sy) = (AX + dx * AR, AY + dy * AR)  

End point

E = (ex, ey) = (BX - dx * BR, BY - dy * BR)  

Example:

AX = 0     AY = 0     AR = 1
BX = 4     BY = 3     BR = 2
D = (4, 3)
Length(D) = 5
dx = 4/5
dy = 3/5
sx = 0.8  sy = 0.6
ex = 4 - 2 * 4/5 = 12/5 = 2.4
ey = 3 - 2 * 3/5 = 9/5 = 1.8
MBo
  • 77,366
  • 5
  • 53
  • 86
  • The angle is totally correct now, but the line is going beyond the circles, see: https://i.stack.imgur.com/zdqZB.png – Deeh Nov 18 '17 at 20:44
  • Perhaps error in your calculations. Look at my example. – MBo Nov 19 '17 at 05:30
0

Looking at the Screenshot, I think you need to find the top right corner of circle A, and then add half of the total distance to the bottom to y. Next, find the top right corner of circle B, and add half of the distance to the top left corner to x. Finally, make a line connecting the two, and render an arrow on the end of it.
Like this:

private int x1, y1, x2, y2 width = 20, height = 20;

private void example(Graphics g) {
    // Set x1, x2, y1, and y2 to something
    g.drawOval(x1, y1, width, height);
    g.drawOval(x2, y2, width, height);
    g.drawLine(x1, y1 + (height/2), x2 + (width/2), y2);
    g.drawImage(/*Image of an arrow*/, (x2 + width/2)-2, y2);
}
BlazeDaBlur2
  • 43
  • 1
  • 12
0

My trick:

Let the two centers be C0 and C1. Using complex numbers, you map these two points to a horizontal segment from the origin by the transformation

P' = (P - C0) (C1 - C0)* / L

where * denotes conjugation and L = |C1 - C0|. (If you don't like the complex number notation, you can express this with matrices as well.)

Now the visible part of the segment goes from (R0, 0) to (L - R1, 0). The two other vertices of the arrow are at (L - R1 - H, W) and (L - R1 - H, -W) for an arrowhead of height H and width 2W.

By applying the inverse transform you get the original coordinates,

P = C0 + L P' / (C1 - C0)*.

enter image description here