/* * 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.Color; import java.awt.Graphics2D; import java.awt.Image; import java.awt.RenderingHints; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.util.Collections; import java.util.EmptyStackException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.TreeSet; /** * A Renderer object allows a client to create an image of a * string of symbols generated, presumably, from an LSystem. *

* * The following symbols have significance: * * @see grammar.lsystem.Expander * @see grammar.lsystem.LSystem * * @author Thomas Finley */ public class Renderer { /** * Instantiates a renderer object. */ public Renderer() { // Set up all them gosh durned command handlers. handlers.put("g", new MoveHandler(true, true)); handlers.put("f", new MoveHandler(false, true)); handlers.put("+", new TurnHandler(true)); handlers.put("-", new TurnHandler(false)); handlers.put("&", new PitchHandler(true)); handlers.put("^", new PitchHandler(false)); handlers.put("/", new RollHandler(true)); handlers.put("*", new RollHandler(false)); handlers.put("[", new PushTurtleHandler()); handlers.put("]", new PopTurtleHandler()); handlers.put("!", new WidthChangeHandler(true)); handlers.put("~", new WidthChangeHandler(false)); handlers.put("{", new BeginPolygonHandler()); handlers.put("}", new ClosePolygonHandler()); handlers.put("%", new ReverseHandler()); handlers.put("#", new HueChangeHandler(false, true)); handlers.put("@", new HueChangeHandler(false, false)); handlers.put("##", new HueChangeHandler(true, true)); handlers.put("@@", new HueChangeHandler(true, false)); // Not to mention the fucking assignment handlers... Jesus Christ. handlers.put("color", new DrawColorHandler()); handlers.put("polygonColor", new PolygonColorHandler()); CommandHandler angleIncrement = new AngleIncrementHandler(); handlers.put("angle", angleIncrement); handlers.put("angleIncrement", angleIncrement); handlers.put("lineWidth", new LineWidthHandler()); handlers.put("lineIncrement", new LineWidthIncrementHandler()); handlers.put("distance", new DistanceHandler()); handlers.put("hueChange", new HueAngleIncrementHandler()); } /** * Returns the command handler for a symbol. * * @param symbol * the symbol * @return the command handler for that symbol, or null if no * handler exists */ public Renderer.CommandHandler getHandler(String symbol) { if (handlers.containsKey(symbol)) return (CommandHandler) handlers.get(symbol); return null; } /** * Returns the progress in the current rendering. * * @return the number of symbols processed, the max value of which is twice * the number of symbols passed into the render * method */ public int getDoneSymbols() { return completedSymbols; } /** * Does an assignment from a key to a value, calling the handler as well as * setting the value in the turtle. * * @param key * the key * @param value * the value, possibly a mathematical expression */ public void assign(String key, String value) { try { try { if (!NONASSIGN_WORDS.contains(key)) { currentTurtle.assign(key, value); value = currentTurtle.get(key).toString(); } } catch (Throwable e) { } Renderer.CommandHandler handler = getHandler(key); handler.handle(value); } catch (Throwable e) { } } /** * Given a list of symbols and a dictionary of parameters, this will render * a representation of those symbols to either a graphics, or a returned * image. * * @param symbols * a list of symbols * @param parameters * the parameters * @param matrix * the initial transform matrix for the turtle, or if null * it is assumed to be the identity matrix * @param graphics * If we want to render to a graphics, pass this in and the * L-system will be drawn in the graphic's clip bounds, or pass * in null to have this function return an image. * This graphics should have a clip area set! * @param origin * stores in the passed in point the location where the turtle * started * @return an image of a rendering of these symbols, or null * if there was a passed in graphics object * @throws IllegalArgumentException * if there is a passed in graphics object and its clip area is * not set */ public Image render(List symbols, Map parameters, Matrix matrix, Graphics2D graphics, Point2D origin) { BufferedImage image = null; Rectangle2D bounds = new Rectangle2D.Double(); if (graphics != null && graphics.getClip() == null) throw new IllegalArgumentException( "Graphics needs a non-null clip!"); if (matrix == null) matrix = new Matrix(); totalSymbols = symbols.size() * 2; completedSymbols = 0; isActive = true; for (int i = 0; i < 2; i++) { areDrawing = i == 1; drawnSofar = 0; // Set up the initial conditions. turtleStack.clear(); currentTurtle = new Turtle(); currentTurtle.matrix = matrix; currentTurtle = new Turtle(currentTurtle); // Set up the graphics object. if (!areDrawing || graphics == null) { image = new BufferedImage((int) bounds.getWidth() + 10, (int) bounds.getHeight() + 10, BufferedImage.TYPE_INT_ARGB); g = image.createGraphics(); if (areDrawing) { g.translate(-bounds.getX() + 5.0, -bounds.getY() + 5.0); origin .setLocation(5.0 - bounds.getX(), 5.0 - bounds .getY()); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } } else { image = null; g = (Graphics2D) graphics.create(); Rectangle2D newBounds = new Rectangle2D.Double( bounds.getX() - 5.0, bounds.getY() - 5.0, bounds .getWidth() + 10.0, bounds.getHeight() + 10.0); Rectangle2D ourBounds = g.getClipBounds(); double aRatio = newBounds.getWidth() / newBounds.getHeight(); double vRatio = ourBounds.getWidth() / ourBounds.getHeight(); if (aRatio > vRatio) { // The L-system is wider than the clip bounds. double targetHeight = newBounds.getWidth() / vRatio; targetHeight -= newBounds.getHeight(); // Must extend by targetHeight. newBounds.setRect(newBounds.getX(), newBounds.getY() - targetHeight / 2.0, newBounds.getWidth(), newBounds.getHeight() + targetHeight); } else { // The L-system is taller than the clip bounds. double targetWidth = newBounds.getHeight() * vRatio; targetWidth -= newBounds.getWidth(); // Extend by targetWidth. newBounds.setRect(newBounds.getX() - targetWidth / 2.0, newBounds.getY(), newBounds.getWidth() + targetWidth, newBounds.getHeight()); } double scale = ourBounds.getWidth() / newBounds.getWidth(); g.scale(scale, scale); g.translate(ourBounds.getX() - newBounds.getX(), ourBounds .getY() - newBounds.getY()); origin.setLocation(ourBounds.getX() - newBounds.getX(), ourBounds.getY() - newBounds.getY()); } // Do the initial parameters. Iterator it = parameters.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); try { assign((String) entry.getKey(), (String) entry.getValue()); } catch (Throwable e) { // We have an error in the handler! } } // Set the initial drawing state. g.setColor(currentTurtle.getColor()); capLinePath(); // Repeatedly read symbols, and call the appropriate // command handler. it = symbols.iterator(); while (it.hasNext()) { completedSymbols++; String symbol = (String) it.next(); Renderer.CommandHandler handler = getHandler(symbol); if (handler != null) { try { handler.handle(null); } catch (Throwable e) { // We have an error! } continue; } // OKAY, perhaps this is an assignment? int equalsPosition = symbol.indexOf('='); if (equalsPosition != -1) { String key = symbol.substring(0, equalsPosition); String value = symbol.substring(equalsPosition + 1); // Get the assignment. assign(key, value); } // Well, let's go on. Perhaps this is a symbol with // an argument. int leftParenPosition = symbol.indexOf('('), rightParenPosition = symbol .lastIndexOf(')'); if (leftParenPosition != -1 && rightParenPosition != -1 && leftParenPosition < rightParenPosition) { String key = symbol.substring(0, leftParenPosition); String value = symbol.substring(leftParenPosition + 1, rightParenPosition); handler = getHandler(key); try { handler.handle(value); } catch (Throwable e) { // Another error. Whew. } continue; } } capLinePath(); g.dispose(); // We pop all the turtle stacks to make sure the bounds // are okay... while (!turtleStack.isEmpty()) popTurtleStack(); bounds = currentTurtle.getBounds(); } isActive = false; areDrawing = false; return image; } public boolean isActive() { return isActive; } /** * This will pop the turtle stack. */ private void popTurtleStack() { try { Turtle lt = (Turtle) turtleStack.pop(); lt.updateBounds(currentTurtle); currentTurtle = lt; g.setColor(currentTurtle.getColor()); g.setStroke(currentTurtle.getStroke()); } catch (EmptyStackException e) { // We just ignore it. } } /** * */ private final void capLinePath() { g.draw(linePath); // Dump the path to the graphics... linePath.reset(); // Clear the path... linePath.moveTo((float) currentTurtle.position.getX(), (float) currentTurtle.position.getY()); // We've started anew! } /** The command handler maps from symbols to the appropriate handler. */ private Map handlers = new HashMap(); /** * true if we are actually drawing, elsewise we're in the * phase where we're still trying to discover the bounds (in which case * actually drawing isn't strictly required). */ private boolean areDrawing = false; private boolean isActive = false; /** The stack of turtles. */ private Stack turtleStack = new Stack(); /** The current turtle. */ private Turtle currentTurtle; /** The current graphics object. */ private Graphics2D g; /** The polygon. Null if no polygon is being drawn right now. */ private GeneralPath polygon = null; /** Lines paths. */ private GeneralPath linePath = new GeneralPath(); /** The number of objects drawn sofar. */ private int drawnSofar; /** The number of symbols completed sofar. */ private int completedSymbols; /** The number of symbols to process. */ private int totalSymbols; /** The set of words that can be assigned to. */ public static Set ASSIGN_WORDS; /** The set of words that cannot be assigned a numerical value. */ public static Set NONASSIGN_WORDS; static { Set s = new TreeSet(); s.add("color"); s.add("polygonColor"); NONASSIGN_WORDS = Collections.unmodifiableSet(new HashSet(s)); s.add("angle"); s.add("lineWidth"); s.add("lineIncrement"); s.add("distance"); s.add("hueChange"); ASSIGN_WORDS = Collections.unmodifiableSet(s); } // / THE COMMAND HANDLERS! /** * This is a command handler. This is the object that responds to the * command. This class is meant to alter the state of the Renderer * object, so it is not a static class. */ protected class CommandHandler { /** * Handles the command. * * @param symbol * an optional argument to the handler, but may be null */ public void handle(String symbol) { // This does nothing, since the default behavior is to // just ignore commands. Subclasses will do something, // presumably. } } /** * This handles moving the cursor. */ private class MoveHandler extends CommandHandler { public MoveHandler(boolean pendown, boolean forward) { this.pendown = pendown; this.forward = forward; } public final void handle(String symbol) { // Evaluate if necessary. if (symbol == null) currentTurtle.go(forward); else { double d = currentTurtle.valueOf(symbol).doubleValue(); currentTurtle.go(forward ? d : -d); } if (!areDrawing) return; if (pendown) { if (polygon == null) { // We're not adding to the polygon! linePath.lineTo((float) currentTurtle.position.getX(), (float) currentTurtle.position.getY()); } else { // We are adding to the polygon! polygon.lineTo((float) currentTurtle.position.getX(), (float) currentTurtle.position.getY()); } } else { linePath.moveTo((float) currentTurtle.position.getX(), (float) currentTurtle.position.getY()); } } private boolean pendown; private boolean forward; private Line2D line = new Line2D.Double(); } /** * This handles turning. */ private class TurnHandler extends CommandHandler { public TurnHandler(boolean clockwise) { this.clockwise = clockwise; } public final void handle(String symbol) { // Evaluate if necessary. if (symbol == null) currentTurtle.turn(clockwise); else { double d = currentTurtle.valueOf(symbol).doubleValue(); currentTurtle.turn(clockwise ? -d : d); } } private boolean clockwise; } /** * This handles pitching. */ private class PitchHandler extends CommandHandler { public PitchHandler(boolean down) { this.down = down; } public final void handle(String symbol) { if (symbol == null) currentTurtle.pitch(down); else { double d = currentTurtle.valueOf(symbol).doubleValue(); currentTurtle.pitch(down ? d : -d); } } private boolean down; } /** * This handles rolling. */ private class RollHandler extends CommandHandler { public RollHandler(boolean right) { this.right = right; } public final void handle(String symbol) { if (symbol == null) currentTurtle.roll(right); else { double d = currentTurtle.valueOf(symbol).doubleValue(); currentTurtle.roll(right ? -d : d); } } private boolean right; } /** * This handles pushing on the turtle stack. */ private class PushTurtleHandler extends CommandHandler { public final void handle(String symbol) { turtleStack.push(currentTurtle.clone()); } } /** * This handles popping the turtle stack. */ private class PopTurtleHandler extends CommandHandler { public final void handle(String symbol) { capLinePath(); popTurtleStack(); capLinePath(); } } /** * This handles changing the width of lines. */ private class WidthChangeHandler extends CommandHandler { public WidthChangeHandler(boolean increment) { this.increment = increment; } public final void handle(String symbol) { capLinePath(); if (symbol == null) currentTurtle.changeLineWidth(increment); else { double d = currentTurtle.valueOf(symbol).doubleValue(); currentTurtle.changeLineWidth(increment ? d : -d); } g.setStroke(currentTurtle.getStroke()); } private boolean increment; } /** * This handles change of the draw color. */ private class DrawColorHandler extends CommandHandler { public final void handle(String symbol) { if (!areDrawing) return; capLinePath(); currentTurtle.setColor(symbol); g.setColor(currentTurtle.getColor()); } } /** * This handles change of the polygon color. */ private class PolygonColorHandler extends CommandHandler { public final void handle(String symbol) { if (!areDrawing) return; currentTurtle.setPolygonColor(symbol); } } /** * This handles change of the angle increment. */ private class AngleIncrementHandler extends CommandHandler { public final void handle(String symbol) { currentTurtle.setAngleChange(Double.parseDouble(symbol)); } } /** * This handles change of the line width. */ private class LineWidthHandler extends CommandHandler { public final void handle(String symbol) { if (!areDrawing) return; capLinePath(); currentTurtle.setLineWidth(Double.parseDouble(symbol)); g.setStroke(currentTurtle.getStroke()); } } /** * This handles change of the line width increment. */ private class LineWidthIncrementHandler extends CommandHandler { public final void handle(String symbol) { currentTurtle.setLineIncrement(Double.parseDouble(symbol)); } } /** * This handles change of individual line lengths. */ private class DistanceHandler extends CommandHandler { public final void handle(String symbol) { currentTurtle.distance = Double.parseDouble(symbol); } } /** * This handler begins a polygon. */ private class BeginPolygonHandler extends CommandHandler { public final void handle(String symbol) { if (!areDrawing || polygon != null) return; // Hrm. capLinePath(); polygon = new GeneralPath(); polygon.moveTo((float) currentTurtle.position.getX(), (float) currentTurtle.position.getY()); } } /** * This handler closes a polygon. */ private class ClosePolygonHandler extends CommandHandler { public final void handle(String symbol) { if (!areDrawing) return; capLinePath(); polygon.closePath(); g.setColor(currentTurtle.polygonColor); g.fill(polygon); polygon = null; g.setColor(currentTurtle.color); drawnSofar++; } } /** * The reverse handler. */ private class ReverseHandler extends CommandHandler { public final void handle(String symbol) { currentTurtle.turn(180.0); } } /** * This handles change of the hue angle increment. */ private class HueAngleIncrementHandler extends CommandHandler { public final void handle(String symbol) { currentTurtle.setHueChange(Double.parseDouble(symbol)); } } /** * This handles changing the hue angle. */ private class HueChangeHandler extends CommandHandler { public HueChangeHandler(boolean polygon, boolean add) { this.polygon = polygon; this.add = add; } public final void handle(String symbol) { if (!areDrawing) return; capLinePath(); if (symbol == null) if (polygon) currentTurtle.changePolygonHue(add); else currentTurtle.changeHue(add); else { double d = currentTurtle.valueOf(symbol).doubleValue(); d = add ? d : -d; if (polygon) currentTurtle.changePolygonHue(d); else currentTurtle.changeHue(d); } g.setColor(currentTurtle.getColor()); } private boolean add, polygon; } }