Finding angle of a line between two revolving points in two orbits, from one pont to other and not viceversa in JavaFX

636 views Asked by At

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.

enter image description here

//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.

2

There are 2 answers

2
Slaw On BEST ANSWER

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:

  1. JavaFX has the Point2D class which can represent vectors and provides convenient methods (e.g. angle).
  2. The angle between two vectors uses inverse cosine. That will give us a value between 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:

GIF of demo application

6
Sai Dandem On

Firstly, I may not be sure that this can be the exact solution. Based on what I understood from your question, I am trying to explain how you can compute an angle between points.

To get an angle between two points, you need a third vertex point where the angle is measured. The first step is to determine all your three points positions with respect to a particular coordinate system. Lets say to determine the positions of points with reference to a Pane.

Now use Point2D API to calculate the angle between three points, by choosing the desired vertex point. This will always result you the acute angle.

Below is the sample demo to calculate the angle between two planets considering the sun as the vertex.

Note that the vertex need not to be a center point. It can be any point in the referred coordinate system.

I hope this can help you to get something to start with :)

import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.binding.DoubleBinding;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
import javafx.util.Duration;

public class EarthJupiterAngle_Demo extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        VBox root = new VBox();
        root.setSpacing(10);
        root.setPadding(new Insets(10));
        Scene scene = new Scene(root, 600, 600);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Angle between Earth & Jupiter");
        primaryStage.show();

        Pane pane = new Pane();
        pane.setPadding(new Insets(10));
        pane.setStyle("-fx-border-width:1px;-fx-border-color:black;-fx-background-color:white;");
        VBox.setVgrow(pane, Priority.ALWAYS);

        Circle sun = new Circle(8, Color.ORANGE);
        sun.centerXProperty().bind(pane.widthProperty().divide(2));
        sun.centerYProperty().bind(pane.heightProperty().divide(2));

        double earthAU = 100;
        Circle earthOrbit = new Circle(earthAU);
        earthOrbit.setFill(null);
        earthOrbit.setStroke(Color.LIGHTBLUE);
        earthOrbit.setStrokeWidth(1);
        earthOrbit.getStrokeDashArray().addAll(10d, 5d);
        earthOrbit.centerXProperty().bind(sun.centerXProperty());
        earthOrbit.centerYProperty().bind(sun.centerYProperty());

        Circle earth = new Circle(5, Color.BLUE);
        earth.layoutXProperty().bind(sun.centerXProperty());
        earth.layoutYProperty().bind(sun.centerYProperty());
        PathTransition earthRotate = new PathTransition();
        earthRotate.setDuration(Duration.millis(10000));
        earthRotate.setNode(earth);
        earthRotate.setPath(earthOrbit);
        earthRotate.setCycleCount(Animation.INDEFINITE);
        earthRotate.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);
        earthRotate.setInterpolator(Interpolator.LINEAR);
        earthRotate.play();

        Line earthLine = new Line();
        earthLine.startXProperty().bind(sun.centerXProperty());
        earthLine.startYProperty().bind(sun.centerYProperty());
        earthLine.endXProperty().bind(earth.layoutXProperty().add(earth.translateXProperty()));
        earthLine.endYProperty().bind(earth.layoutYProperty().add(earth.translateYProperty()));
        earthLine.setStroke(Color.GRAY);
        earthLine.setStrokeWidth(1);
        earthLine.getStrokeDashArray().addAll(10d, 5d);

        double jupiterAU = 180;
        Circle jupiterOrbit = new Circle(jupiterAU);
        jupiterOrbit.setFill(null);
        jupiterOrbit.setStroke(Color.SANDYBROWN);
        jupiterOrbit.setStrokeWidth(1);
        jupiterOrbit.getStrokeDashArray().addAll(10d, 5d);
        jupiterOrbit.centerXProperty().bind(sun.centerXProperty());
        jupiterOrbit.centerYProperty().bind(sun.centerYProperty());

        Circle jupiter = new Circle(7, Color.BROWN);
        jupiter.layoutXProperty().bind(sun.centerXProperty());
        jupiter.layoutYProperty().bind(sun.centerYProperty());
        PathTransition jupiterRotate = new PathTransition();
        jupiterRotate.setDuration(Duration.millis(18000));
        jupiterRotate.setNode(jupiter);
        jupiterRotate.setPath(jupiterOrbit);
        jupiterRotate.setCycleCount(Animation.INDEFINITE);
        jupiterRotate.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);
        jupiterRotate.setInterpolator(Interpolator.LINEAR);
        jupiterRotate.play();

        Line jupiterLine = new Line();
        jupiterLine.startXProperty().bind(sun.centerXProperty());
        jupiterLine.startYProperty().bind(sun.centerYProperty());
        jupiterLine.endXProperty().bind(jupiter.layoutXProperty().add(jupiter.translateXProperty()));
        jupiterLine.endYProperty().bind(jupiter.layoutYProperty().add(jupiter.translateYProperty()));
        jupiterLine.setStroke(Color.GRAY);
        jupiterLine.setStrokeWidth(1);
        jupiterLine.getStrokeDashArray().addAll(10d, 5d);


        DoubleBinding angle = new DoubleBinding() {
            {
                bind(earth.translateXProperty(), earth.layoutXProperty(), earth.translateYProperty(), earth.layoutYProperty()
                        , jupiter.translateXProperty(), jupiter.layoutXProperty(), jupiter.translateYProperty(), jupiter.layoutYProperty());
            }

            @Override
            protected double computeValue() {
                // Sun position in pane
                double sX = sun.getCenterX();
                double sY = sun.getCenterY();

                // Earth position in pane
                double eX = earth.getLayoutX() + earth.getTranslateX();
                double eY = earth.getLayoutY() + earth.getTranslateY();

                // Jupiter position in pane
                double jX = jupiter.getLayoutX() + jupiter.getTranslateX();
                double jY = jupiter.getLayoutY() + jupiter.getTranslateY();

                // Use Point2D API to calculate angle between three points
                return Math.round(new Point2D(sX, sY).angle(new Point2D(eX, eY), new Point2D(jX, jY)));
            }
        };

        Label angleLabel = new Label("Angle : ");
        Label valLabel = new Label("");

        Timeline angleTimeline = new Timeline(new KeyFrame(Duration.millis(100), e -> valLabel.setText(angle.get() + " deg")));
        angleTimeline.setCycleCount(Animation.INDEFINITE);
        angleTimeline.play();

        pane.getChildren().addAll(earthLine, jupiterLine, sun, earthOrbit, earth, jupiterOrbit, jupiter);
        ToggleButton button = new ToggleButton("Pause");
        button.setPrefWidth(120);
        button.selectedProperty().addListener((obs, old, selected) -> {
            if (selected) {
                button.setText("Play");
                earthRotate.pause();
                jupiterRotate.pause();
                angleTimeline.pause();
            } else {
                button.setText("Pause");
                earthRotate.play();
                jupiterRotate.play();
                angleTimeline.play();
            }
        });
        HBox hb = new HBox(button, angleLabel, valLabel);
        hb.setStyle("-fx-font-size:18px;");
        hb.setAlignment(Pos.CENTER_LEFT);
        hb.setSpacing(10);
        root.getChildren().addAll(hb, pane);
    }
}

enter image description here

Update: Please check below screenshot regarding my understanding about computing angles. enter image description here