Wrote tests for the Vector class

This commit is contained in:
Quinn
2023-04-06 10:39:31 -05:00
parent fc9d4e497b
commit 7438d0f64e
19 changed files with 328 additions and 106 deletions

View File

@@ -4,6 +4,7 @@
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
@@ -16,5 +17,31 @@
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library" scope="TEST">
<library name="JUnit4">
<CLASSES>
<root url="jar://$MODULE_DIR$/lib/junit-4.13.1.jar!/" />
<root url="jar://$MODULE_DIR$/lib/hamcrest-core-1.3.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library" scope="TEST">
<library name="JUnit5.8.1">
<CLASSES>
<root url="jar://$MODULE_DIR$/lib/junit-jupiter-5.8.1.jar!/" />
<root url="jar://$MODULE_DIR$/lib/junit-jupiter-api-5.8.1.jar!/" />
<root url="jar://$MODULE_DIR$/lib/opentest4j-1.2.0.jar!/" />
<root url="jar://$MODULE_DIR$/lib/junit-platform-commons-1.8.1.jar!/" />
<root url="jar://$MODULE_DIR$/lib/apiguardian-api-1.1.2.jar!/" />
<root url="jar://$MODULE_DIR$/lib/junit-jupiter-params-5.8.1.jar!/" />
<root url="jar://$MODULE_DIR$/lib/junit-jupiter-engine-5.8.1.jar!/" />
<root url="jar://$MODULE_DIR$/lib/junit-platform-engine-1.8.1.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
</component>
</module>

Binary file not shown.

BIN
lib/hamcrest-core-1.3.jar Normal file

Binary file not shown.

BIN
lib/junit-4.13.1.jar Normal file

Binary file not shown.

BIN
lib/junit-jupiter-5.8.1.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/opentest4j-1.2.0.jar Normal file

Binary file not shown.

View File

