1 package org.opentrafficsim.animation;
2
3 import java.awt.Color;
4 import java.awt.Dimension;
5 import java.awt.Font;
6 import java.awt.FontMetrics;
7 import java.awt.Graphics2D;
8 import java.awt.RenderingHints;
9 import java.awt.Shape;
10 import java.awt.geom.Point2D;
11 import java.awt.geom.Rectangle2D;
12 import java.awt.geom.RoundRectangle2D;
13 import java.awt.image.ImageObserver;
14 import java.util.function.Supplier;
15
16 import org.djutils.draw.Directed;
17 import org.djutils.draw.bounds.Bounds2d;
18 import org.djutils.draw.line.Polygon2d;
19 import org.djutils.draw.point.DirectedPoint2d;
20 import org.djutils.draw.point.Point2d;
21 import org.opentrafficsim.base.geometry.OtsShape;
22
23 import nl.tudelft.simulation.dsol.animation.d2.Renderable2d;
24 import nl.tudelft.simulation.dsol.animation.d2.RenderableScale;
25 import nl.tudelft.simulation.language.d2.Angle;
26 import nl.tudelft.simulation.naming.context.Contextualized;
27
28
29
30
31
32
33
34
35
36
37
38
39
40 public abstract class RenderableTextSource<L extends OtsShape, T extends RenderableTextSource<L, T>> implements OtsShape
41 {
42
43 private final L source;
44
45
46 private Supplier<String> text;
47
48
49 private float dx;
50
51
52 private float dy;
53
54
55 private final TextAlignment textAlignment;
56
57
58 private Color color;
59
60
61 private final float fontSize;
62
63
64 private final float minFontSize;
65
66
67 private final float maxFontSize;
68
69
70 private final RenderableText animationImpl;
71
72
73 private Font font;
74
75
76 private final ContrastToBackground background;
77
78
79 private final ScaleDependentRendering scaleDependentRendering;
80
81
82 private boolean dynamic = false;
83
84
85 private DirectedPoint2d location;
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102 @SuppressWarnings("checkstyle:parameternumber")
103 public RenderableTextSource(final L source, final Supplier<String> text, final float dx, final float dy,
104 final TextAlignment textAlignment, final Color color, final float fontSize, final float minFontSize,
105 final float maxFontSize, final Contextualized contextualized, final ContrastToBackground background,
106 final ScaleDependentRendering scaleDependentRendering)
107 {
108 this.source = source;
109 this.text = text;
110 this.dx = dx;
111 this.dy = dy;
112 this.textAlignment = textAlignment;
113 this.color = color;
114 this.fontSize = fontSize;
115 this.minFontSize = minFontSize;
116 this.maxFontSize = maxFontSize;
117 this.background = background;
118 this.scaleDependentRendering = scaleDependentRendering;
119
120 this.font = new Font("SansSerif", Font.PLAIN, 2);
121 if (this.fontSize != 2.0f)
122 {
123 this.font = this.font.deriveFont(this.fontSize);
124 }
125
126 this.animationImpl = new RenderableText(this, contextualized);
127 setScaleY(false);
128 }
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144 @SuppressWarnings("checkstyle:parameternumber")
145 public RenderableTextSource(final L source, final Supplier<String> text, final float dx, final float dy,
146 final TextAlignment textAlignment, final Color color, final float fontSize, final float minFontSize,
147 final float maxFontSize, final Contextualized contextualized, final ScaleDependentRendering scaleDependentRendering)
148 {
149 this(source, text, dx, dy, textAlignment, color, fontSize, minFontSize, maxFontSize, contextualized, null,
150 scaleDependentRendering);
151 }
152
153
154
155
156
157
158
159
160
161
162
163
164 public RenderableTextSource(final L source, final Supplier<String> text, final float dx, final float dy,
165 final TextAlignment textAlignment, final Color color, final Contextualized contextualized,
166 final ScaleDependentRendering scaleDependentRendering)
167 {
168 this(source, text, dx, dy, textAlignment, color, 2.0f, 12.0f, 50f, contextualized, scaleDependentRendering);
169 }
170
171
172
173
174
175
176 @SuppressWarnings("all")
177 public T setDynamic(final boolean dynamic)
178 {
179 this.dynamic = dynamic;
180 return (T) this;
181 }
182
183 @Override
184 public DirectedPoint2d getLocation()
185 {
186 if (this.location == null || this.dynamic)
187 {
188 Point2d p = getSource().getLocation();
189 if (isRotate() && p instanceof Directed dir)
190 {
191
192 double a = Angle.normalizePi(dir.getDirZ());
193 if (a > Math.PI / 2.0 || a < -0.99 * Math.PI / 2.0)
194 {
195 a += Math.PI;
196 }
197 this.location = new DirectedPoint2d(p, a);
198 }
199 else
200 {
201 this.location = new DirectedPoint2d(p, 0.0);
202 }
203 }
204 return this.location;
205 }
206
207
208
209
210
211 public L getSource()
212 {
213 return this.source;
214 }
215
216 @Override
217 public final Bounds2d getRelativeBounds()
218 {
219 return new Bounds2d(2.0, 2.0);
220 }
221
222 @Override
223 public Polygon2d getAbsoluteContour()
224 {
225 return getSource().getAbsoluteContour();
226 }
227
228 @Override
229 public Polygon2d getRelativeContour()
230 {
231 return getSource().getRelativeContour();
232 }
233
234
235
236
237
238
239 @SuppressWarnings("checkstyle:designforextension")
240 public void paint(final Graphics2D graphics, final ImageObserver observer)
241 {
242 double scale = Math.sqrt(graphics.getTransform().getDeterminant());
243 Rectangle2D scaledFontRectangle;
244 String str = this.text.get();
245 synchronized (this.font)
246 {
247 if (!this.scaleDependentRendering.isRendered(scale))
248 {
249 return;
250 }
251 if (scale < this.minFontSize / this.fontSize)
252 {
253 graphics.setFont(this.font.deriveFont((float) (this.minFontSize / scale)));
254 FontMetrics fm = graphics.getFontMetrics();
255 scaledFontRectangle = fm.getStringBounds(str, graphics);
256 }
257 else if (scale > this.maxFontSize / this.fontSize)
258 {
259 graphics.setFont(this.font.deriveFont((float) (this.maxFontSize / scale)));
260 FontMetrics fm = graphics.getFontMetrics();
261 scaledFontRectangle = fm.getStringBounds(str, graphics);
262 }
263 else
264 {
265 graphics.setFont(this.font);
266 FontMetrics fm = graphics.getFontMetrics();
267 scaledFontRectangle = fm.getStringBounds(str, graphics);
268 }
269 Color useColor = this.color;
270 Color bgColor = new Color(255, 255, 255, 48);
271 if (null != this.background && isSimilar(useColor, this.background.getBackgroundColor()))
272 {
273
274 if (Color.BLACK.equals(useColor))
275 {
276 useColor = Color.WHITE;
277 bgColor = new Color(0, 0, 0, 48);
278 }
279 else
280 {
281 useColor = Color.BLACK;
282 }
283 }
284
285 float dxText =
286 this.textAlignment.equals(TextAlignment.LEFT) ? 0.0f : this.textAlignment.equals(TextAlignment.CENTER)
287 ? (float) -scaledFontRectangle.getWidth() / 2.0f : (float) -scaledFontRectangle.getWidth();
288 Object antialias = graphics.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
289 graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
290 if (null != this.background)
291 {
292
293
294
295 double r = scaledFontRectangle.getHeight() / 2.0;
296 double dh = scaledFontRectangle.getHeight() / 5.0;
297 Shape s = new RoundRectangle2D.Double(this.dx - scaledFontRectangle.getWidth() - dxText,
298 this.dy + dh - scaledFontRectangle.getHeight(), scaledFontRectangle.getWidth(),
299 scaledFontRectangle.getHeight(), r, r);
300 Color bg = this.background.getBackgroundColor();
301 graphics.setColor(new Color(bg.getRed(), bg.getGreen(), bg.getBlue(), 96));
302 graphics.fill(s);
303 }
304
305 graphics.setColor(bgColor);
306 float delta = (float) (1.0 / Math.sqrt(graphics.getTransform().getDeterminant()));
307 graphics.drawString(str, dxText + this.dx - delta, -this.dy);
308 graphics.drawString(str, dxText + this.dx + delta, -this.dy);
309 graphics.drawString(str, dxText + this.dx, -this.dy - delta);
310 graphics.drawString(str, dxText + this.dx, -this.dy + delta);
311
312 graphics.setColor(useColor);
313 graphics.drawString(str, dxText + this.dx, -this.dy);
314
315 graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialias);
316 }
317 }
318
319
320
321
322
323
324
325 private boolean isSimilar(final Color color1, final Color color2)
326 {
327 int r = color1.getRed() - color2.getRed();
328 int g = color1.getGreen() - color2.getGreen();
329 int b = color1.getBlue() - color2.getBlue();
330 return r * r + g * g + b * b < 2000;
331
332 }
333
334
335
336
337
338 public final void destroy(final Contextualized contextProvider)
339 {
340 this.animationImpl.destroy(contextProvider);
341 }
342
343
344
345
346
347 protected final float getDx()
348 {
349 return this.dx;
350 }
351
352
353
354
355
356 protected final float getDy()
357 {
358 return this.dy;
359 }
360
361
362
363
364
365
366 protected final void setXY(final float x, final float y)
367 {
368 this.dx = x;
369 this.dy = y;
370 }
371
372 @Override
373 public double getZ()
374 {
375 return DrawLevel.LABEL.getZ();
376 }
377
378
379
380
381
382 protected TextAlignment getTextAlignment()
383 {
384 return this.textAlignment;
385 }
386
387
388
389
390
391 protected float getFontSize()
392 {
393 return this.fontSize;
394 }
395
396
397
398
399
400 protected Font getFont()
401 {
402 return this.font;
403 }
404
405
406
407
408
409 protected String getText()
410 {
411 return this.text.get();
412 }
413
414
415
416
417
418 public void setText(final Supplier<String> text)
419 {
420 this.text = text;
421 }
422
423
424
425
426
427 protected Color getColor()
428 {
429 return this.color;
430 }
431
432
433
434
435
436 protected void setColor(final Color color)
437 {
438 this.color = color;
439 }
440
441
442
443
444
445 public boolean isFlip()
446 {
447 return this.animationImpl.isFlip();
448 }
449
450
451
452
453
454 public void setFlip(final boolean flip)
455 {
456 this.animationImpl.setFlip(flip);
457 }
458
459
460
461
462
463 public boolean isRotate()
464 {
465 return this.animationImpl.isRotate();
466 }
467
468
469
470
471
472 public void setRotate(final boolean rotate)
473 {
474 this.animationImpl.setRotate(rotate);
475
476 }
477
478
479
480
481
482 public boolean isScale()
483 {
484 return this.animationImpl.isScale();
485 }
486
487
488
489
490
491 public void setScale(final boolean scale)
492 {
493 this.animationImpl.setScale(scale);
494 }
495
496
497
498
499
500 public boolean isTranslate()
501 {
502 return this.animationImpl.isTranslate();
503 }
504
505
506
507
508
509 public void setTranslate(final boolean translate)
510 {
511 this.animationImpl.setTranslate(translate);
512 }
513
514
515
516
517
518 public boolean isScaleY()
519 {
520 return this.animationImpl.isScaleY();
521 }
522
523
524
525
526
527 public void setScaleY(final boolean scaleY)
528 {
529 this.animationImpl.setScaleY(scaleY);
530 }
531
532
533
534
535
536 public boolean isScaleObject()
537 {
538 return this.animationImpl.isScaleObject();
539 }
540
541
542
543
544
545
546 public void setScaleObject(final boolean scaleObject)
547 {
548 this.animationImpl.setScaleObject(scaleObject);
549 }
550
551
552
553
554
555
556
557
558
559
560
561
562 private static class RenderableText extends Renderable2d<RenderableTextSource<?, ?>>
563 {
564
565
566
567
568
569 RenderableText(final RenderableTextSource<?, ?> source, final Contextualized contextualized)
570 {
571 super(source, contextualized);
572 }
573
574 @Override
575 public final void paint(final Graphics2D graphics, final ImageObserver observer)
576 {
577 getSource().paint(graphics, observer);
578 }
579
580 @Override
581 public boolean contains(final Point2D pointScreenCoordinates, final Bounds2d extent, final Dimension screenSize,
582 final RenderableScale scale, final double worldMargin, final double pixelMargin)
583 {
584 return false;
585 }
586
587 @Override
588 public final String toString()
589 {
590 return "RenderableText []";
591 }
592 }
593
594
595
596
597
598 protected ScaleDependentRendering getScaleDependentRendering()
599 {
600 return this.scaleDependentRendering;
601 }
602
603
604
605
606 public interface ContrastToBackground
607 {
608
609
610
611
612 Color getBackgroundColor();
613 }
614
615
616
617
618 public interface ScaleDependentRendering
619 {
620
621
622
623
624
625 boolean isRendered(double scale);
626 }
627
628
629 public static final ScaleDependentRendering RENDERALWAYS = new ScaleDependentRendering()
630 {
631 @Override
632 public boolean isRendered(final double scale)
633 {
634 return true;
635 }
636 };
637
638
639 public static final ScaleDependentRendering RENDERWHEN1 = new ScaleDependentRendering()
640 {
641 @Override
642 public boolean isRendered(final double scale)
643 {
644 return scale >= 1.0;
645 }
646 };
647
648
649 public static final ScaleDependentRendering RENDERWHEN10 = new ScaleDependentRendering()
650 {
651 @Override
652 public boolean isRendered(final double scale)
653 {
654 return scale >= 0.1;
655 }
656 };
657
658
659 public static final ScaleDependentRendering RENDERWHEN100 = new ScaleDependentRendering()
660 {
661 @Override
662 public boolean isRendered(final double scale)
663 {
664 return scale >= 0.01;
665 }
666 };
667
668 }