JavaFX: Pathfinding using Point2D and Line - java

I need to implement sort of a pathfinding algorithm, the context is the following:
I have a starting Point2D, and and objective (a Circle).
I draw a line between the starting point and the circle center.
I try to calculate a path that does not cross any other circles.
(The blue square is my object I want to move (at starting point)) and the red circle is my objective).
What I wanted to do first was to do something like this:
But the code I have seems to be buggy as sometimes, I've got negatives intersection coordonates (black points).
Is there any other way to solve this problem ? Am I seeing the problem from a correct point of view ? There is also a problem as I'm iterating over the circles to determines which intersects or not, but if the line intersect 2 or more circles, the order of which it intersect planets is different from the order I see the points on screen.
My goal is to create a PathTransition between starting point and objective following the correct path (no intersection).
I've not mentioned it, but the container is a Pane.
EDIT:
public static Point2D getMidPoint(Point2D p1, Point2D p2) {
return new Point2D((p1.getX() + p2.getX()) / 2, (p1.getY() + p2.getY()) / 2);
}
public static Circle createCircleFromPoint2D(Point2D p) {
return new Circle(p.getX(), p.getY(), 5);
}
public static Point2D createPoint2D(double x, double y) {
return new Point2D(x, y);
}
public static Pair<Point2D, Point2D> translate(int distance, Point2D p1, Point2D p2, double reference, double startX) {
double pente = (p2.getY() - p1.getY()) / (p2.getX() - p1.getX());
double newX1 = p1.getX() + (startX < reference ? -1 : 1) * (Math.sqrt(((distance*distance) / (1 + (pente*pente)))));
double newX2 = p2.getX() + (startX > reference ? -1 : 1) * (Math.sqrt(((distance*distance) / (1 + (pente*pente)))));
double newY1 = pente * (newX1 - p1.getX()) + p1.getY();
double newY2 = pente * (newX2 - p2.getX()) + p2.getY();
return new Pair<>(new Point2D(newX1, newY1), new Point2D(newX2, newY2));
}
public void start(Stage primaryStage) throws Exception{
Pane pane = new Pane();
Circle objective = new Circle(800, 250, 25);
Circle circle2 = new Circle(500, 250, 125);
Circle circle3 = new Circle(240, 400, 75);
Circle circle4 = new Circle(700, 500, 150, Color.VIOLET);
Circle circle5 = new Circle(1150, 300, 115, Color.ORANGE);
Rectangle myObject = new Rectangle(175, 175, 15, 15);
objective.setFill(Color.RED);
circle2.setFill(Color.BLUE);
circle3.setFill(Color.GREEN);
myObject.setFill(Color.BLUE);
ArrayList<Circle> circles = new ArrayList<>();
circles.add(objective);
circles.add(circle2);
circles.add(circle3);
circles.add(circle4);
circles.add(circle5);
Line straightLine = new Line();
pane.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
myObject.setX(event.getX());
myObject.setY(event.getY());
// My starting coordinates (at mouse position)
double fromX = myObject.getX();
double fromY = myObject.getY();
// Where I want to go
double toX = objective.getCenterX();
double toY = objective.getCenterY();
// Line style
straightLine.setStartX(event.getX());
straightLine.setStartY(event.getY());
straightLine.setEndX(toX);
straightLine.setEndY(toY);
straightLine.setStrokeWidth(2);
straightLine.setStroke(Color.GRAY.deriveColor(0, 1, 1, 0.5));
straightLine.setStrokeLineCap(StrokeLineCap.BUTT);
straightLine.getStrokeDashArray().setAll(10.0, 5.0);
straightLine.setMouseTransparent(true);
// Coordinates to Point2D
Point2D from = new Point2D(fromX, fromY);
Point2D to = new Point2D(toX, toY);
Path path = new Path();
path.getElements().add(new MoveTo(fromX, fromY));
for (Circle c : circles) {
if (straightLine.intersects(c.getLayoutBounds())) {
// I don't want to do anything if I'm intersecting the objective (for now)
if (c == objective)
continue;
Shape s = Shape.intersect(straightLine, c);
double xmin = s.getBoundsInLocal().getMinX();
double ymin = s.getBoundsInLocal().getMinY();
double xmax = s.getBoundsInLocal().getMaxX();
double ymax = s.getBoundsInLocal().getMaxY();
Point2D intersectionPt1 = createPoint2D((fromX < objective.getCenterX()) ? xmin : xmax , (fromY < objective.getCenterY()) ? ymin : ymax);
Point2D intersectionPt2 = createPoint2D((fromX > objective.getCenterX()) ? xmin : xmax , (fromY < objective.getCenterY()) ? ymax : ymin);
Point2D middlePt = getMidPoint(intersectionPt1, intersectionPt2);
Circle circlePt1 = new Circle(intersectionPt1.getX(), intersectionPt1.getY(), 5);
Circle circlePt2 = new Circle(intersectionPt2.getX(), intersectionPt2.getY(), 5);
Circle circleMiddle = new Circle(middlePt.getX(), middlePt.getY(), 5, Color.RED);
if (c != objective) {
// To calculate the points just before/after the first/second points (green points)
Pair<Point2D, Point2D> pts = translate(50, intersectionPt1, intersectionPt2, objective.getCenterX(), fromX);
Point2D beforePt1 = pts.getKey();
Point2D beforePt2 = pts.getValue();
Circle circleBeforePt1 = createCircleFromPoint2D(beforePt1);
Circle circleBeforePt2 = createCircleFromPoint2D(beforePt2);
circleBeforePt1.setFill(Color.GREEN);
circleBeforePt2.setFill(Color.GREEN);
pane.getChildren().addAll(circleBeforePt1, circleBeforePt2);
}
pane.getChildren().addAll(s, circlePt1, circlePt2, circleMiddle);
}
}
PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.seconds(2));
pathTransition.setNode(myObject);
pathTransition.setPath(path);
pathTransition.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);
pathTransition.play();
}
});
pane.getChildren().addAll(circles);
pane.getChildren().addAll(myObject, straightLine);
Scene scene = new Scene(pane, 1600, 900);
primaryStage.setScene(scene);
primaryStage.show();
}
I want to calculate a path (not necessarily a shortest path) from Point A to Point B, but can't figure it out how. Now I have the points where I would like to pass, I don't know how to link them togethers.

