// A class that represents line drawings. // History: // Feb. 2002 -- Created by Doug Baldwin. package geneseo.cs.sc; import java.awt.*; import javax.swing.JFrame; import java.util.Vector; /** * Represents a drawing made up of one or more sets of connected line segments. * Each of these drawings is associated with a window that makes it visible to * users. These windows define a coordinate system relative to which to define * lines. The center of the window corresponds to coordinate (0,0), the coordinate * system extends symmetrically out from the window's center. Coordinates measure in * units of pixels. To place lines in a drawing, think of the drawing containing a * (invisible) pen, which you can move for a specified distance in a specified direction. * Messages to line drawings allow you to say where this pen should start from (if you * don't say, it starts wherever it last ended, thus connecting line segments), what * direction it should move in, how far it should move, et cetera. */ public class LineDrawing extends Canvas { // A line drawing is a Canvas (on which the lines can be drawn). Internally // a line drawing also keeps track of... // o A frame in which to display the canvas // o A vector of the sets of line segments that comprise the picture. Each element // of this vector is itself a vector of vertices that define the ends of the segments // in that set. // o The sine and cosine of the angle that the pen draws at. This angle is measure // counterclockwise from due right. // o The color in which the pen is currently drawing. private JFrame window; private Vector lines; private double sine; private double cosine; private Color penColor; // A vertex of a line segment is a record that stores the vertex's coordinates, and // the color of the segment ending at that vertex. private class Vertex { public double x; public double y; public Color color; public Vertex( double x, double y, Color c ) { this.x = x; this.y = y; this.color = c; } } /** * Initialize a line drawing. */ // This requires creating the frame that displays the drawing, placing the line // drawing in it, and making the whole group visible. Then it initializes the // drawing proper by erasing it. public LineDrawing() { final int preferedWidth = 400; // Width and height of a line final int preferedHeight = 400; // drawing window = new JFrame( "Line Drawing" ); window.setSize( preferedWidth, preferedHeight ); window.getContentPane().add( this ); window.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); window.show(); this.clear(); } /** * Erase a line drawing and prepare any new segments to start at (0,0), with the pen * moving straight right and drawing in black. */ // This works by replacing any existing vector of lines with a new one, and initializing // it with a single line that contains only one point, namely (0,0). public void clear() { // Set the pen's direction and color: sine = 0.0; cosine = 1.0; penColor = Color.black; // Set up the set of lines: lines = new Vector(); Vector firstLine = new Vector(); firstLine.add( new Vertex( 0.0, 0.0, penColor ) ); lines.add( firstLine ); } /** * Position the pen to start a new set of line segments. * @param x The x coordinate at which the first segment of the set will start. * @param y The y coordinate at which the first segment of the set will start. */ // This saves the point specified by "x" and "y" as either the start of a new set // of line segments, if the current set has more than one point in it, or as a // replacement for the starting point in the current set if the current set contains // only one point (a set of segments with only one point doesn't have enough points // to define even one segment, that point must therefore be a starting point that // has never really been used). public void setPosition( double x, double y ) { Vertex thisPoint = new Vertex( x, y, penColor ); Vector curSegment = (Vector)lines.elementAt( lines.size()-1 ); if ( curSegment.size() <= 1 ) { curSegment.setElementAt( thisPoint, 0 ); } else { Vector newLine = new Vector(); newLine.add( thisPoint ); lines.add( newLine ); } } /** * Set the direction the pen will move next time a line drawing receives a "movePen" * message. * @param angle The counterclockwise angle from due right that the pen should move, * measured in degrees. */ // This sets the angle by saving its sine and cosine in the corresponding member // variables. The angle has to be converted from degrees to radians before taking // its sine and cosine. public void setAngle( double angle ) { double radians = angle * Math.PI / 180.0; sine = Math.sin( radians ); cosine = Math.cos( radians ); } /** * Set the color in which the pen draws future line segments. * @param newColor The color to use for future line segments. */ public void setColor( Color newColor ) { penColor = newColor; } /** * Find out the X coordinate of the pen's position. * @return The X coordinate of the pen position. */ // This works by returning the x coordinate of the last vertex in the // last line segment in the current drawing. public double getPositionX() { Vector lastSegment = (Vector)lines.elementAt( lines.size() - 1 ); Vertex lastPoint = (Vertex)lastSegment.elementAt( lastSegment.size() - 1 ); return lastPoint.x; } /** * Find out the Y coordinate of the pen's position. * @return The Y coordinate of the pen position. */ // This works by returning the y coordinate of the last vertex in the // last line segment in the current drawing. public double getPositionY() { Vector lastSegment = (Vector)lines.elementAt( lines.size() - 1 ); Vertex lastPoint = (Vertex)lastSegment.elementAt( lastSegment.size() - 1 ); return lastPoint.y; } /** * Find out what direction the pen is moving. * @return The pen's current heading, in degrees counterclockwise from * straight right. */ // The computation in this method starts with the member variable that holds // the cosine of the current angle. Taking the arccosine of this value gives // me an angle between 0 and Pi radians, which is either the true angle or its // negative (since theta and -theta have the same cosine). To tell which, I // look at the saved sine of the angle -- if it is negative, then the angle // is between Pi and 2*Pi radians, and the arccosine calculation basically // gave me its negative. Once I have the right angle in radians, I convert // it to degrees to return. public double getAngle() { double angle = Math.acos( cosine ); if ( sine < 0 ) { angle = -angle; } return angle * 180.0 / Math.PI; } /** * Find out what color the pen is currently drawing in. * @return The current color. */ public Color getColor() { return penColor; } /** * Move the pen, adding a line segment to the current set of segments. The new segment * starts at the pen's current position and has the pen's current color. * @param distance The distance the pen should move. */ // I calculate a vector that gives the pen's displacement from the previous point as // a result of this move. The displacement is simply the distance moved times the // cosine of the current angle in the horizontal direction, and the distance moved // times the sine of the current angle in the vertical direction. Once I have this // displacement, I calculate the endpoint of the new segment by adding the displacement // to the endpoint of the previous segment (i.e., the last point recorded in the // current set of segments). I repaint the drawing after adding the new segment so // that users can see their drawings develop without having to remember to repaint for // themselves. public void movePen( double distance ) { double hDisplacement = distance * cosine; double vDisplacement = distance * sine; Vector currentSegments = (Vector)lines.elementAt( lines.size()-1 ); Vertex oldEnd = (Vertex)currentSegments.elementAt( currentSegments.size()-1 ); Vertex newEnd = new Vertex( oldEnd.x + hDisplacement, oldEnd.y + vDisplacement, penColor ); currentSegments.add( newEnd ); this.repaint( 1 ); } /** * Draw the contents of a line drawing. Normally called by the Java runtime system * when the line drawing needs to be (re)displayed, not called directly by client * code. * @param g The graphics context for drawing the lines. */ // This steps through each element of the "lines" vector. Each of these elements is // itself a vector of points, so this method draws line segments connecting each pair // of consecutive points in that vector. If a vector only contains one point (i.e., too // few to have even one pair) this method does nothing with that vector. Also note that // this method has to transform points as recorded in "lines", where the coordinate // system has (0,0) at the center of the window and Y coordinates increasing upward, to // the coordinate system used by awt, which has (0,0) at the upper left corner of the // window and Y coordinates increasing down. Thus this adds the horizontal coordinate of // the window's center to the horizontal coordinates of points from "lines" to get // drawable horizontal coordinates, and subtracts vertical coordinates from "lines" // from half the window's height to get drawable vertical coordinates. This method can // get called before a line drawing is fully initialized, in which case "lines" will // be null and there is nothing for this method to do. public void paint( Graphics g ) { if ( lines != null ) { double centerV = this.getHeight() / 2.0; double centerH = this.getWidth() / 2.0; for ( int line = 0; line < lines.size(); line++ ) { Vector currentLine = (Vector)lines.elementAt( line ); for ( int pt = 1; pt < currentLine.size(); pt++ ) { Vertex prevPoint = (Vertex)currentLine.elementAt( pt-1 ); Vertex curPoint = (Vertex)currentLine.elementAt( pt ); g.setColor( curPoint.color ); g.drawLine( (int)Math.round( prevPoint.x + centerH ), (int)Math.round( centerV - prevPoint.y ), (int)Math.round( curPoint.x + centerH ), (int)Math.round( centerV - curPoint.y ) ); } } } } }