import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
/**
* A world for turtles to play inside of.
* Usage example:
*
* World basic = new World();
* World fancy = new World(640, 480, Color.YELLOW);
*
* Turtle t1 = new Turtle(basic);
* Turtle t2 = new Turtle(fancy);
*
* t1.forward(100);
* basic.erase();
* t1.turnLeft(90);
* t1.forward(100);
*
* t2.forward(100);
* t2.turnLeft(90);
* t2.forward(100);
*
* basic.saveAs("basicWorld.png");
* fancy.saveAs("fancyWorld.png");
*
*
*
* @author Luther Tychonievich. Released to the public domain.
*/
public class World extends JFrame {
/// version number based on date of creation
private static final long serialVersionUID = 20130902L;
private BufferedImage overlay, ground, back, front;
private Graphics2D og, gg, bg, fg;
private ArrayList turtles;
private int width;
private int height;
private double lowerLeftX;
private double lowerLeftY;
private double upperRightX;
private double upperRightY;
// these variables determine how to scale and offset
// coordinates from the coordinate system to the screen
private double scaleX;
private double offsetX;
private double scaleY;
private double offsetY;
private boolean updating;
/**
* Creates a new World for Turtles to play in.
*/
public World() {
this(600, 600);
}
public World(int width, int height) {
this(width, height, Color.WHITE, -width / 2, -height / 2,
width / 2, height / 2);
}
public World(int width, int height, Color backgroundColor,
double lowerLeftX, double lowerLeftY, double upperRightX, double upperRightY) {
super("Turtle World");
this.width = width;
this.height = height;
this.setWorldCoordinates(lowerLeftX, lowerLeftY, upperRightX, upperRightY);
this.overlay = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
this.ground = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
this.back = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
this.front = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
this.og = (Graphics2D)this.overlay.getGraphics();
this.gg = (Graphics2D)this.ground.getGraphics();
this.bg = (Graphics2D)this.back.getGraphics();
this.fg = (Graphics2D)this.front.getGraphics();
og.setBackground(new Color(0,0,0,0));
gg.setBackground(backgroundColor);
Graphics2D[] gs = {og, gg};
for (Graphics2D g : gs) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
}
this.setContentPane(new JLabel(new ImageIcon(this.front)));
this.pack();
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
this.addKeyListener(new java.awt.event.KeyListener() {
@Override public void keyPressed(KeyEvent arg0) { }
@Override public void keyReleased(KeyEvent arg0) { }
@Override public void keyTyped(KeyEvent arg0) { World.this.dispose(); }
});
this.updating = true;
this.clearOverlay();
this.erase();
this.repaint();
this.setVisible(true);
this.turtles = new ArrayList();
}
/**
* Erases all existing paths
*/
public void erase() {
this.gg.clearRect(0, 0, this.ground.getWidth(), this.ground.getHeight());
}
/**
* Erases all existing paths
*/
private void clearOverlay() {
this.og.clearRect(0, 0, this.overlay.getWidth(), this.overlay.getHeight());
}
/**
* Should only be called by the Turtle class constructor
*/
void addTurtle(Turtle t) {
this.turtles.add(t);
this.turtleMoved();
}
/**
* Should only be called by Turtle class methods
*/
void drawLine(Point2D p1, Point2D p2, double width, Color color) {
// draw the line
this.gg.setColor(color);
this.gg.setStroke(new BasicStroke((float)width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
Line2D.Double line = new Line2D.Double(p1, p2);
this.gg.draw(line);
// show the drawn lines
if (this.updating) {
this.blit();
}
}
/**
* Draws a dot of the given diameter and color at the given location.
* Should only be called by Turtle class methods.
*/
void drawDot(Point2D.Double p1, double diameter, Color color) {
this.gg.setColor(color);
Ellipse2D.Double ellipse = new Ellipse2D.Double(p1.x, p1.y,
diameter, diameter);
this.gg.fill(ellipse);
if (this.updating) {
this.blit();
}
}
/**
* Fills the given path in the given color.
* Should only be called by Turtle class methods.
*/
void fill(Path2D path, Color c) {
Color saveColor = this.gg.getColor();
this.gg.setColor(c);
this.gg.fill(path);
this.gg.setColor(saveColor);
if (this.updating) {
this.blit();
}
}
private void blit() {
this.bg.drawImage(this.ground,0,0, null);
this.bg.drawImage(this.overlay, 0,0, null);
this.fg.drawImage(this.back, 0,0, this);
this.repaint();
}
/**
* Should only be called by Turtle class methods
*/
void drawLine(Point2D p1, double nx, double ny, double width, Color color) {
this.drawLine(p1, new Point2D.Double(nx,ny), width, color);
}
/**
* Should only be called by Turtle class methods
*/
void turtleMoved() {
// show the drawn lines
this.clearOverlay();
// add the turtles over top
for(Turtle t : this.turtles) {
if (t.isVisible()) {
t._how_world_draw_turtles(this.og);
}
}
// force the OS to show what's shown
if (updating) {
this.blit();
}
}
/**
* Saves the current image to the specified file
*
* @param filename The name of the file to write
* @throws IllegalArgumentException if any parameter is null or if the filename is not an image filename
*/
public void saveAs(String filename) {
try {
int dot = filename.lastIndexOf('.');
if (dot < 0 || dot == filename.length()-1) {
throw new IllegalArgumentException("The filename must end in a valid image extension, like .png or .jpg");
}
String ext = filename.substring(dot+1).toLowerCase();
File f = new File(filename);
ImageIO.write(this.front, ext, f);
} catch(Throwable t) {
System.err.println("Error saving file: " + t.getMessage());
}
}
/**
* To be used by Turtle class only
* @param img the Image to draw
* @param placement the Affine Transform to use in drawing it
*/
void drawImage(Image img, AffineTransform placement) {
this.gg.drawImage(img, placement, this);
if (updating) {
this.blit();
}
}
/**
* Set the lower left and upper right coordinates to use
* as the system of coordinates for the turtle.
* @param lowerLeftX the x-coordinate of the lower left of the world
* @param lowerLeftY the y-coordinate of the lower left of the world
* @param upperRightX the x-coordinate of the upper right of the world
* @param upperRightY the y-coordinate of the upper right of the world
*/
void setWorldCoordinates(double lowerLeftX, double lowerLeftY,
double upperRightX, double upperRightY) {
System.err.printf(
"set world coords: %d x %d: (%.1f, %.1f) to (%.1f, %.1f).%n",
width, height, lowerLeftX, lowerLeftY, upperRightX, upperRightY);
this.lowerLeftX = lowerLeftX;
this.lowerLeftY = lowerLeftY;
this.upperRightX = upperRightX;
this.upperRightY = upperRightY;
this.scaleX = (double) this.width / (upperRightX - lowerLeftX);
this.offsetX = -this.scaleX * lowerLeftX;
this.scaleY = (double) this.height / (lowerLeftY - upperRightY);
this.offsetY = -this.scaleY * upperRightY;
System.err.printf("Scale factor: %.3fx + %.3f, %.3fy + %.3f%n",
this.scaleX, this.offsetX, this.scaleY, this.offsetY);
}
double getScaleX() {
return this.scaleX;
}
double getOffsetX() {
return this.offsetX;
}
double getScaleY() {
return this.scaleY;
}
double getOffsetY() {
return this.offsetY;
}
/**
* Should we update every time a line is drawn?
* If yes, then do a blit to force a repaint.
* @param status true to update, false otherwise
*/
void setUpdating(boolean updating) {
this.updating = updating;
if (updating) {
this.blit();
}
}
/**
* Determine whether updating or not
* @return updating status; true if yes, false if no
*/
boolean getUpdating() {
return this.updating;
}
}