Solution strategy and implementation
I built a solution with the following strategy: On a given line from(X,Y) to to(X,Y) I compute the closest intersection with one of the obstacle shapes. From that shape I take the length of the intersection as a measure of how large the obstacle is, and take a look at the points left and right by 1/2 of that length from some point shortly before the intersection. The first of the left and right points that is not inside an obstacle is then used to sub-divide the task of finding a path around the obstacles.
protected void computeIntersections(double fromX, double fromY, double toX, double toY) {
// recursively test for obstacles and try moving around them by
// calling this same procedure on the segments to and from
// a suitable new point away from the line
Line testLine = new Line(fromX, fromY, toX, toY);
//compute the unit direction of the line
double dX = toX-fromX, dY = toY-fromY;
double ds = Math.hypot(dX,dY);
dX /= ds; dY /= ds;
// get the length from the initial point of the minimal intersection point
// and the opposite point of the same obstacle, remember also the closest obstacle
double t1=-1, t2=-1;
Shape obst = null;
for (Shape c : lstObstacles) {
if (testLine.intersects(c.getLayoutBounds())) {
Shape s = Shape.intersect(testLine, c);
if( s.getLayoutBounds().isEmpty() ) continue;
// intersection bounds of the current shape
double s1, s2;
if(Math.abs(dX) < Math.abs(dY) ) {
s1 = ( s.getBoundsInLocal().getMinY()-fromY ) / dY;
s2 = ( s.getBoundsInLocal().getMaxY()-fromY ) / dY;
} else {
s1 = ( s.getBoundsInLocal().getMinX()-fromX ) / dX;
s2 = ( s.getBoundsInLocal().getMaxX()-fromX ) / dX;
}
// ensure s1 < s2
if ( s2 < s1 ) { double h=s2; s2=s1; s1=h; }
// remember the closest intersection
if ( ( t1 < 0 ) || ( s1 < t1 ) ) { t1 = s1; t2 = s2; obst = c; }
}
}
// at least one intersection found
if( ( obst != null ) && ( t1 > 0 ) ) {
intersectionDecorations.getChildren().add(Shape.intersect(testLine, obst));
// coordinates for the vertex point of the path
double midX, midY;
// go to slightly before the intersection set
double intersectX = fromX + 0.8*t1*dX, intersectY = fromY + 0.8*t1*dY;
// orthogonal segment of half the length of the intersection, go left and right
double perpX = 0.5*(t2-t1)*dY, perpY = 0.5*(t1-t2)*dX;
Rectangle testRect = new Rectangle( 10, 10);
// go away from the line to hopefully have less obstacle from the new point
while( true ) {
// go "left", test if free
midX = intersectX + perpX; midY = intersectY + perpY;
testRect.setX(midX-5); testRect.setY(midY-5);
if( Shape.intersect(testRect, obst).getLayoutBounds().isEmpty() ) break;
// go "right"
midX = intersectX - perpX; midY = intersectY - perpY;
testRect.setX(midX-5); testRect.setY(midY-5);
if( Shape.intersect(testRect, obst).getLayoutBounds().isEmpty() ) break;
// if obstacles left and right, try closer points next
perpX *= 0.5; perpY *= 0.5;
}
intersectionDecorations.getChildren().add(new Line(intersectX, intersectY, midX, midY));
// test the first segment for intersections with obstacles
computeIntersections(fromX, fromY, midX, midY);
// add the middle vertex to the solution path
connectingPath.getElements().add(new LineTo(midX, midY));
// test the second segment for intersections with obstacles
computeIntersections(midX, midY, toX, toY);
}
}
This first chosen point might not be the most optimal one, as one can see, but it does the job. To do better one would have to construct some kind of decision tree of the left-right decisions and then chose the shortest path among the variants. All the usual strategies then apply, like starting a second tree from the target location, depth-first search etc.
The auxillary lines are the intersections that were used and the perpendicular lines to the new midpoints.
PathfinderApp.java
I used this problem to familiarize myself with the use of FXML, thus the main application has the usual boilerplate code.
package pathfinder;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class PathfinderApp extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("pathfinder.fxml"));
primaryStage.setTitle("Finding a path around obstacles");
primaryStage.setScene(new Scene(root, 1600, 900));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
pathfinder.fxml
The FXML file contains the "most" static (in the sense of always present for the given type of task) elements of the user interface. These are the cursor rectangle, the target circle and a line between them. Then groups for the obstacles and "decorations" from the path construction, and the path itself. This separation allows to clear and populate these groupings independent from each other with no other organizational effort.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.Group?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.shape.Line?>
<?import javafx.scene.shape.Path?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.paint.Color?>
<Pane xmlns:fx="http://javafx.com/fxml"
fx:controller="pathfinder.PathfinderController" onMouseClicked="#setCursor">
<Circle fx:id="target" centerX="800" centerY="250" radius="25" fill="red"/>
<Rectangle fx:id="cursor" x="175" y="175" width="15" height="15" fill="lightblue"/>
<Line fx:id="straightLine" startX="${cursor.X}" startY="${cursor.Y}" endX="${target.centerX}" endY="${target.centerY}"
strokeWidth="2" stroke="gray" strokeLineCap="butt" strokeDashArray="10.0, 5.0" mouseTransparent="true" />
<Group fx:id="obstacles" />
<Group fx:id="intersectionDecorations" />
<Path fx:id="connectingPath" strokeWidth="2" stroke="blue" />
</Pane>
PathfinderController.java
The main work is done in the controller. Some minimal initialization binding the target and cursor to their connecting line and the mouse event handler (with code that prevents the cursor to be placed inside some obstacle) and then the path finding procedures. One framing procedure and the recursive workhorse from above.
package pathfinder;
import javafx.fxml.FXML;
import javafx.geometry.Bounds;
import javafx.scene.layout.Pane;
import javafx.scene.Group;
import javafx.scene.text.Text;
import javafx.scene.text.Font;
import javafx.scene.shape.Shape;
import javafx.scene.shape.Line;
import javafx.scene.shape.Path;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.paint.Color;
import javafx.scene.input.MouseEvent;
import java.util.*;
public class PathfinderController {
#FXML
private Circle target;
#FXML
private Rectangle cursor;
#FXML
private Line straightLine;
#FXML
private Path connectingPath;
#FXML
private Group obstacles, intersectionDecorations;
private static List<Shape> lstObstacles = Arrays.asList(
new Circle( 500, 250, 125, Color.BLUE ),
new Circle( 240, 400, 75, Color.GREEN ),
new Circle( 700, 500, 150, Color.VIOLET),
new Circle(1150, 300, 115, Color.ORANGE)
);
#FXML
public void initialize() {
straightLine.startXProperty().bind(cursor.xProperty());
straightLine.startYProperty().bind(cursor.yProperty());
obstacles.getChildren().addAll(lstObstacles);
findPath();
}
#FXML
protected void setCursor(MouseEvent e) {
Shape test = new Rectangle(e.getX()-5, e.getY()-5, 10, 10);
for (Shape c : lstObstacles) {
if( !Shape.intersect(c, test).getLayoutBounds().isEmpty() ) return;
}
cursor.setX(e.getX());
cursor.setY(e.getY());
findPath();
}
protected void findPath() {
double fromX = cursor.getX();
double fromY = cursor.getY();
double toX = target.getCenterX();
double toY = target.getCenterY();
intersectionDecorations.getChildren().clear();
connectingPath.getElements().clear();
// first point of path
connectingPath.getElements().add(new MoveTo(fromX, fromY));
// check path for intersections, move around if necessary
computeIntersections(fromX, fromY, toX, toY);
// last point of the path
connectingPath.getElements().add(new LineTo(toX, toY));
}
protected void computeIntersections(double fromX, double fromY, double toX, double toY) {
...
}
// end class
}

It may not be the desired answer, but did you think about unit testing your math code? It is easy to do for math code and then you can be sure the low level functions work correct.
If you still have the bug afterwards, you can write a unit test for easier reproducing it and post it here.
On Topic:
Your algorithm with the lines can get quite complex or even find no solution with more and/or overlapping circles.
Why not use the standard A* algorithm, where all non-white pixels are obstacles. Is that overkill?

Related

Rotate a group of rectangles around their common center

