I am trying to use the PathIterator to calculate the center of any Shape object, so that curved paths can be accounted for, but upon finding the center of a standard 1x1 rectangle, my getCenter() method returns the point:
Point2D.Double[0.3333333333333333, 0.3333333333333333]
My getCenter() method:
shape = new Rectangle2D.Double(0, 0, 1, 1);
public Point2D.Double getCenter()
{
ArrayList<Point2D.Double> points = new ArrayList<Point2D.Double>();
double[] arr = new double[6];
for(PathIterator pi = shape.getPathIterator(null); !pi.isDone(); pi.next())
{
pi.currentSegment(arr);
points.add(new Point2D.Double(arr[0], arr[1]));
}
double cX = 0;
double cY = 0;
for(Point2D.Double p : points)
{
cX += p.x;
cY += p.y;
}
System.out.println(points.toString());
return new Point2D.Double(cX / points.size(), cY / points.size());
}
I have discovered that upon printing points.toString(), I get this in the Console:
[Point2D.Double[0.0, 0.0], Point2D.Double[1.0, 0.0], Point2D.Double[1.0, 1.0], Point2D.Double[0.0, 1.0], Point2D.Double[0.0, 0.0], Point2D.Double[0.0, 0.0]]
I noticed that there are six entries in the points array, as opposed to four which I was expecting, given that the input Shape object is Rectangle2D.Double(0, 0, 1, 1). Obviously it is accounting for the point (0, 0) two more times than I want it to, and I am confused as to why that is. Is it a result of the PathIterator.isDone() method? Am I using it incorrectly? What would solve my problem if PathIterator can't?
As it was already pointed out, the PathIterator returns different types of segments. When only considering the points that are involved in SEG_LINETO, you should already obtain satisfactory results. However, consider that there may also be SEG_QUADTO and SEG_CUBICTO in other shapes. These can easily be avoided by using a flattening PathIterator: When you create a PathIterator with
PathIterator pi = shape.getPathIterator(null, flatness);
with an appropriate flatness, then it will only contain straight line segments.
import java.awt.Shape;
import java.awt.geom.Ellipse2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
public class ShapeCenter
{
public static void main(String[] args)
{
System.out.println(computeCenter(new Ellipse2D.Double(-10,-10,20,20)));
System.out.println(computeCenter(new Rectangle2D.Double(0,0,1,1)));
}
public static Point2D computeCenter(Shape shape)
{
final double flatness = 0.1;
PathIterator pi = shape.getPathIterator(null, flatness);
double coords[] = new double[6];
double sumX = 0;
double sumY = 0;
int numPoints = 0;
while (!pi.isDone())
{
int s = pi.currentSegment(coords);
switch (s)
{
case PathIterator.SEG_MOVETO:
// Ignore
break;
case PathIterator.SEG_LINETO:
sumX += coords[0];
sumY += coords[1];
numPoints++;
break;
case PathIterator.SEG_CLOSE:
// Ignore
break;
case PathIterator.SEG_QUADTO:
throw new AssertionError(
"SEG_QUADTO in flattening path iterator");
case PathIterator.SEG_CUBICTO:
throw new AssertionError(
"SEG_CUBICTO in flattening path iterator");
}
pi.next();
}
double x = sumX / numPoints;
double y = sumY / numPoints;
return new Point2D.Double(x,y);
}
}
PathIterator defines different types of segments, you should pay attention to this fact. You get 6 segments in your example, because it returns additionally also the SEG_MOVETO segment, which defines the start of a subpath, and SEG_CLOSE at the end of the sub path. If you just want to get the endpoints of the lines of your shape, you should change your code like this:
for(PathIterator pi = shape.getPathIterator(null); !pi.isDone(); pi.next())
{
if(pi.currentSegment(arr) == PathIterator.SEG_LINETO) {
points.add(new Point2D.Double(arr[0], arr[1]));
}
}
I am not sure you're using it incorrectly but you aren't accounting for an aspect of PathIterator. PathIterator doesn't so much represent a geometric shape but rather a path that should be taken while drawing. So its points also represent the type of path the 'pen' should take. For example, for a Rectangle, the path makes the following segments:
SEG_MOVETO
SEG_LINETO
SEG_LINETO
SEG_LINETO
SEG_LINETO
SEG_CLOSE
Because obviously the path should:
Move, not draw, from wherever the pen was before.
Close off this path from whatever the pen draws next.
The type of segment is the return value of currentSegment. If you only want to capture points that are on the polygon you can check for the 'line to' segment:
if(pi.currentSegment(arr) == PathIterator.SEG_LINETO) {
points.add(new Point2D.Double(arr[0], arr[1]));
}
That will work for simple polygons like Rectangle. For the given Rectangle, it will return [0.5, 0.5] which is I assume the result you're interested in.
On the other hand, there are Shapes that are not polygons so I'd be careful with this approach.
Related
I am trying to get all positions in a radius from a 3 dimensional world(In this case the game Minecraft) this is the current code I use.
public static List<BlockPos> getBlocksInRadius(double radius) {
List<BlockPos> circleblocks = new ArrayList<>();
int centralx = mc.player.posX;
int centraly = mc.player.posY;
int centralz = mc.player.posZ;
for (int x = centralx - radius; x <= centralx + radius; x++) {
for (int z = centralz - radius; z <= centralz + radius; z++) {
for (int y = centraly - radius; y < centraly + radius; y++) {
double dist = mc.player.getDistance(x, y, z);
if (dist < radius) {
BlockPos l = new BlockPos(x, y, z);
circleblocks.add(l);
}
}
}
}
return circleblocks;
}
This method goes from the x coord farthest away and keeps coming closer to the player. I want it to iterate it by starting at central x,y,z and then increase distance from the player. This is to make it easier to find block x closest to player. Any help would be apreciated!
Depending on how large of a radius you have, you might try the static method BlockPos::getAllInBox. There doesn't seem to be any official documentation on it, but it looks like it takes two BlockPos parameters and returns an Iterable<BlockPos>. It finds all the blocks in a cube in between the two parameters, so you probably want to center it on the player.
Here's what I would do. This code hasn't been tested, and you might need to adapt it for all of the 1.14 and 1.13 changes, but the theory should be the same, with just name changes.
BlockPos playerPos = player.getPosition(); // Or some other method of getting a BlockPos of the player
positiveRadiusPosition = playerPos.add(radius, radius, radius); // Gets one corner of the cube in the positive X, Y, and Z direction
negativeRadiusPosition = playerPos.add(-1 * radius, -1 * radius, -1 * radius); // Gets the opposite corner
Iterable<BlockPos> cubeResult = BlockPos.getAllInBox(positiveRadiusPosition, negativeRadiusPosition);
for (BlockPos pos: cubeResult) {
// cubeResult will contain blocks that are outside of the sphere with the
// radius you want. If that's okay, cool! If that's not okay, you should
// check each pos' distance from the player. If it's outside of the radius,
// remove it from the list.
}
Now you need to figure out which block is closest. The method I would use would be to use a Comparator to sort the Iterable, which is copied into a List. For reference:
public static Iterator sortedIterator(Iterator it, Comparator comparator) {
List list = new ArrayList();
while (it.hasNext()) {
list.add(it.next());
}
Collections.sort(list, comparator);
return list.iterator();
}
In the Comparator, you should check the distance from the player to each block.
public static double getDistanceToEntity(Entity entity, BlockPos pos) {
double deltaX = entity.posX - pos.getX();
double deltaY = entity.posY - pos.getY();
double deltaZ = entity.posZ - pos.getZ();
return Math.sqrt((deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ));
}
Of course, this method doesn't actually start at the player and work outwards. It's just a cleaner and expanded version of your original method that should do what you want. If you are working with a very large radius, it's probably not a good idea to use this, as you'll have to work with the entire cube.
I have a use case where I need to generate GPS coordinates to cover an specific area (marked by a list og GPS coordinates).
So I need to generate coordinates. I have the following functions tested and implemented:
//determines wether a Waypoint is inside the specified area
public boolean isInsideArea(Waypoint waypoint)
public double distanceInKmBetweenGPSCoordinates(Waypoint from, Waypoint to)
public double[] calculateCoordinatesAfterMove(Waypoint waypoint, double dx, double dy)
Here is my naive implementation of how to generate coordinates for a square (or approximately a square):
public static List<drone.Waypoint> addAutoGeneratedWaypoints(List<Waypoint> polygon_lat_long_pairs) {
List<Waypoint> waypoints = new ArrayList<>();
AreaTester tester = new GPSPolygonTester(polygon_lat_long_pairs);
GPSCoordinateCalculator coordinateCalculator = new GPSCoordinateCalculator();
CustomGPSMapper mapper = new CustomGPSMapper();
boolean finish = false;
String mode = "r";
Waypoint prev = polygon_lat_long_pairs.get(0);
int count = 0;
double distanceDown = mapper.distanceInKmBetweenGPSCoordinates(polygon_lat_long_pairs.get(2), polygon_lat_long_pairs.get(3));
double travelledDown = 0;
while (!finish) {
if (mode.equals("r")) {
double[] nextA = coordinateCalculator.calculateCoordinatesAfterMove(prev.getPosition().getLatitude(), prev.getPosition().getLongitude(), 5.0, 0.0);
Waypoint next = new DefaultWaypoint(nextA[0], nextA[1]);
if (tester.isInsideArea(next)) {
System.out.println("Waypoint was in area");
waypoints.add(next);
prev = next;
} else {
System.err.println("Left area, switching position");
mode = "l";
double[] nextB = coordinateCalculator.calculateCoordinatesAfterMove(prev.getPosition().getLatitude(), prev.getPosition().getLongitude(), 0.0, -3.0);
Waypoint next2 = new DefaultWaypoint(nextB[0], nextB[1]);
waypoints.add(next2);
travelledDown += mapper.distanceInKmBetweenGPSCoordinates(prev, next);
prev = next2;
}
count++;
} else {
double[] nextA = coordinateCalculator.calculateCoordinatesAfterMove(prev.getPosition().getLatitude(), prev.getPosition().getLongitude(), -5.0, 0.0);
Waypoint next = new DefaultWaypoint(nextA[0], nextA[1]);
if (tester.isInsideArea(next)) {
waypoints.add(next);
prev = next;
} else {
System.out.println("Left are, switching");
mode = "r";
double[] nextB = coordinateCalculator.calculateCoordinatesAfterMove(prev.getPosition().getLatitude(), prev.getPosition().getLongitude(), 0.0, -3.0);
Waypoint next2 = new DefaultWaypoint(nextB[0], nextB[1]);
waypoints.add(next2);
travelledDown += mapper.distanceInKmBetweenGPSCoordinates(prev, next);
prev = next2;
}
count++;
}
if (travelledDown >= distanceDown) {
finish = true;
}
}
List<de.dhbw.drone.waypoints.Waypoint> mapped = waypoints.stream().map(p -> new de.dhbw.drone.waypoints.Waypoint(p)).collect(Collectors.toList());
return mapped;
I stop the algorithm when it has travelled down the right/left side of the square.
However, I am struggeling if the shape of the marked area is no square and has sloped lines.
Therefore I would have to move with a bearing, like shown in this image:
When I could add this angle to my movement, I could use this algorithm for each shape of area, resulting in the following path:
Can someone help me how to move a distance in meters in this specific angle and how to calculate this angle between two GPS coordinates and how to calculate the angle after turning the side? (right to left instead of left to right and vice versa). Or could this be solved by calculating angle(point1, point2) and angle(point2, point1) ?
To calculate azimuth from one coordinate to another and point at given distancem you can use formulas from latlong page (bearing and destination point sections)
You can use the same approach as usually implemented in 2d polygon rasterization.
Horizontal picture lines are continous in memory, so filling horizontal lines is the most reliable way.
In your case you can use, for example, lines at constant latitude.
Sort vertices of your earth polygon by latitude and separate ranges between vertex latitude values, then fill resulting "trapezoids" with point sequences (plane example)
Sometimes (complex polygon etc) it would be simpler to use rejection method - generate grid points in escribed rectangle and ignore those outside of polygon.
I was wondering how I'd use different points in Java to draw a line. Here is the code I have so far. I would prefer to not get the x and y of each point and just have the program use the points from x, y on its own. I am still pretty new to programming and not very good at using javadoc at this point. The following is the code I have now:
//Create a Polygon class. A polygon is a closed shape with lines joining the corner points.
//You will keep the points in an array list. Use object of java.awt.Point for the point.
//Polygon will have as an instance variable an ArrayList of Points to hold the points
//The constructor takes no parameters but initializes the instance variable. The
//Polygon class also has the following methods:
// add: adds a Point to the polygon
// perimeter: returns the perimeter of the polygon
// draw: draws the polygon by connecting consecutive points and then
// connecting the last point to the first.
//
//No methods headers or javadoc is provided this time. You get to try your hand at writing
//a class almost from scratch
// Need help starting this question? In the lesson titled
// "Starting points: Problem Set Questions", go to the
// problem titled "Problem Set 6 - Question 3" for some tips on
// how to begin.
import java.util.ArrayList; import java.awt.Point;
public class Polygon {
ArrayList<Point> points;
/**
** a Polygon represents a shape with flat lines and several points.
*/
public Polygon(){
/**
* Constructs empty array of points.
*/
points = new ArrayList<Point>();
} /** ** Adds points to the polygon points Array. *#param = points x and y coordinates to add to class */
public void addPoints(Point point){
points.add(point);
}
/**
**gets distance between points in ArrayList
*/
public double perimeter(){
double perimeter = 0;
int i = 0;
while(i<points.size()){
if(i<points.size()-1){
perimeter = perimeter + points.get(i).distance(points.get(i+1));
}
else{
perimeter = perimeter + points.get(i).distance(points.get(0));
}
}
return perimeter;
}
/** * Draws Polygon using points from points ArrayList */
public void drawPolygon(){
int i = 0;
while(i < points.size()-1){
Line2D line = Line2D.Float(points.get(i), points.get(i+1));
line.draw();
}
Line2D lastLine = Line2D.Float(points.get(0), points.get(points.size()-1));
lastLine.draw();
} }
Let me understand this correctly, you want to implement the draw() method?
If so, what you need to do is search for Bresenham's Algorithm which draws a line based on two given points.
I know this exact question, because I encountered some MAJOR issues with it too.. I stumbled across this question during Udacity's Java Basics course.
I was so stuck, until I moved onto the proceeding lessons which go onto explain more about classes. I downloaded the Problem Set 6 zip file from https://www.udacity.com/wiki/cs046/code and navigated to the Polygon Project folder. In there, I opened the BlueJay project file and could see all the other classes available for use in my Polygon class.
Once I had initialised a Line in my Polygon draw method, the connection between the Polygon class & the Line class appeared (HOORAY).
You can open the Line class to see what its constructors & methods are. Obviously the Line class is working in with a few other classes to actually be able to draw the line, but we can't see any of this in the Udacity IDE and the question doesn't really tell us either..
Nonetheless, I have added my code, that worked in the Udacity IDE to get the correct output.
I see you have used a distance method on the Point class. I don't think this exists.. Hence the garbage in the middle of my code. I probably could have condensed this somehow, but I was just happy to get the right outputs.
public class Polygon
{
private ArrayList<Point> shape; //instance variable
public Polygon() //constructs a new ArrayList of points called shape
{
shape = new ArrayList<Point>();
}
public void add(Point point) //takes in a Point and adds it to the ArrayList "shape"
{
shape.add(point);
}
public double perimeter()
{
double total = 0;
if (shape.size() > 2) //a polygon must have more than 2 sides
{
for (int i = 0; i < shape.size() - 1; i++) //for every point in the list, except for the last
{
double x1 = shape.get(i).getX();
double y1 = shape.get(i).getY();
double x2 = shape.get(i + 1).getX();
double y2 = shape.get(i + 1).getY();
double dist = Math.sqrt(Math.abs(Math.pow((x2 - x1), 2) //get the distance between the point
+ Math.pow((y2 - y1), 2))); //and the following point
total = total + dist; //add this distance to the total
}
double firstX = shape.get(0).getX();
double firstY = shape.get(0).getY();
double lastX = shape.get(shape.size() - 1).getX();
double lastY = shape.get(shape.size() - 1).getY();
double finalDist = Math.sqrt(Math.abs(Math.pow((lastX - firstX), 2) //get the distance between
+ Math.pow((lastY - firstY), 2))); //the first & last points
total = total + finalDist; //add it to the total
}
return total;
}
public void draw()
{
for (int i = 0; i < shape.size() - 1; i++) //for every point in the list, except for the last
{
double x1 = shape.get(i).getX();
double y1 = shape.get(i).getY();
double x2 = shape.get(i + 1).getX();
double y2 = shape.get(i + 1).getY();
Line line = new Line(x1, y1, x2, y2); //create a new Line "line"
line.draw(); //draw the line
}
double firstX = shape.get(0).getX();
double firstY = shape.get(0).getY();
double lastX = shape.get(shape.size() - 1).getX();
double lastY = shape.get(shape.size() - 1).getY();
Line lastLine = new Line(lastX, lastY, firstX, firstY); //get the first & last points & create a line
lastLine.draw(); //draw this line too
}
}
Rotating Asteroids ( Polygons )
I am trying to rotate asteroids(polygons) so that they look nice. I am doing this through multiple mathematical equations. To start I give the individual asteroid a rotation velocity:
rotVel = ((Math.random()-0.5)*Math.PI/16);
Then I create the polygon shape,
this.shape = new Polygon();
Followed by generating the points,
for (j = 0; j < s; j++) {
theta = 2 * Math.PI / s * j;
r = MIN_ROCK_SIZE + (int) (Math.random() * (MAX_ROCK_SIZE - MIN_ROCK_SIZE));
x = (int) -Math.round(r * Math.sin(theta)) + asteroidData[0];
y = (int) Math.round(r * Math.cos(theta)) + asteroidData[1];
shape.addPoint(x, y);
}
Finally, in a loop a method is being called in which it attempts to move the polygon and its points down as well as rotating them. (I'm just pasting the rotating part as the other one is working)
for (int i = 0; i < shape.npoints; i++) {
// Subtract asteroid's x and y position
double x = shape.xpoints[i] - asteroidData[0];
double y = shape.ypoints[i] - asteroidData[1];
double temp_x = ((x * Math.cos(rotVel)) - (y * Math.sin(rotVel)));
double temp_y = ((x * Math.sin(rotVel)) + (y * Math.cos(rotVel)));
shape.xpoints[i] = (int) Math.round(temp_x + asteroidData[0]);
shape.ypoints[i] = (int) Math.round(temp_y + asteroidData[1]);
}
now, the problem is that when it prints to the screen the asteroids appear to 'warp' or rather the x and y positions on some of the polygon points 'float' off course.
I've noticed that when I make 'rotVel' be a whole number the problem is solved however the asteroid will rotate at mach speeds. So I've concluded that the problem has to be in the rounding but no matter what I do I can't seem to find a way to get it to work as the Polygon object requires an array of ints.
Does anyone know how to fix this?
Currently your asteroids rotate around (0 , 0) as far as i can see. Correct would be to rotate them around the center of the shape, which would be (n , m), where n is the average of all x-coordinates of the shape, and m is the average of all y-coordinates of the shape.
Your problem is definitely caused by rounding to int! The first improvement is to make all shape coordinates to be of type double. This will solve most of your unwanted 'effects'.
But even with double you might experience nasty rounding errors in case you do a lot of very small updates of the coordinates. The solution is simple: Just avoid iterative updates of the asteroid points. Every time, you update the coordinates based on the previous coordinates, the rounding error will get worse.
Instead, add a field for the rotation angle to the shape and increment it instead of the points themselves. Not until drawing the shape, you compute the final positions by applying the rotation to the points. Note that this will never change the points themselves.
You can extend this concept to other transformations (e.g. translation) too. What you get is some kind of local coordinate system for every shape/object. The points of the shape are defined in the local coordinate system. By moving and rotating this system, you can reposition the entire object anywhere in space.
public class Shape {
// rotation and position of the local coordinate system
private double rot, x, y;
// points of the shape in local coordinate system
private double[] xp, yp;
private int npoints;
// points of the shape in world coordinates
private int[][] wxp, wyp;
private boolean valid;
public void setRotation(double r) { this.rot = r; valid = false; }
public void setPosition(double x, double y) { this.x = x; this.y = y; valid = false; }
public void addPoint(double x, double y) {
// TODO: add point to xp, yp
valid = false;
}
public void draw(...) {
if (!valid) {
computeWorldCoordinates(wxp, wyp);
valid = true;
}
// TODO: draw shape at world coordaintes wxp and wyp
}
protected void computeWorldCoordinates(int[] xcoord, int[] ycoord) {
for (int i = 0; i < npoints; i++) {
double temp_x = xp[i] * Math.cos(rot) - yp[i] * Math.sin(rot);
double temp_y = xp[i] * Math.sin(rot) + yp[i] * Math.cos(rot);
xcoord[i] = (int) Math.round(x + temp_x);
ycoord[i] = (int) Math.round(y + temp_y);
}
}
}
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;
}
}