Issues (37)

java/org/gannacademy/cdf/turtlelogo/Turtle.java (4 issues)

1
package org.gannacademy.cdf.turtlelogo;
2
3
import javax.imageio.ImageIO;
4
import java.awt.*;
5
import java.awt.geom.AffineTransform;
6
import java.awt.image.BufferedImage;
7
import java.io.IOException;
8
9
/**
10
 * <p>A turtles lives in a {@link Terrarium}. We imagine that turtles all hold a pen in their mouth. As they walk around
11
 * the terrarium, they leave a trail (a series of {@link Track} segments) behind them. The turtles can avoid leaving
12
 * a track if they pick up their pen.</p>
13
 *
14
 * <p><img src="doc-files/trail.png" alt="Turtle leaving a trail"></p>
15
 *
16
 * <p>Turtles understand a limited number of instructions:</p>
17
 *
18
 * <table style="margin-left: 4em;">
19
 * <tr>
20
 * <td><img src="doc-files/move.png" alt="move() example"></td>
21
 * <td><b>move(</b>
22
 * <i>steps</i><b>)</b> (a.k.a <i>forward</i> or <i>fd</i> and <i>back</i> or <i>bk</i>) &mdash; the turtle will step
23
 * forward, in the direction that it is currently facing, some number of steps (i.e. pixels).</td>
24
 * </tr>
25
 * <tr>
26
 * <td><img src="doc-files/turn.png" alt="turn() example"></td>
27
 * <td><b>turn(</b><i>degrees</i><b>)</b>
28
 * (a.k.a. <i>left</i> or <i>lt</i> and <i>right</i> or <i>rt</i>) &mdash; the turtle will turn from its current heading
29
 * some number of degrees.</td>
30
 * </tr>
31
 * <tr>
32
 * <td><img src="doc-files/moveTo.png" alt="moveTo() example"></td>
33
 * <td><b>moveTo(</b><i>x</i>, <i>y</i><b>)</b> (a.k.a. <i>to</i>) &mdash; the turtle will move from its current
34
 * location to the coordinates given, without changing its heading.</td>
35
 * </tr>
36
 * <tr>
37
 * <td><img src="doc-files/teleport.png" alt="teleport() example"></td>
38
 * <td><b>teleport(</b><i>x</i>, <i>y</i><b>)</b> (a.k.a. <i>tp</i>) &mdash; the turtle will teleport (like in Star
39
 * Trek) from its current location to the new coordinates, without changing its heading <i>and</i> without allowing the
40
 * pen to drag between locations.</td>
41
 * </tr>
42
 * <tr>
43
 * <td><img src="doc-files/penColor.png" alt="penColor() example"></td>
44
 * <td><b>penColor(</b><i>color</i><b>)</b> (a.k.a. <i>pc</i>) &mdash; the turtle will change the color of its pen
45
 * (initially the pen is black)</td>
46
 * </tr>
47
 * <tr>
48
 * <td><img src="doc-files/penWidth.png" alt="penWidth() example"></td>
49
 * <td><b>penWidth(</b><i>width</i><b>)</b> (a.k.a. <i>pw</i>) &mdash; the turtle will change the width of its pen
50
 * stroke (measured in pixels)</td>
51
 * </tr>
52
 * <tr>
53
 * <td><img src="doc-files/hide.png" alt="hide() example"></td>
54
 * <td><b>hide()</b> (a.k.a. <i>ht</i>) &mdash; the turtle will hide itself (but remain at its current location and
55
 * heading)</td>
56
 * </tr>
57
 * <tr>
58
 * <td><img src="doc-files/show.png" alt="show() example"></td>
59
 * <td><b>show()</b> (a.k.a. <i>st</i>) &mdash; the turtle, if hidden, will show itself again</td>
60
 * </tr>
61
 * <caption>&nbsp;</caption>
62
 * </table>
63
 */
