View Javadoc
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   * Display a text for another Locatable object.
30   * <p>
31   * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
32   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
33   * </p>
34   * @author Alexander Verbraeck
35   * @author Peter Knoppers
36   * @author Wouter Schakel
37   * @param <L> locatable type
38   * @param <T> text animation type
39   */
40  public abstract class RenderableTextSource<L extends OtsShape, T extends RenderableTextSource<L, T>> implements OtsShape
41  {
42      /** The source. */
43      private final L source;
44  
45      /** The text to display. */
46      private Supplier<String> text;
47  
48      /** The horizontal movement of the text, in meters. */
49      private float dx;
50  
51      /** The vertical movement of the text, in meters. */
52      private float dy;
53  
54      /** Whether to center or not. */
55      private final TextAlignment textAlignment;
56  
57      /** The color of the text. */
58      private Color color;
59  
60      /** FontSize the size of the font; default = 2.0 (meters). */
61      private final float fontSize;
62  
63      /** Minimum font size to trigger scaling. */
64      private final float minFontSize;
65  
66      /** Maximum font size to trigger scaling. */
67      private final float maxFontSize;
68  
69      /** The animation implementation. */
70      private final RenderableText animationImpl;
71  
72      /** The font. */
73      private Font font;
74  
75      /** Access to the current background color. */
76      private final ContrastToBackground background;
77  
78      /** Render dependent on font scale. */
79      private final ScaleDependentRendering scaleDependentRendering;
80  
81      /** Whether the location is dynamic. */
82      private boolean dynamic = false;
83  
84      /** Location of this text. */
85      private DirectedPoint2d location;
86  
87      /**
88       * Construct a new TextAnimation.
89       * @param source the object for which the text is displayed
90       * @param text the text to display
91       * @param dx the horizontal movement of the text, in meters
92       * @param dy the vertical movement of the text, in meters
93       * @param textAlignment where to place the text
94       * @param color the color of the text
95       * @param fontSize the size of the font; default = 2.0 (meters)
96       * @param minFontSize minimum font size resulting from scaling
97       * @param maxFontSize maximum font size resulting from scaling
98       * @param contextualized context provider.
99       * @param background allows querying the background color and adaptation of the actual color of the text to ensure contrast
100      * @param scaleDependentRendering suppress rendering when font scale is too small
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      * Construct a new TextAnimation without contrast to background protection and no minimum font scale.
132      * @param source the object for which the text is displayed
133      * @param text the text to display
134      * @param dx the horizontal movement of the text, in meters
135      * @param dy the vertical movement of the text, in meters
136      * @param textAlignment where to place the text
137      * @param color the color of the text
138      * @param fontSize the size of the font; default = 2.0 (meters)
139      * @param minFontSize minimum font size resulting from scaling
140      * @param maxFontSize maximum font size resulting from scaling
141      * @param contextualized context provider
142      * @param scaleDependentRendering render text only when bigger than minimum scale
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      * Constructor.
155      * @param source the object for which the text is displayed
156      * @param text the text to display
157      * @param dx the horizontal movement of the text, in meters
158      * @param dy the vertical movement of the text, in meters
159      * @param textAlignment where to place the text
160      * @param color the color of the text
161      * @param contextualized context provider
162      * @param scaleDependentRendering render text only when bigger than minimum scale
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      * Sets whether the location of this text is dynamic.
173      * @param dynamic whether the location of this text is dynamic.
174      * @return for method chaining.
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                 // draw not upside down.
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      * Returns the source.
209      * @return the source
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      * paint() method so it can be overridden or extended.
236      * @param graphics the graphics object
237      * @param observer the observer
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                 // Construct an alternative color
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                 // Draw transparent rectangle with background color to makes sure all of the text is visible, even when it is
293                 // drawn outside of the bounds of the object that supplies the background color, or on parts of the object that
294                 // have a different color (e.g. driver dot, brake lights, etc.).
295                 double r = scaledFontRectangle.getHeight() / 2.0; // rounding
296                 double dh = scaledFontRectangle.getHeight() / 5.0; // baseline shift
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             // slight outline
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             // text itself
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      * Returns whether two colors are similar.
321      * @param color1 color 1.
322      * @param color2 color 2.
323      * @return whether two colors are similar.
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         // this threshold may need to be tweaked, it used to be color.equals(color) which is too narrow
332     }
333 
334     /**
335      * Destroy the text animation.
336      * @param contextProvider the object with a Context
337      */
338     public final void destroy(final Contextualized contextProvider)
339     {
340         this.animationImpl.destroy(contextProvider);
341     }
342 
343     /**
344      * Retrieve dx.
345      * @return the value of dx
346      */
347     protected final float getDx()
348     {
349         return this.dx;
350     }
351 
352     /**
353      * Retrieve dy.
354      * @return the value of dy
355      */
356     protected final float getDy()
357     {
358         return this.dy;
359     }
360 
361     /**
362      * Sets a new offset.
363      * @param x dx
364      * @param y dy
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      * Retrieve the text alignment.
380      * @return the text alignment
381      */
382     protected TextAlignment getTextAlignment()
383     {
384         return this.textAlignment;
385     }
386 
387     /**
388      * Retrieve the font size.
389      * @return the font size
390      */
391     protected float getFontSize()
392     {
393         return this.fontSize;
394     }
395 
396     /**
397      * Retrieve the font.
398      * @return the font
399      */
400     protected Font getFont()
401     {
402         return this.font;
403     }
404 
405     /**
406      * Retrieve the current text.
407      * @return the current text
408      */
409     protected String getText()
410     {
411         return this.text.get();
412     }
413 
414     /**
415      * Update the text.
416      * @param text the new text
417      */
418     public void setText(final Supplier<String> text)
419     {
420         this.text = text;
421     }
422 
423     /**
424      * Retrieve the current color.
425      * @return the current color
426      */
427     protected Color getColor()
428     {
429         return this.color;
430     }
431 
432     /**
433      * Update the color.
434      * @param color the new color
435      */
436     protected void setColor(final Color color)
437     {
438         this.color = color;
439     }
440 
441     /**
442      * Retrieve the current flip status.
443      * @return the current flip status
444      */
445     public boolean isFlip()
446     {
447         return this.animationImpl.isFlip();
448     }
449 
450     /**
451      * Update the flip status.
452      * @param flip the new flip status
453      */
454     public void setFlip(final boolean flip)
455     {
456         this.animationImpl.setFlip(flip);
457     }
458 
459     /**
460      * Retrieve the current rotation status.
461      * @return the current rotation status
462      */
463     public boolean isRotate()
464     {
465         return this.animationImpl.isRotate();
466     }
467 
468     /**
469      * Update the rotation status.
470      * @param rotate the new rotation status
471      */
472     public void setRotate(final boolean rotate)
473     {
474         this.animationImpl.setRotate(rotate);
475 
476     }
477 
478     /**
479      * Retrieve the current scale status.
480      * @return the current scale status
481      */
482     public boolean isScale()
483     {
484         return this.animationImpl.isScale();
485     }
486 
487     /**
488      * Update the scale status.
489      * @param scale the new scale status
490      */
491     public void setScale(final boolean scale)
492     {
493         this.animationImpl.setScale(scale);
494     }
495 
496     /**
497      * Retrieve the current translate status.
498      * @return the current translate status
499      */
500     public boolean isTranslate()
501     {
502         return this.animationImpl.isTranslate();
503     }
504 
505     /**
506      * Update the translate status.
507      * @param translate the new translate status
508      */
509     public void setTranslate(final boolean translate)
510     {
511         this.animationImpl.setTranslate(translate);
512     }
513 
514     /**
515      * Return whether to scale the renderable in the Y-direction when there is a compressed Y-axis or not.
516      * @return whether to scale the renderable in the Y-direction when there is a compressed Y-axis or not
517      */
518     public boolean isScaleY()
519     {
520         return this.animationImpl.isScaleY();
521     }
522 
523     /**
524      * Set whether to scale the renderable in the Y-direction when there is a compressed Y-axis or not.
525      * @param scaleY whether to scale the renderable in the Y-direction when there is a compressed Y-axis or not
526      */
527     public void setScaleY(final boolean scaleY)
528     {
529         this.animationImpl.setScaleY(scaleY);
530     }
531 
532     /**
533      * Return whether to scale the renderable in the X/Y-direction with the value of RenderableScale.objectScaleFactor or not.
534      * @return whether to scale the renderable in the X/Y-direction with the value of RenderableScale.objectScaleFactor or not
535      */
536     public boolean isScaleObject()
537     {
538         return this.animationImpl.isScaleObject();
539     }
540 
541     /**
542      * Set whether to scale the renderable in the X/Y-direction with the value of RenderableScale.objectScaleFactor or not.
543      * @param scaleObject whether to scale the renderable in the X/Y-direction with the value of
544      *            RenderableScale.objectScaleFactor or not
545      */
546     public void setScaleObject(final boolean scaleObject)
547     {
548         this.animationImpl.setScaleObject(scaleObject);
549     }
550 
551     /**
552      * Renderable text.
553      * <p>
554      * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
555      * <br>
556      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
557      * </p>
558      * @author Alexander Verbraeck
559      * @author Peter Knoppers
560      * @author Wouter Schakel
561      */
562     private static class RenderableText extends Renderable2d<RenderableTextSource<?, ?>>
563     {
564         /**
565          * Construct a new AnimationImpl.
566          * @param source the source
567          * @param contextualized context provider.
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      * Retrieve the scale dependent rendering qualifier (used in cloning).
596      * @return the rendering qualifier of this TextAnimation
597      */
598     protected ScaleDependentRendering getScaleDependentRendering()
599     {
600         return this.scaleDependentRendering;
601     }
602 
603     /**
604      * Interface to obtain the color of the background.
605      */
606     public interface ContrastToBackground
607     {
608         /**
609          * Retrieve the color of the background.
610          * @return the (current) color of the background
611          */
612         Color getBackgroundColor();
613     }
614 
615     /**
616      * Determine if a Feature object should be rendered.
617      */
618     public interface ScaleDependentRendering
619     {
620         /**
621          * Determine if a Text should be rendered, depending on the scale.
622          * @param scale the current font scale
623          * @return true if the text should be rendered at the scale; false if the text should not be rendered at the scale
624          */
625         boolean isRendered(double scale);
626     }
627 
628     /** Always render the Text. */
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     /** Don't render texts when smaller than 1. */
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     /** Don't render texts when smaller than 2. */
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     /** Don't render texts when smaller than 2. */
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 }