draw(Graphics2D,UnderTheSurface)   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
c 0
b 0
f 0
cc 1
rs 10
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
Best Practice introduced by
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
Bug introduced by
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...
Security Bug introduced by
A "NullPointerException" could be thrown; "getIcon" is nullable here.
Loading history...
556
      context.drawImage(getIcon(), transform, null);
557
    }
558
  }
559
}
560