64
public class Turtle {
65
  /**
66
   * The parts of the turtle that are "under the shell" are not meant to be used by students. This mechanism
67
   * (inspired by <a href="https://stackoverflow.com/a/18634125">this awesome Stack Overflow answer</a>) recreates a
68
   * version of the C++ <code>friend</code> concept: a public method that is only available to <i>some</i> other
69
   * objects, rather than <i>all</i> other objects.
70
   *
71
   * @author <a href="https://github.com/gann-cdf/turtlelogo/issues">Seth Battis</a>
72
   */
73
  public static final class UnderTheShell {
74
    private UnderTheShell() {
75
    }
76
  }
77
78
  protected static final UnderTheShell UNDER_THE_SHELL = new UnderTheShell();
79
80
  /**
81
   * 270&deg;
82
   */
83
  public static final double NORTH = 270;
84
85
  /**
86
   * 90&deg;
87
   */
88
  public static final double SOUTH = 90;
89
90
  /**
91
   * 0&deg;
92
   */
93
  public static final double EAST = 0;
94
95
  /**
96
   * 180&deg;
97
   */
98
  public static final double WEST = 180;
99
100
  /**
101
   * {@link #EAST}
102
   */
103
  public static final double DEFAULT_HEADING_IN_DEGREES = EAST; // degrees
104
105
  /**
106
   * {@link java.awt.Color#BLACK}
107
   */
108
  public static final Color DEFAULT_PEN_COLOR = Color.BLACK;
109
110
  /**
111
   * 1.0 pixels
112
   */
113
  public static final float DEFAULT_PEN_WIDTH = 1;
114
115
  /**
116
   * <code>true</code>
117
   */
118
  public static final boolean DEFAULT_PEN_DOWN = true;
119
120
  /**
121
   * <code>false</code>
122
   */
123
  public static final boolean DEFAULT_HIDDEN = false;
124
125
126
  private Terrarium terrarium;
127
  private static BufferedImage icon;
128
129
  private double x, y;
130
  private double headingInDegrees;
131
  private Color penColor;
132
  private BasicStroke penStroke;
133
  private boolean penDown;
134
  private boolean hidden;
135
136
  /**
137
   * Construct a turtle in the default terrarium
138
   */
139
  public Turtle() {
140
    this(Terrarium.getInstance());
141
  }
142
143
  /**
144
   * Construct a turtle in a custom terrarium
145
   *
146
   * @param terrarium to house the turtle
147
   */
148
  public Turtle(Terrarium terrarium) {
149
    this.x = terrarium.getWidth() / 2.0;
150
    this.y = terrarium.getHeight() / 2.0;
151
    this.headingInDegrees = DEFAULT_HEADING_IN_DEGREES;
152
    this.penColor = DEFAULT_PEN_COLOR;
153
    this.penStroke = new BasicStroke(DEFAULT_PEN_WIDTH);
154
    this.penDown = DEFAULT_PEN_DOWN;
155
    this.hidden = DEFAULT_HIDDEN;
156
    this.terrarium = terrarium;
157
    this.terrarium.add(this, UNDER_THE_SHELL);
158
  }
159
160
  /**
161
   * @return X-coordinate of turtle
162
   */
163
  public double getX() {
164
    return x;
165
  }
166
167
  /**
168
   * @return Y-coordinate of turtle
169
   */
170
  public double getY() {
171
    return y;
172
  }
173
174
  /**
175
   * @return Current turtle heading in degrees
176
   */
177
  public double getHeadingInDegrees() {
178
    return headingInDegrees;
179
  }
180
181
  /**
182
   * @return Current turtle heading in radians
183
   */
184
  public double getHeadingInRadians() {
185
    return Math.toRadians(headingInDegrees);
186
  }
187
188
  /**
189
   * @return Current pen color
190
   */
191
  public Color getPenColor() {
192
    return penColor;
193
  }
194
195
  /**
196
   * @return Current pen width
197
   */
198
  public double getPenWidth() {
199
    return penStroke.getLineWidth();
200
  }
201
202
  protected BasicStroke getPenStroke() {
203
    return penStroke;
204
  }
205
206
  /**
207
   * @return <code>true</code> if the pen is down, <code>false</code> otherwise
208
   */
209
  public boolean isPenDown() {
210
    return penDown;
211
  }
212
213
  /**
214
   * @return <code>true</code> if the turtle is hidden, <code>false</code> otherwise
215
   */
216
  public boolean isHidden() {
217
    return hidden;
218
  }
219
220
  private BufferedImage getIcon() {
221
    if (icon == null) {
222
      try {
223
        icon = ImageIO.read(getClass().getResource("/turtle.png"));
0 ignored issues
show
Bug Multi Threading introduced by
Instance methods writing to static fields may lead to concurrency problems. Consider making the enclosing method static or removing this assignment to a static field.

If you really need to set this static field, consider writing a thread-safe setter and atomic getter.

Loading history...
224
      } catch (IOException e) {
225
        System.err.println("The image file containing the turtle icon could not be found and/or opened.");
226
        e.printStackTrace();
0 ignored issues
show
Throwable.printStackTrace writes to the console which might not be available at runtime. Using a logger is preferred.
Loading history...
227
      }
228
    }
229
    return icon;
230
  }
231
232
  /**
233
   * @return Terrarium currently housing the turtle
234
   */
235
  public Terrarium getTerrarium() {
236
    return terrarium;
237
  }
238
239
  /**
240
   * Move the turtle to another terrarium
241
   *
242
   * @param terrarium to house the turtle
243
   */
244
  public void setTerrarium(Terrarium terrarium) {
245
    if (this.terrarium != null) {
246
      this.terrarium.remove(this, UNDER_THE_SHELL);
247
    }
248
    this.terrarium = terrarium;
249
    terrarium.add(this, UNDER_THE_SHELL);
250
  }
251
252
  /**
253
   * Alias for {@link #back(double)}
254
   *
255
   * @param steps in pixels
256
   */
257
  public void bk(double steps) {
258
    back(steps);
259
  }
260
261
  /**
262
   * Alias for {@link #move(double)}
263
   *
264
   * @param steps in pixels
265
   */
266
  public void back(double steps) {
267
    move(-1 * steps);
268
  }
269
270
  /**
271
   * Alias for {@link #forward(double)}
272
   *
273
   * @param steps in pixels
274
   */
275
  public void fd(double steps) {
276
    forward(steps);
277
  }
278
279
  /**
280
   * Alias for {@link #move(double)}
281
   *
282
   * @param steps in pixels
283
   */
284
  public void forward(double steps) {
285
    move(steps);
286
  }
287
288
  /**
289
   * <p>Move the turtle in the direction of its current heading</p>
290
   * <p>A positive value for <code>steps</code> is interpreted as forward movement and a negative value as backward
291
   * movement</p>
292
   *
293
   * @param steps in pixels
294
   */
295
  public void move(double steps) {
296
    double newX = x + Math.cos(getHeadingInRadians()) * steps,
297
            newY = y + Math.sin(getHeadingInRadians()) * steps;
298
    if (penDown) {
299
      getTerrarium().add(new Track(x, y, newX, newY, penColor, penStroke, UNDER_THE_SHELL), UNDER_THE_SHELL);
300
    }
301
    x = newX;
302
    y = newY;
303
  }
304
305
  /**
306
   * Alias for {@link #moveTo(double, double)}
307
   *
308
   * @param x coordinate
309
   * @param y coordinate
310
   */
311
  public void to(double x, double y) {
312
    moveTo(x, y);
313
  }
314
315
  /**
316
   * <p>Move the turtle to a particular location</p>
317
   * <p>The turtle moves directly to the window coordinates (<code>x</code>, <code>y</code>). Note that the origin of
318
   * the wind ow is in the top, left corner and that, while the X-axis increases from left to right, the Y-axis
319
   * increases <i>from to to bottom</i>.</p>
320
   * <p>If the turtle's pen is currently down, the move will create a track from the old location to the new location.</p>
321
   *
322
   * @param x coordinate
323
   * @param y coordinate
324
   */
325
  public void moveTo(double x, double y) {
326
    if (penDown) {
327
      getTerrarium().add(new Track(this.x, this.y, x, y, penColor, penStroke, UNDER_THE_SHELL), UNDER_THE_SHELL);
328
    }
329
    this.x = x;
330
    this.y = y;
331
  }
332
333
  /**
334
   * Alias for {@link #teleport(double, double)}
335
   *
336
   * @param x coordinate
337
   * @param y coordinate
338
   */
339
  public void tp(double x, double y) {
340
    teleport(x, y);
341
  }
342
343
  /**
344
   * <p>Move the turtle instantaneously to a particular location</p>
345
   * <p>The turtle moves directly to the window coordinates (<code>x</code>, <code>y</code>). Note that the origin of
346
   * the wind ow is in the top, left corner and that, while the X-axis increases from left to right, the Y-axis
347
   * increases <i>from to to bottom</i>.</p>
348
   * <p>No track is left by a teleportation.</p>
349
   *
350
   * @param x coordinate
351
   * @param y coordinate
352
   */
353
  public void teleport(double x, double y) {
354
    this.x = x;
355
    this.y = y;
356
  }
357
358
  /**
359
   * <p>Reset the turtle to its home position</p>
360
   * <p>The turtle's home position is at the center of the window, heading {@link #EAST}</p>
361
   */
362
  public void home() {
363
    teleport(getTerrarium().getWidth() / 2.0, getTerrarium().getHeight() / 2.0);
364
    head(EAST);
365
  }
366
367
  /**
368
   * Alias for {@link #right(double)}
369
   *
370
   * @param angle in degrees
371
   */
372
  public void rt(double angle) {
373
    right(angle);
374
  }
375
376
  /**
377
   * Alias for {@link #turn(double)}
378
   *
379
   * @param angle in degrees
380
   */
381
  public void right(double angle) {
382
    turn(angle);
383
  }
384
385
  /**
386
   * Alias for {@link #left(double)}
387
   *
388
   * @param angle in degrees
389
   */
390
  public void lt(double angle) {
391
    left(angle);
392
  }
393
394
  /**
395
   * Alias for {@link #turn(double)}
396
   *
397
   * @param angle in degrees
398
   */
399
  public void left(double angle) {
400
    turn(-1 * angle);
401
  }
402
403
  /**
404
   * <p>Turn the turtle from its current heading</p>
405
   * <p>A positive angle is interpreted as a <i>right</i> turn and a negative angle is a left turn. This is mildly
406
   * surprising if you know about the unit circle, but is because the Y-axis of the window increases from top to bottom,
407
   * which results in a mirrored unit circle around the X-axis, with 90&deg; at the {@link #SOUTH} and270&deg; at the
408
   * {@link #NORTH}</p>
409
   *
410
   * @param angle in degrees
411
   */
412
  public void turn(double angle) {
413
    headingInDegrees = (headingInDegrees + angle) % 360;
414
    getTerrarium().repaint();
415
  }
416
417
  /**
418
   * Alias for {@link #head(double)}
419
   *
420
   * @param heading in degrees
421
   */
422
  public void hd(double heading) {
423
    head(heading);
424
  }
425
426
  /**
427
   * <p>Turn the turtle to a particular heading</p>
428
   * <p>Note that, because the Y-axis of the window increases from top to bottom, the usual angles of the unit circle
429
   * have been mirrored around the X-axis, with 90&deg; at the {@link #SOUTH} and 270%deg; at the {@link #NORTH}.
430
   * Convenience constants have been provided for the cardinal directions.</p>
431
   *
432
   * @param heading [0..360) in degrees
433
   */
434
  public void head(double heading) {
435
    this.headingInDegrees = heading % 360;
436
  }
437
438
  /**
439
   * Alis for {@link #penUp()}
440
   */
441
  public void pu() {
442
    penUp();
443
  }
444
445
  /**
446
   * Lift the turtle's pen up, causing it not to leave a track
447
   */
448
  public void penUp() {
449
    penDown = false;
450
  }
451
452
  /**
453
   * Alias for {@link #penDown()}
454
   */
455
  public void pd() {
456
    penDown();
457
  }
458
459
  /**
460
   * Lower the turtle's pen, causing it to leave a trail
461
   */
462
  public void penDown() {
463
    penDown = true;
464
  }
465
466
  /**
467
   * Alias for {@link #penColor(Color)}
468
   *
469
   * @param color to use
470
   */
471
  public void pc(Color color) {
472
    penColor(color);
473
  }
474
475
  /**
476
   * <p>Set the color of the turtle's pen</p>
477
   * <p>Color values are given as {@link Color} values. {@link Color} has a number of helpful constants like
478
   * {@link Color#GREEN} or {@link Color#GREEN}. Custom colors can also be constructed. Refer to the
479
   * <a href="https://docs.oracle.com/javase/10/docs/api/java/awt/Color.html" target="_blank">j<code>ava.awt.Color</code>
480
   * API documentation</a> more details (including how to create transparent colors!)</p>
481
   *
482
   * @param color to use
483
   */
484
  public void penColor(Color color) {
485
    penColor = color;
486
  }
487
488
  /**
489
   * Alias for {@link #penWidth(double)}
490
   *
491
   * @param width in pixels
492
   */
493
  public void pw(double width) {
494
    penWidth(width);
495
  }
496
497
  /**
498
   * Set the width of the turtle's pen
499
   *
500
   * @param width in pixels
501
   */
502
  public void penWidth(double width) {
503
    penStroke = new BasicStroke((float) width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
504
  }
505
506
  /**
507
   * Alias for {@link #hide()}
508
   */
509
  public void ht() {
510
    hide();
511
  }
512
513
  /**
514
   * <p>Hide the turtle</p>
515
   * <p>Hiding the turtle causes it to become invisible &mdash; it can still be moved and create tracks, but the turtle
516
   * itself is not visible</p>
517
   */
518
  public void hide() {
519
    hidden = true;
520
    getTerrarium().repaint();
521
  }
522
523
  /**
524
   * Alias for {@link #show()}
525
   */
526
  public void st() {
527
    show();
528
  }
529
530
  /**
531
   * Show the turtle (if it was hidden)
532
   */
533
  public void show() {
534
    hidden = false;
535
    getTerrarium().repaint();
536
  }
537
538
  /**
539
   * <p>Draw the turtle</p>
540
   * <p>May only be called by {@link Terrarium} and its subclasses, enforced by {@link Terrarium.UnderTheSurface}</p>
541
   *
542
   * @param context for drawing commands
543
   * @param key     to authenticate "Terrarium-iality"
544
   */
545
  public void draw(Graphics2D context, Terrarium.UnderTheSurface key) {
546
    key.hashCode();
547
    drawIcon(x, y, getHeadingInRadians(), context);
548
  }
549
550
  protected void drawIcon(double x, double y, double headingInRadians, Graphics2D context) {
551
    if (!hidden) {
552
      AffineTransform transform = new AffineTransform(); // transformations are applied in reverse order
553
      transform.translate(x, y); // move turtle to location
554
      transform.rotate(headingInRadians); // orient turtle to heading
555
      transform.translate(-1 * getIcon().getWidth(), getIcon().getHeight() / -2.0); // move icon origin to turtle nose
0 ignored issues
show
Math operands should be cast to prevent unwanted loss of precision when mixing types. Consider casting one of the operands of this multiplication to double.
Loading history...
A "NullPointerException" could be thrown; "getIcon" is nullable here.
Loading history...
556
      context.drawImage(getIcon(), transform, null);
557
    }
558
  }
559
}
560