/*
GeoGebra - Dynamic Mathematics for Everyone
http://www.geogebra.org
This file is part of GeoGebra.
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation.
*/
package org.geogebra.common.kernel.geos;
import java.util.ArrayList;
import org.geogebra.common.awt.GColor;
import org.geogebra.common.kernel.Construction;
import org.geogebra.common.kernel.StringTemplate;
import org.geogebra.common.kernel.kernelND.GeoElementND;
import org.geogebra.common.kernel.kernelND.GeoPointND;
import org.geogebra.common.plugin.GeoClass;
import org.geogebra.common.util.StringUtil;
/**
* GeoElement for drawing turtle graphics.
*
* @author G. Sturr, arnaud
*
*/
public class GeoTurtle extends GeoPoint {
// private GeoPointND[] points;
private boolean defined = true;
// List to store sequential turtle drawing commands.
// TODO: use a better data structure?
private ArrayList<TurtleCommand> cmdList;
// turtle status fields
private GeoPointND startPoint = new GeoPoint(cons, 0d, 0d, 1d);
/** current position */
protected double[] position = { 0d, 0d, 1d };
/** current position as point */
// protected GeoPointND currentPoint = new GeoPoint(cons, 0d, 0d, 1d);
/** pen color */
protected GColor penColor = GColor.BLACK;
/** pen thickness */
protected int penThickness = 1;
/** whether pen is down (active) */
protected boolean penDown = true;
/** direction angle (wrt positive x-axis) */
protected double turnAngle = 0d;
/** sine of current direction angle */
protected double sinAngle = 0d;
/** cosine of current direction angle */
protected double cosAngle = 1d;
private int turtleImageIndex = 1;
private int nCompletedCommands = 0;
private double currentCommandProgress = 0d;
private double speed = 1d;
private boolean autoUpdate = true;
// private MyImage turtleImage = null;
/**
* Constructor with label
*
* @param c
* construction
* @param label
* label
*/
public GeoTurtle(Construction c, String label) {
this(c);
setLabel(label);
}
/**
* Constructor without label.
*
* @param c
* construction
*/
public GeoTurtle(Construction c) {
super(c);
cmdList = new ArrayList<TurtleCommand>();
// TODO: put this in default construction?
this.setObjColor(GColor.GRAY);
setCoords(0, 0, 1);
}
// ==================================================
// Copy constructors
// TODO code is copied from GeoPolyLine, needs correcting
// ==================================================
@Override
public String toValueString(StringTemplate tpl) {
return null;
}
/**
* The copy of a polygon is a number (!) with its value set to the polygons
* current area
*/
@Override
public GeoPoint copy() {
return new GeoPoint(cons);
}
@Override
public GeoElement copyInternal(Construction cons1) {
GeoTurtle ret = new GeoTurtle(cons1, null);
// ret.points = GeoElement.copyPoints(cons1, points);
ret.set(this);
return ret;
}
// ===============================================
// GETTERS/SETTERS
// ===============================================
@Override
public GeoClass getGeoClassType() {
return GeoClass.TURTLE;
}
/**
* @return list of turtle commands that define the current turtle drawing
*/
public ArrayList<TurtleCommand> getTurtleCommandList() {
return cmdList;
}
/**
* @return current turn angle in degrees [0,360)
*/
public double getTurnAngle() {
return (turnAngle * 180 / Math.PI) % 360;
}
/**
* @param a
* the new turning angle
*/
public void setTurnAngle(double a) {
turn(a - turnAngle * 180 / Math.PI);
}
/**
* @return current sin and cos of turn angle
*/
public double[] getAngleRotators() {
double[] ar = { this.cosAngle, this.sinAngle };
return ar;
}
/**
* @return current turtle coordinates
*/
public GeoPointND getPosition() {
// return currentPoint;
return this;
}
/**
* @return current pen thickness
*/
public int getPenThickness() {
return penThickness;
}
/**
* @return current pen color
*/
public GColor getPenColor() {
return penColor;
}
/**
* @return true if the pen is down
*/
public boolean getPenDown() {
return penDown;
}
/**
* @return start point
*/
public GeoPointND getStartPoint() {
return startPoint;
}
/**
* @return image index
*/
public int getTurtle() {
return turtleImageIndex;
}
/**
* @param index
* image index (may be arbitrary, %4 is done here)
*/
public void setTurtle(int index) {
int index1 = index % 4;
this.turtleImageIndex = index1;
}
/**
* @return whether the turtle is repainted automatically after every command
*/
public boolean isAutoUpdate() {
return autoUpdate;
}
/**
* @param autoUpdate
* whether the turtle is repainted automatically after every
* command
*/
public void setAutoUpdate(boolean autoUpdate) {
this.autoUpdate = autoUpdate;
}
/**
* @return speed of the turtle
*/
public double getSpeed() {
return speed;
}
/**
* @param s
* speed of the turtle
*/
public void setSpeed(double s) {
if (s < 0d) {
speed = 0d;
} else {
speed = s;
}
}
/**
* @return number of completed commands
*/
public int getNumberOfCompletedCommands() {
return nCompletedCommands;
}
/**
* @return current progress (remaining commands)
*/
public double getCurrentCommandProgress() {
if (currentCommandProgress == 0d) {
return 0d;
}
return currentCommandProgress
/ cmdList.get(nCompletedCommands).getTime();
}
/**
* Reset current progress to 0
*/
public void resetProgress() {
nCompletedCommands = 0;
currentCommandProgress = 0d;
doUpdate();
}
/**
* Do one step
*/
public void stepTurtle() {
stepTurtle(1d);
}
private boolean doStepTurtle(double nSteps) {
int totalNCommands = cmdList.size();
if (speed == 0d || nCompletedCommands >= totalNCommands) {
return false;
}
currentCommandProgress += speed * nSteps;
double t;
while (currentCommandProgress >= (t = cmdList.get(nCompletedCommands)
.getTime())) {
nCompletedCommands += 1;
currentCommandProgress -= t;
if (nCompletedCommands == totalNCommands) {
currentCommandProgress = 0d;
break;
}
}
return true;
}
/**
* @param nSteps
* do n steps
*/
public void stepTurtle(double nSteps) {
if (doStepTurtle(nSteps)) {
doUpdate();
}
}
// ===============================================
// LOGO COMMANDS
// ===============================================
/**
* Moves the turtle forward (in direction given by current turn angle)
*
* @param distance
* distance
*/
public void forward(double distance) {
addCommand(new CmdForward(distance));
}
/**
* @param x
* x-coordinate
* @param y
* y-coordinate
*/
public void setPosition(double x, double y) {
addCommand(new CmdSetPosition(x, y));
}
/**
* @param x
* x-coordinate
* @param y
* y-coordinate
*/
public void setCoords(double x, double y) {
boolean currPenDown = getPenDown();
setPenDown(false);
addCommand(new CmdSetCoords(x, y));
setPenDown(currPenDown);
}
/**
* @param turnAngleChange
* change of turn angle in degrees
*/
public void turn(double turnAngleChange) {
addCommand(new CmdTurn(turnAngleChange));
}
/**
* Puts the pen down or up, i.e. starts / stops drawing
*
* @param penDown
* true to put pen down
*/
public void setPenDown(boolean penDown) {
addCommand(new CmdSetPen(penDown));
}
/**
* Changes pen color used by the turtle
*
* @param r
* red component
* @param g
* green component
* @param b
* blue component
*/
public void setPenColor(int r, int g, int b) {
setPenColor(GColor.newColor(r, g, b));
}
/**
* Changes pen color used by the turtle
*
* @param penColor
* new pen color
*/
public void setPenColor(GColor penColor) {
addCommand(new CmdSetColor(penColor));
}
/**
* Set the thickness of the turtle pen
*
* @param thickness
* new thickness
*/
public void setPenThickness(int thickness) {
addCommand(new CmdSetThickness(thickness));
}
/**
*
*/
public void clear() {
// Temporarily set speed to 0 in order to avoid stepping
double s = speed;
speed = 0;
resetProgress();
cmdList.clear();
turnAngle = 0d;
sinAngle = 0d;
cosAngle = 1d;
position[0] = 0d;
position[1] = 0d;
// currentPoint.setCoords(0d, 0d, 1d);
setCoords(0d, 0d, 1d);
speed = s;
doUpdate();
}
private void doUpdate() {
if (autoUpdate) {
updateRepaint();
}
}
// ===========================================================
// Overridden GeoElement methods
// TODO: review these
// ===========================================================
@Override
public boolean isGeoTurtle() {
return true;
}
/*
* @Override public void set(GeoElement geo) { // TODO Auto-generated method
* stub
*
* }
*/
@Override
public boolean isDefined() {
return defined;
}
@Override
public void setUndefined() {
// TODO Auto-generated method stub
defined = false;
}
/*
* Animatable implementation
*/
@Override
public boolean isAnimatable() {
return true;
}
@Override
public synchronized GeoElementND doAnimationStep(double frameRate,
GeoList parent) {
return doStepTurtle(1.0 / frameRate) ? this : null;
}
/*
* Turtle commands
*/
/**
* Add command to the turtle's command list and perform the command
*
* @param cmd
* the command to add to the command list
*/
public void addCommand(TurtleCommand cmd) {
cmdList.add(cmd);
cmd.perform();
doUpdate();
}
/** command types */
public enum CmdType {
/** forward */
FORWARD,
/** set_position */
SET_POSITION,
/** turn (left / right) */
TURN,
/** set_color */
SET_COLOR,
/** set_pen */
SET_PEN,
/** set_thickness */
SET_THICKNESS
}
/**
* @author arno Interface for turtle commands
*/
public interface TurtleCommand {
/**
* @return the type of the command
*/
public CmdType getType();
/**
* @return the time taken to execute the command
*/
public double getTime();
/**
* perform the command on the enclosed GeoTurtle
*/
public void perform();
/**
* Draw the command
*
* @param ds
* the DrawState object to use for drawing
*/
public void draw(DrawState ds);
/**
* Draw the command partially
*
* @param ds
* the drawState object to use for drawing
* @param progress
* the fraction of the command which is completed (between 0
* and 1)
*/
public void partialDraw(DrawState ds, double progress);
}
/**
* @author arno Interface for drawing turtle paths. DrawTurtle classes must
* implement this interface
*/
public interface DrawState {
/**
* Set pen status
*
* @param down
* true to put pen down, false to lift it
*/
public void setPen(boolean down);
/**
* Move turtle to new position
*
* @param newPosition
* the new turtle position
*/
public void move(GeoPointND newPosition);
/**
* Turn turtle
*
* @param angle
* anticlockwise angle in radians
*/
public void turn(double angle);
/**
* Partially move turtle
*
* @param newPosition
* the new turtle position
* @param progress
* between 0 (not started) and 1 (all done)
*/
public void partialMove(GeoPointND newPosition, double progress);
/**
* Partially turn turtle
*
* @param angle
* anticlockwise angle in radians
* @param progress
* between 0 (not started) and 1 (all done)
*/
public void partialTurn(double angle, double progress);
/**
* Set the pen color
*
* @param color
* new color
*/
public void setColor(GColor color);
/**
* Set the pen thickness
*
* @param th
* new thickness
*/
public void setThickness(int th);
}
/**
* @author arno Command: Move turtle forward
*/
public class CmdForward implements TurtleCommand {
private double length;
private double time;
private GeoPoint destination;
/**
* @param l
* how far to move
*/
public CmdForward(double l) {
length = l;
time = Math.abs(l);
}
@Override
public CmdType getType() {
return CmdType.FORWARD;
}
@Override
public double getTime() {
return time;
}
@Override
public void perform() {
position[0] += length * cosAngle;
position[1] += length * sinAngle;
destination = new GeoPoint(cons, position[0], position[1], 1d);
// currentPoint.setCoords(position[0], position[1], 1d);
setCoords(position[0], position[1], 1d);
}
@Override
public void draw(DrawState ds) {
ds.move(destination);
}
@Override
public void partialDraw(DrawState ds, double progress) {
ds.partialMove(destination, progress);
}
@Override
public String toString() {
return "fd " + length;
}
}
/**
* @author arno + judit Set turtle position immediately
*/
public class CmdSetCoords implements TurtleCommand {
/** x-coord */
protected double destX;
/** y-coord */
protected double destY;
/** destination point */
protected GeoPoint destination;
/**
* @param x
* new x-coord
* @param y
* new y-coord
*/
public CmdSetCoords(double x, double y) {
destX = x;
destY = y;
}
@Override
public CmdType getType() {
// TODO or CmdType.SET_COORDS ?
// I don't know what this enum must do,
// currently not used
return CmdType.SET_POSITION;
}
@Override
public double getTime() {
return 0;
}
@Override
public void perform() {
position[0] = destX;
position[1] = destY;
destination = new GeoPoint(cons, position[0], position[1], 1d);
// currentPoint.setCoords(position[0], position[1], 1d);
boolean currPenDown = getPenDown();
setPenDown(false);
setCoords(position[0], position[1], 1d);
setPenDown(currPenDown);
}
@Override
public void draw(DrawState ds) {
ds.move(destination);
}
@Override
public void partialDraw(DrawState ds, double progress) {
ds.partialMove(destination, progress);
}
}
/**
* @author arno + judit Set turtle position
*/
public class CmdSetPosition extends CmdSetCoords {
private double time;
/**
* @param x
* new x-coord
* @param y
* new y-coord
*/
public CmdSetPosition(double x, double y) {
super(x, y);
time = Math.hypot(x - position[0], y - position[1]);
}
@Override
public CmdType getType() {
return CmdType.SET_POSITION;
}
@Override
public double getTime() {
return time;
}
}
/**
* @author arno Command: turn turtle
*/
public class CmdTurn implements TurtleCommand {
private double degAngle;
private double angle;
private double time;
/**
* @param a
* anticlokwise angle in degrees
*/
public CmdTurn(double a) {
degAngle = a;
angle = a * Math.PI / 180;
time = Math.abs(a) / 90;
}
@Override
public CmdType getType() {
return CmdType.TURN;
}
@Override
public double getTime() {
return time;
}
@Override
public void perform() {
turnAngle += angle;
sinAngle = Math.sin(turnAngle);
cosAngle = Math.cos(turnAngle);
}
@Override
public void draw(DrawState ds) {
ds.turn(angle);
}
@Override
public void partialDraw(DrawState ds, double progress) {
ds.partialTurn(angle, progress);
}
@Override
public String toString() {
if (degAngle > 0) {
return "tl " + degAngle;
}
return "tr " + (-degAngle);
}
}
/**
* @author arno Command: set pen color
*/
public class CmdSetColor implements TurtleCommand {
private GColor color;
/**
* @param c
* the new pen color
*/
public CmdSetColor(GColor c) {
color = c;
}
@Override
public CmdType getType() {
return CmdType.SET_COLOR;
}
@Override
public double getTime() {
return 0d;
}
@Override
public void perform() {
penColor = color;
}
@Override
public void draw(DrawState ds) {
ds.setColor(color);
}
@Override
public void partialDraw(DrawState ds, double progress) {
// nothing to do
}
@Override
public String toString() {
return "color " + color;
}
}
/**
* @author arno Command: set pen state (up or down)
*/
public class CmdSetPen implements TurtleCommand {
private boolean down;
/**
* @param d
* true for pen down, false for up
*/
public CmdSetPen(boolean d) {
down = d;
}
@Override
public CmdType getType() {
return CmdType.SET_PEN;
}
@Override
public double getTime() {
return 0d;
}
@Override
public void perform() {
penDown = down;
}
@Override
public void draw(DrawState ds) {
ds.setPen(down);
}
@Override
public void partialDraw(DrawState ds, double progress) {
// nothing to do
}
@Override
public String toString() {
return down ? "pd" : "pu";
}
}
/**
* @author arno Command: set pen thickness
*/
public class CmdSetThickness implements TurtleCommand {
private int thickness;
/**
* @param th
* the new pen thickness
*/
public CmdSetThickness(int th) {
thickness = th;
}
@Override
public CmdType getType() {
return CmdType.SET_THICKNESS;
}
@Override
public double getTime() {
return 0d;
}
@Override
public void perform() {
penThickness = thickness;
}
@Override
public void draw(DrawState ds) {
ds.setThickness(thickness);
}
@Override
public void partialDraw(DrawState ds, double progress) {
// nothing to do
}
@Override
public String toString() {
return "thickness " + thickness;
}
}
@Override
public boolean isFillable() {
return true;
}
@Override
public FillType getFillType() {
return FillType.IMAGE;
}
@Override
protected void getXMLtags(StringBuilder sb) {
super.getXMLtags(sb);
// name of image file
if (getFillImage() != null) {
sb.append("\t<file name=\"");
sb.append(StringUtil
.encodeXML(this.getGraphicsAdapter().getImageFileName()));
sb.append("\"/>\n");
}
}
}