I am new to JavaFX and really weak in Trigonometry and Math and have been trying to find the angle (of a line) between two points. These two points revolve around a common central point in two different perfect circular orbits. These points represent Earth and Jupiter and I need to find the angle between Earth and Jupiter, to be exact, angle from Earth to Jupiter. Jupiter revolves in an orbit with higher radius as in our solar system. I tried 'atan' and 'atan2' but it doesn't give expected answer for all angles, I am too dumb to use it properly. Upon further research I found out something like ' it only calculates the angle with respect to +ve side of X axis', but I got the point. With further reading and help from 'Slope Calculator' (https://www.calculator.net/slope-calculator.html) I managed to solve that issue as they did. By adding appropriate degrees like 180,360 to the atan / atan2 results and getting correct and expected (by me) final results. This adding of 90/180/360 degrees (I haven't seen them adding 90 degrees, or why would one want to add 0 degree) is not fully understood by me. My poor Math skills says its the difference of the measured angle (the old +ve x axis, 360/180 - measured angle ?) to 360 degrees, or simply put, the unmeasured angle (total of 180 or 360 degrees) The problem is, the results are not always the expected, rarely it goes wrong, very big difference, totally wrong. This is owing to the adding of wrong degrees to atan / atan2 results. The method used at the Calculator Site gives correct results, except for line of 180 degrees (3'O clock to 9'O clock line), it gives 0 degrees. (Shouldn't be it 180 degrees? What ever, I need 180 degree for such a line). The site adds 180 degree or 360 degree to the result to get the final result and is correct and as per my requirement, expect for 3'O Clock ----> 9'O Clock line case. The angle for 9'O ---> 3'O is correct and as per my requirement. To figure out how much degrees to add to the atan / atan2 results, I am currently finding the slope of the line and adding 0/90/180/360 degrees to the atan / atan2 results and getting expected result even for 3'O clock ----> 9 'O clock line. Still something is wrong.
//PANE_HEIGHT is 960, the height and width of pane. PositionX is subtracted from it to get the //Cartesian plane coordinates instead of screen coordinates
currentJupiterAngleRetroRough = (Math.toDegrees(Math.atan((((PANE_HEIGHT - jupiterPositionY) - ( PANE_HEIGHT-earthPositionY))) / ((jupiterPositionX) - (earthPositionX)))));
//Finding the slope of the line
slope = (((PANE_HEIGHT-jupiterPositionY) - (PANE_HEIGHT-earthPositionY)) / ((jupiterPositionX) - (earthPositionX)));
//Adding required angles to output of atan to get final degrees,based on the slope
currentJupiterAngleRetro = (Math.toDegrees( Math.atan((((PANE_HEIGHT - jupiterPositionY) - ( PANE_HEIGHT-earthPositionY))) / ((jupiterPositionX) - (earthPositionX) )))) +
(slope<0 ? 360:(slope==0?0:(slope>0 & slope<1 ? 0:(slope>1 & slope<2 ? 90:180 ))));
//Various approaches to find the appropriate degrees to add to atan result
(slope<0 ? 360:180);
(slope<0 ? 360:(slope==0?0:180 ));
// Different One
// Another one
// and so on
(slope<0 ? 360:(slope==0?0:(slope>0 & slope<1 ? 0:(slope>1 & slope<2 ? 90:180 )))); //Improved one, still not fully correct
The app simulates the position of planets continuously for any date/time and continuously updates the position and degree in both graphics and text. So it is a time taking task to find if the calculations are wrong and I sure found it is, but very hard to find. Apart from that, the degrees which are to be calculated are of angle between planets that are retrograding (backward moving optical illusion), so way hard to spot the calculation errors by comparing to anything. Will have to log all the original angles,positions,retro angles to console and ready line by line to see big jumps or will have the watch the original angle and calculate the retro angle approximately in mind and verify.
It took two complete days to find a almost-fully-correct working solution and cant longer pull my hair. Have read similar questions on StackOverflow and even someone had almost the same issue, but no answer I believe, and the discussion were continued on chat but couldn't find more info. I hope it would be very easy for people who are good at math. Please provide a perfect solution. Thanks in advance.
You essentially have a vector from Earth to Jupiter and you want to find the angle (i.e. direction) of this vector. You also want the angle measured counterclockwise from the positive x-axis. What that means is you can measure the same angle using two vectors—your vector and the unit vector in the positive x direction. This is important because it can simplify the implementation since:
Point2D
class which can represent vectors and provides convenient methods (e.g. angle
).0
and 180
degrees which I personally find easier to work with than inverse tangent's range of -90
to 90
degrees.For example, if you have two points then you can calculate the angle between the implicit vector and the positive x-axis using the following:
/**
* Computes the angle (in degrees) of the vector from {@code p1} to {@code p2}. The angle
* will be in the range {@code 0} (inclusive) to {@code 360} (exclusive) as measured
* counterclockwise from the positive x-axis.
*
* @param p1 the start point of the vector
* @param p2 the end point of the vector
* @return the angle, in degrees, of the vector from {@code p1} to {@code p2} measured
* counterclockwise from the positive x-axis
*/
public static double computeAngleOfVector(Point2D p1, Point2D p2) {
Point2D vector = new Point2D(p2.getX() - p1.getX(), p2.getY() - p1.getY());
double angle = vector.angle(1.0, 0.0);
if (vector.getY() > 0) {
// vector pointing downwards and thus is in the 3rd or 4th quadrant
return 360.0 - angle;
}
// vector pointing upwards and thus is in the 1st or 2nd quadrant
return angle;
}
Note the reason I use vector.getY() > 0
rather than vector.getY() < 0
is because JavaFX, like most(?) GUI frameworks, has the positive y direction pointing down the screen. Depending on how you represent the coordinate system in your model you may have to modify the code slightly.
Here's an application demonstrating the above in a way I believe matches what you want:
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.PathTransition;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.value.ObservableDoubleValue;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
private static final double SCENE_WIDTH = 1000;
private static final double SCENE_HEIGHT = 700;
@Override
public void start(Stage primaryStage) {
Circle sun = createCelestialBody(50, Color.YELLOW);
Circle earth = createCelestialBody(20, Color.BLUE);
Circle earthOrbitIndicator = createOrbitIndicator(150);
Circle jupiter = createCelestialBody(35, Color.BROWN);
Circle jupiterOrbitIndicator = createOrbitIndicator(300);
Line earthJupiterVector = createBodyToBodyVector(earth, jupiter);
DoubleBinding angleObservable = createAngleBinding(earthJupiterVector);
Line xAxisIndicator = createXAxisIndicator(earth);
Arc angleIndicator = createAngleIndicator(earth, angleObservable);
Pane root =
new Pane(
createAngleLabel(angleObservable),
earthOrbitIndicator,
jupiterOrbitIndicator,
sun,
earth,
jupiter,
earthJupiterVector,
xAxisIndicator,
angleIndicator);
primaryStage.setScene(new Scene(root, SCENE_WIDTH, SCENE_HEIGHT));
primaryStage.setTitle("Earth-Jupiter Vector Angle");
primaryStage.setResizable(false);
primaryStage.show();
animateOrbit(Duration.seconds(7), earth, earthOrbitIndicator.getRadius());
animateOrbit(Duration.seconds(16), jupiter, jupiterOrbitIndicator.getRadius());
}
private Label createAngleLabel(ObservableDoubleValue angleObservable) {
Label label = new Label();
label.setPadding(new Insets(10));
label.setUnderline(true);
label.setFont(Font.font("Monospaced", FontWeight.BOLD, 18));
label
.textProperty()
.bind(
Bindings.createStringBinding(
() -> String.format("Angle: %06.2f", angleObservable.get()), angleObservable));
return label;
}
private Circle createCelestialBody(double radius, Color fill) {
Circle body = new Circle(radius, fill);
body.setCenterX(SCENE_WIDTH / 2);
body.setCenterY(SCENE_HEIGHT / 2);
return body;
}
private Circle createOrbitIndicator(double radius) {
Circle indicator = new Circle(radius, Color.TRANSPARENT);
indicator.setStroke(Color.DARKGRAY);
indicator.getStrokeDashArray().add(5.0);
indicator.setCenterX(SCENE_WIDTH / 2);
indicator.setCenterY(SCENE_HEIGHT / 2);
return indicator;
}
private void animateOrbit(Duration duration, Circle celestialBody, double orbitRadius) {
Circle path = new Circle(SCENE_WIDTH / 2, SCENE_HEIGHT / 2, orbitRadius);
PathTransition animation = new PathTransition(duration, path, celestialBody);
animation.setCycleCount(Animation.INDEFINITE);
animation.setInterpolator(Interpolator.LINEAR);
animation.playFromStart();
}
private Line createBodyToBodyVector(Circle firstBody, Circle secondBody) {
Line vectorLine = new Line();
vectorLine.setStroke(Color.BLACK);
vectorLine.setStrokeWidth(2);
vectorLine.getStrokeDashArray().add(5.0);
vectorLine.startXProperty().bind(centerXInParentOf(firstBody));
vectorLine.startYProperty().bind(centerYInParentOf(firstBody));
vectorLine.endXProperty().bind(centerXInParentOf(secondBody));
vectorLine.endYProperty().bind(centerYInParentOf(secondBody));
return vectorLine;
}
private Line createXAxisIndicator(Circle anchor) {
Line xAxisIndicator = new Line();
xAxisIndicator.setStroke(Color.GREEN);
xAxisIndicator.setStrokeWidth(2);
xAxisIndicator.startXProperty().bind(centerXInParentOf(anchor));
xAxisIndicator.startYProperty().bind(centerYInParentOf(anchor));
xAxisIndicator.endXProperty().bind(xAxisIndicator.startXProperty().add(75));
xAxisIndicator.endYProperty().bind(xAxisIndicator.startYProperty());
return xAxisIndicator;
}
private Arc createAngleIndicator(Circle anchor, ObservableDoubleValue angleObservable) {
Arc arc = new Arc();
arc.setFill(Color.TRANSPARENT);
arc.setStroke(Color.RED);
arc.setStrokeWidth(2);
arc.getStrokeDashArray().add(5.0);
arc.centerXProperty().bind(centerXInParentOf(anchor));
arc.centerYProperty().bind(centerYInParentOf(anchor));
arc.setRadiusX(50);
arc.setRadiusY(50);
arc.setStartAngle(0);
arc.lengthProperty().bind(angleObservable);
return arc;
}
// NOTE: getCenterX() and getCenterY() were added in JavaFX 11. The calculations
// are simple, however. It's just (minX + maxX) / 2 and similar for y.
private DoubleBinding centerXInParentOf(Node node) {
return Bindings.createDoubleBinding(
() -> node.getBoundsInParent().getCenterX(), node.boundsInParentProperty());
}
private DoubleBinding centerYInParentOf(Node node) {
return Bindings.createDoubleBinding(
() -> node.getBoundsInParent().getCenterY(), node.boundsInParentProperty());
}
private DoubleBinding createAngleBinding(Line line) {
return Bindings.createDoubleBinding(
() -> {
Point2D vector =
new Point2D(line.getEndX() - line.getStartX(), line.getEndY() - line.getStartY());
double angle = vector.angle(1, 0);
if (vector.getY() > 0) {
return 360 - angle;
}
return angle;
},
line.startXProperty(),
line.endXProperty(),
line.startYProperty(),
line.endYProperty());
}
}
And here's what the example looks like: