gann-cdf /
turtle-logo
| 1 | package org.gannacademy.cdf.turtlelogo; |
||
| 2 | |||
| 3 | import javax.swing.*; |
||
| 4 | import java.awt.*; |
||
| 5 | import java.util.ArrayList; |
||
| 6 | import java.util.List; |
||
| 7 | |||
| 8 | /** |
||
| 9 | * <p>A {@link Turtle} lives (and draws) inside a <code>Terrarium</code>.</p> |
||
| 10 | * |
||
| 11 | * <p><img src="doc-files/trail.png" alt="Turtle leaving a trail"></p> |
||
| 12 | * |
||
| 13 | * <p>A terrarium can be resized using the {@link #setSize(int, int)} method and repositioned on the screen using the |
||
| 14 | * {@link #setPosition(int, int)}. The terrarium can also have a custom background color (set via the |
||
| 15 | * {@link #setBackground(Color)} method — it defaults to white.</p> |
||
| 16 | * |
||
| 17 | * <p><img src="doc-files/setBackground.png" alt="setBackground() example"></p> |
||
| 18 | * |
||
| 19 | * <p>Initially, there is only a single terrarium, into which all new turtles are added. However, it is possible to |
||
| 20 | * instantiate additional terraria, and to direct Turtles to them using the {@link Turtle#setTerrarium(Terrarium)} |
||
| 21 | * method. From a technical standpoint, the initial terrarium is a quasi-singleton, and will continue to be treated as |
||
| 22 | * a singleton by any new turtles as they are instantiated. The singleton terrarium instance can be accessed statically |
||
| 23 | * via the {@link #getInstance()} method. When additional terraria have been instantiated, they may also be accessed |
||
| 24 | * statically via their index (in instantiation order) using the {@link #getInstance(int)} method.</p> |
||
| 25 | * |
||
| 26 | * @author <a href="https://github.com/gann-cdf/turtlelogo/issues">Seth Battis</a> |
||
| 27 | */ |
||
| 28 | public class Terrarium extends JPanel { |
||
| 29 | /** |
||
| 30 | * The parts of the terrarium that are "under the surface" are not meant to be used by students. This mechanism |
||
| 31 | * (inspired by <a href="https://stackoverflow.com/a/18634125">this awesome Stack Overflow answer</a>) recreates a |
||
| 32 | * version of the C++ <code>friend</code> concept: a public method that is only available to <i>some</i> other |
||
| 33 | * objects, rather than <i>all</i> other objects. |
||
| 34 | * |
||
| 35 | * @author <a href="https://github.com/gann-cdf/turtlelogo/issues">Seth Battis</a> |
||
| 36 | */ |
||
| 37 | public static final class UnderTheSurface { |
||
| 38 | private UnderTheSurface() { |
||
| 39 | } |
||
| 40 | } |
||
| 41 | |||
| 42 | protected static final UnderTheSurface UNDER_THE_SURFACE = new UnderTheSurface(); |
||
| 43 | |||
| 44 | /** |
||
| 45 | * 600 pixels |
||
| 46 | */ |
||
| 47 | public static final int DEFAULT_WIDTH = 600; |
||
| 48 | |||
| 49 | /** |
||
| 50 | * 400 pixels |
||
| 51 | */ |
||
| 52 | public static final int DEFAULT_HEIGHT = 400; |
||
| 53 | |||
| 54 | /** |
||
| 55 | * {@link Color#WHITE} |
||
| 56 | */ |
||
| 57 | public static final Color DEFAULT_BACKGROUND = Color.WHITE; |
||
| 58 | |||
| 59 | private static List<Terrarium> terraria; |
||
| 60 | private JFrame frame; |
||
| 61 | private List<Turtle> turtles; |
||
|
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
| 62 | private List<Track> tracks; |
||
|
0 ignored issues
–
show
|
|||
| 63 | |||
| 64 | /** |
||
| 65 | * Construct a new terrarium of default dimensions, centered on the screen in its own window |
||
| 66 | */ |
||
| 67 | public Terrarium() { |
||
| 68 | super(); |
||
| 69 | setPreferredSize(new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT)); |
||
| 70 | setBackground(DEFAULT_BACKGROUND); |
||
| 71 | turtles = new ArrayList<>(); |
||
| 72 | tracks = new ArrayList<>(); |
||
| 73 | getFrame(); |
||
| 74 | addInstance(this); |
||
| 75 | } |
||
| 76 | |||
| 77 | private static void addInstance(Terrarium terrarium) { |
||
| 78 | if (terraria == null) { |
||
| 79 | terraria = new ArrayList<>(); |
||
| 80 | } |
||
| 81 | terraria.add(terrarium); |
||
| 82 | } |
||
| 83 | |||
| 84 | /** |
||
| 85 | * Get the default terrarium instance (instantiating it, if necessary) |
||
| 86 | * |
||
| 87 | * @return The default terrarium |
||
| 88 | */ |
||
| 89 | public static Terrarium getInstance() { |
||
| 90 | return getInstance(0); |
||
| 91 | } |
||
| 92 | |||
| 93 | /** |
||
| 94 | * Get a particular Terrarium instance |
||
| 95 | * |
||
| 96 | * @param index [0..<i>n</i>) if there are <i>n</i> terraria, sequenced by instantiation |
||
| 97 | * order |
||
| 98 | * @return The terrarium at this index |
||
| 99 | */ |
||
| 100 | public static Terrarium getInstance(int index) { |
||
| 101 | if (terraria == null) { |
||
| 102 | terraria = new ArrayList<>(); |
||
| 103 | new Terrarium(); |
||
| 104 | } |
||
| 105 | return terraria.get(index); |
||
| 106 | } |
||
| 107 | |||
| 108 | private JFrame getFrame() { |
||
| 109 | if (frame == null) { |
||
| 110 | frame = new JFrame("Turtle Logo"); |
||
| 111 | frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); |
||
| 112 | frame.add(this); |
||
| 113 | frame.pack(); |
||
| 114 | frame.setLocationRelativeTo(null); |
||
| 115 | frame.setVisible(true); |
||
| 116 | } |
||
| 117 | return frame; |
||
| 118 | } |
||
| 119 | |||
| 120 | /** |
||
| 121 | * <p>Adds a new turtle track to the terrarium</p> |
||
| 122 | * |
||
| 123 | * <p>May only be called by {@link Turtle} and its subclasses, enforced via {@link Turtle.UnderTheShell}.</p> |
||
| 124 | * |
||
| 125 | * @param track to be added |
||
| 126 | * @param key to authenticate "Turtleness" |
||
| 127 | */ |
||
| 128 | public synchronized void add(Track track, Turtle.UnderTheShell key) { |
||
| 129 | key.hashCode(); |
||
| 130 | tracks.add(track); |
||
| 131 | repaint(); |
||
| 132 | } |
||
| 133 | |||
| 134 | /** |
||
| 135 | * Clear all turtle tracks from the terrarium |
||
| 136 | */ |
||
| 137 | public synchronized void clear() { |
||
| 138 | tracks.clear(); |
||
| 139 | repaint(); |
||
| 140 | } |
||
| 141 | |||
| 142 | /** |
||
| 143 | * <p>Adds a new turtle to the terrarium</p> |
||
| 144 | * |
||
| 145 | * <p>May only be called by {@link Turtle} and its subclasses, enforced via {@link Turtle.UnderTheShell}.</p> |
||
| 146 | * |
||
| 147 | * @param turtle to be added |
||
| 148 | * @param key to authenticate "Turtleness" |
||
| 149 | */ |
||
| 150 | public synchronized void add(Turtle turtle, Turtle.UnderTheShell key) { |
||
| 151 | key.hashCode(); |
||
| 152 | turtles.add(turtle); |
||
| 153 | repaint(); |
||
| 154 | } |
||
| 155 | |||
| 156 | /** |
||
| 157 | * <p>Remove a turtle from the terrarium</p> |
||
| 158 | * |
||
| 159 | * <p>May only be called by {@link Turtle} and its subclasses, enforced via {@link Turtle.UnderTheShell}.</p> |
||
| 160 | * |
||
| 161 | * @param turtle to be removed |
||
| 162 | * @param key to authenticate "Turtleness" |
||
| 163 | */ |
||
| 164 | public synchronized void remove(Turtle turtle, Turtle.UnderTheShell key) { |
||
| 165 | key.hashCode(); |
||
| 166 | turtles.remove(turtle); |
||
| 167 | repaint(); |
||
| 168 | } |
||
| 169 | |||
| 170 | /** |
||
| 171 | * Adjust the dimensions of the terrarium view |
||
| 172 | * |
||
| 173 | * @param width in pixels |
||
| 174 | * @param height in pixels |
||
| 175 | */ |
||
| 176 | public void setSize(int width, int height) { |
||
| 177 | setPreferredSize(new Dimension(width, height)); |
||
| 178 | getFrame().pack(); |
||
| 179 | getFrame().repaint(); |
||
| 180 | } |
||
| 181 | |||
| 182 | /** |
||
| 183 | * Adjust the location of the terrarium window. Note that the screen origin is in the top, left corner of the display |
||
| 184 | * and that, while the X-axis increases from left to right, the Y-axis <i>increases</i> from top to bottom. The |
||
| 185 | * coordinates given are for the origin of the window (the top, left corner) |
||
| 186 | * |
||
| 187 | * @param x coordinate |
||
| 188 | * @param y coordinate |
||
| 189 | */ |
||
| 190 | public void setPosition(int x, int y) { |
||
| 191 | getFrame().setLocation(x, y); |
||
| 192 | getFrame().repaint(); |
||
| 193 | } |
||
| 194 | |||
| 195 | /** |
||
| 196 | * <p>Repaint the contents of the terrarium (tracks and turtles) as-needed</p> |
||
| 197 | * |
||
| 198 | * <p>This method is called automatically by the enclosing {@link JFrame} to repaint the terrarium as-needed. It is |
||
| 199 | * not meant to be called at will. If the terrarium needs to be updated, a {@link #repaint()} request will schedule |
||
| 200 | * the update.</p> |
||
| 201 | * |
||
| 202 | * @param context for drawing commands |
||
| 203 | */ |
||
| 204 | @Override |
||
| 205 | public synchronized void paintComponent(Graphics context) { |
||
| 206 | super.paintComponent(context); |
||
| 207 | Graphics2D context2D = (Graphics2D) context; |
||
| 208 | draw(context2D); |
||
| 209 | } |
||
| 210 | |||
| 211 | /** |
||
| 212 | * For synchronous drawing requests (e.g. saving images) |
||
| 213 | * |
||
| 214 | * @param context for drawing commands |
||
| 215 | */ |
||
| 216 | protected void draw(Graphics2D context) { |
||
| 217 | context.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
||
| 218 | for (Track track : tracks) { |
||
| 219 | track.draw(context, UNDER_THE_SURFACE); |
||
| 220 | } |
||
| 221 | for (Turtle turtle : turtles) { |
||
| 222 | turtle.draw(context, UNDER_THE_SURFACE); |
||
| 223 | } |
||
| 224 | } |
||
| 225 | } |
||
| 226 |