BHTree
/**
* BHTree.java
* <p>
* Represents a quadtree for the Barnes-Hut algorithm.
* <p>
* Dependencies: Body.java Quad.java
*
* @author chindesaurus
* @version 1.00
*/
public class BHTree {
// threshold value
private final double Theta = 0.5;
private Body body; // body or aggregate body stored in this node
private Quad quad; // square region that the tree represents
private BHTree NW; // tree representing northwest quadrant
private BHTree NE; // tree representing northeast quadrant
private BHTree SW; // tree representing southwest quadrant
private BHTree SE; // tree representing southeast quadrant
/**
* Constructor: creates a new Barnes-Hut tree with no bodies.
* Each BHTree represents a quadrant and an aggregate body
* that represents all bodies inside the quadrant.
*
* @param q the quadrant this node is contained within
*/
public BHTree(Quad q) {
this.quad = q;
this.body = null;
this.NW = null;
this.NE = null;
this.SW = null;
this.SE = null;
}
/**
* Adds the Body b to the invoking Barnes-Hut tree.
*/
public void insert(Body b) {
// if this node does not contain a body, put the new body b here
if (body == null) {
body = b;
return;
}
// internal node
if (!isExternal()) {
// update the center-of-mass and total mass
body = body.plus(b);
// recursively insert Body b into the appropriate quadrant
putBody(b);
}
// external node
else {
// subdivide the region further by creating four children
NW = new BHTree(quad.NW());
NE = new BHTree(quad.NE());
SE = new BHTree(quad.SE());
SW = new BHTree(quad.SW());
// recursively insert both this body and Body b into the appropriate quadrant
putBody(this.body);
putBody(b);
// update the center-of-mass and total mass
body = body.plus(b);
}
}
/**
* Inserts a body into the appropriate quadrant.
*/
private void putBody(Body b) {
if (b.in(quad.NW()))
NW.insert(b);
else if (b.in(quad.NE()))
NE.insert(b);
else if (b.in(quad.SE()))
SE.insert(b);
else if (b.in(quad.SW()))
SW.insert(b);
}
/**
* Returns true iff this tree node is external.
*/
private boolean isExternal() {
// a node is external iff all four children are null
return (NW == null && NE == null && SW == null && SE == null);
}
/**
* Approximates the net force acting on Body b from all bodies
* in the invoking Barnes-Hut tree, and updates b's force accordingly.
*/
public void updateForce(Body b) {
if (body == null || b.equals(body))
return;
// if the current node is external, update net force acting on b
if (isExternal())
b.addForce(body);
// for internal nodes
else {
// width of region represented by internal node
double s = quad.length();
// distance between Body b and this node's center-of-mass
double d = body.distanceTo(b);
// compare ratio (s / d) to threshold value Theta
if ((s / d) < Theta)
b.addForce(body); // b is far away
// recurse on each of current node's children
else {
NW.updateForce(b);
NE.updateForce(b);
SW.updateForce(b);
SE.updateForce(b);
}
}
}
/**
* Returns a string representation of the Barnes-Hut tree
* in which spaces represent external nodes, and asterisks
* represent internal nodes.
*
* @return a string representation of this quadtree
*/
public String toString() {
if (isExternal())
return " " + body + "\n";
else
return "*" + body + "\n" + NW + NE + SW + SE;
}
}
Body
/**
* Body.java
*
* Represents a Body (a point mass) and its position,
* velocity, mass, color, and the net force acting upon it.
*
* @author chindesaurus
* @version 1.00
*/
import java.awt.Color;
public class Body {
// gravitational constant
private static final double G = 6.67e-11;
private double rx, ry; // position
private double vx, vy; // velocity
private double fx, fy; // force
private double mass; // mass
private Color color; // color
/**
* Constructor: creates and initializes a new Body.
*
* @param rx the x-position of this new body
* @param ry the y-position of this new body
* @param vx the x-velocity of this new body
* @param vy the y-velocity of this new body
* @param mass the mass of this new body
* @param color the color of this new body (RGB)
*/
public Body(double rx, double ry, double vx, double vy, double mass, Color color) {
this.rx = rx;
this.ry = ry;
this.vx = vx;
this.vy = vy;
this.mass = mass;
this.color = color;
}
/**
* Updates the velocity and position of the invoking Body
* using leapfrom method, with timestep dt.
*
* @param dt the timestep for this simulation
*/
public void update(double dt) {
vx += dt * fx / mass;
vy += dt * fy / mass;
rx += dt * vx;
ry += dt * vy;
}
/**
* Returns the Euclidean distance between the invoking Body and b.
*
* @param b the body from which to determine the distance
* @return the distance between this and Body b
*/
public double distanceTo(Body b) {
double dx = rx - b.rx;
double dy = ry - b.ry;
return Math.sqrt(dx*dx + dy*dy);
}
/**
* Resets the force (both x- and y-components) of the invoking Body to 0.
*/
public void resetForce() {
fx = 0.0;
fy = 0.0;
}
/**
* Computes the net force acting between the invoking body and b, and
* adds this to the net force acting on the invoking Body.
*
* @param b the body whose net force on this body to calculate
*/
public void addForce(Body b) {
Body a = this;
double EPS = 3E4; // softening parameter
double dx = b.rx - a.rx;
double dy = b.ry - a.ry;
double dist = Math.sqrt(dx*dx + dy*dy);
double F = (G * a.mass * b.mass) / (dist*dist + EPS*EPS);
a.fx += F * dx / dist;
a.fy += F * dy / dist;
}
/**
* Draws the invoking Body.
*/
public void draw() {
StdDraw.setPenColor(color);
StdDraw.point(rx, ry);
}
/**
* Returns a string representation of this body formatted nicely.
*
* @return a formatted string containing this body's x- and y- positions,
* velocities, and mass
*/
public String toString() {
return String.format("%10.3E %10.3E %10.3E %10.3E %10.3E", rx, ry, vx, vy, mass);
}
/**
* Returns true if the body is in quadrant q, else false.
*
* @param q the Quad to check
* @return true iff body is in Quad q, else false
*/
public boolean in(Quad q) {
return q.contains(this.rx, this.ry);
}
/**
* Returns a new Body object that represents the center-of-mass
* of the invoking body and b.
*
* @param b the body to aggregate with this Body
* @return a Body object representing an aggregate of this
* and b, having this and b's center of gravity and
* combined mass
*/
public Body plus(Body b) {
Body a = this;
double m = a.mass + b.mass;
double x = (a.rx * a.mass + b.rx * b.mass) / m;
double y = (a.ry * a.mass + b.ry * b.mass) / m;
return new Body(x, y, a.vx, b.vx, m, a.color);
}
}
NBodyBH
/**
* NBodyBH.java
* <p>
* Reads in a universe of N bodies from stdin, and performs an
* N-Body simulation in O(N log N) using the Barnes-Hut algorithm.
* <p>
* Compilation: javac NBodyBH.java
* Execution: java NBodyBH < inputs/[filename].txt
* Dependencies: BHTree.java Body.java Quad.java StdDraw.java
* Input files: ./inputs/*.txt
*
* @author chindesaurus
* @version 1.00
*/
import java.awt.Color;
public class NBodyBH {
public static void main(String[] args) {
final double dt = 0.1; // time quantum
int N = 10000; // number of particles
double radius = 1000; // radius of universe
// turn on animation mode and rescale coordinate system
StdDraw.show(0);
StdDraw.setXscale(-radius, +radius);
StdDraw.setYscale(-radius, +radius);
// read in and initialize bodies
Body[] bodies = new Body[N]; // array of N bodies
for (int i = 0; i < N; i++) {
double px = radius * 0.5 * (Math.random() - 1);
double py = radius * 0.5 * (Math.random() - 1);
Color color = new Color(225, 225, 0);
bodies[i] = new Body(px, py, 2, 2, 1, color);
}
// simulate the universe
for (double t = 0.0; true; t = t + dt) {
Quad quad = new Quad(0, 0, radius * 2);
BHTree tree = new BHTree(quad);
// build the Barnes-Hut tree
for (int i = 0; i < N; i++)
if (bodies[i].in(quad))
tree.insert(bodies[i]);
// update the forces, positions, velocities, and accelerations
for (int i = 0; i < N; i++) {
bodies[i].resetForce();
tree.updateForce(bodies[i]);
bodies[i].update(dt);
}
// draw the bodies
StdDraw.clear(StdDraw.BLACK);
for (int i = 0; i < N; i++)
bodies[i].draw();
StdDraw.show(10);
}
}
}
Quad
/**
* Quad.java
*
* Represents quadrants for the Barnes-Hut algorithm.
*
* Dependencies: StdDraw.java
*
* @author chindesaurus
* @version 1.00
*/
public class Quad {
private double xmid;
private double ymid;
private double length;
/**
* Constructor: creates a new Quad with the given
* parameters (assume it is square).
*
* @param xmid x-coordinate of center of quadrant
* @param ymid y-coordinate of center of quadrant
* @param length the side length of the quadrant
*/
public Quad(double xmid, double ymid, double length) {
this.xmid = xmid;
this.ymid = ymid;
this.length = length;
}
/**
* Returns the length of one side of the square quadrant.
*
* @return side length of the quadrant
*/
public double length() {
return length;
}
/**
* Does this quadrant contain (x, y)?
*
* @param x x-coordinate of point to test
* @param y y-coordinate of point to test
* @return true if quadrant contains (x, y), else false
*/
public boolean contains(double x, double y) {
double halfLen = this.length / 2.0;
return (x <= this.xmid + halfLen &&
x >= this.xmid - halfLen &&
y <= this.ymid + halfLen &&
y >= this.ymid - halfLen);
}
/**
* Returns a new object that represents the northwest quadrant.
*
* @return the northwest quadrant of this Quad
*/
public Quad NW() {
double x = this.xmid - this.length / 4.0;
double y = this.ymid + this.length / 4.0;
double len = this.length / 2.0;
Quad NW = new Quad(x, y, len);
return NW;
}
/**
* Returns a new object that represents the northeast quadrant.
*
* @return the northeast quadrant of this Quad
*/
public Quad NE() {
double x = this.xmid + this.length / 4.0;
double y = this.ymid + this.length / 4.0;
double len = this.length / 2.0;
Quad NE = new Quad(x, y, len);
return NE;
}
/**
* Returns a new object that represents the southwest quadrant.
*
* @return the southwest quadrant of this Quad
*/
public Quad SW() {
double x = this.xmid - this.length / 4.0;
double y = this.ymid - this.length / 4.0;
double len = this.length / 2.0;
Quad SW = new Quad(x, y, len);
return SW;
}
/**
* Returns a new object that represents the southeast quadrant.
*
* @return the southeast quadrant of this Quad
*/
public Quad SE() {
double x = this.xmid + this.length / 4.0;
double y = this.ymid - this.length / 4.0;
double len = this.length / 2.0;
Quad SE = new Quad(x, y, len);
return SE;
}
/**
* Draws an unfilled rectangle that represents the quadrant.
*/
public void draw() {
StdDraw.rectangle(xmid, ymid, length / 2.0, length / 2.0);
}
/**
* Returns a string representation of this quadrant for debugging.
*
* @return string representation of this quadrant
*/
public String toString() {
String ret = "\n";
for (int row = 0; row < this.length; row++) {
for (int col = 0; col < this.length; col++) {
if (row == 0 || col == 0 || row == this.length - 1 || col == this.length - 1)
ret += "*";
else
ret += " ";
}
ret += "\n";
}
return ret;
}
}
StdDraw
/*************************************************************************
* Compilation: javac StdDraw.java
* Execution: java StdDraw
*
* Standard drawing library. This class provides a basic capability for
* creating drawings with your programs. It uses a simple graphics model that
* allows you to create drawings consisting of points, lines, and curves
* in a window on your computer and to save the drawings to a file.
*
* Todo
* ----
* - Add support for gradient fill, etc.
*
* Remarks
* -------
* - don't use AffineTransform for rescaling since it inverts
* images and strings
* - careful using setFont in inner loop within an animation -
* it can cause flicker
*
*************************************************************************/
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import java.net.*;
import java.util.LinkedList;
import java.util.TreeSet;
import javax.imageio.ImageIO;
import javax.swing.*;
/**
* <i>Standard draw</i>. This class provides a basic capability for
* creating drawings with your programs. It uses a simple graphics model that
* allows you to create drawings consisting of points, lines, and curves
* in a window on your computer and to save the drawings to a file.
* <p>
* For additional documentation, see <a href="http://introcs.cs.princeton.edu/15inout">Section 1.5</a> of
* <i>Introduction to Programming in Java: An Interdisciplinary Approach</i> by Robert Sedgewick and Kevin Wayne.
*
* @author Robert Sedgewick
* @author Kevin Wayne
*/
public final class StdDraw implements ActionListener, MouseListener, MouseMotionListener, KeyListener {
// pre-defined colors
public static final Color BLACK = Color.BLACK;
public static final Color BLUE = Color.BLUE;
public static final Color CYAN = Color.CYAN;
public static final Color DARK_GRAY = Color.DARK_GRAY;
public static final Color GRAY = Color.GRAY;
public static final Color GREEN = Color.GREEN;
public static final Color LIGHT_GRAY = Color.LIGHT_GRAY;
public static final Color MAGENTA = Color.MAGENTA;
public static final Color ORANGE = Color.ORANGE;
public static final Color PINK = Color.PINK;
public static final Color RED = Color.RED;
public static final Color WHITE = Color.WHITE;
public static final Color YELLOW = Color.YELLOW;
/**
* Shade of blue used in Introduction to Programming in Java.
* It is Pantone 300U. The RGB values are approximately (9, 90, 166).
*/
public static final Color BOOK_BLUE = new Color( 9, 90, 166);
public static final Color BOOK_LIGHT_BLUE = new Color(103, 198, 243);
/**
* Shade of red used in Algorithms 4th edition.
* It is Pantone 1805U. The RGB values are approximately (150, 35, 31).
*/
public static final Color BOOK_RED = new Color(150, 35, 31);
// default colors
private static final Color DEFAULT_PEN_COLOR = BLACK;
private static final Color DEFAULT_CLEAR_COLOR = WHITE;
// current pen color
private static Color penColor;
// default canvas size is DEFAULT_SIZE-by-DEFAULT_SIZE
private static final int DEFAULT_SIZE = 512;
private static int width = DEFAULT_SIZE;
private static int height = DEFAULT_SIZE;
// default pen radius
private static final double DEFAULT_PEN_RADIUS = 0.002;
// current pen radius
private static double penRadius;
// show we draw immediately or wait until next show?
private static boolean defer = false;
// boundary of drawing canvas, 5% border
private static final double BORDER = 0.05;
private static final double DEFAULT_XMIN = 0.0;
private static final double DEFAULT_XMAX = 1.0;
private static final double DEFAULT_YMIN = 0.0;
private static final double DEFAULT_YMAX = 1.0;
private static double xmin, ymin, xmax, ymax;
// for synchronization
private static Object mouseLock = new Object();
private static Object keyLock = new Object();
// default font
private static final Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN, 16);
// current font
private static Font font;
// double buffered graphics
private static BufferedImage offscreenImage, onscreenImage;
private static Graphics2D offscreen, onscreen;
// singleton for callbacks: avoids generation of extra .class files
private static StdDraw std = new StdDraw();
// the frame for drawing to the screen
private static JFrame frame;
// mouse state
private static boolean mousePressed = false;
private static double mouseX = 0;
private static double mouseY = 0;
// queue of typed key characters
private static LinkedList<Character> keysTyped = new LinkedList<Character>();
// set of key codes currently pressed down
private static TreeSet<Integer> keysDown = new TreeSet<Integer>();
// singleton pattern: client can't instantiate
private StdDraw() { }
// static initializer
static { init(); }
/**
* Set the window size to the default size 512-by-512 pixels.
*/
public static void setCanvasSize() {
setCanvasSize(DEFAULT_SIZE, DEFAULT_SIZE);
}
/**
* Set the window size to w-by-h pixels.
*
* @param w the width as a number of pixels
* @param h the height as a number of pixels
* @throws a IllegalArgumentException if the width or height is 0 or negative
*/
public static void setCanvasSize(int w, int h) {
if (w < 1 || h < 1) throw new IllegalArgumentException("width and height must be positive");
width = w;
height = h;
init();
}
// init
private static void init() {
if (frame != null) frame.setVisible(false);
frame = new JFrame();
offscreenImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
onscreenImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
offscreen = offscreenImage.createGraphics();
onscreen = onscreenImage.createGraphics();
setXscale();
setYscale();
offscreen.setColor(DEFAULT_CLEAR_COLOR);
offscreen.fillRect(0, 0, width, height);
setPenColor();
setPenRadius();
setFont();
clear();
// add antialiasing
RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
offscreen.addRenderingHints(hints);
// frame stuff
ImageIcon icon = new ImageIcon(onscreenImage);
JLabel draw = new JLabel(icon);
draw.addMouseListener(std);
draw.addMouseMotionListener(std);
frame.setContentPane(draw);
frame.addKeyListener(std); // JLabel cannot get keyboard focus
frame.setResizable(false);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // closes all windows
// frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // closes only current window
frame.setTitle("Standard Draw");
frame.setJMenuBar(createMenuBar());
frame.pack();
frame.requestFocusInWindow();
frame.setVisible(true);
}
// create the menu bar (changed to private)
private static JMenuBar createMenuBar() {
JMenuBar menuBar = new JMenuBar();
JMenu menu = new JMenu("File");
menuBar.add(menu);
JMenuItem menuItem1 = new JMenuItem(" Save... ");
menuItem1.addActionListener(std);
menuItem1.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
menu.add(menuItem1);
return menuBar;
}
/*************************************************************************
* User and screen coordinate systems
*************************************************************************/
/**
* Set the x-scale to be the default (between 0.0 and 1.0).
*/
public static void setXscale() { setXscale(DEFAULT_XMIN, DEFAULT_XMAX); }
/**
* Set the y-scale to be the default (between 0.0 and 1.0).
*/
public static void setYscale() { setYscale(DEFAULT_YMIN, DEFAULT_YMAX); }
/**
* Set the x-scale (a 10% border is added to the values)
* @param min the minimum value of the x-scale
* @param max the maximum value of the x-scale
*/
public static void setXscale(double min, double max) {
double size = max - min;
synchronized (mouseLock) {
xmin = min - BORDER * size;
xmax = max + BORDER * size;
}
}
/**
* Set the y-scale (a 10% border is added to the values).
* @param min the minimum value of the y-scale
* @param max the maximum value of the y-scale
*/
public static void setYscale(double min, double max) {
double size = max - min;
synchronized (mouseLock) {
ymin = min - BORDER * size;
ymax = max + BORDER * size;
}
}
/**
* Set the x-scale and y-scale (a 10% border is added to the values)
* @param min the minimum value of the x- and y-scales
* @param max the maximum value of the x- and y-scales
*/
public static void setScale(double min, double max) {
double size = max - min;
synchronized (mouseLock) {
xmin = min - BORDER * size;
xmax = max + BORDER * size;
ymin = min - BORDER * size;
ymax = max + BORDER * size;
}
}
// helper functions that scale from user coordinates to screen coordinates and back
private static double scaleX(double x) { return width * (x - xmin) / (xmax - xmin); }
private static double scaleY(double y) { return height * (ymax - y) / (ymax - ymin); }
private static double factorX(double w) { return w * width / Math.abs(xmax - xmin); }
private static double factorY(double h) { return h * height / Math.abs(ymax - ymin); }
private static double userX(double x) { return xmin + x * (xmax - xmin) / width; }
private static double userY(double y) { return ymax - y * (ymax - ymin) / height; }
/**
* Clear the screen to the default color (white).
*/
public static void clear() { clear(DEFAULT_CLEAR_COLOR); }
/**
* Clear the screen to the given color.
* @param color the Color to make the background
*/
public static void clear(Color color) {
offscreen.setColor(color);
offscreen.fillRect(0, 0, width, height);
offscreen.setColor(penColor);
draw();
}
/**
* Get the current pen radius.
*/
public static double getPenRadius() { return penRadius; }
/**
* Set the pen size to the default (.002).
*/
public static void setPenRadius() { setPenRadius(DEFAULT_PEN_RADIUS); }
/**
* Set the radius of the pen to the given size.
* @param r the radius of the pen
* @throws IllegalArgumentException if r is negative
*/
public static void setPenRadius(double r) {
if (r < 0) throw new IllegalArgumentException("pen radius must be nonnegative");
penRadius = r;
float scaledPenRadius = (float) (r * DEFAULT_SIZE);
BasicStroke stroke = new BasicStroke(scaledPenRadius, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
// BasicStroke stroke = new BasicStroke(scaledPenRadius);
offscreen.setStroke(stroke);
}
/**
* Get the current pen color.
*/
public static Color getPenColor() { return penColor; }
/**
* Set the pen color to the default color (black).
*/
public static void setPenColor() { setPenColor(DEFAULT_PEN_COLOR); }
/**
* Set the pen color to the given color. The available pen colors are
* BLACK, BLUE, CYAN, DARK_GRAY, GRAY, GREEN, LIGHT_GRAY, MAGENTA,
* ORANGE, PINK, RED, WHITE, and YELLOW.
* @param color the Color to make the pen
*/
public static void setPenColor(Color color) {
penColor = color;
offscreen.setColor(penColor);
}
/**
* Set the pen color to the given RGB color.
* @param red the amount of red (between 0 and 255)
* @param green the amount of green (between 0 and 255)
* @param blue the amount of blue (between 0 and 255)
* @throws IllegalArgumentException if the amount of red, green, or blue are outside prescribed range
*/
public static void setPenColor(int red, int green, int blue) {
if (red < 0 || red >= 256) throw new IllegalArgumentException("amount of red must be between 0 and 255");
if (green < 0 || green >= 256) throw new IllegalArgumentException("amount of red must be between 0 and 255");
if (blue < 0 || blue >= 256) throw new IllegalArgumentException("amount of red must be between 0 and 255");
setPenColor(new Color(red, green, blue));
}
/**
* Get the current font.
*/
public static Font getFont() { return font; }
/**
* Set the font to the default font (sans serif, 16 point).
*/
public static void setFont() { setFont(DEFAULT_FONT); }
/**
* Set the font to the given value.
* @param f the font to make text
*/
public static void setFont(Font f) { font = f; }
/*************************************************************************
* Drawing geometric shapes.
*************************************************************************/
/**
* Draw a line from (x0, y0) to (x1, y1).
* @param x0 the x-coordinate of the starting point
* @param y0 the y-coordinate of the starting point
* @param x1 the x-coordinate of the destination point
* @param y1 the y-coordinate of the destination point
*/
public static void line(double x0, double y0, double x1, double y1) {
offscreen.draw(new Line2D.Double(scaleX(x0), scaleY(y0), scaleX(x1), scaleY(y1)));
draw();
}
/**
* Draw one pixel at (x, y).
* @param x the x-coordinate of the pixel
* @param y the y-coordinate of the pixel
*/
private static void pixel(double x, double y) {
offscreen.fillRect((int) Math.round(scaleX(x)), (int) Math.round(scaleY(y)), 1, 1);
}
/**
* Draw a point at (x, y).
* @param x the x-coordinate of the point
* @param y the y-coordinate of the point
*/
public static void point(double x, double y) {
double xs = scaleX(x);
double ys = scaleY(y);
double r = penRadius;
float scaledPenRadius = (float) (r * DEFAULT_SIZE);
// double ws = factorX(2*r);
// double hs = factorY(2*r);
// if (ws <= 1 && hs <= 1) pixel(x, y);
if (scaledPenRadius <= 1) pixel(x, y);
else offscreen.fill(new Ellipse2D.Double(xs - scaledPenRadius/2, ys - scaledPenRadius/2,
scaledPenRadius, scaledPenRadius));
draw();
}
/**
* Draw a circle of radius r, centered on (x, y).
* @param x the x-coordinate of the center of the circle
* @param y the y-coordinate of the center of the circle
* @param r the radius of the circle
* @throws IllegalArgumentException if the radius of the circle is negative
*/
public static void circle(double x, double y, double r) {
if (r < 0) throw new IllegalArgumentException("circle radius must be nonnegative");
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*r);
double hs = factorY(2*r);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.draw(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
draw();
}
/**
* Draw filled circle of radius r, centered on (x, y).
* @param x the x-coordinate of the center of the circle
* @param y the y-coordinate of the center of the circle
* @param r the radius of the circle
* @throws IllegalArgumentException if the radius of the circle is negative
*/
public static void filledCircle(double x, double y, double r) {
if (r < 0) throw new IllegalArgumentException("circle radius must be nonnegative");
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*r);
double hs = factorY(2*r);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.fill(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
draw();
}
/**
* Draw an ellipse with given semimajor and semiminor axes, centered on (x, y).
* @param x the x-coordinate of the center of the ellipse
* @param y the y-coordinate of the center of the ellipse
* @param semiMajorAxis is the semimajor axis of the ellipse
* @param semiMinorAxis is the semiminor axis of the ellipse
* @throws IllegalArgumentException if either of the axes are negative
*/
public static void ellipse(double x, double y, double semiMajorAxis, double semiMinorAxis) {
if (semiMajorAxis < 0) throw new IllegalArgumentException("ellipse semimajor axis must be nonnegative");
if (semiMinorAxis < 0) throw new IllegalArgumentException("ellipse semiminor axis must be nonnegative");
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*semiMajorAxis);
double hs = factorY(2*semiMinorAxis);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.draw(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
draw();
}
/**
* Draw an ellipse with given semimajor and semiminor axes, centered on (x, y).
* @param x the x-coordinate of the center of the ellipse
* @param y the y-coordinate of the center of the ellipse
* @param semiMajorAxis is the semimajor axis of the ellipse
* @param semiMinorAxis is the semiminor axis of the ellipse
* @throws IllegalArgumentException if either of the axes are negative
*/
public static void filledEllipse(double x, double y, double semiMajorAxis, double semiMinorAxis) {
if (semiMajorAxis < 0) throw new IllegalArgumentException("ellipse semimajor axis must be nonnegative");
if (semiMinorAxis < 0) throw new IllegalArgumentException("ellipse semiminor axis must be nonnegative");
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*semiMajorAxis);
double hs = factorY(2*semiMinorAxis);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.fill(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
draw();
}
/**
* Draw an arc of radius r, centered on (x, y), from angle1 to angle2 (in degrees).
* @param x the x-coordinate of the center of the circle
* @param y the y-coordinate of the center of the circle
* @param r the radius of the circle
* @param angle1 the starting angle. 0 would mean an arc beginning at 3 o'clock.
* @param angle2 the angle at the end of the arc. For example, if
* you want a 90 degree arc, then angle2 should be angle1 + 90.
* @throws IllegalArgumentException if the radius of the circle is negative
*/
public static void arc(double x, double y, double r, double angle1, double angle2) {
if (r < 0) throw new IllegalArgumentException("arc radius must be nonnegative");
while (angle2 < angle1) angle2 += 360;
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*r);
double hs = factorY(2*r);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.draw(new Arc2D.Double(xs - ws/2, ys - hs/2, ws, hs, angle1, angle2 - angle1, Arc2D.OPEN));
draw();
}
/**
* Draw a square of side length 2r, centered on (x, y).
* @param x the x-coordinate of the center of the square
* @param y the y-coordinate of the center of the square
* @param r radius is half the length of any side of the square
* @throws IllegalArgumentException if r is negative
*/
public static void square(double x, double y, double r) {
if (r < 0) throw new IllegalArgumentException("square side length must be nonnegative");
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*r);
double hs = factorY(2*r);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.draw(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
draw();
}
/**
* Draw a filled square of side length 2r, centered on (x, y).
* @param x the x-coordinate of the center of the square
* @param y the y-coordinate of the center of the square
* @param r radius is half the length of any side of the square
* @throws IllegalArgumentException if r is negative
*/
public static void filledSquare(double x, double y, double r) {
if (r < 0) throw new IllegalArgumentException("square side length must be nonnegative");
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*r);
double hs = factorY(2*r);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.fill(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
draw();
}
/**
* Draw a rectangle of given half width and half height, centered on (x, y).
* @param x the x-coordinate of the center of the rectangle
* @param y the y-coordinate of the center of the rectangle
* @param halfWidth is half the width of the rectangle
* @param halfHeight is half the height of the rectangle
* @throws IllegalArgumentException if halfWidth or halfHeight is negative
*/
public static void rectangle(double x, double y, double halfWidth, double halfHeight) {
if (halfWidth < 0) throw new IllegalArgumentException("half width must be nonnegative");
if (halfHeight < 0) throw new IllegalArgumentException("half height must be nonnegative");
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*halfWidth);
double hs = factorY(2*halfHeight);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.draw(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
draw();
}
/**
* Draw a filled rectangle of given half width and half height, centered on (x, y).
* @param x the x-coordinate of the center of the rectangle
* @param y the y-coordinate of the center of the rectangle
* @param halfWidth is half the width of the rectangle
* @param halfHeight is half the height of the rectangle
* @throws IllegalArgumentException if halfWidth or halfHeight is negative
*/
public static void filledRectangle(double x, double y, double halfWidth, double halfHeight) {
if (halfWidth < 0) throw new IllegalArgumentException("half width must be nonnegative");
if (halfHeight < 0) throw new IllegalArgumentException("half height must be nonnegative");
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*halfWidth);
double hs = factorY(2*halfHeight);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.fill(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
draw();
}
/**
* Draw a polygon with the given (x[i], y[i]) coordinates.
* @param x an array of all the x-coordindates of the polygon
* @param y an array of all the y-coordindates of the polygon
*/
public static void polygon(double[] x, double[] y) {
int N = x.length;
GeneralPath path = new GeneralPath();
path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
for (int i = 0; i < N; i++)
path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
path.closePath();
offscreen.draw(path);
draw();
}
/**
* Draw a filled polygon with the given (x[i], y[i]) coordinates.
* @param x an array of all the x-coordindates of the polygon
* @param y an array of all the y-coordindates of the polygon
*/
public static void filledPolygon(double[] x, double[] y) {
int N = x.length;
GeneralPath path = new GeneralPath();
path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
for (int i = 0; i < N; i++)
path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
path.closePath();
offscreen.fill(path);
draw();
}
/*************************************************************************
* Drawing images.
*************************************************************************/
// get an image from the given filename
private static Image getImage(String filename) {
// to read from file
ImageIcon icon = new ImageIcon(filename);
// try to read from URL
if ((icon == null) || (icon.getImageLoadStatus() != MediaTracker.COMPLETE)) {
try {
URL url = new URL(filename);
icon = new ImageIcon(url);
} catch (Exception e) { /* not a url */ }
}
// in case file is inside a .jar
if ((icon == null) || (icon.getImageLoadStatus() != MediaTracker.COMPLETE)) {
URL url = StdDraw.class.getResource(filename);
if (url == null) throw new IllegalArgumentException("image " + filename + " not found");
icon = new ImageIcon(url);
}
return icon.getImage();
}
/**
* Draw picture (gif, jpg, or png) centered on (x, y).
* @param x the center x-coordinate of the image
* @param y the center y-coordinate of the image
* @param s the name of the image/picture, e.g., "ball.gif"
* @throws IllegalArgumentException if the image is corrupt
*/
public static void picture(double x, double y, String s) {
Image image = getImage(s);
double xs = scaleX(x);
double ys = scaleY(y);
int ws = image.getWidth(null);
int hs = image.getHeight(null);
if (ws < 0 || hs < 0) throw new IllegalArgumentException("image " + s + " is corrupt");
offscreen.drawImage(image, (int) Math.round(xs - ws/2.0), (int) Math.round(ys - hs/2.0), null);
draw();
}
/**
* Draw picture (gif, jpg, or png) centered on (x, y),
* rotated given number of degrees
* @param x the center x-coordinate of the image
* @param y the center y-coordinate of the image
* @param s the name of the image/picture, e.g., "ball.gif"
* @param degrees is the number of degrees to rotate counterclockwise
* @throws IllegalArgumentException if the image is corrupt
*/
public static void picture(double x, double y, String s, double degrees) {
Image image = getImage(s);
double xs = scaleX(x);
double ys = scaleY(y);
int ws = image.getWidth(null);
int hs = image.getHeight(null);
if (ws < 0 || hs < 0) throw new IllegalArgumentException("image " + s + " is corrupt");
offscreen.rotate(Math.toRadians(-degrees), xs, ys);
offscreen.drawImage(image, (int) Math.round(xs - ws/2.0), (int) Math.round(ys - hs/2.0), null);
offscreen.rotate(Math.toRadians(+degrees), xs, ys);
draw();
}
/**
* Draw picture (gif, jpg, or png) centered on (x, y), rescaled to w-by-h.
* @param x the center x coordinate of the image
* @param y the center y coordinate of the image
* @param s the name of the image/picture, e.g., "ball.gif"
* @param w the width of the image
* @param h the height of the image
* @throws IllegalArgumentException if the width height are negative
* @throws IllegalArgumentException if the image is corrupt
*/
public static void picture(double x, double y, String s, double w, double h) {
Image image = getImage(s);
double xs = scaleX(x);
double ys = scaleY(y);
if (w < 0) throw new IllegalArgumentException("width is negative: " + w);
if (h < 0) throw new IllegalArgumentException("height is negative: " + h);
double ws = factorX(w);
double hs = factorY(h);
if (ws < 0 || hs < 0) throw new IllegalArgumentException("image " + s + " is corrupt");
if (ws <= 1 && hs <= 1) pixel(x, y);
else {
offscreen.drawImage(image, (int) Math.round(xs - ws/2.0),
(int) Math.round(ys - hs/2.0),
(int) Math.round(ws),
(int) Math.round(hs), null);
}
draw();
}
/**
* Draw picture (gif, jpg, or png) centered on (x, y), rotated
* given number of degrees, rescaled to w-by-h.
* @param x the center x-coordinate of the image
* @param y the center y-coordinate of the image
* @param s the name of the image/picture, e.g., "ball.gif"
* @param w the width of the image
* @param h the height of the image
* @param degrees is the number of degrees to rotate counterclockwise
* @throws IllegalArgumentException if the image is corrupt
*/
public static void picture(double x, double y, String s, double w, double h, double degrees) {
Image image = getImage(s);
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(w);
double hs = factorY(h);
if (ws < 0 || hs < 0) throw new IllegalArgumentException("image " + s + " is corrupt");
if (ws <= 1 && hs <= 1) pixel(x, y);
offscreen.rotate(Math.toRadians(-degrees), xs, ys);
offscreen.drawImage(image, (int) Math.round(xs - ws/2.0),
(int) Math.round(ys - hs/2.0),
(int) Math.round(ws),
(int) Math.round(hs), null);
offscreen.rotate(Math.toRadians(+degrees), xs, ys);
draw();
}
/*************************************************************************
* Drawing text.
*************************************************************************/
/**
* Write the given text string in the current font, centered on (x, y).
* @param x the center x-coordinate of the text
* @param y the center y-coordinate of the text
* @param s the text
*/
public static void text(double x, double y, String s) {
offscreen.setFont(font);
FontMetrics metrics = offscreen.getFontMetrics();
double xs = scaleX(x);
double ys = scaleY(y);
int ws = metrics.stringWidth(s);
int hs = metrics.getDescent();
offscreen.drawString(s, (float) (xs - ws/2.0), (float) (ys + hs));
draw();
}
/**
* Write the given text string in the current font, centered on (x, y) and
* rotated by the specified number of degrees
* @param x the center x-coordinate of the text
* @param y the center y-coordinate of the text
* @param s the text
* @param degrees is the number of degrees to rotate counterclockwise
*/
public static void text(double x, double y, String s, double degrees) {
double xs = scaleX(x);
double ys = scaleY(y);
offscreen.rotate(Math.toRadians(-degrees), xs, ys);
text(x, y, s);
offscreen.rotate(Math.toRadians(+degrees), xs, ys);
}
/**
* Write the given text string in the current font, left-aligned at (x, y).
* @param x the x-coordinate of the text
* @param y the y-coordinate of the text
* @param s the text
*/
public static void textLeft(double x, double y, String s) {
offscreen.setFont(font);
FontMetrics metrics = offscreen.getFontMetrics();
double xs = scaleX(x);
double ys = scaleY(y);
int hs = metrics.getDescent();
offscreen.drawString(s, (float) (xs), (float) (ys + hs));
draw();
}
/**
* Write the given text string in the current font, right-aligned at (x, y).
* @param x the x-coordinate of the text
* @param y the y-coordinate of the text
* @param s the text
*/
public static void textRight(double x, double y, String s) {
offscreen.setFont(font);
FontMetrics metrics = offscreen.getFontMetrics();
double xs = scaleX(x);
double ys = scaleY(y);
int ws = metrics.stringWidth(s);
int hs = metrics.getDescent();
offscreen.drawString(s, (float) (xs - ws), (float) (ys + hs));
draw();
}
/**
* Display on screen, pause for t milliseconds, and turn on
* <em>animation mode</em>: subsequent calls to
* drawing methods such as <tt>line()</tt>, <tt>circle()</tt>, and <tt>square()</tt>
* will not be displayed on screen until the next call to <tt>show()</tt>.
* This is useful for producing animations (clear the screen, draw a bunch of shapes,
* display on screen for a fixed amount of time, and repeat). It also speeds up
* drawing a huge number of shapes (call <tt>show(0)</tt> to defer drawing
* on screen, draw the shapes, and call <tt>show(0)</tt> to display them all
* on screen at once).
* @param t number of milliseconds
*/
public static void show(int t) {
defer = false;
draw();
try { Thread.sleep(t); }
catch (InterruptedException e) { System.out.println("Error sleeping"); }
defer = true;
}
/**
* Display on-screen and turn off animation mode:
* subsequent calls to
* drawing methods such as <tt>line()</tt>, <tt>circle()</tt>, and <tt>square()</tt>
* will be displayed on screen when called. This is the default.
*/
public static void show() {
defer = false;
draw();
}
// draw onscreen if defer is false
private static void draw() {
if (defer) return;
onscreen.drawImage(offscreenImage, 0, 0, null);
frame.repaint();
}
/*************************************************************************
* Save drawing to a file.
*************************************************************************/
/**
* Save onscreen image to file - suffix must be png, jpg, or gif.
* @param filename the name of the file with one of the required suffixes
*/
public static void save(String filename) {
File file = new File(filename);
String suffix = filename.substring(filename.lastIndexOf('.') + 1);
// png files
if (suffix.toLowerCase().equals("png")) {
try { ImageIO.write(onscreenImage, suffix, file); }
catch (IOException e) { e.printStackTrace(); }
}
// need to change from ARGB to RGB for jpeg
// reference: http://archives.java.sun.com/cgi-bin/wa?A2=ind0404&L=java2d-interest&D=0&P=2727
else if (suffix.toLowerCase().equals("jpg")) {
WritableRaster raster = onscreenImage.getRaster();
WritableRaster newRaster;
newRaster = raster.createWritableChild(0, 0, width, height, 0, 0, new int[] {0, 1, 2});
DirectColorModel cm = (DirectColorModel) onscreenImage.getColorModel();
DirectColorModel newCM = new DirectColorModel(cm.getPixelSize(),
cm.getRedMask(),
cm.getGreenMask(),
cm.getBlueMask());
BufferedImage rgbBuffer = new BufferedImage(newCM, newRaster, false, null);
try { ImageIO.write(rgbBuffer, suffix, file); }
catch (IOException e) { e.printStackTrace(); }
}
else {
System.out.println("Invalid image file type: " + suffix);
}
}
/**
* This method cannot be called directly.
*/
public void actionPerformed(ActionEvent e) {
FileDialog chooser = new FileDialog(StdDraw.frame, "Use a .png or .jpg extension", FileDialog.SAVE);
chooser.setVisible(true);
String filename = chooser.getFile();
if (filename != null) {
StdDraw.save(chooser.getDirectory() + File.separator + chooser.getFile());
}
}
/*************************************************************************
* Mouse interactions.
*************************************************************************/
/**
* Is the mouse being pressed?
* @return true or false
*/
public static boolean mousePressed() {
synchronized (mouseLock) {
return mousePressed;
}
}
/**
* What is the x-coordinate of the mouse?
* @return the value of the x-coordinate of the mouse
*/
public static double mouseX() {
synchronized (mouseLock) {
return mouseX;
}
}
/**
* What is the y-coordinate of the mouse?
* @return the value of the y-coordinate of the mouse
*/
public static double mouseY() {
synchronized (mouseLock) {
return mouseY;
}
}
/**
* This method cannot be called directly.
*/
public void mouseClicked(MouseEvent e) { }
/**
* This method cannot be called directly.
*/
public void mouseEntered(MouseEvent e) { }
/**
* This method cannot be called directly.
*/
public void mouseExited(MouseEvent e) { }
/**
* This method cannot be called directly.
*/
public void mousePressed(MouseEvent e) {
synchronized (mouseLock) {
mouseX = StdDraw.userX(e.getX());
mouseY = StdDraw.userY(e.getY());
mousePressed = true;
}
}
/**
* This method cannot be called directly.
*/
public void mouseReleased(MouseEvent e) {
synchronized (mouseLock) {
mousePressed = false;
}
}
/**
* This method cannot be called directly.
*/
public void mouseDragged(MouseEvent e) {
synchronized (mouseLock) {
mouseX = StdDraw.userX(e.getX());
mouseY = StdDraw.userY(e.getY());
}
}
/**
* This method cannot be called directly.
*/
public void mouseMoved(MouseEvent e) {
synchronized (mouseLock) {
mouseX = StdDraw.userX(e.getX());
mouseY = StdDraw.userY(e.getY());
}
}
/*************************************************************************
* Keyboard interactions.
*************************************************************************/
/**
* Has the user typed a key?
* @return true if the user has typed a key, false otherwise
*/
public static boolean hasNextKeyTyped() {
synchronized (keyLock) {
return !keysTyped.isEmpty();
}
}
/**
* What is the next key that was typed by the user? This method returns
* a Unicode character corresponding to the key typed (such as 'a' or 'A').
* It cannot identify action keys (such as F1
* and arrow keys) or modifier keys (such as control).
* @return the next Unicode key typed
*/
public static char nextKeyTyped() {
synchronized (keyLock) {
return keysTyped.removeLast();
}
}
/**
* Is the keycode currently being pressed? This method takes as an argument
* the keycode (corresponding to a physical key). It can handle action keys
* (such as F1 and arrow keys) and modifier keys (such as shift and control).
* See <a href = "http://download.oracle.com/javase/6/docs/api/java/awt/event/KeyEvent.html">KeyEvent.java</a>
* for a description of key codes.
* @return true if keycode is currently being pressed, false otherwise
*/
public static boolean isKeyPressed(int keycode) {
synchronized (keyLock) {
return keysDown.contains(keycode);
}
}
/**
* This method cannot be called directly.
*/
public void keyTyped(KeyEvent e) {
synchronized (keyLock) {
keysTyped.addFirst(e.getKeyChar());
}
}
/**
* This method cannot be called directly.
*/
public void keyPressed(KeyEvent e) {
synchronized (keyLock) {
keysDown.add(e.getKeyCode());
}
}
/**
* This method cannot be called directly.
*/
public void keyReleased(KeyEvent e) {
synchronized (keyLock) {
keysDown.remove(e.getKeyCode());
}
}
/**
* Test client.
*/
public static void main(String[] args) {
StdDraw.square(.2, .8, .1);
StdDraw.filledSquare(.8, .8, .2);
StdDraw.circle(.8, .2, .2);
StdDraw.setPenColor(StdDraw.BOOK_RED);
StdDraw.setPenRadius(.02);
StdDraw.arc(.8, .2, .1, 200, 45);
// draw a blue diamond
StdDraw.setPenRadius();
StdDraw.setPenColor(StdDraw.BOOK_BLUE);
double[] x = { .1, .2, .3, .2 };
double[] y = { .2, .3, .2, .1 };
StdDraw.filledPolygon(x, y);
// text
StdDraw.setPenColor(StdDraw.BLACK);
StdDraw.text(0.2, 0.5, "black text");
StdDraw.setPenColor(StdDraw.WHITE);
StdDraw.text(0.8, 0.8, "white text");
}
}