I have a few rectangles that I assign rotation one by one ((javafx.scene.shape.Rectangle) shape).rotateProperty().bind(rotate);
For example, 45 degrees
My rectangle rotates around its center. Please tell me how to make the enemy a few right-angles around their common center.
To get something like this
this.rotate.addListener((obs, old, fresh) -> {
for (VObject vObject : children ) {
vObject.rotate.set(this.rotate.get());
}
});
This is how I add rotation. How can I specify the angle
Update: I used the advice below and now I set the rotation individually for each rectangle. (The selection is still a bit wrong)
this.rotate.addListener((obs, old, fresh) -> {
Rotate groupRotate = new Rotate(rotate.get(),
this.x.getValue().doubleValue() + this.width.getValue().doubleValue() / 2 ,
this.y.getValue().doubleValue() + this.height.getValue().doubleValue() / 2);
for (VObject vObject : children ) {
vObject.getShape().getTransforms().clear();
vObject.getShape().getTransforms().add(groupRotate);
}
});
But now the axis also rotates depending on the rotation.
Can I set the rotation to the rectangles without turning the coordinate axis?
If you put all rectangles into a common Group, you can rotate them at once:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class RotateAllApplication extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane root = new BorderPane();
// the common group
Group group = new Group();
group.getChildren().addAll(new Rectangle(10, 10, 80, 40), //
new Rectangle(110, 10, 80, 40), //
new Rectangle(10, 110, 80, 40), //
new Rectangle(110, 110, 80, 40));
// rotate the group instead of each rectangle
group.setRotate(45.0);
root.setCenter(group);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
Update: If you don't want to create a parent Group object, you can apply the same rotation transformation to each child instead. While Node#setRotate(double) always rotates around the center of the node, adding a transformation to Node#getTransforms() is more general and not restricted to simple rotations.
The following statement will apply a rotation around the point (100.0/100.0) of the parent coordinate system to all children in the list:
childrenList.forEach(child -> child.getTransforms().add(Transform.rotate(45.0, 100.0, 100.0)));
Use a Rotate transform and specify the appropriate pivot point:
#Override
public void start(Stage primaryStage) throws IOException {
Pane pane = new Pane();
pane.setPrefSize(600, 600);
Rectangle[] rects = new Rectangle[4];
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
Rectangle rect = new Rectangle(100 + i * 200, 100 + j * 100, 150, 50);
rects[i * 2 + j] = rect;
pane.getChildren().add(rect);
}
}
Slider slider = new Slider(0, 360, 0);
double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
Rotate rotate = new Rotate();
// find pivot point
for (Rectangle rect : rects) {
double val = rect.getX();
if (minX > val) {
minX = val;
}
val += rect.getWidth();
if (maxX < val) {
maxX = val;
}
val = rect.getY();
if (minY > val) {
minY = val;
}
val += rect.getHeight();
if (maxY < val) {
maxY = val;
}
rect.getTransforms().add(rotate);
}
rotate.setPivotX(0.5 * (maxX + minX));
rotate.setPivotY(0.5 * (maxY + minY));
rotate.angleProperty().bind(slider.valueProperty());
Scene scene = new Scene(new VBox(10, pane, slider));
primaryStage.setScene(scene);
primaryStage.sizeToScene();
primaryStage.show();
}
If you're planing to apply multiple transformations, you may need to adjust the code for finding the pivot point to use transforms for calculating the bounds...

Is there a "fill" function for arbitrary shapes in javafx?

I need to know in which way I can color the following image (PNG) by using JavaFX. This image is currently included in a ImageView of JavaFX:
I want to color region 1 blue, the second one red, and the last two purple. How can I do this in JavaFX? Isn't there some kind of function as in Windows Paint? (You know, the painting bucket that fills a certain area with a color between borders).
Suggested Approach
You can use a flood fill algorithm.
Sample Code
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.image.*;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Stack;
public class UnleashTheKraken extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(final Stage stage) {
Image original = new Image(
"http://s12.postimg.org/wofhjvy2h/image_2.jpg"
);
WritableImage updateable = new WritableImage(
original.getPixelReader(),
(int) original.getWidth(),
(int) original.getHeight()
);
Kraken kraken = new Kraken(updateable, Color.WHITE);
kraken.unleash(new Point2D(40, 40), Color.BLUE);
kraken.unleash(new Point2D(40, 100), Color.RED);
kraken.unleash(new Point2D(100, 100), Color.GREEN);
kraken.unleash(new Point2D(120, 40), Color.YELLOW);
ImageView originalView = new ImageView(original);
ImageView filledView = new ImageView(updateable);
HBox layout = new HBox(10, originalView, filledView);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout));
stage.show();
}
class Kraken {
private final WritableImage image;
private final Color colorToFill;
// tolerance for color matching (on a scale of 0 to 1);
private final double E = 0.3;
public Kraken(WritableImage image, Color colorToFill) {
this.image = image;
this.colorToFill = colorToFill;
}
public void unleash(Point2D start, Color color) {
PixelReader reader = image.getPixelReader();
PixelWriter writer = image.getPixelWriter();
Stack<Point2D> stack = new Stack<>();
stack.push(start);
while (!stack.isEmpty()) {
Point2D point = stack.pop();
int x = (int) point.getX();
int y = (int) point.getY();
if (filled(reader, x, y)) {
continue;
}
writer.setColor(x, y, color);
push(stack, x - 1, y - 1);
push(stack, x - 1, y );
push(stack, x - 1, y + 1);
push(stack, x , y + 1);
push(stack, x + 1, y + 1);
push(stack, x + 1, y );
push(stack, x + 1, y - 1);
push(stack, x, y - 1);
}
}
private void push(Stack<Point2D> stack, int x, int y) {
if (x < 0 || x > image.getWidth() ||
y < 0 || y > image.getHeight()) {
return;
}
stack.push(new Point2D(x, y));
}
private boolean filled(PixelReader reader, int x, int y) {
Color color = reader.getColor(x, y);
return !withinTolerance(color, colorToFill, E);
}
private boolean withinTolerance(Color a, Color b, double epsilon) {
return
withinTolerance(a.getRed(), b.getRed(), epsilon) &&
withinTolerance(a.getGreen(), b.getGreen(), epsilon) &&
withinTolerance(a.getBlue(), b.getBlue(), epsilon);
}
private boolean withinTolerance(double a, double b, double epsilon) {
return Math.abs(a - b) < epsilon;
}
}
}
Answers to additional questions
But wouldn't the image be colored pixel by pixel?
Yes, that's the point, you need to shade the pixels. Everything in computer graphics with bitmapped displays eventually comes down to coloring pixels.
Is this an efficient way in coloring?
It's instantaneous (as far as I can tell) on the sample image you provided. Space-wise it takes up some memory, but all such algorithms will use memory. The sample code I provided is not the most efficient flood fill shading algorithm which could be devised (time or space wise). The wikipedia page I linked has alternate more efficient (and more complicated) algorithms you could apply if you needed to.
Alternate Approach
If you have a cut-out stencil shape for each area, you could stack the stencils and apply ColorAdjust effects to them (such as in: How to change color of image in JavaFX). The ColorAdjust is (likely) a hardware accelerated effect. This alternate is not a general approach though as it requires you to know the stencil shapes.
Shape circle = new Circle(x,y,r);
Shape rect = new Rectangle(x,y,w,h);
Shape region1 = Shape.subtract(circle, rect);// to "cut" the rect away from a circle.
// You'll need to do this twice for each piece.
region1 = Shape.subtract(region1,anotherRect);
region1.setFill(Color.BLUE);
// Then simply add your shape to a node and set it's translation.
The way this works is that where the rectangle overlaps the circle, that part of the circle will be removed.

Java draw circle and lines on Swing

