Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions src/main/java/com/thealgorithms/physics/ProjectileMotion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.thealgorithms.physics;

/**
*
* This implementation calculates the flight path of a projectile launched from any INITIAL HEIGHT.
* It is a more flexible version of the ground-to-ground model.
*
* @see <a href="https://en.wikipedia.org/wiki/Projectile_motion">Wikipedia - Projectile Motion</a>
* @author [Priyanshu Kumar Singh](https://github.com/Priyanshu1303d)
*/
public final class ProjectileMotion {

private ProjectileMotion() {
}

/** Standard Earth gravity constant*/
private static final double GRAVITY = 9.80665;

/**
* A simple container for the results of a projectile motion calculation.
*/
public static final class Result {
private final double timeOfFlight;
private final double horizontalRange;
private final double maxHeight;

public Result(double timeOfFlight, double horizontalRange, double maxHeight) {
this.timeOfFlight = timeOfFlight;
this.horizontalRange = horizontalRange;
this.maxHeight = maxHeight;
}

/** @return The total time the projectile is in the air (seconds). */
public double getTimeOfFlight() {
return timeOfFlight;
}

/** @return The total horizontal distance traveled (meters). */
public double getHorizontalRange() {
return horizontalRange;
}

/** @return The maximum vertical height from the ground (meters). */
public double getMaxHeight() {
return maxHeight;
}
}

/**
* Calculates projectile trajectory using standard Earth gravity.
*
* @param initialVelocity Initial speed of the projectile (m/s).
* @param launchAngleDegrees Launch angle from the horizontal (degrees).
* @param initialHeight Starting height of the projectile (m).
* @return A {@link Result} object with the trajectory data.
*/
public static Result calculateTrajectory(double initialVelocity, double launchAngleDegrees, double initialHeight) {
return calculateTrajectory(initialVelocity, launchAngleDegrees, initialHeight, GRAVITY);
}

/**
* Calculates projectile trajectory with a custom gravity value.
*
* @param initialVelocity Initial speed (m/s). Must be non-negative.
* @param launchAngleDegrees Launch angle (degrees).
* @param initialHeight Starting height (m). Must be non-negative.
* @param gravity Acceleration due to gravity (m/s^2). Must be positive.
* @return A {@link Result} object with the trajectory data.
*/
public static Result calculateTrajectory(double initialVelocity, double launchAngleDegrees, double initialHeight, double gravity) {
if (initialVelocity < 0 || initialHeight < 0 || gravity <= 0) {
throw new IllegalArgumentException("Velocity, height, and gravity must be non-negative, and gravity must be positive.");
}

double launchAngleRadians = Math.toRadians(launchAngleDegrees);
double v_iy = initialVelocity * Math.sin(launchAngleRadians); // Initial vertical velocity
double v_ix = initialVelocity * Math.cos(launchAngleRadians); // Initial horizontal velocity

// Correctly calculate total time of flight using the quadratic formula for vertical motion.
// y(t) = y0 + v_iy*t - 0.5*g*t^2. We solve for t when y(t) = 0.
double totalTimeOfFlight = (v_iy + Math.sqrt(v_iy * v_iy + 2 * gravity * initialHeight)) / gravity;

// Calculate max height. If launched downwards, max height is the initial height.
double maxHeight;
if (v_iy > 0) {
double heightGained = (v_iy * v_iy) / (2 * gravity);
maxHeight = initialHeight + heightGained;
} else {
maxHeight = initialHeight;
}

double horizontalRange = v_ix * totalTimeOfFlight;

return new Result(totalTimeOfFlight, horizontalRange, maxHeight);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.thealgorithms.physics;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
* Test class for the general-purpose ProjectileMotion calculator.
*
*/
final class ProjectileMotionTest {

private static final double DELTA = 1e-4; // Tolerance for comparing double values

@Test
@DisplayName("Test ground-to-ground launch (initial height is zero)")
void testGroundToGroundLaunch() {
ProjectileMotion.Result result = ProjectileMotion.calculateTrajectory(50, 30, 0);
assertEquals(5.0986, result.getTimeOfFlight(), DELTA);
assertEquals(220.7750, result.getHorizontalRange(), DELTA);
assertEquals(31.8661, result.getMaxHeight(), DELTA);
}

@Test
@DisplayName("Test launch from an elevated position")
void testElevatedLaunch() {
ProjectileMotion.Result result = ProjectileMotion.calculateTrajectory(30, 45, 100);
assertEquals(7.1705, result.getTimeOfFlight(), DELTA);
assertEquals(152.1091, result.getHorizontalRange(), DELTA);
assertEquals(122.9436, result.getMaxHeight(), DELTA); // Final corrected value
}

@Test
@DisplayName("Test launch straight up (90 degrees)")
void testVerticalLaunch() {
ProjectileMotion.Result result = ProjectileMotion.calculateTrajectory(40, 90, 20);
assertEquals(8.6303, result.getTimeOfFlight(), DELTA);
assertEquals(0.0, result.getHorizontalRange(), DELTA);
assertEquals(101.5773, result.getMaxHeight(), DELTA);
}

@Test
@DisplayName("Test horizontal launch from a height (0 degrees)")
void testHorizontalLaunch() {
ProjectileMotion.Result result = ProjectileMotion.calculateTrajectory(25, 0, 80);
assertEquals(4.0392, result.getTimeOfFlight(), DELTA);
assertEquals(100.9809, result.getHorizontalRange(), DELTA);
assertEquals(80.0, result.getMaxHeight(), DELTA);
}

@Test
@DisplayName("Test downward launch from a height (negative angle)")
void testDownwardLaunchFromHeight() {
ProjectileMotion.Result result = ProjectileMotion.calculateTrajectory(20, -30, 100);
assertEquals(3.6100, result.getTimeOfFlight(), DELTA);
assertEquals(62.5268, result.getHorizontalRange(), DELTA);
assertEquals(100.0, result.getMaxHeight(), DELTA);
}

@Test
@DisplayName("Test invalid arguments throw an exception")
void testInvalidInputs() {
assertThrows(IllegalArgumentException.class, () -> ProjectileMotion.calculateTrajectory(-10, 45, 100));
assertThrows(IllegalArgumentException.class, () -> ProjectileMotion.calculateTrajectory(10, 45, -100));
assertThrows(IllegalArgumentException.class, () -> ProjectileMotion.calculateTrajectory(10, 45, 100, 0));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ void testReproducibility() {
void testNegativeInterval() {
// Integral of f(x) = x from -1 to 1 is 0
Function<Double, Double> linear = Function.identity();
double result = approximate(linear, -1, 1, 10000);
assertEquals(0.0, result, EPSILON);
// The integral of x from 2 to 1 is -1.5
double result = approximate(linear, 2, 1, 10000);
assertEquals(-1.5, result, EPSILON);
}

@Test
Expand Down
Loading