/* * JFLAP - Formal Languages and Automata Package * * * Susan H. Rodger * Computer Science Department * Duke University * August 27, 2009 * Copyright (c) 2002-2009 * All rights reserved. * JFLAP is open source software. Please see the LICENSE for terms. * */ package gui.lsystem; import gui.transform.Matrix; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Stroke; import java.awt.geom.*; import java.io.*; import java.util.*; /** * This represents the current context for rendering strings of symbols. Aside * from various methods. * * @author Thomas Finley */ class Turtle implements Cloneable, Serializable { /** * Instantiates a turtle. */ public Turtle() { parametersToNumbers = new HashMap(); setDistance(15.0); setAngleChange(15.0); setHueChange(10.0); setLineWidth(1.0); setLineIncrement(1.0); updateBounds(); } /** * Instantiates a turtle with the settings of an existing turtle. * * @param turtle * the turtle to copy */ public Turtle(Turtle turtle) { // The matrix. matrix = new Matrix(turtle.matrix); // The position settings. distance = turtle.distance; bounds = null; // Invalidate matrix.origin(position); // The line width variables. lineWidth = turtle.lineWidth; incrementWidth = turtle.incrementWidth; // The color settings. color = turtle.color; polygonColor = turtle.polygonColor; hueChange = turtle.hueChange; // The direction settings. angleChange = turtle.angleChange; // The parameters to the number values. parametersToNumbers = new HashMap(turtle.parametersToNumbers); } /** * Updates the bounds to include the current position. */ private final void updateBounds() { try { bounds.add(position); } catch (NullPointerException e) { bounds = new Rectangle2D.Double(position.getX(), position.getY(), 0.0, 0.0); } } /** * Returns the bounds. */ public final Rectangle2D getBounds() { if (bounds == null) updateBounds(); return bounds; } /** * Given a turtle, this will update this turtle's bounds to include those of * the bounds of the passed in turtle. * * @param turtle * the turtle whose bounds we want to include */ public final void updateBounds(Turtle turtle) { bounds.add(turtle.bounds); } /** * Creates a copy of the current turtle. * * @return a copy of the current turtle */ public Object clone() { return new Turtle(this); } // METHODS RELATING TO DIRECTION /** * Turns the turtle either left or right. * * @param clockwise * if true this is a clockwise turn, and if false * this is a counter-clockwise turn */ public final void turn(boolean clockwise) { turn(clockwise ? -angleChange : angleChange); } /** * Turns the turtle a specified number of degrees. * * @param degrees * the amount counter-clockwise to turn the turtle */ public final void turn(double degrees) { matrix.yaw(degrees); } /** * Pitches the turtle either down or up. * * @param down * if true this is a down pitch and if false * this is an up pitch. */ public final void pitch(boolean down) { pitch(down ? angleChange : -angleChange); } /** * Pitches the turtle the specified number of degrees. * * @param degrees * the amount counter-clockwise to turn the turtle */ public final void pitch(double degrees) { matrix.pitch(degrees); } /** * Rolls the turtle either to the right or left. * * @param right * if true this is a right roll and if false * this is a left roll. */ public final void roll(boolean right) { roll(right ? -angleChange : angleChange); } /** * Rolls the turtle the specified number of degrees to the left * * @param degrees * the amount left to roll the turtle */ public final void roll(double degrees) { matrix.roll(degrees); } /** * Returns the angle increment. * * @return the angle increment */ public final double getAngleChange() { return angleChange; } /** * Sets the angle increment. * * @param change * the new angle change */ public final void setAngleChange(double change) { angleChange = Math.IEEEremainder(change, 360.0); parametersToNumbers.put("angle", new Double(change)); } // METHODS RELATING TO POSITION /** * Sets the new change in distance for moves. * * @param distance * the new distance */ public final void setDistance(double distance) { this.distance = distance; parametersToNumbers.put("distance", new Double(distance)); } /** * Moves the turtle in the current direction of the angle. * * @param distance * the distance to move forward (negative value is backward) */ public final void go(double distance) { matrix.translate(0.0, -distance, 0.0); matrix.origin(position); } /** * Moves the turtle the default distance forward (or backward) specified in * the distance field. * * @param forward * will be true if the user wishes to move * forward, false if the user wishes to move * backward */ public final void go(boolean forward) { go(forward ? distance : -distance); } // / METHODS RELATING TO COLOR /** * Returns a color name. * * @param colorName * the name of the color to find, which should be a field of * java.awt.Color (e.g. "red", "black", etc) * @return the named color, or null if the color could not be * found * @see java.awt.Color */ public static Color colorForString(String colorName) { try { return (Color) Color.class.getField(colorName).get(null); } catch (NoSuchFieldException e) { // The field was not found. } catch (IllegalAccessException e) { // Cannot access it! } catch (NullPointerException e) { // This is actually an instance field of java.awt.Color. } // Maybe it's in the map? Color c = (Color) COLORS.get(colorName); if (c != null) return c; // Okay, maybe it's an interpretable color? try { StringTokenizer st = new StringTokenizer(colorName, ","); float c1 = Float.parseFloat(st.nextToken()); float c2 = Float.parseFloat(st.nextToken()); float c3 = Float.parseFloat(st.nextToken()); if (c1 < 0f || c1 > 255f || c2 < 0f || c2 > 255f || c3 < 0f || c3 > 255f) return null; // Out of range! if (c1 <= 1f && c2 <= 1f && c3 <= 1f) // A HSB color! return Color.getHSBColor(c1, c2, c3); // An RGB color! return new Color((int) c1, (int) c2, (int) c3); } catch (Throwable e) { // We ran into trouble in the formatting, so we can't do // anything with it... } return null; } /** * Sets the draw color. * * @param colorName * the name of the color to find * @see #colorForString * @throws IllegalArgumentException * if the color name could not be retrieved */ public final void setColor(String colorName) { Color c = colorForString(colorName); if (c == null) throw new IllegalArgumentException("No color named " + colorName + " found!"); setColor(c); } /** * Sets the draw color. * * @param color * the new color to change to */ public final void setColor(Color color) { this.color = color; } /** * Returns the current draw color of the turtle. * * @reutrn the current draw color of the turtle */ public final Color getColor() { return color; } /** * Sets the polygon color. * * @param colorName * the name of the color to find * @see #colorForString * @throws IllegalArgumentException * if the color name could not be retrieved */ public final void setPolygonColor(String colorName) { Color c = colorForString(colorName); if (c == null) throw new IllegalArgumentException("No color named " + colorName + " found!"); setPolygonColor(c); } /** * Sets the polygon color. * * @param color * the new color to change to */ public final void setPolygonColor(Color color) { this.polygonColor = color; } /** * Returns the current polygon color of the turtle. * * @reutrn the current polygon color of the turtle */ public final Color getPolygonColor() { return polygonColor; } /** * Sets the hue angle change. * * @param change * the value in degrees to change the hue angle */ public void setHueChange(double change) { hueChange = Math.IEEEremainder(change, 360.0); parametersToNumbers.put("hueChange", new Double(change)); } /** * Changes the current color's hue angle by the turtle's value. * * @param increment * true if we want to progress, and false * if we want to regress */ public void changeHue(boolean increment) { changeHue(increment ? hueChange : -hueChange); } /** * Changes the current color by the given hue angle. * * @param change * the amount to change the hue angle by */ public void changeHue(double change) { float[] hsbvals = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); hsbvals[0] += ((float) change) / 360f; setColor(Color.getHSBColor(hsbvals[0], hsbvals[1], hsbvals[2])); } /** * Changes the current polygon color's hue angle by the turtle's value. * * @param increment * true if we want to progress, and false * if we want to regress */ public void changePolygonHue(boolean increment) { changePolygonHue(increment ? hueChange : -hueChange); } /** * Changes the current polygon color's hue angle by the given hue angle. */ public void changePolygonHue(double change) { float[] hsbvals = Color.RGBtoHSB(polygonColor.getRed(), polygonColor .getGreen(), polygonColor.getBlue(), null); hsbvals[0] += ((float) change) / 360f; setPolygonColor(Color.getHSBColor(hsbvals[0], hsbvals[1], hsbvals[2])); } // METHODS RELATING TO LINE WIDTH /** * Changes the amount line width changes. * * @param increment * the new line increment */ public final void setLineIncrement(double increment) { incrementWidth = increment; parametersToNumbers.put("lineIncrement", new Double(increment)); } /** * Changes the line width. * * @param broaden * should be true if the user wants to add the * width increment to the width, or false if the * user wants to decrement the width increment from the width */ public final void changeLineWidth(boolean broaden) { changeLineWidth(broaden ? incrementWidth : -incrementWidth); } /** * Changes the line width by the specified amount. * * @param increment * the amount to add to the line width; if negative, naturally * the width shall decrease */ public final void changeLineWidth(double increment) { setLineWidth(lineWidth + increment); stroke = null; } /** * Explicitly sets the line width. * * @param width * the new line width */ public final void setLineWidth(double width) { lineWidth = width; parametersToNumbers.put("lineWidth", new Double(width)); stroke = null; } /** * Returns the line width of the turtle. * * @return the line width of the turtle */ public final double getLineWidth() { return lineWidth; } /** * Returns the current stroke object given the line width. * * @return the current stroke object */ public final Stroke getStroke() { if (stroke == null) stroke = new BasicStroke((float) Math.max(0.0, getLineWidth())); return stroke; } /** * Returns a string representation of this turtle. * * @return a string representation of this turtle */ public final String toString() { StringBuffer sb = new StringBuffer(); sb.append("{ " + super.toString()); sb.append(", distance=" + distance); sb.append(", position=(" + position.getX() + "," + position.getY() + ")"); sb.append(", lineWidth=" + lineWidth); sb.append(", incrementWidth=" + incrementWidth); sb.append(", angleChange=" + angleChange); sb.append(", color=" + color); sb.append(", polygonColor=" + polygonColor); sb.append(" }"); return sb.toString(); } /** * /** Given a string representing a mathematical expression, this returns * the value of that expression. * * @param string * the mathematical expression * @return the value of the evaluation */ public Number valueOf(String string) { return valueOf(string, parametersToNumbers); } /** * Given a string representing a mathematical expression, this returns the * value of that expression. If there are any variables in the expression * they should be contained within the map of values. * * @param string * the mathematical expression * @param values * the map of string objects to number objects */ private static Number valueOf(String string, Map values) { string = string.replaceAll("-", " -"); StringReader reader = new StringReader(string); StreamTokenizer st = new StreamTokenizer(reader); st.ordinaryChar('/'); ArrayList list = new ArrayList(); Number zero = new Integer(0); boolean number = false; Character plus = new Character('+'); try { while (st.nextToken() != StreamTokenizer.TT_EOF) { switch (st.ttype) { case StreamTokenizer.TT_WORD: // Attempt to resolve the symbol to a number. Number n = (Number) values.get(st.sval); if (number) list.add(plus); number = true; list.add(n == null ? zero : n); break; case StreamTokenizer.TT_NUMBER: if (number) list.add(plus); number = true; list.add(new Double(st.nval)); break; case StreamTokenizer.TT_EOL: // Who cares? break; default: number = false; list.add(new Character((char) st.ttype)); break; } } } catch (IOException e) { return new Double(Double.NaN); // We canna do it, captain! } // So now we have all these symbols in a list... great! Iterator it = list.iterator(); return valueOf(it); } /** * The recursive helper function for the valueOf function. * * @param it * the iterator through operators and numbers */ private static Number valueOf(Iterator it) { Stack values = new Stack(); Stack operators = new Stack(); values.push(new Double(0.0)); while (it.hasNext()) { Object o = it.next(); if (o instanceof Number) { values.push(o); continue; } Character character = (Character) o; char c = character.charValue(); if (c == ')') break; // Done! if (c == '(') { values.push(valueOf(it)); continue; } while (!operators.isEmpty()) { boolean toCollapse = false; char last = ((Character) operators.peek()).charValue(); switch (c) { case '+': case '-': if (last == '-' || last == '+') toCollapse = true; case '*': case '/': if (last == '*' || last == '/') toCollapse = true; case '^': if (last == '^') toCollapse = true; break; default: throw new IllegalArgumentException("Bad operator " + c); // Eh. } if (!toCollapse) break; // Let it be. // Collapse! double b = ((Number) values.pop()).doubleValue(), a = ((Number) values .pop()).doubleValue(); operators.pop(); // Get rid of it... switch (last) { case '^': a = Math.pow(a, b); break; case '*': a *= b; break; case '/': a /= b; break; case '+': a += b; break; case '-': a -= b; break; default: // Eh. } values.push(new Double(a)); } operators.push(character); continue; } // We've run out, or it's time to return. Do pending ops and leave. while (!operators.isEmpty()) { // Collapse! char last = ((Character) operators.pop()).charValue(); double b = ((Number) values.pop()).doubleValue(), a = ((Number) values .pop()).doubleValue(); switch (last) { case '^': a = Math.pow(a, b); break; case '*': a *= b; break; case '/': a /= b; break; case '+': a += b; break; case '-': a -= b; break; default: // Eh. } values.push(new Double(a)); } return (Number) values.pop(); } /** * Assigns a value to a parameter from a mathematical expression which may * include other parameters. * * @param parameter * the parameter name * @param expression * the mathematical expression */ public void assign(String parameter, String expression) { parametersToNumbers.put(parameter, valueOf(expression)); } /** * Returns the value for a parameter. * * @param parameter * the parameter to get the value for * @return the number for the parameter */ public Number get(String parameter) { return (Number) parametersToNumbers.get(parameter); } /** The distance to travel per time. */ public double distance = 15; /** The current location. */ public final Point2D position = new Point2D.Double(0.0, 0.0) { public void setLocation(double x, double y) { // This should ensure our bounds are always kept up // to date. Excellent... oldPosition.setLocation(this); super.setLocation(x, y); updateBounds(); } }; /** The old location. */ public final Point2D oldPosition = new Point2D.Double(); /** The current bounds that this turtle has travelled. */ public Rectangle2D bounds = null; /** The line width. */ public double lineWidth = 1.0; /** The amount the line changes on increment. */ public double incrementWidth = 1.0; /** The current stroke object. */ private Stroke stroke = null; /** The color for the L-system. */ public Color color = Color.black; /** The polygon color for the L-system. */ public Color polygonColor = Color.red; /** The hue angle change. */ public double hueChange = 10.0; /** The amount the angle changes in degrees. */ public double angleChange = 15.0; /** The mapping of string parameter names to numbers. */ public Map parametersToNumbers; /** * The current matrix. The translation of the origin into this matrix * represents the current point. */ public Matrix matrix = new Matrix(); /** The mapping of strings to special colors. */ public static Map COLORS; static { Map m = new HashMap(); m.put("dukeBlue", new Color(0, 0, 156)); m.put("brown", new Color(129, 0, 0)); m.put("oliveDrab", new Color(114, 93, 0)); m.put("darkOliveGreen", new Color(109, 111, 0)); m.put("orangeRed", new Color(252, 118, 0)); m.put("maroon", new Color(190, 0, 0)); m.put("forestGreen", new Color(0, 127, 0)); m.put("purple", new Color(209, 0, 255)); m.put("springGreen", new Color(193, 255, 157)); m.put("violetRed", new Color(210, 0, 205)); m.put("goldenrod", new Color(255, 214, 0)); m.put("darkOliveGreen2", new Color(10, 127, 0)); COLORS = Collections.unmodifiableMap(m); } }