I'm trying to draw a circle with a random center inside a big bigger circular surface. (I'm actually trying to simulate a human and his eyesight inside a room!) I need to draw a random line (call it line1) passing through its center which will intersect with the surface. line1 does not necessarily pass the center of circular surface. I also need to draw two lines forming 60 degree, facing on one side of line1. Can anyone help me with that?
I created an example of what I need to draw.
import java.awt.Color;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Point;
import java.util.Random;
import javax.swing.JFrame;
public class ShapeTest extends JFrame{
int width=500;
int height=500;
public ShapeTest(){
setSize(width,height);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String a[]){
new ShapeTest();
}
public void paint(Graphics g){
// Circular Surface
drawCircleByCenter(g, width/2, height/2, width/2);
Random r = new Random();
Point center = new Point();
center.x=r.nextInt(width/2);
center.y=r.nextInt(width/2);
drawCircleByCenter(g, center.x, center.y, width/15);
}
void drawCircleByCenter(Graphics g, int x, int y, int radius){
//g.setColor(Color.LIGHT_GRAY);
g.drawOval(x-radius, y-radius, 2*radius, 2*radius);
}
}
Start by changing your method to draw a circle based on its center and radius to a method which returns a Ellipse2D object representing the circle. This will allow us to do some clipping and other things with the shape besides just draw it.
Setting the clip to be the shape of your large circle prevents stray marks from being made where you don't want them (think "color inside the lines"). This is important because when we draw the circles and lines inside the big circle, some of them will be too big and would otherwise mark outside the bounds of the big circle.
Once we set the clip, we use the method Line2D getVector(Point2D, double, length) with an origin at the center of the large circle, a random angle and a random length (capped to keep the small blue circle inside the big circle). Think of this a random polar coordinate with the center of the large circle as the origin. The end point of this vector is used to mark the center of the small circle.
Using the center of the small circle as a starting point, we can generate two vectors in opposite directions (just negate the length of one to get it going the other direction) by using a random direction angle. We use a length equal to the diameter of the big circle to make certain that the lines will always go all the way up to the edge of the big circle (but not past, thanks to our clip).
We simply add 60 and 120 degrees to the angle of our blue dashed line and draw two green lines calculating the vectors the same way we did for the two blue dashed lines, except we don't need to create ones with negated lengths. We can also add a normal vector in for good measure simply by adding 90 degrees to the angle of the blue dashed line.
Lastly, we pick some random polar coordinates (just like we did for the small blue circle) to represent some people, and using the intersection of the people with the areas created by the various lines, we can see where they are at and draw them up with color coded values.
Now that we have all the people, we eliminate the clip and draw the big circle and voila!
Check out Draw a line at a specific angle in Java for details on how I calculated the vectors for the lines.
But enough talk, here's the code:
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class ShapeTest extends JFrame {
private static final long serialVersionUID = 1L;
private int width = 500;
private int height = 500;
private int padding = 50;
private BufferedImage graphicsContext;
private JPanel contentPanel = new JPanel();
private JLabel contextRender;
private Stroke dashedStroke = new BasicStroke(3.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 2f, new float[] {3f, 3f}, 0f);
private Stroke solidStroke = new BasicStroke(3.0f);
private RenderingHints antialiasing;
private Random random = new Random();
public static void main(String[] args) {
//you should always use the SwingUtilities.invodeLater() method
//to perform actions on swing elements to make certain everything
//is happening on the correct swing thread
Runnable swingStarter = new Runnable()
{
#Override
public void run(){
new ShapeTest();
}
};
SwingUtilities.invokeLater(swingStarter);
}
public ShapeTest(){
antialiasing = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphicsContext = new BufferedImage(width + (2 * padding), width + (2 * padding), BufferedImage.TYPE_INT_RGB);
contextRender = new JLabel(new ImageIcon(graphicsContext));
contentPanel.add(contextRender);
contentPanel.setSize(width + padding * 2, height + padding * 2);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setResizable(false);
this.setContentPane(contentPanel);
//take advantage of auto-sizing the window based on the size of its contents
this.pack();
this.setLocationRelativeTo(null);
this.paint();
setVisible(true);
}
public void paint() {
Graphics2D g2d = graphicsContext.createGraphics();
g2d.setRenderingHints(antialiasing);
//Set up the font to print on the circles
Font font = g2d.getFont();
font = font.deriveFont(Font.BOLD, 14f);
g2d.setFont(font);
FontMetrics fontMetrics = g2d.getFontMetrics();
//clear the background
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, graphicsContext.getWidth(), graphicsContext.getHeight());
//set up the large circle
Point2D largeCircleCenter = new Point2D.Double((double)width / 2 + padding, (double)height / 2 + padding);
double largeCircleRadius = (double)width / 2;
Ellipse2D largeCircle = getCircleByCenter(largeCircleCenter, largeCircleRadius);
//here we build the small circle
Point2D smallCircleCenter = new Point2D.Double();
double smallCircleRadius = 15;
//we need to make certain it is confined inside the larger circle
//so we choose the following values carefully
//we want to go a random direction from the circle, so chose an
//angle randomly in any direction
double smallCenterVectorAngle = random.nextDouble() * 360.0d;
//and we want to be a random distance from the center of the large circle, but
//we limit the distance based on the radius of the small circle to prevent it
//from appearing outside the large circle
double smallCenterVectorLength = random.nextDouble() * (largeCircleRadius - smallCircleRadius);
Line2D vectorToSmallCenter = getVector(largeCircleCenter, smallCenterVectorAngle, smallCenterVectorLength);
//the resulting end point of the vector is a random distance from the center of the large circle
//in a random direction, and guaranteed to not place the small circle outside the large
smallCircleCenter.setLocation(vectorToSmallCenter.getP2());
Ellipse2D smallCircle = getCircleByCenter(smallCircleCenter, smallCircleRadius);
//before we draw any of the circles or lines, set the clip to the large circle
//to prevent drawing outside our boundaries
g2d.setClip(largeCircle);
//chose a random angle for the line through the center of the small circle
double angle = random.nextDouble() * 360.0d;
//we create two lines that start at the center and go out at the angle in
//opposite directions. We use 2*largeCircleRadius to make certain they
//will be large enough to fill the circle, and the clip we set prevent stray
//marks outside the big circle
Line2D centerLine1 = getVector(smallCircleCenter, angle, largeCircleRadius * 2);
Line2D centerLine2 = getVector(smallCircleCenter, angle, -largeCircleRadius * 2);
//now we just add 20 and 120 to our angle for the center-line, start at the center
//and again, use largeCircleRadius*2 to make certain the lines are big enough
Line2D sightVector1 = getVector(smallCircleCenter, angle + 60, largeCircleRadius * 2);
Line2D sightVector2 = getVector(smallCircleCenter, angle + 120, largeCircleRadius * 2);
Path2D visible = new Path2D.Double();
visible.moveTo(sightVector1.getX2(), sightVector1.getY2());
visible.lineTo(smallCircleCenter.getX(), smallCircleCenter.getY());
visible.lineTo(sightVector2.getX2(), sightVector2.getY2());
visible.closePath();
Path2D greenSide = new Path2D.Double();
greenSide.moveTo(centerLine1.getX2(), centerLine1.getY2());
greenSide.lineTo(smallCircleCenter.getX(), smallCircleCenter.getY());
greenSide.lineTo(centerLine2.getX2(), centerLine2.getY2());
greenSide.lineTo(sightVector1.getX2(), sightVector1.getY2());
greenSide.closePath();
int personCount = 5;
Area visibleArea = new Area(visible);
visibleArea.intersect(new Area(largeCircle));
Area greenSideArea = new Area(greenSide);
greenSideArea.intersect(new Area(largeCircle));
//we create a list of the people in the circle to
//prevent overlap
ArrayList<Shape> people = new ArrayList<Shape>();
people.add(smallCircle);
int i = 0;
personLoop: while (i < personCount){
double personCenterVectorAngle = random.nextDouble() * 360.0d;
double personCenterVectorLength = random.nextDouble() * (largeCircleRadius - smallCircleRadius);
Line2D vectorToPersonCenter = getVector(largeCircleCenter, personCenterVectorAngle, personCenterVectorLength);
Point2D personCircleCenter = vectorToPersonCenter.getP2();
Ellipse2D personCircle = getCircleByCenter(personCircleCenter, smallCircleRadius);
//this little loop lets us skip a person if they have overlap
//with another person, since people don't generally overlap
Area personArea = new Area(personCircle);
for (Shape person : people)
{
Area overlapArea = new Area(person);
overlapArea.intersect(personArea);
//this means that we have found a conflicting
//person, so should skip them
if (!overlapArea.isEmpty()){
continue personLoop;
}
}
people.add(personCircle);
personArea.intersect(visibleArea);
Area greenSideAreaTest = new Area(personCircle);
greenSideAreaTest.intersect(greenSideArea);
if (personArea.isEmpty()){
if (greenSideAreaTest.isEmpty()){
g2d.setColor(Color.orange);
System.out.println("Person " + i + " is behind the blue line");
}
else {
System.out.println("Person " + i + " is in front of the blue line");
g2d.setColor(Color.cyan);
}
}
else
{
System.out.println("Person " + i + " is between the green lines");
g2d.setColor(Color.magenta);
}
//alternatively to circles intersecting the area of interest, we can check whether the center
//is in the area of interest which may make more intuitive sense visually
// if (visibleArea.contains(personCircleCenter)){
// System.out.println("Person " + i + " is between the green lines");
// g2d.setColor(Color.magenta);
// }
// else {
// if (greenSideArea.contains(personCircleCenter)) {
// System.out.println("Person " + i + " is in front of the blue line");
// g2d.setColor(Color.cyan);
// }
// else{
// g2d.setColor(Color.orange);
// System.out.println("Person " + i + " is behind the blue line");
// }
// }
g2d.fill(personCircle);
g2d.setColor(Color.black);
String itemString = "" + i;
Rectangle2D itemStringBounds = fontMetrics.getStringBounds(itemString, g2d);
double textX = personCircleCenter.getX() - (itemStringBounds.getWidth() / 2);
double textY = personCircleCenter.getY() + (itemStringBounds.getHeight()/ 2);
g2d.drawString("" + i, (float)textX, (float)textY);
i++;
}
//fill the small circle with blue
g2d.setColor(Color.BLUE);
g2d.fill(smallCircle);
//draw the two center lines lines
g2d.setStroke(dashedStroke);
g2d.draw(centerLine1);
g2d.draw(centerLine2);
//create and draw the black offset vector
Line2D normalVector = getVector(smallCircleCenter, angle + 90, largeCircleRadius * 2);
g2d.setColor(Color.black);
g2d.draw(normalVector);
//draw the offset vectors
g2d.setColor(new Color(0, 200, 0));
g2d.draw(sightVector1);
g2d.draw(sightVector2);
//we save the big circle for last, to cover up any stray marks under the stroke
//of its perimeter. We also set the clip back to null to prevent the large circle
//itselft from accidentally getting clipped
g2d.setClip(null);
g2d.setStroke(solidStroke);
g2d.setColor(Color.BLACK);
g2d.draw(largeCircle);
g2d.dispose();
//force the container for the context to re-paint itself
contextRender.repaint();
}
private static Line2D getVector(Point2D start, double degrees, double length){
//we just multiply the unit vector in the direction we want by the length
//we want to get a vector of correct direction and magnitute
double endX = start.getX() + (length * Math.sin(Math.PI * degrees/ 180.0d));
double endY = start.getY() + (length * Math.cos(Math.PI * degrees/ 180.0d));
Point2D end = new Point2D.Double(endX, endY);
Line2D vector = new Line2D.Double(start, end);
return vector;
}
private static Ellipse2D getCircleByCenter(Point2D center, double radius)
{
Ellipse2D.Double myCircle = new Ellipse2D.Double(center.getX() - radius, center.getY() - radius, 2 * radius, 2 * radius);
return myCircle;
}
}
The logic of the geometry turned out to be more tricky than I'd presumed, but this is what I think you are after.
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Random;
import javax.imageio.ImageIO;
import javax.swing.*;
class HumanEyesightLines {
int rad = 150;
int radSmall = 15;
int pad = 10;
JPanel gui = new JPanel(new BorderLayout());
BufferedImage img = new BufferedImage(
2 * (rad + pad),
2 * (rad + pad),
BufferedImage.TYPE_INT_RGB);
Timer timer;
JLabel imgDisplay;
Random rnd = new Random();
RenderingHints rh = new RenderingHints(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
HumanEyesightLines() {
imgDisplay = new JLabel(new ImageIcon(img));
gui.add(imgDisplay);
File f = new File(System.getProperty("user.home"));
final File f0 = new File("HumanEyesiteLines");
f0.mkdirs();
try {
Desktop.getDesktop().open(f0);
} catch (IOException ex) {
ex.printStackTrace();
}
ActionListener animationListener = new ActionListener() {
int ii = 0;
#Override
public void actionPerformed(ActionEvent e) {
paintImage();
ii++;
if (ii < 100) {
System.out.println(ii);
File f1 = new File(f0, "eg" + ii + ".png");
try {
ImageIO.write(img, "png", f1);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
};
timer = new Timer(100, animationListener);
paintImage();
}
float[] dash = {3f, 3f};
float phase = 0f;
private final void paintImage() {
Graphics2D g = img.createGraphics();
g.setRenderingHints(rh);
g.setStroke(new BasicStroke(2f));
// fill the BG
g.setColor(Color.WHITE);
g.fillRect(0, 0, 2 * (rad + pad), 2 * (rad + pad));
// draw the big circle
Point center = new Point(rad + pad, rad + pad);
Shape bigCircle = new Ellipse2D.Double(pad, pad, 2 * rad, 2 * rad);
g.setColor(Color.MAGENTA.darker());
g.fill(bigCircle);
// set the clip to that of the big circle
g.setClip(bigCircle);
// draw the small circle
int xOff = rnd.nextInt(rad) - rad / 2;
int yOff = rnd.nextInt(rad) - rad / 2;
int x = center.x - xOff;
int y = center.y - yOff;
Shape smallCircle = new Ellipse2D.Double(
x - radSmall, y - radSmall,
2 * radSmall, 2 * radSmall);
g.setColor(Color.YELLOW);
g.fill(smallCircle);
g.setColor(Color.ORANGE);
g.draw(smallCircle);
g.setStroke(new BasicStroke(
1.5f,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_ROUND,
2f,
dash,
phase));
// I don't know what the rule is for where the blue line goes, so
// will use the top left corner of the image as a 2nd anchor point.
int x0 = 0;
int y0 = 0;
double grad = (double) (y - y0) / (double) (x - x0);
// now calculate the RHS point from y = mx + b
// where b = 0 and m is the gradient
int x1 = 2 * (pad + rad);
int y1 = (int) (grad * x1);
Line2D.Double line1 = new Line2D.Double(x0, y0, x1, y1);
g.setColor(Color.BLUE);
g.draw(line1);
//find the perpendicular gradient.
double perpGrad = -1d / grad;
double perpTheta = Math.atan(perpGrad);
// angle from perp
double diffTheta = Math.PI / 6d;
g.setColor(Color.GREEN);
double viewLine1Theta = perpTheta + diffTheta;
Line2D.Double viewLine1 = getLine(x, y, viewLine1Theta);
double viewLine2Theta = perpTheta - diffTheta;
Line2D.Double viewLine2 = getLine(x, y, viewLine2Theta);
g.draw(viewLine1);
g.draw(viewLine2);
g.setColor(Color.BLACK);
Line2D.Double viewPerp = getLine(x, y, perpTheta);
g.draw(viewPerp);
g.setColor(Color.RED);
g.draw(bigCircle);
g.dispose();
imgDisplay.repaint();
}
/**
* Returns a Line2D starting at the point x1,y1 at angle theta.
*/
private final Line2D.Double getLine(double x1, double y1, double theta) {
double m;
double b;
double x2;
double y2;
if (theta < (-Math.PI / 2d)) {
System.out.println("CHANGE IT! " + theta);
m = Math.tan(theta);
b = y1 - (m * x1);
x2 = 0;
y2 = (m * x2) + b;
} else {
m = Math.tan(theta);
b = y1 - (m * x1);
x2 = 2 * (rad + pad);
y2 = (m * x2) + b;
}
/*
* System.out.println("Perp theta: " + theta); System.out.println("Line
* grad: " + m); System.out.println("Line off: " + b);
* System.out.println("x1,y1: " + x1 + "," + y1);
* System.out.println("x2,y2: " + x2 + "," + y2);
*
*/
return new Line2D.Double(x1, y1, x2, y2);
}
public JComponent getGui() {
return gui;
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
HumanEyesightLines hel = new HumanEyesightLines();
hel.start();
JOptionPane.showMessageDialog(null, hel.getGui());
hel.stop();
}
};
// Swing GUIs should be created and updated on the EDT
// http://docs.oracle.com/javase/tutorial/uiswing/concurrency
SwingUtilities.invokeLater(r);
}
}

Snap to Edge Effect

My final goal is to have a method, lets say:
Rectangle snapRects(Rectangle rec1, Rectangle rec2);
Imagine a Rectangle having info on position, size and angle.
Dragging and dropping the ABDE rectangle close to the BCGF rectangle would call the method with ABDE as first argument and BCGF as second argument, and the resulting rectangle is a rectangle lined up with BCGF's edge.
The vertices do not have to match (and preferrably won't so the snapping isn't so restrictive).
I can only understand easily how to give the same angle, but the position change is quite confusing to me. Also, i believe even if i reached a solution it would be quite badly optimized (excessive resource cost), so I would appreciate guidance on this.
(This has already been asked but no satisfatory answer was given and the question forgotten.)
------------------------------------------------------------------
Edit: It seems my explanation was insufficient so I will try to clarify my wishes:
The following image shows the goal of the method in a nutshell:
Forget about "closest rectangle", imagine there are just two rectangles. The lines inside the rectangles represent the direction they are facing (visual aid for the angle).
There is a static rectangle, which is not to be moved and has an angle (0->360), and a rectangle (also with an angle) which I want to Snap to the closest edge of the static rectangle. By this, i mean, i want the least transformations possible for the "snap to edge" to happen.
This brings many possible cases, depending on the rotation of the rectangles and their position relative to each other.
The next image shows the static rectangle and how the position of the "To Snap" rectangle changes the snapping result:
The final rotations might not be perfect since it was done by eye, but you get the point, it matters the relative position and also both angles.
Now, in my point of view, which may be completely naive, I see this problem solved on two important and distinct steps on transforming the "To Snap" rectangle: Positioning and Rotation
Position: The objective of the new position is to stick to the closest edge, but since we want it to stick paralell to the static rectangle, the angle of the static rectangle matters. The next image shows examples of positioning:
In this case, the static rectangle has no angle, so its easy to determine up, down, left and right. But with angle, there are alot more possibilities:
As for the rotation, the goal is for the "to snap" rectangle to rotate the minimum needed to become paralell with the static rectangle:
As a final note, in regard of implementation input, the goal is to actually drag the "to snap" rectangle to wherever position i wish around the static rectangle and by pressing a keyboard key, the snap happens.
Also, it appears i have exagerated a little when i asked for optimization, to be honest i do not need or require optimization, I do prefer an easy to read, step by step clear code (if its the case), rather than any optimization at all.
I hope i was clear this time, sorry for the lack of clarity in the first place, if you have any more doubts please do ask.
The problem is obviously underspecified: What does "line up" for the edges mean? A common start point (but not necessarily a common end point)? A common center point for both edges? (That's what I assumed now). Should ALL edges match? What is the criterion for deciding WHICH edge of the first rectangle should be "matched" with WHICH edge of the second rectangle? That is, imagine one square consists exactly of the center points of the edges of the other square - how should it be aligned then?
(Secondary question: In how far is optimization (or "low resource cost") important?)
However, I wrote a few first lines, maybe this can be used to point out more clearly what the intended behavior should be - namely by saying in how far the intended behavior differs from the actual behavior:
EDIT: Old code omitted, update based on the clarification:
The conditions for the "snapping" are still not unambiguous. For example, it is not clear whether the change in position or the change in the angle should be preferred. But admittedly, I did not figure out in detail all possible cases where this question could arise. In any case, based on the updated question, this might be closer to what you are looking for.
NOTE: This code is neither "clean" nor particularly elegant or efficient. The goal until now was to find a method that delivers "satisfactory" results. Optimizations and beautifications are possible.
The basic idea:
Given are the static rectangle r1, and the rectangle to be snapped, r0
Compute the edges that should be snapped together. This is divided in two steps:
The method computeCandidateEdgeIndices1 computes the "candidate edges" (resp. their indices) of the static rectangle that the moving rectangle may be snapped to. This is based on the folowing criterion: It checks how many vertices (corners) of the moving rectangle are right of the particular edge. For example, if all 4 vertices of the moving rectangle are right of edge 2, then edge 2 will be a candidate for snapping the rectangle to.
Since there may be multiple edges for which the same number of vertices may be "right", the method computeBestEdgeIndices computes the candidate edge whose center has the least distance to the center of any edge of the moving rectangle. The indices of the respective edges are returned
Given the indices of the edges to be snapped, the angle between these edges is computed. The resulting rectangle will be the original rectangle, rotated by this angle.
The rotated rectangle will be moved so that the centers of the snapped edges are at the same point
I tested this with several configurations, and the results at least seem "feasible" for me. Of course, this does not mean that it works satisfactory in all cases, but maybe it can serve as a starting point.
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class RectangleSnap
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
RectangleSnapPanel panel = new RectangleSnapPanel();
f.getContentPane().add(panel);
f.setSize(1000,1000);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class SnapRectangle
{
private Point2D position;
private double sizeX;
private double sizeY;
private double angleRad;
private AffineTransform at;
SnapRectangle(
double x, double y,
double sizeX, double sizeY, double angleRad)
{
this.position = new Point2D.Double(x,y);
this.sizeX = sizeX;
this.sizeY = sizeY;
this.angleRad = angleRad;
at = AffineTransform.getRotateInstance(
angleRad, position.getX(), position.getY());
}
double getAngleRad()
{
return angleRad;
}
double getSizeX()
{
return sizeX;
}
double getSizeY()
{
return sizeY;
}
Point2D getPosition()
{
return position;
}
void draw(Graphics2D g)
{
Color oldColor = g.getColor();
Rectangle2D r = new Rectangle2D.Double(
position.getX(), position.getY(), sizeX, sizeY);
AffineTransform at = AffineTransform.getRotateInstance(
angleRad, position.getX(), position.getY());
g.draw(at.createTransformedShape(r));
g.setColor(Color.RED);
for (int i=0; i<4; i++)
{
Point2D c = getCorner(i);
Ellipse2D e = new Ellipse2D.Double(c.getX()-3, c.getY()-3, 6, 6);
g.fill(e);
g.drawString(""+i, (int)c.getX(), (int)c.getY()+15);
}
g.setColor(Color.GREEN);
for (int i=0; i<4; i++)
{
Point2D c = getEdgeCenter(i);
Ellipse2D e = new Ellipse2D.Double(c.getX()-3, c.getY()-3, 6, 6);
g.fill(e);
g.drawString(""+i, (int)c.getX(), (int)c.getY()+15);
}
g.setColor(oldColor);
}
Point2D getCorner(int i)
{
switch (i)
{
case 0:
return new Point2D.Double(position.getX(), position.getY());
case 1:
{
Point2D.Double result = new Point2D.Double(
position.getX(), position.getY()+sizeY);
return at.transform(result, null);
}
case 2:
{
Point2D.Double result = new Point2D.Double
(position.getX()+sizeX, position.getY()+sizeY);
return at.transform(result, null);
}
case 3:
{
Point2D.Double result = new Point2D.Double(
position.getX()+sizeX, position.getY());
return at.transform(result, null);
}
}
return null;
}
Line2D getEdge(int i)
{
Point2D p0 = getCorner(i);
Point2D p1 = getCorner((i+1)%4);
return new Line2D.Double(p0, p1);
}
Point2D getEdgeCenter(int i)
{
Point2D p0 = getCorner(i);
Point2D p1 = getCorner((i+1)%4);
Point2D c = new Point2D.Double(
p0.getX() + 0.5 * (p1.getX() - p0.getX()),
p0.getY() + 0.5 * (p1.getY() - p0.getY()));
return c;
}
void setPosition(double x, double y)
{
this.position.setLocation(x, y);
at = AffineTransform.getRotateInstance(
angleRad, position.getX(), position.getY());
}
}
class RectangleSnapPanel extends JPanel implements MouseMotionListener
{
private final SnapRectangle rectangle0;
private final SnapRectangle rectangle1;
private SnapRectangle snappedRectangle0;
RectangleSnapPanel()
{
this.rectangle0 = new SnapRectangle(
200, 300, 250, 200, Math.toRadians(-21));
this.rectangle1 = new SnapRectangle(
500, 300, 200, 150, Math.toRadians(36));
addMouseMotionListener(this);
}
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
g.setColor(Color.BLACK);
rectangle0.draw(g);
rectangle1.draw(g);
if (snappedRectangle0 != null)
{
g.setColor(Color.BLUE);
snappedRectangle0.draw(g);
}
}
#Override
public void mouseDragged(MouseEvent e)
{
rectangle0.setPosition(e.getX(), e.getY());
snappedRectangle0 = snapRects(rectangle0, rectangle1);
repaint();
}
#Override
public void mouseMoved(MouseEvent e)
{
}
private static SnapRectangle snapRects(
SnapRectangle r0, SnapRectangle r1)
{
List<Integer> candidateEdgeIndices1 =
computeCandidateEdgeIndices1(r0, r1);
int bestEdgeIndices[] = computeBestEdgeIndices(
r0, r1, candidateEdgeIndices1);
int bestEdgeIndex0 = bestEdgeIndices[0];
int bestEdgeIndex1 = bestEdgeIndices[1];
System.out.println("Best to snap "+bestEdgeIndex0+" to "+bestEdgeIndex1);
Line2D bestEdge0 = r0.getEdge(bestEdgeIndex0);
Line2D bestEdge1 = r1.getEdge(bestEdgeIndex1);
double edgeAngle = angleRad(bestEdge0, bestEdge1);
double rotationAngle = edgeAngle;
if (rotationAngle <= Math.PI)
{
rotationAngle = Math.PI + rotationAngle;
}
else if (rotationAngle <= -Math.PI / 2)
{
rotationAngle = Math.PI + rotationAngle;
}
else if (rotationAngle >= Math.PI)
{
rotationAngle = -Math.PI + rotationAngle;
}
SnapRectangle result = new SnapRectangle(
r0.getPosition().getX(), r0.getPosition().getY(),
r0.getSizeX(), r0.getSizeY(), r0.getAngleRad()-rotationAngle);
Point2D edgeCenter0 = result.getEdgeCenter(bestEdgeIndex0);
Point2D edgeCenter1 = r1.getEdgeCenter(bestEdgeIndex1);
double dx = edgeCenter1.getX() - edgeCenter0.getX();
double dy = edgeCenter1.getY() - edgeCenter0.getY();
result.setPosition(
r0.getPosition().getX()+dx,
r0.getPosition().getY()+dy);
return result;
}
// Compute for the edge indices for r1 in the given list
// the one that has the smallest distance to any edge
// of r0, and return this pair of indices
private static int[] computeBestEdgeIndices(
SnapRectangle r0, SnapRectangle r1,
List<Integer> candidateEdgeIndices1)
{
int bestEdgeIndex0 = -1;
int bestEdgeIndex1 = -1;
double minCenterDistance = Double.MAX_VALUE;
for (int i=0; i<candidateEdgeIndices1.size(); i++)
{
int edgeIndex1 = candidateEdgeIndices1.get(i);
for (int edgeIndex0=0; edgeIndex0<4; edgeIndex0++)
{
Point2D p0 = r0.getEdgeCenter(edgeIndex0);
Point2D p1 = r1.getEdgeCenter(edgeIndex1);
double distance = p0.distance(p1);
if (distance < minCenterDistance)
{
minCenterDistance = distance;
bestEdgeIndex0 = edgeIndex0;
bestEdgeIndex1 = edgeIndex1;
}
}
}
return new int[]{ bestEdgeIndex0, bestEdgeIndex1 };
}
// Compute the angle, in radians, between the given lines,
// in the range (-2*PI, 2*PI)
private static double angleRad(Line2D line0, Line2D line1)
{
double dx0 = line0.getX2() - line0.getX1();
double dy0 = line0.getY2() - line0.getY1();
double dx1 = line1.getX2() - line1.getX1();
double dy1 = line1.getY2() - line1.getY1();
double a0 = Math.atan2(dy0, dx0);
double a1 = Math.atan2(dy1, dx1);
return (a0 - a1) % (2 * Math.PI);
}
// In these methods, "right" refers to screen coordinates, which
// unfortunately are upside down in Swing. Mathematically,
// these relation is "left"
// Compute the "candidate" edges of r1 to which r0 may
// be snapped. These are the edges to which the maximum
// number of corners of r0 are right of
private static List<Integer> computeCandidateEdgeIndices1(
SnapRectangle r0, SnapRectangle r1)
{
List<Integer> bestEdgeIndices = new ArrayList<Integer>();
int maxRight = 0;
for (int i=0; i<4; i++)
{
Line2D e1 = r1.getEdge(i);
int right = countRightOf(e1, r0);
if (right > maxRight)
{
maxRight = right;
bestEdgeIndices.clear();
bestEdgeIndices.add(i);
}
else if (right == maxRight)
{
bestEdgeIndices.add(i);
}
}
//System.out.println("Candidate edges "+bestEdgeIndices);
return bestEdgeIndices;
}
// Count the number of corners of the given rectangle
// that are right of the given line
private static int countRightOf(Line2D line, SnapRectangle r)
{
int count = 0;
for (int i=0; i<4; i++)
{
if (isRightOf(line, r.getCorner(i)))
{
count++;
}
}
return count;
}
// Returns whether the given point is right of the given line
// (referring to the actual line *direction* - not in terms
// of coordinates in 2D!)
private static boolean isRightOf(Line2D line, Point2D point)
{
double d00 = line.getX1() - point.getX();
double d01 = line.getY1() - point.getY();
double d10 = line.getX2() - point.getX();
double d11 = line.getY2() - point.getY();
return d00 * d11 - d10 * d01 > 0;
}
}

Java- Intersection point of a Polygon and Line

Is there any function that will give me the intersection point of a Polygon and Line2D ?
I have a Polygon and a line segment that I know intersect I want the actual value of the intersection point not a boolean answer.
Here you are. The interesting methods are getIntersections and getIntersection. The former parses over all polygon segments and checks for intersections, the latter does the actual calculation. Do keep in mind that the calculation can be seriously optimized and doesn't check for division by 0. This will also work only for polygons. It could be adapted to work with other shapes if you introduce calculations for cubic and quadratic curves. It is assumed that Line2D.Double is used instead of Line2D.Float. A Set is used to avoid duplicate points (might occur on polygon corner intersections).
Please don't use this without extensive testing, since I've just hacked it together quickly and am not sure it's completely sound.
package quickpolygontest;
import java.awt.Polygon;
import java.awt.geom.Line2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class Main {
public static void main(String[] args) throws Exception {
final Polygon poly = new Polygon(new int[]{1,2,2,1}, new int[]{1,1,2,2}, 4);
final Line2D.Double line = new Line2D.Double(2.5, 1.3, 1.3, 2.5);
final Set<Point2D> intersections = getIntersections(poly, line);
for(Iterator<Point2D> it = intersections.iterator(); it.hasNext();) {
final Point2D point = it.next();
System.out.println("Intersection: " + point.toString());
}
}
public static Set<Point2D> getIntersections(final Polygon poly, final Line2D.Double line) throws Exception {
final PathIterator polyIt = poly.getPathIterator(null); //Getting an iterator along the polygon path
final double[] coords = new double[6]; //Double array with length 6 needed by iterator
final double[] firstCoords = new double[2]; //First point (needed for closing polygon path)
final double[] lastCoords = new double[2]; //Previously visited point
final Set<Point2D> intersections = new HashSet<Point2D>(); //List to hold found intersections
polyIt.currentSegment(firstCoords); //Getting the first coordinate pair
lastCoords[0] = firstCoords[0]; //Priming the previous coordinate pair
lastCoords[1] = firstCoords[1];
polyIt.next();
while(!polyIt.isDone()) {
final int type = polyIt.currentSegment(coords);
switch(type) {
case PathIterator.SEG_LINETO : {
final Line2D.Double currentLine = new Line2D.Double(lastCoords[0], lastCoords[1], coords[0], coords[1]);
if(currentLine.intersectsLine(line))
intersections.add(getIntersection(currentLine, line));
lastCoords[0] = coords[0];
lastCoords[1] = coords[1];
break;
}
case PathIterator.SEG_CLOSE : {
final Line2D.Double currentLine = new Line2D.Double(coords[0], coords[1], firstCoords[0], firstCoords[1]);
if(currentLine.intersectsLine(line))
intersections.add(getIntersection(currentLine, line));
break;
}
default : {
throw new Exception("Unsupported PathIterator segment type.");
}
}
polyIt.next();
}
return intersections;
}
public static Point2D getIntersection(final Line2D.Double line1, final Line2D.Double line2) {
final double x1,y1, x2,y2, x3,y3, x4,y4;
x1 = line1.x1; y1 = line1.y1; x2 = line1.x2; y2 = line1.y2;
x3 = line2.x1; y3 = line2.y1; x4 = line2.x2; y4 = line2.y2;
final double x = (
(x2 - x1)*(x3*y4 - x4*y3) - (x4 - x3)*(x1*y2 - x2*y1)
) /
(
(x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4)
);
final double y = (
(y3 - y4)*(x1*y2 - x2*y1) - (y1 - y2)*(x3*y4 - x4*y3)
) /
(
(x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4)
);
return new Point2D.Double(x, y);
}
}
There is java.awt.geom.Area.intersect(Area) using the constructor Area(Shape) with your Polygon and passing your Line2D as an Area to intersect will give you the Area which is intersected.
With great success, i used this approach:
Area a = new Area(shape1);
Area b = new Area(shape2);
b.intersect(a);
if (!b.isEmpty()) {
//Shapes have non-empty intersection, so do you actions.
//In case of need, actual intersection is in Area b. (its destructive operation)
}
You need to bear in mind that it might intersect at multiple places.
Let's call the line segment of the polygon P and the real line segment L.
We find the slope of each line (slope is m)
ml = (ly1-ly2) / (lx1-lx2);
mp = (ply-pl2) / (px1-px2);
Find the y intercept of each line
// y = mx+b where b is y-intercept
bl = ly1 - (ml*lx1);
bp = py1 - (pl*px1);
You can solve for the x value with:
x = (bp - bl) / (ml - mp)
Then plug that X into one of the equations to get the Y
y = ml * x + bl
Here's an implemented version of the algorithm
class pointtest {
static float[] intersect(float lx1, float ly1, float lx2, float ly2,
float px1, float py1, float px2, float py2) {
// calc slope
float ml = (ly1-ly2) / (lx1-lx2);
float mp = (py1-py2) / (px1-px2);
// calc intercept
float bl = ly1 - (ml*lx1);
float bp = py1 - (mp*px1);
float x = (bp - bl) / (ml - mp);
float y = ml * x + bl;
return new float[]{x,y};
}
public static void main(String[] args) {
float[] coords = intersect(1,1,5,5,1,4,5,3);
System.out.println(coords[0] + " " + coords[1]);
}
}
and results:
3.4 3.4
If you are not restricted to use the Polygon and Line2D Objects I would recommend to use JTS.
Create LinearRing geometry (your polygon).
Create LineString geometry.
Create intersection Point(s) using the intersection method.
Simple code example:
// create ring: P1(0,0) - P2(0,10) - P3(10,10) - P4(0,10)
LinearRing lr = new GeometryFactory().createLinearRing(new Coordinate[]{new Coordinate(0,0), new Coordinate(0,10), new Coordinate(10,10), new Coordinate(10,0), new Coordinate(0,0)});
// create line: P5(5, -1) - P6(5, 11) -> crossing the ring vertically in the middle
LineString ls = new GeometryFactory().createLineString(new Coordinate[]{new Coordinate(5,-1), new Coordinate(5,11)});
// calculate intersection points
Geometry intersectionPoints = lr.intersection(ls);
// simple output of points
for(Coordinate c : intersectionPoints.getCoordinates()){
System.out.println(c.toString());
}
Result is:
(5.0, 0.0, NaN)
(5.0, 10.0, NaN)

Categories