import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Path2D;
import java.awt.geom.Path2D.Double;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
import java.util.TreeMap;
import javax.imageio.ImageIO;
/**
* Turtles inspired by Logo (by Bobrow, Feurzeig, Papert, and Soloon, 1967),
* which in turn was inspired by the turtle robots built by Walter in the late 1940s.
*
* Usage example:
*
* World w = new World();
*
* Turtle t = new Turtle(w);
* t.forward(300);
* t.setPenWidth(10);
* t.backward(100);
* t.right(135);
* t.penUp();
* t.forward(50);
* t.drop("http://www.extremenxt.com/elsie.jpg");
*
*
* @author Luther Tychonievich. Released to the public domain.
*/
public class Turtle {
/// version number based on date of creation
@SuppressWarnings("unused")
private static final long serialVersionUID = 20140120L;
private static final Map cachedPictures = new TreeMap();
private World world;
private double theta;
private Point2D.Double location; // screen coordinates
private Point2D.Double turtleCoords; // turtle coordinates
private boolean isdown;
private Path2D.Double fillPath = null;
private Color color;
private double shellSize;
private int pause = 200;
private boolean visible = true;
private boolean filling = false; // is path to be filled in?
private Color fillColor;
private static final Color[] base = {
Color.BLACK,
Color.RED,
Color.BLUE,
Color.MAGENTA,
Color.CYAN,
};
private static int baseIndex = 0;
/**
* Makes a new turtle in the center of the world.
* @param w the world
*/
public Turtle(World w) {
this(w, 0, 0);
}
/**
* Makes a new turtle at the specified point within the world.
* @param x the x coordinate, in pixels; 0 is the center; bigger numbers to right
* @param y the y coordinate, in pixels; 0 is the center; bigger numbers down
* @param w the world
*/
public Turtle(double x, double y, World w) {
this(w, x, y);
}
/**
* Makes a new turtle at the specified point within the world.
* @param w the world
* @param x the x coordinate, in pixels; 0 is the center; bigger numbers to right
* @param y the y coordinate, in pixels; 0 is the center; bigger numbers down
*/
public Turtle(World w, double x, double y) {
this.turtleCoords = new Point2D.Double(x, y);
this.location = new Point2D.Double(
w.getScaleX() * x + w.getOffsetX(),
w.getScaleY() * y + w.getOffsetY());
/* System.err.printf("Turtle initial location %s%n", this.location.toString()); */
this.theta = 0;
this.world = w;
this.color = Turtle.base[Turtle.baseIndex];
Turtle.baseIndex = (Turtle.baseIndex+1) % Turtle.base.length;
this.penWidth = 1;
this.isdown = true;
this.shellSize = 8;
this.filling = false;
w.addTurtle(this);
}
/**
* Moves the turtle 100 pixels in the direction it is facing.
*/
public void forward() {
this.forward(100);
}
/**
* Moves the turtle the specified distance in the direction it is facing.
* @param d the number of pixels to move
*/
public void forward(double d) {
this.cornerGoTo(this.turtleCoords.x + Math.cos(this.theta) * d,
this.turtleCoords.y - Math.sin(this.theta) * d);
}
/**
* Moves the turtle 100 pixels in the opposite of the direction it is facing.
*/
public void backward() {
this.backward(100);
}
/**
* Moves the turtle the specified distance in the opposite direction from the one it is facing.
* @param d the number of pixels to move
*/
public void backward(double d) {
this.forward(-d);
}
/**
* Turns the turtle clockwise in place.
* @param degrees the number of degrees to turn
*/
public void turnRight(double degrees) {
this.theta += Math.PI*degrees/180;
while (this.theta > Math.PI) this.theta -= Math.PI*2;
while (this.theta <= -Math.PI) this.theta += Math.PI*2;
world.turtleMoved();
this.pause();
}
/**
* Turns the turtle counterclockwise in place.
* @param degrees the number of degrees to turn
*/
public void turnLeft(double degrees) {
this.turnRight(-degrees);
}
/**
* Stops the turtle from leaving a trail.
*/
public void penUp() {
this.isdown = false;
}
/**
* Causes the turtle to leave a trail.
*/
public void penDown() {
this.isdown = true;
}
/**
* Check the pen state
* @return true if the pen is down, false otherwise
*/
public boolean isPenDown() {
return this.isdown;
}
/**
* Check the turtle state
* @return true if turtle is visible, false otherwise
*/
public boolean isVisible() {
return this.visible;
}
/**
* Set visibility of turtle
*/
public void setVisible(boolean isVisible) {
this.visible = isVisible;
}
/**
* Convenience routine to hide the turtle
*/
public void hide() {
this.setVisible(false);
world.turtleMoved();
}
/**
* Convenience routine to show the turtle
*/
public void show() {
this.setVisible(true);
world.turtleMoved();
}
/**
* Draws the shell of the turtle.
* Should only be called by World class
* @param g the graphics object to draw with
*/
void _how_world_draw_turtles(Graphics2D g) {
// // Other way to draw trails; can't change color part-way through though
// g.setColor(color);
// g.setStroke(new BasicStroke((float)this.width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
// g.draw(this.trail);
// // end other way to draw tails
// The following draws a picture of a turtle
// three shapes
GeneralPath back = new GeneralPath(); // the bigger shell
GeneralPath back2 = new GeneralPath(); // the paler inner shell
GeneralPath body = new GeneralPath(); // the head, legs, and tail
double c = Math.cos(this.theta);
double s = Math.sin(this.theta);
double x = this.location.x;
double y = this.location.y;
double w = this.shellSize;
Ellipse2D leftEye = new Ellipse2D.Double(x + 1.55*w*c + 0.15*w*s -0.1*w, y + 1.55*w*s - 0.15*w*c -0.1*w, 0.2*w, 0.2*w);
Ellipse2D rightEye = new Ellipse2D.Double(x + 1.55*w*c - 0.15*w*s -0.1*w, y + 1.55*w*s + 0.15*w*c -0.1*w, 0.2*w, 0.2*w);
body.moveTo(x + w*0.9*c + w*0.4*s, y + w*0.9*s - w*0.4*c);
body.curveTo(
x + w*1.6*c + w*0.4*s, y + w*1.6*s - w*0.4*c,
x + w*1.8*c + w*0.3*s, y + w*1.8*s - w*0.3*c,
x + w*1.8*c + w*0.0*s, y + w*1.8*s - w*0.0*c
);
body.curveTo(
x + w*1.8*c - w*0.3*s, y + w*1.8*s + w*0.3*c,
x + w*1.6*c - w*0.4*s, y + w*1.6*s + w*0.4*c,
x + w*0.9*c - w*0.4*s, y + w*0.9*s + w*0.4*c
);
body.lineTo(x + w*0.8*c - w*1.2*s, y + w*0.8*s + w*1.2*c);
body.lineTo(x + w*0.5*c - w*1.2*s, y + w*0.5*s + w*1.2*c);
body.lineTo(x + w*0.5*c - w*0.6*s, y + w*0.5*s + w*0.6*c);
body.lineTo(x - w*0.5*c - w*0.6*s, y - w*0.5*s + w*0.6*c);
body.lineTo(x - w*0.6*c - w*1.2*s, y - w*0.6*s + w*1.2*c);
body.lineTo(x - w*0.9*c - w*1.15*s, y - w*0.9*s + w*1.15*c);
body.lineTo(x - w*0.85*c - w*0.2*s, y - w*0.85*s + w*0.2*c);
body.lineTo(x - w*1.6*c - w*0.0*s, y - w*1.6*s + w*0.0*c);
body.lineTo(x - w*0.85*c + w*0.2*s, y - w*0.85*s - w*0.2*c);
body.lineTo(x - w*0.9*c + w*1.15*s, y - w*0.9*s - w*1.15*c);
body.lineTo(x - w*0.6*c + w*1.2*s, y - w*0.6*s - w*1.2*c);
body.lineTo(x - w*0.5*c + w*0.6*s, y - w*0.5*s - w*0.6*c);
body.lineTo(x + w*0.5*c + w*0.6*s, y + w*0.5*s - w*0.6*c);
body.lineTo(x + w*0.5*c + w*1.2*s, y + w*0.5*s - w*1.2*c);
body.lineTo(x + w*0.8*c + w*1.2*s, y + w*0.8*s - w*1.2*c);
body.closePath();
back.moveTo(x + w*1.2*c, y + w*1.2*s);
back.curveTo(
x + w*1.2*c + w*0.6*s, y + w*1.2*s - w*0.6*c,
x + w*0.7*c + w*s, y + w*0.7*s - w*c,
x + w*s, y - w*c
);
back.curveTo(
x - w*0.7*c + w*s, y - w*0.7*s - w*c,
x - w*1.2*c + w*0.6*s, y - w*1.2*s - w*0.6*c,
x - w*1.2*c, y - w*1.2*s
);
back.curveTo(
x - w*1.2*c - w*0.6*s, y - w*1.2*s + w*0.6*c,
x - w*0.7*c - w*s, y - w*0.7*s + w*c,
x - w*s, y + w*c
);
back.curveTo(
x + w*0.7*c - w*s, y + w*0.7*s + w*c,
x + w*1.2*c - w*0.6*s, y + w*1.2*s + w*0.6*c,
x + w*1.2*c, y + w*1.2*s
);
w *= 0.7;
back2.moveTo(x + w*1.2*c, y + w*1.2*s);
back2.curveTo(
x + w*1.2*c + w*0.6*s, y + w*1.2*s - w*0.6*c,
x + w*0.7*c + w*s, y + w*0.7*s - w*c,
x + w*s, y - w*c
);
back2.curveTo(
x - w*0.7*c + w*s, y - w*0.7*s - w*c,
x - w*1.2*c + w*0.6*s, y - w*1.2*s - w*0.6*c,
x - w*1.2*c, y - w*1.2*s
);
back2.curveTo(
x - w*1.2*c - w*0.6*s, y - w*1.2*s + w*0.6*c,
x - w*0.7*c - w*s, y - w*0.7*s + w*c,
x - w*s, y + w*c
);
back2.curveTo(
x + w*0.7*c - w*s, y + w*0.7*s + w*c,
x + w*1.2*c - w*0.6*s, y + w*1.2*s + w*0.6*c,
x + w*1.2*c, y + w*1.2*s
);
int gap = 48;
Color midColor = new Color(
Math.max(Math.min(color.getRed(),255-gap),gap),
Math.max(Math.min(color.getGreen(),255-gap),gap),
Math.max(Math.min(color.getBlue(),255-gap),gap)
);
Color lightColor = new Color(
midColor.getRed()+gap,
midColor.getGreen()+gap,
midColor.getBlue()+gap
);
Color darkColor = new Color(
midColor.getRed()-gap,
midColor.getGreen()-gap,
midColor.getBlue()-gap
);
g.setColor(darkColor);
g.fill(body);
g.setColor(midColor);
g.fill(back);
g.setColor(lightColor);
g.fill(back2);
g.setColor(Color.WHITE);
g.fill(leftEye);
g.fill(rightEye);
}
/**
* Place a picture on the screen where the turtle currently is;
* make it 100 pixels wide.
* @param filename the file name or URL of the image to be drawn
* @return true if the image was found, false otherwise
*/
public boolean drop(String filename) {
return this.drop(filename, 100);
}
/**
* Place a picture on the screen where the turtle currently is.
* @param filename the file name or URL of the image to be drawn
* @param size how big the image should be in pixels
* @return true if the image was found, false otherwise
*/
public boolean drop(String filename, double size) {
try {
BufferedImage pic;
if (cachedPictures.containsKey(filename)) {
pic = cachedPictures.get(filename);
} else {
try {
pic = ImageIO.read(new URL(filename).openStream());
} catch (Throwable ex) {
pic = ImageIO.read(new File(filename));
}
cachedPictures.put(filename, pic);
}
double scale = size/Math.max(pic.getWidth(), pic.getHeight());
AffineTransform af = new AffineTransform();
af.translate(this.location.x, this.location.y);
af.rotate(this.theta+Math.PI/2);
af.translate(-size/2, -size/2);
af.scale(scale, scale);
this.world.drawImage(pic, af);
this.pause();
return true;
} catch (IOException e) {
return false;
}
}
/**
* Returns the current Color of the Turtle.
* @return The current Color of the pen.
*/
public Color getColor() {
return color;
}
/**
* Changes the current Color of the Turtle.
* @param color The new Color to use in drawing
*/
public void setColor(Color color) {
this.color = color;
world.turtleMoved();
this.pause();
}
private double penWidth;
/**
* Returns the current width of the turtle's pen.
* @return The new pen width, in pixels.
*/
public double getPenWidth() {
return penWidth;
}
/**
* Sets the width of the pen.
* @param width The new pen width, in pixels.
*/
public void setPenWidth(double width) {
if (width <= 0) throw new IllegalArgumentException("Width must be positive");
this.penWidth = width;
}
public double getShellSize() {
return shellSize;
}
public void setShellSize(double shellSize) {
this.shellSize = shellSize;
world.turtleMoved();
this.pause();
}
/**
* Find out what direction the Turtle is facing
* @return angle in degrees; 0 is right, 90 is up, etc
*/
public double getHeading() {
return Math.toDegrees(theta); // was theta * 180 / Math.PI;
}
/**
* Set the direction the Turtle is facing
* @param angle in degrees; 0 is right, 90 is up, etc
*/
public void setHeading(double angle) {
this.theta = Math.toRadians(angle); // was angle * Math.PI / 180;
world.turtleMoved();
this.pause();
}
/**
* Find angle between current location and given coordinates
* @param x destination x coordinate
* @param y destination y coordinate
*/
public double towards(double x, double y) {
double radians = Math.atan2(x - this.turtleCoords.x,
y - this.turtleCoords.y);
return Math.toDegrees(-radians);
}
/**
* Find out where the turtle is located
* @return The location of the turtle. (0,0) is the center of the screen, +x is rightward, +y is downward.
*/
public Point2D getLocation() {
// fix in following line suggested Anna Cuddeback 2018-11-28
// return new Point2D.Double(this.location.x - world.centerX, (-this.location.y + world.centerY));
return this.turtleCoords;
}
/**
* Move the turtle to a particular location. It might leave a trail depending on if the pen is down or not.
* @param where The new location for the turtle. (0,0) is the top left of the screen, +x is rightward, +y is downward.
*/
protected void cornerGoTo(Point2D where) {
this.cornerGoTo(where.getX(), where.getY());
}
/**
* Move the turtle to a particular location. It might leave a trail depending on if the pen is down or not.
* @param where The new location for the turtle. (0,0) is the center of the screen, +x is rightward, +y is downward.
*/
public void goTo(Point2D where) {
this.cornerGoTo(where.getX(), where.getY());
}
public void goTo(double x, double y) {
this.cornerGoTo(x, y);
}
/**
* Move the turtle to a particular location. It might leave a trail depending on if the pen is down or not.
* @param x The new x location for the turtle. 0 is the center of the screen, bigger numbers to the right
* @param y The new y location for the turtle. 0 is the center of the screen, bigger numbers lower down
*/
protected void cornerGoTo(double x, double y) {
double ox = this.location.x;
double oy = this.location.y;
this.turtleCoords.x = x;
this.turtleCoords.y = y;
this.location.x = this.world.getScaleX() * x + this.world.getOffsetX();
this.location.y = this.world.getScaleY() * y + this.world.getOffsetY();
/* System.err.printf("turtle (%.1f, %.1f) --> screen(%.1f, %.1f).%n",
x, y, this.location.x, this.location.y); */
if (this.isdown) {
world.drawLine(this.location, ox, oy, this.penWidth, this.color);
if (filling) {
this.fillPath.lineTo(this.location.x, this.location.y);
}
world.turtleMoved();
this.pause();
} else {
if (filling) {
this.fillPath.moveTo(this.location.x, this.location.y);
}
world.turtleMoved();
this.pause();
}
}
public void drawDot(double diameter) {
diameter = this.world.getScaleX() * diameter;
world.drawDot(this.location, diameter, this.getColor());
}
/**
* Begin a path that will be filled in the current color
*/
public void beginFill() {
this.fillPath = new Path2D.Double();
this.fillPath.moveTo(this.location.x, this.location.y);
this.filling = true;
}
/**
* End a path that will be filled
*/
public void endFill() {
this.filling = false;
this.world.fill(fillPath, fillColor);
}
/**
* Set color for filling paths
*/
public void setFillColor(Color fillColor) {
this.fillColor = fillColor;
}
/**
* Retrieve color for filling paths
* @return java.awt.Color object
*/
public Color getFillColor() {
return this.fillColor;
}
/**
* Seconds to pause between each turtle movement
* @return the seconds currently paused
*/
public double getDelay() {
return pause * 0.001;
}
/**
* Seconds to pause between each turtle movement
* @param seconds The seconds to pause
*/
public void setDelay(double seconds) {
this.pause = (int)(seconds * 1000);
}
private void pause() {
try { Thread.sleep(this.pause); } catch (InterruptedException e) { }
}
}