package org.samcrow.vehicle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.TimerTask;
import org.lekan.graphics.SGLine;
import org.lekan.graphics.SGObject;
import org.lekan.graphics.SGText;
import org.samcrow.environment.Obstacle;
import org.samcrow.sensor.MultiRangefinder;
import org.samcrow.sensor.Sensor;
import org.samcrow.sensor.SensorData;
import org.samcrow.sensor.SensorEventListener;
import org.samcrow.sensor.SingleRangefinder;
import org.samcrow.util.DrawQueue;
/**
* Base class for a vehicle.
*
* @author scamper
*
*/
public abstract class Vehicle extends TimerTask implements SensorEventListener {
// constants
/** The number of miliseconds to wait between loops */
public static final long DELAY_TIME = 10;
/**
* The rate of acceleration, in pixels per second per second, that the
* vehicle can accelerate at when at full throttle
*/
private static final double MAX_ACCELERATION = 4.0;
/**
* The rate of acceleration, in pixels per second per second, that the
* vehicle can brake at when at full braking power. This must be positive.
*/
private static final double MAX_BRAKING = 8.0;
/**
* The rate of acceleration, in pixels per second per second forwards
* (negative = backwards), that acts upon the vehicle to slow it down
* because of friction.
*/
/* package */static final double DYNAMIC_FRICTION = -2.0;
/**
* The rate of acceleratoin, in pixels per second per second forwards
* (negative = backwards), that acts upon the vehicle to slow it down
* because of friction when the vehicle is at 90 degrees steering, either
* direction. It decreases linearly to zero degrees steering.
*/
/* package */static final double STEERING_FRICTION = -4.0;
// private instance variables
/**
* Current total acceleration, positive being forward, in pixels per second
* per second.
*/
private double acceleration = 0.0;
/** If the vehicle is in reverse mode */
private boolean isReversed = false;
private int width = 30;
private int length = 60;
// X and Y coordinates of the Vehicle origin
private double x;
private double y;
private Point2D.Double lastLocation;
/** rotation, clockwise from above, from north */
private double heading;
/**
* Current speed, as measured at the non-steering wheels, in pixels per
* second
*/
private double speed;
/**
* The rear, non-steering, wheels of the vehicle. Currently only two of
* these are supported.
*/
private Wheel[] rearWheels;
/**
* The front, steering, wheels of the vehicle. Currently only two of these
* are supported.
*/
private SteerableWheel[] frontWheels;
/** Physics task that manages this vehicle */
private VehiclePhysicsTask physics;
// drawing stuff
/** A reference to the main queue to draw to */
private DrawQueue queue;
/** sensors */
private Collection<Sensor> sensors;
/** Obstacles */
protected Collection<Obstacle> obstacles;
/* package */Date lastFrameDate;
/**
* Construct a new Vehicle with its origin at a specified location in the
* environment
*
* @param inX
* the X location
* @param inY
* the Y location
* @param inQueue
* The DrawQueue to draw to
*/
public Vehicle(int inX, int inY, DrawQueue inQueue,
Collection<Obstacle> inObstacles) {
x = inX;
y = inY;
queue = inQueue;
obstacles = inObstacles;
sensors = new HashSet<Sensor>();
rearWheels = new Wheel[2];
frontWheels = new SteerableWheel[2];
System.out.println("Setting up vehicle task.");
setup();
physics = new VehiclePhysicsTask(this, obstacles);
obstacles = inObstacles;
lastFrameDate = new Date();
}
// threading methods
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public final void run() {
// System.out.println("Vehicle task looping");
lastLocation = new Point2D.Double(x, y);
for (Sensor sensor : sensors) {
sensor.run();// check sensors
}
runLoop();
physics.run();
draw();
Date currentDate = new Date();
long renderTime = (currentDate.getTime() - lastFrameDate.getTime());
double renderTimeSeconds = renderTime / 1000d;
double fps = 1 / renderTimeSeconds;
queue.addTransient(new SGText(Math.round(fps) + " FPS", 5, 15));
queue.redrawAndClear();
// System.out.println("Current vehicle location ("+x+", "+y+")");
// System.out.println("Vehicle task sleeping");
lastFrameDate = new Date();
}
/**
* Reset the vehicle to its default location and heading
*/
public final void reset(){
x = 1024 / 2;
y = 768 / 2;
heading = 0;
}
/**
* Add objects to the draw queue and redraw it
*/
private final void draw() {
// System.out.println("Drawing.");
List<SGObject> objects = new LinkedList<SGObject>();
// path marker
// queue.addPersistent(new SGLine(lastLocation.x, lastLocation.y, x,
// y));
Shape result = getBoundingRectangle();
PathIterator iterator = result.getPathIterator(null);
Point2D.Double lastPoint = null;
while (!iterator.isDone()) {// iterate through all points
double[] coords = new double[8];
int type = iterator.currentSegment(coords);
if (type == PathIterator.SEG_MOVETO) {
lastPoint = new Point2D.Double(coords[0], coords[1]);
} else if (type == PathIterator.SEG_LINETO) {
assert (lastPoint != null);// this must be after a point of type
// 0
SGLine line = new SGLine(lastPoint.x, lastPoint.y, coords[0],
coords[1]);
objects.add(line);
lastPoint = new Point2D.Double(coords[0], coords[1]);
// System.out.println("Adding line "+line);
}
iterator.next();
}
for (Sensor sensor : sensors) {
objects.addAll(sensor.getDrawObjects());
}
objects.addAll(rearWheels[0].getDrawObjects());
objects.addAll(rearWheels[1].getDrawObjects());
objects.addAll(frontWheels[0].getDrawObjects());
objects.addAll(frontWheels[1].getDrawObjects());
queue.addTransient(objects);
}
/**
* Get the Shape, in the form of a Rectanlge, with all transformations
* applied, that represents the rectangle that can represent the exterior of
* this vehicle
*
* @return the shape
*/
/* package */Shape getBoundingRectangle() {
final int PADDING = 7;// padding on each side for clearing wheels
// get the lines that form the rectangle representing this vehicle
final double width = Math.abs(frontWheels[0].getX()
- frontWheels[0].getX())
+ 2 * PADDING;
Rectangle2D.Double rect = new Rectangle2D.Double(x - width - PADDING
/ 2d, y + frontWheels[0].getY() - PADDING, width + 3 * PADDING,
Math.abs(frontWheels[0].getY())
+ Math.abs(rearWheels[0].getY()) + PADDING * 3);
AffineTransform transform = new AffineTransform();
transform.translate(x, y);
transform.rotate(Math.toRadians(heading));
transform.translate(-x, -y);
Shape result = transform.createTransformedShape(rect);
return result;
}
// methods for vehicles to override
/**
* This is called once at the start of the simulation. Vehicles should
* override this method to set their attributes.<br />
* Here are some things that every Vehicle should do:
* <ul>
* <li>Define wheels using addWheel(int inX, int inY) and
* addSteerableWheel(int inX, int inY)</li>
* <li>Define sensors</li>
* </ul>
*/
protected void setup() {
System.err.println(toString() + ": setup() not overriden.");
}
/**
* This is called periodically when the simulation. Vehicles should override
* this method.<br />
* The physics engine will not calculate physics for the vehicle until after
* the first time this method executes.
*/
protected void runLoop() {
System.err.println(toString() + ": runLoop() not overriden.");
}
/**
* This is called when new sensor data is received from a sensor. Vehicles
* should override this class to receive data from their sensors.
*
* @param data
* the data from the sensor
*/
@Override
public void sensorDataReceived(SensorData data) {
System.err.println("A Vehicle (" + toString()
+ ") has not overriden the sensorDataReceived method!");
}
/**
* This is called as soon as a collision between a vehicle and an obstacle
* is detected.<br />
* Vehicles should override this to handle crashes.<br />
* Immediately before this is called, the vehicle's brake, throttle, and
* speed are set to zero.
*
* @param target
* the Obstacle that the Vehicle crashed into
*/
public void crashed(Obstacle target) {
}
// Methods that vehicles should not override. These should all be protected
// final.
/**
* Add a rear (non-steering) wheel at a specified location. A Vehicle must
* add exactly two rear wheels. Any attempt to add a subsequent rear wheel
* after two have been added will be ignored.
*
* @param inX
* the X-axis location, relative to the car origin
* @param inY
* the Y-axis location, relative to the car origin
*/
protected final void addRearWheel(double inX, double inY) {
inY = -inY;// Positive Y in the global coordinate system means down.
// Positive Y hear means up. Reverse it.
if (rearWheels[0] == null) {
rearWheels[0] = new Wheel(inX, inY, this);// add to slot 0
} else if (rearWheels[1] == null) {
rearWheels[1] = new Wheel(inX, inY, this);// add to slot 1
}
// else: both are set
// do nothing
}
/**
* Add a front (steering) wheel at a specified location. A Vehicle must add
* exactly two front wheels. Any attempt to add a subsequent front wheel
* after two have been added will be ignored.
*
* @param inX
* the X-axis location, relative to the car origin
* @param inY
* the Y-axis location, relative to the car origin
*/
protected final void addFrontWheel(double inX, double inY) {
inY = -inY;// Positive Y in the global coordinate system means down.
// Positive Y hear means up. Reverse it.
if (frontWheels[0] == null) {
frontWheels[0] = new SteerableWheel(inX, inY, this);// add to slot 0
} else if (frontWheels[1] == null) {
frontWheels[1] = new SteerableWheel(inX, inY, this);// add to slot 1
}
// else: both are set
// do nothing
}
/**
* Set the power being applied to move the car forward. If any braking is
* occuring, all braking will stop.
*
* @param throttle
* The ratio from 0 to 1 of throttle to apply
*/
protected final void setThrottle(double throttle) {
acceleration = throttle * MAX_ACCELERATION;
if (isReversed) {
acceleration = -acceleration;
}
}
/**
* Set the degree of braking being applied to stop the car. If any power is
* being applied to move the vehicle forward, all forward power application
* will stop.
*
* @param brake
* The ratio from 0 to 1 of brake to apply
*/
protected final void setBrake(double brake) {
acceleration = -brake * MAX_BRAKING;
if (isReversed) {
acceleration = -acceleration;
}
}
/**
* Set the steering angle
*
* @param inAngle
* The desired angle in degrees to steer at, where zero is
* straight ahead and positive is right.
*/
protected final void setSteeringAngle(double inAngle) {
if (frontWheels[0] != null && rearWheels[1] != null) {
for (int i = 0; i < frontWheels.length; i++) {
if (inAngle <= 90 && inAngle >= -90) {
frontWheels[i].setAngle(inAngle);
}
}
}
}
/**
* Add a sensor to the vehicle. This sensor does not need to have its
* obstacles already set. If they are, the will be overwritten.
*
* @param sensor
* the Sensor to add
*/
protected final void addSensor(Sensor sensor) {
sensor.localY = -sensor.localY;
// sensor.localX = -sensor.localX;
if (sensor instanceof SingleRangefinder
|| sensor instanceof MultiRangefinder) {
sensor.localRotation += 180;
if (sensor.localRotation > 359) {
sensor.localRotation -= 360;
}
if (sensor instanceof SingleRangefinder) {
sensor.localX = -sensor.localX;
}
}
sensor.addEventListener(this);
sensors.add(sensor);
}
// package getters and setters. Used by the VehiclePhysicsTask.
/**
* Get the current absolute X-axis position of the Vehicle.
*
* @return the current absolute X-axis position of the Vehicle
*/
public double getX() {
return x;
}
/**
* Get the current absolute Y-axis position of the Vehicle.
*
* @return the current absolute Y-axis position of the Vehicle
*/
public double getY() {
return y;
}
/**
* Get the absolute heading of the vehicle
*
* @return The absolute heading of the vehicle, in degrees clockwise from
* above, from north
*/
public double getHeading() {// this is public because Sensors need to access
// it.
if (heading > 359) {
heading -= 359;
}
return heading;
}
/**
* Get the current speed of the vehicle
*
* @return The speed in pixels per second
*/
/* package */double getSpeed() {
return speed;
}
/**
* Get the average Y location of the Vehicle's rear wheels
*
* @return the average Y-axis location of the rear wheels relative to the
* Vehicle origin
*/
/* package */int getRearWheelY() {
if (rearWheels[0] != null && rearWheels[1] != null) {
return (int) Math.round((rearWheels[0].getY() + rearWheels[1]
.getY()) / 2);
}
return 0;
}
/**
* Get the average Y location of the Vehicle's front wheels
*
* @return the average Y-axis location of the front wheels relative to the
* Vehicle origin
*/
/* package */int getFrontWheelY() {
if (frontWheels[0] != null && frontWheels[1] != null) {
return (int) Math.round((frontWheels[0].getY() + frontWheels[1]
.getY()) / 2);
}
return 0;
}
/**
* Get the acceleration currently being applied to the car
*
* @return the current total acceleration in pixels per second per second
*/
/* package */double getAcceleration() {
return acceleration;
}
/**
* Get the current angle of the wheels that steer.
*
* @return The angle in degrees, with positive being right and zero being
* straight ahead, that the wheels are turned
*/
/* package */double getSteeringAngle() {
if (frontWheels[0] != null && frontWheels[1] != null) {
return (frontWheels[0].getAngle() + frontWheels[1].getAngle()) / 2;
}
return 0;
}
/**
* Determine if the vehicle is in reverse
*
* @return if the vehicle is in reverse
*/
/* package */boolean isReversed() {
return isReversed;
}
/**
* Set the reverse status of the vehicle
*
* @param inIsReversed
*/
/* package */void setIsReversed(boolean inIsReversed) {
speed = 0;
if (!isReversed && inIsReversed || isReversed && !inIsReversed) {
// invert the acceleration if the reversal state is changed
acceleration = -acceleration;
}
isReversed = inIsReversed;
}
/**
* Set the vehicle's absolute location
*
* @param inX
* The X-axis location
* @param inY
* The Y-axis location
*/
/* package */void setLocation(double inX, double inY) {
x = inX;
y = inY;
}
/**
* Set the absolute heading of the Vehicle
*
* @param inHeading
* The desired heading
*/
/* package */void setHeading(double inHeading) {
heading = inHeading;
}
/**
* Set the Vehicle's absolute location and heading
*
* @param inX
* The X-axis location
* @param inY
* The Y-axis location
* @param inHeading
* The heading
*/
/* package */void setLocationAndHeading(double inX, double inY,
double inHeading) {
x = inX;
y = inY;
heading = inHeading;
}
/**
* Set the speed of the Vehicle
*
* @param inSpeed
* The speed in pixels per second
*/
/* package */void setSpeed(double inSpeed) {
speed = inSpeed;
}
/**
* Set the acceleration of the Vehicle
*
* @param inAcceleration
* The acceleration in pixels per second per second, positive
* being forward
*/
/* package */void setAcceleration(double inAcceleration) {
acceleration = inAcceleration;
}
// private methods
/**
* Determine if this Vehicle has two front wheels and two rear wheels. If
* the wheels are invalid, also prints a message to <code>System.err</code>.
*
* @return true if it does, false if it doesn't.
*/
private boolean validateWheels() {
boolean isValid = ((rearWheels[0] != null && rearWheels[1] != null) && (frontWheels[0] != null && frontWheels[1] != null));
if (!isValid) {
System.err
.println("This Vehicle has not specified the correct wheels.");
}
return isValid;
}
}