1 package org.opentrafficsim.draw.road;
2
3 import java.awt.BasicStroke;
4 import java.awt.Color;
5 import java.awt.Font;
6 import java.awt.FontMetrics;
7 import java.awt.Graphics2D;
8 import java.awt.Rectangle;
9 import java.awt.font.TextAttribute;
10 import java.awt.geom.Ellipse2D;
11 import java.awt.geom.Path2D;
12 import java.awt.geom.RoundRectangle2D;
13 import java.awt.image.ImageObserver;
14 import java.rmi.RemoteException;
15 import java.util.Map;
16
17 import org.opentrafficsim.draw.ClickableLocatable;
18 import org.opentrafficsim.draw.DrawLevel;
19 import org.opentrafficsim.draw.OtsRenderable;
20 import org.opentrafficsim.draw.road.PriorityAnimation.PriorityData;
21
22 import nl.tudelft.simulation.naming.context.Contextualized;
23
24
25
26
27
28
29
30
31
32 public class PriorityAnimation extends OtsRenderable<PriorityData>
33 {
34
35
36 private static final long serialVersionUID = 20240228L;
37
38
39 private static final Color SHADOW = new Color(0, 0, 0, 128);
40
41
42 private static final double SHADOW_DX = 0.1;
43
44
45 private static final double SHADOW_DY = 0.05;
46
47
48
49
50
51
52 public PriorityAnimation(final PriorityData source, final Contextualized contextProvider)
53 {
54 super(source, contextProvider);
55 }
56
57 @Override
58 public boolean isRotate()
59 {
60 return false;
61 }
62
63 @Override
64 public void paint(final Graphics2D graphics, final ImageObserver observer)
65 {
66 if (getSource().isNone())
67 {
68 return;
69 }
70 setRendering(graphics);
71 if (getSource().isAllStop() || getSource().isStop())
72 {
73 paintOctagon(graphics, 1.0, SHADOW, true, true);
74 paintOctagon(graphics, 1.0, Color.WHITE, true, false);
75 paintOctagon(graphics, 1.0, Color.BLACK, false, false);
76 paintOctagon(graphics, 0.868, new Color(230, 0, 0), true, false);
77 paintString(graphics, "STOP", Color.WHITE, 0.9f, getSource().isAllStop() ? -0.1f : 0.0f);
78 if (getSource().isAllStop())
79 {
80 paintString(graphics, "ALL WAY", Color.WHITE, 0.4f, 0.45f);
81 }
82 }
83 else if (getSource().isBusStop())
84 {
85 graphics.setColor(SHADOW);
86 graphics.fill(new Ellipse2D.Double(-1.0 + SHADOW_DX, -1.0 + SHADOW_DY, 2.0, 2.0));
87 Color blue = new Color(20, 94, 169);
88 graphics.setColor(blue);
89 graphics.fill(new Ellipse2D.Double(-1.0, -1.0, 2.0, 2.0));
90 graphics.setColor(Color.WHITE);
91 graphics.setStroke(new BasicStroke(0.04f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
92 graphics.draw(new Ellipse2D.Double(-0.94, -0.94, 1.88, 1.88));
93 paintBus(graphics, blue);
94 }
95 else if (getSource().isPriority())
96 {
97 paintDiamond(graphics, 1.0, SHADOW, true, true);
98 paintDiamond(graphics, 1.0, Color.WHITE, true, false);
99 paintDiamond(graphics, 11.0 / 12.0, Color.BLACK, false, false);
100 paintDiamond(graphics, 11.0 / 18.0, new Color(255, 204, 0), true, false);
101 }
102 else if (getSource().isYield())
103 {
104 paintTriangle(graphics, 1.0, SHADOW, true, true);
105 paintTriangle(graphics, 1.0, new Color(230, 0, 0), true, false);
106 paintTriangle(graphics, 0.9, Color.WHITE, false, false);
107 paintTriangle(graphics, 0.55, Color.WHITE, true, false);
108 }
109 resetRendering(graphics);
110 }
111
112
113
114
115
116
117
118
119
120 private void paintOctagon(final Graphics2D graphics, final double radius, final Color color, final boolean fill,
121 final boolean shadow)
122 {
123 double k = Math.tan(Math.PI / 8.0) * radius;
124 double dx = shadow ? SHADOW_DX : 0.0;
125 double dy = shadow ? SHADOW_DY : 0.0;
126 Path2D.Float path = new Path2D.Float();
127 path.moveTo(dx + radius, dy);
128 path.lineTo(dx + radius, dy + k);
129 path.lineTo(dx + k, dy + radius);
130 path.lineTo(dx - k, dy + radius);
131 path.lineTo(dx - radius, dy + k);
132 path.lineTo(dx - radius, dy - k);
133 path.lineTo(dx - k, dy - radius);
134 path.lineTo(dx + k, dy - radius);
135 path.lineTo(dx + radius, dy - k);
136 path.lineTo(dx + radius, dy);
137 graphics.setColor(color);
138 if (fill)
139 {
140 graphics.fill(path);
141 }
142 else
143 {
144 graphics.setStroke(new BasicStroke(0.02f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER));
145 graphics.draw(path);
146 }
147 }
148
149
150
151
152
153
154 private void paintBus(final Graphics2D graphics, final Color blue)
155 {
156
157 Path2D.Double path = new Path2D.Double();
158 path.moveTo(0.77, -0.07);
159 path.lineTo(0.74, -0.36);
160 path.lineTo(-0.69, -0.36);
161 path.lineTo(-0.77, -0.07);
162 path.lineTo(-0.77, 0.22);
163 path.lineTo(0.43, 0.22);
164 path.lineTo(0.77, 0.17);
165 path.lineTo(0.77, -0.07);
166 graphics.fill(path);
167
168 graphics.fill(new Ellipse2D.Double(-0.43 - 0.125, 0.22 - 0.125, 0.25, 0.25));
169 graphics.fill(new Ellipse2D.Double(0.43 - 0.125, 0.22 - 0.125, 0.25, 0.25));
170 graphics.setColor(blue);
171 graphics.setStroke(new BasicStroke(0.015f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
172 graphics.draw(new Ellipse2D.Double(-0.43 - 0.125, 0.22 - 0.125, 0.25, 0.25));
173 graphics.draw(new Ellipse2D.Double(0.43 - 0.125, 0.22 - 0.125, 0.25, 0.25));
174
175 graphics.setColor(blue);
176 path = new Path2D.Double();
177 path.moveTo(-0.52, -0.32);
178 path.lineTo(-0.66, -0.32);
179 path.lineTo(-0.73, -0.07);
180 path.lineTo(-0.52, -0.07);
181 path.lineTo(-0.52, -0.32);
182 graphics.fill(path);
183 for (double x : new double[] {-0.48, -0.23, 0.02, 0.27})
184 {
185 graphics.fill(new Rectangle.Double(x, -0.32, 0.21, 0.21));
186 }
187 path = new Path2D.Double();
188 path.moveTo(0.71, -0.32);
189 path.lineTo(0.52, -0.32);
190 path.lineTo(0.52, -0.11);
191 path.lineTo(0.73, -0.11);
192 path.lineTo(0.71, -0.32);
193 graphics.fill(path);
194 }
195
196
197
198
199
200
201
202
203
204 private void paintDiamond(final Graphics2D graphics, final double radius, final Color color, final boolean fill,
205 final boolean shadow)
206 {
207 double dx = shadow ? SHADOW_DX : 0.0;
208 double dy = shadow ? SHADOW_DY : 0.0;
209 graphics.setColor(color);
210 if (fill)
211 {
212 Path2D.Float path = new Path2D.Float();
213 path.moveTo(dx + radius, dy);
214 path.lineTo(dx, dy + radius);
215 path.lineTo(dx - radius, dy);
216 path.lineTo(dx, dy - radius);
217 path.lineTo(dx + radius, dy);
218 graphics.fill(path);
219 }
220 else
221 {
222
223 graphics.rotate(Math.PI / 4);
224 graphics.setStroke(new BasicStroke(0.04f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER));
225 double r = radius / Math.sqrt(2.0);
226 RoundRectangle2D.Double shape = new RoundRectangle2D.Double(-r, -r, 2.0 * r, 2.0 * r, 0.15 * r, 0.15 * r);
227 graphics.draw(shape);
228 graphics.rotate(-Math.PI / 4);
229 }
230 }
231
232
233
234
235
236
237
238
239
240 private void paintTriangle(final Graphics2D graphics, final double radius, final Color color, final boolean fill,
241 final boolean shadow)
242 {
243 double k = radius * Math.sqrt(3.0) / 3.0;
244 double g = (radius * Math.sqrt(3.0)) - k;
245 double dx = shadow ? SHADOW_DX : 0.0;
246 double dy = shadow ? SHADOW_DY : 0.0;
247 Path2D.Float path = new Path2D.Float();
248 path.moveTo(dx + 0.0, dy - k);
249 path.lineTo(dx + -radius, dy - k);
250 path.lineTo(dx + 0.0, dy + g);
251 path.lineTo(dx + radius, dy - k);
252 path.lineTo(dx + 0.0, dy - k);
253 graphics.setColor(color);
254 if (fill)
255 {
256 graphics.fill(path);
257 }
258 else
259 {
260 graphics.setStroke(new BasicStroke(0.04f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER));
261 graphics.draw(path);
262 }
263 }
264
265
266
267
268
269
270
271
272
273 private void paintString(final Graphics2D graphics, final String text, final Color color, final float fontSize,
274 final float dy)
275 {
276 if (graphics.getTransform().getDeterminant() > 400000)
277 {
278
279
280
281
282
283
284
285 return;
286 }
287 graphics.setColor(color);
288 int fontSizeMetrics = 100;
289 float factor = fontSize / fontSizeMetrics;
290 Font font = new Font("Arial", Font.BOLD, fontSizeMetrics).deriveFont(Map.of(TextAttribute.WIDTH, 0.67f));
291 graphics.setFont(font.deriveFont(fontSize));
292 FontMetrics metrics = graphics.getFontMetrics(font);
293 float w = metrics.stringWidth(text) * factor;
294 float d = metrics.getDescent() * factor;
295 float h = metrics.getHeight() * factor;
296 graphics.drawString(text, -w / 2.0f, dy + h / 2.0f - d);
297 }
298
299
300
301
302
303
304
305
306
307
308
309
310 public interface PriorityData extends ClickableLocatable
311 {
312
313 @Override
314 default double getZ() throws RemoteException
315 {
316 return DrawLevel.NODE.getZ();
317 }
318
319
320
321
322
323 boolean isAllStop();
324
325
326
327
328
329 boolean isBusStop();
330
331
332
333
334
335 boolean isNone();
336
337
338
339
340
341 boolean isPriority();
342
343
344
345
346
347 boolean isStop();
348
349
350
351
352
353 boolean isYield();
354 }
355
356 }