@@ -3,11 +3,10 @@ import java.util.ArrayList;
import static java.lang.Math.PI;
import static processing.core.PApplet.degrees;
import static processing.core.PApplet.radians;
import processing.core.PVector;
import processing.core.PApplet;
public class Car{
PVector pose = new PVector(0,0); // the car's x, y position
Vector pose = new Vector(0,0); // the car's x, y position
float angle = 0; // the current angle that the car is at.
int carLength = 50;
int carWidth = 40;
@@ -25,7 +24,7 @@ public class Car{
Car(PApplet processing, int xPos, int yPos, int carLength, int carWidth){
proc = processing;
slam = new SLAM(proc);
this.pose = new PVector(xPos, yPos);
this.pose = new Vector(xPos, yPos);
this.carLength = carLength;
this.carWidth = carWidth;
}
@@ -40,6 +39,7 @@ public class Car{
proc.stroke(255);
proc.ellipse(pose.x, pose.y, carWidth, carLength);
this.updateScan(walls);
this.slam.drawLines();
}
//With all the views that the car has, get their point list
@@ -49,12 +49,12 @@ public class Car{
}
for(View view : views){
ArrayList<PVector> pointList = view.getPoints();
ArrayList<Vector> pointList = view.getPoints();
slam.RANSAC(pointList, view.getFOV() / view.getRayNum());
}
}
public PVector getPose(){
public Vector getPose(){
return pose;
}
@@ -75,7 +75,7 @@ public class Car{
}
}
public void setPose(PVector newPose){
public void setPose(Vector newPose){
pose = newPose;
for(View view : views){
view.setPos(pose);

98
src/Line.java Normal file
View File

@@ -0,0 +1,98 @@
import processing.core.PApplet;
import java.util.List;
import static processing.core.PApplet.abs;
import static processing.core.PApplet.pow;
public class Line{
Vector direction = new Vector(0,0);
Vector position = new Vector(0,0);
float length = 0;
private static PApplet proc;
Line(PApplet processing, Vector startPosition, Vector endPosition){
this.proc = processing;
this.position = startPosition;
this.direction = endPosition.sub(startPosition).normalize();
this.length = direction.mag();
}
Line(PApplet processing, Vector direction, Vector position, float lineLength){
this.direction = direction.normalize();
this.position = position;
this.length = lineLength;
proc = processing;
}
/**
* attempt to find the line of best fit for the given points
* @param points the points to get the line of best for
*/
Line(PApplet processing, List<Vector> points){
bestFit(points);
proc = processing;
}
// least squares line of best fit algorithm
private void bestFit(List<Vector> points){
// get the mean of all the points
Vector mean = new Vector();
for(Vector point : points){
mean.add(point);
}
mean.div(points.size());
// this section calculates the direction vector of the line of best fit
Vector direction = new Vector();
// get the rise and run of the line of best fit
for(Vector point : points){
direction.y += (point.x - mean.x)*(point.y - mean.y); // rise
direction.x += pow((point.x - mean.x),2);
// find the point that's furthest from the mean and use it to set the line length.
float dist = abs(point.sub(mean).mag());
if(dist > this.length){
this.length = 2*dist;
}
}
this.position = mean.sub(direction.div(direction.mag()).mul(this.length / 2));
this.direction = direction.normalize();
}
public Vector getSlopeIntForm(){
float slope = direction.y / direction.x;
float intercept = position.y - slope * position.x;
return new Vector(slope, intercept);
}
public Vector getDirection(){
return direction;
}
public Vector getPosition(){
return position;
}
public float getLength(){
return length;
}
/**
* Draw the line on screen
*/
public void draw(){
Vector endPoint = position.add(direction.mul(length));
proc.line(position.x, position.y, endPoint.x, endPoint.y);
}
/**
* @param point
* @return the smallest distance from the point to this line
*/
public float getDistance(Vector point){
return (point.sub(position).cross(direction)).mag() / direction.mag();
}
}

View File

@@ -1,5 +1,4 @@
import processing.core.PApplet;
import processing.core.PVector;
import java.util.ArrayList;
@@ -20,15 +19,15 @@ public class Processing extends PApplet {
size(1000, 1000);
car.addView(180,180);
for(int i = 0; i < 20; i++){
Wall wall = new Wall(processing, new PVector((int)random(40, 1840), (int)random(40, 960)), (int)random(360), (int)random(100, 1000));
Wall wall = new Wall(processing, new Vector((int)random(40, 1840), (int)random(40, 960)), (int)random(360), (int)random(100, 1000));
objects.add(wall);
}
}
public void draw(){
background(0);
for(Wall object : objects){
object.drawWall();
}
// for(Wall object : objects){
// object.drawWall();
// }
car.drawCar(objects);
//car.drive(new int[] {0, 0});
}

View File

@@ -1,11 +1,10 @@
import static processing.core.PApplet.radians;
import processing.core.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static processing.core.PApplet.pow;
import static processing.core.PApplet.*;
public class SLAM{
ArrayList<Line> lines = new ArrayList<>();
@@ -21,15 +20,18 @@ public class SLAM{
* @param subSampleSize the size of the sub sample
* @return A random subset of the set within an indexRange and of size: subSampleSize
*/
private List<PVector> randomSample(ArrayList<PVector> set, int indexRange, int subSampleSize){
private List<Vector> randomSample(ArrayList<Vector> set, int indexRange, int subSampleSize){
// select a random laser data reading
int randomIdx = (int) proc.random(set.size() - 1); // index of starter reading
PVector point = set.get(randomIdx); // point of starter reading
Vector point = set.get(randomIdx); // point of starter reading
// get a random sample of size numSampleReadings within degreeRange degrees of this laser reading.
List<PVector> subSample = set.subList(randomIdx - indexRange, randomIdx + indexRange); // get the sub-sample
List<Vector> subSample;
int rangeStart = randomIdx - indexRange >= 0 ? randomIdx - indexRange : 0;
int rangeEnd = randomIdx + indexRange < set.size() ? randomIdx + indexRange : set.size()-1;
subSample = set.subList(rangeStart, rangeEnd); // get the sub-sample
Collections.shuffle(subSample); // shuffle the list
List<PVector> randomSample = subSample.subList(0, subSampleSize); // get our random sample
List<Vector> randomSample = subSample.subList(0, rangeEnd-rangeStart); // get our random sample
if (!randomSample.contains(point)) {
randomSample.add(point);
}
@@ -43,12 +45,12 @@ public class SLAM{
* @param maxRange the maximum distance away from the line of best fit of the subSample of points for a given point's consensus to count.
* @param consensus the number of points that have to give their consensus for the line of best fit to count as a valid feature.
*/
private void extractFeature(ArrayList<PVector> originalList, List<PVector> randomSample, float maxRange, int consensus){
private void extractFeature(ArrayList<Vector> originalList, List<Vector> randomSample, float maxRange, int consensus){
// get a line of best fit for this list.
Line bestFit = new Line(proc, randomSample);
int count = 0;
ArrayList<PVector> newRandomSample = new ArrayList<>();
for (PVector v : randomSample) {
ArrayList<Vector> newRandomSample = new ArrayList<>();
for (Vector v : randomSample) {
if (bestFit.getDistance(v) <= maxRange) {
count++;
newRandomSample.add(v);
@@ -59,12 +61,17 @@ public class SLAM{
bestFit = new Line(proc, newRandomSample.subList(0, newRandomSample.size() - 1));
lines.add(bestFit);
// remove the associated readings from the total available readings.
for (PVector v : newRandomSample) {
for (Vector v : newRandomSample) {
originalList.remove(v);
}
}
}
public void RANSAC(ArrayList<PVector> newPoints, float raysPerDegree){
/**
* @param newPoints a new scan of points to perform feature detection on
* @param raysPerDegree How many degrees apart are each ray that was cast
*/
public void RANSAC(ArrayList<Vector> newPoints, float raysPerDegree){
float degreeRange = radians(10/2); // range to randomly sample readings within
int indexRange = (int) (degreeRange / raysPerDegree);
int numSampleReadings = 10; // number of readings to randomly sample
@@ -83,7 +90,7 @@ public class SLAM{
}
// get a random sub sample of newPoints within the index range of a given size
List<PVector> randomSample = this.randomSample(newPoints, indexRange, numSampleReadings);
List<Vector> randomSample = this.randomSample(newPoints, indexRange, numSampleReadings);
// check if the sub sample forms a valid line and remove the randomSample points if it does.
extractFeature(newPoints, randomSample, maxRange, consensus);
@@ -92,71 +99,10 @@ public class SLAM{
}
}
class Line{
PVector direction = new PVector(0,0);
PVector position = new PVector(0,0);
private static PApplet proc;
Line(PApplet processing){
proc = processing;
}
Line(PApplet processing, PVector direction, PVector position){
this.direction = direction;
this.position = position;
proc = processing;
}
/**
* attempt to find the line of best fit for the given points
* @param points the points to get the line of best for
*/
Line(PApplet processing, List<PVector> points){
bestFit(points);
proc = processing;
}
// least squares line of best fit algorithm
private void bestFit(List<PVector> points){
// get the mean of all the points
PVector mean = new PVector();
for(PVector point : points){
mean.add(point);
public void drawLines(){
for(Line line : lines){
line.draw();
}
mean.div(points.size());
// this section calculates the direction vector of the line of best fit
PVector direction = new PVector();
// get the rise and run of the line of best fit
for(PVector point : points){
direction.y += (point.x - mean.x)*(point.y - mean.y); // rise
direction.x += pow((point.x - mean.x),2);
}
this.position = mean;
this.direction = direction;
}
public PVector getSlopeIntForm(){
float slope = direction.y / direction.x;
float intercept = position.y - slope * position.x;
return new PVector(slope, intercept);
}
public PVector getDirection(){
return direction;
}
public PVector getPosition(){
return position;
}
/**
* @param point
* @return the smallest distance from the point to this line
*/
public float getDistance(PVector point){
return (point.sub(position).cross(direction)).mag() / direction.mag();
}
}

68
src/Vector.java Normal file
View File

@@ -0,0 +1,68 @@
import static java.lang.Math.sqrt;
public class Vector {
public float x = 0;
public float y = 0;
public float z = 0;
Vector(){}
Vector(float x, float y){
this.x = x;
this.y = y;
}
Vector(float x, float y, float z){
this.x = x;
this.y = y;
this.z = z;
}
Vector add(Vector other){
return new Vector(this.x + other.x, this.y + other.y, this.z + other.z);
}
Vector add(float x, float y){
return new Vector(this.x + x, this.y + y);
}
Vector add(float x, float y, float z){
return new Vector(this.x + x, this.y + y, this.z + z);
}
Vector sub(Vector other){
return new Vector(this.x - other.x, this.y - other.y, this.z - other.z);
}
Vector sub(float x, float y){
return new Vector(this.x - x, this.y - y);
}
Vector sub(float x, float y, float z){
return new Vector(this.x - x, this.y - y, this.z - z);
}
Vector mul(float scalar){
return new Vector(this.x * scalar, this.y * scalar, this.z * scalar);
}
Vector div(float scalar){
return mul(1/scalar);
}
float mag(){
return (float)sqrt(x*x + y*y + z*z);
}
float dot(Vector other){
return x * other.x + y * other.y + z * other.z;
}
Vector cross(Vector other){
return new Vector(this.y*other.z - this.z*other.y, this.z*other.x - this.x*other.z, this.x*other.y - this.y*other.x);
}
Vector normalize(){
float mag = this.mag();
return new Vector(x / mag, y / mag, z / mag);
}
}

View File

@@ -5,14 +5,14 @@ import java.util.Objects;
import static processing.core.PApplet.*;
public class View{
PVector pose;
Vector pose;
float angle = 0;
float FOV;
ArrayList<Ray> rays = new ArrayList<>();
private static PApplet proc;
//the x,y position of the view, what angle it's looking at and its FOV
View(PApplet processing, PVector newPose, int numberOfRays, float FOV){
View(PApplet processing, Vector newPose, int numberOfRays, float FOV){
proc = processing;
this.pose = newPose;
this.FOV = FOV;
@@ -23,7 +23,7 @@ public class View{
public void setRayNum(int numberOfRays, float FOV, float angleOffset){
float rayStep = FOV/numberOfRays;
rays.clear();
float angle = (float) (0.01-angleOffset); //the 0.01 fixes some bugs
float angle = (float)(0.01-angleOffset); //the 0.01 fixes some bugs
for(int i = 0; i < numberOfRays; i++){
Ray ray = new Ray(proc, pose, 100000, angle);
angle = angle + rayStep;
@@ -40,7 +40,7 @@ public class View{
}
//changes the position of the view
public void setPos(PVector newPose){
public void setPos(Vector newPose){
pose = newPose;
for(Ray ray : rays){ray.setPos(pose);}
}
@@ -57,7 +57,7 @@ public class View{
this.setRayNum(this.rays.size(), this.FOV, this.angle);
}
public PVector getPos(){return pose;}
public Vector getPos(){return pose;}
public float getAngle(){return this.angle;}
@@ -66,11 +66,11 @@ public class View{
public int getRayNum(){return this.rays.size();}
//gets the point that each ray has collided with
public ArrayList<PVector> getPoints(){
ArrayList<PVector> points = new ArrayList<>();
public ArrayList<Vector> getPoints(){
ArrayList<Vector> points = new ArrayList<>();
for(Ray ray : rays){
if(!Objects.equals(ray.getPoint(), new PVector(0, 0) {
if(!Objects.equals(ray.getPoint(), new Vector(0, 0) {
})){
points.add(ray.getPoint());
}
@@ -80,14 +80,14 @@ public class View{
}
class Ray{
PVector pose;
Vector pose;
int rayLength;
int defaultRayLength;
float angle; // IN RADIANS
private static PApplet proc;
//takes the starting position of the ray, the length of the ray, and it's casting angle (radians)
Ray(PApplet processing, PVector position, int defaultRayLength, float angle){
Ray(PApplet processing, Vector position, int defaultRayLength, float angle){
proc = processing;
this.pose = position;
this.defaultRayLength = defaultRayLength;
@@ -107,7 +107,7 @@ class Ray{
for(Wall object : objects){
float theta1 = angle;
float theta2 = radians(object.getAngle());
PVector wallPos = object.getPos();
Vector wallPos = object.getPos();
//finds where along the wall the ray collides
float b = (pose.x*sin(theta1) + wallPos.y*cos(theta1) - pose.y*cos(theta1) - wallPos.x*sin(theta1)) / (cos(theta2)*sin(theta1) - sin(theta2)*cos(theta1));
@@ -134,7 +134,7 @@ class Ray{
else this.rayLength = defaultRayLength;
}
public PVector getPos(){ return pose;}
public Vector getPos(){ return pose;}
public int getRayLength(){return this.rayLength;}
@@ -145,16 +145,16 @@ class Ray{
}
//returns the absolute position of the point
public PVector getPoint(){
public Vector getPoint(){
if(this.rayLength != this.defaultRayLength){
return new PVector(rayLength * (int)cos(this.angle) + pose.x, rayLength * (int)sin(this.angle) + pose.y);
return new Vector(rayLength * (int)cos(this.angle) + pose.x, rayLength * (int)sin(this.angle) + pose.y);
}
else{
return new PVector(0,0);
return new Vector(0,0);
}
}
public void setPos(PVector newPose){
public void setPos(Vector newPose){
pose = newPose;
}

View File

@@ -3,7 +3,7 @@ import processing.core.*;
import static processing.core.PApplet.*;
public class Wall{
PVector pos;
Vector pos;
float angle;
int wallLength;
private static PApplet proc;
@@ -11,7 +11,7 @@ public class Wall{
int g;
int b;
Wall(PApplet processing, PVector pos, float angle, int wallLength){
Wall(PApplet processing, Vector pos, float angle, int wallLength){
proc = processing;
this.pos = pos;
this.angle = angle;
@@ -27,7 +27,7 @@ public class Wall{
//ellipse((xPos + cos(radians(angle))*wallLength), (yPos + sin(radians(angle))*wallLength), 20, 20);
}
PVector getPos(){
Vector getPos(){
return pos;
}

84
tests/VectorTest.java Normal file
View File

@@ -0,0 +1,84 @@
import org.junit.jupiter.api.Test;
import processing.core.PApplet;
import static java.lang.Math.*;
import static org.junit.jupiter.api.Assertions.*;
class VectorTest{
@Test
public void vector2DOperations(){
for(int i = 0; i < 20; i++){
float x1 = (float)(1000*random() - 500);
float y1 = (float)(1000*random() - 500);
Vector v1 = new Vector(x1, y1);
for(int j = 0; j < 20; j++){
float x2 = (float)(1000*random());
float y2 = (float)(1000*random());
Vector v2 = new Vector(x2, y2);
// test general setters
assertFloatEquals(x1, v1.x);
assertFloatEquals(y1, v1.y);
assertFloatEquals(x2, v2.x);
assertFloatEquals(y2, v2.y);
// test magnitude
assertFloatEquals((float)sqrt(x1*x1 + y1*y1), v1.mag());
assertFloatEquals((float)sqrt(x2*x2 + y2*y2), v2.mag());
// test dot product
assertFloatEquals((float)(x1*x2+y1*y2), v1.dot(v2));
assertFloatEquals((float)(x1*x2+y1*y2), v2.dot(v1));
// test addition
Vector vSum = v1.add(v2);
assertFloatEquals(x1+x2, vSum.x);
assertFloatEquals(y1+y2, vSum.y);
// test subtraction
Vector vSub = v1.sub(v2);
assertFloatEquals(x1-x2, vSub.x);
assertFloatEquals(y1-y2, vSub.y);
vSub = v2.sub(v1);
assertFloatEquals(x2-x1, vSub.x);
assertFloatEquals(y2-y1, vSub.y);
// test scaling
Vector vScale = v1.mul(x2);
assertFloatEquals(x1*x2, vScale.x);
assertFloatEquals(y1*x2, vScale.y);
// test normalization
Vector vNorm = v1.normalize();
assertFloatEquals(1, vNorm.mag());
}
}
}
public void assertFloatEquals(float expected, float actual){
assertFloatEquals(expected, actual, (float)0.0001);
}
public void assertFloatEquals(float expected, float actual, float range){
assertTrue(abs(expected-actual) < range);
}
@Test
public void testCrossProduct(){
Vector v1 = new Vector(1, 2, 3);
Vector v2 = new Vector(4, 5, 6);
Vector cross = v1.cross(v2);
assertFloatEquals(-3, cross.x);
assertFloatEquals(6, cross.y);
assertFloatEquals(-3, cross.z);
v1 = new Vector(-3, 7, -9);
v2 = new Vector((float)2.6, 66, (float)-3.14159);
cross = v1.cross(v2);
assertFloatEquals((float)572.00887, cross.x, (float)0.1);
assertFloatEquals((float)-32.82477, cross.y, (float)0.1);
assertFloatEquals((float)-216.2, cross.z, (float)0.1);
}
}