View Javadoc
1   package org.opentrafficsim.draw;
2   
3   import java.awt.Color;
4   import java.awt.Font;
5   import java.awt.FontMetrics;
6   import java.awt.Graphics2D;
7   import java.awt.RenderingHints;
8   import java.awt.Shape;
9   import java.awt.geom.Rectangle2D;
10  import java.awt.geom.RoundRectangle2D;
11  import java.awt.image.ImageObserver;
12  import java.io.Serializable;
13  import java.rmi.RemoteException;
14  import java.util.function.Supplier;
15  
16  import javax.naming.NamingException;
17  
18  import org.djutils.draw.Oriented;
19  import org.djutils.draw.bounds.Bounds2d;
20  import org.djutils.draw.point.OrientedPoint2d;
21  import org.djutils.draw.point.Point2d;
22  import org.opentrafficsim.base.geometry.BoundingCircle;
23  import org.opentrafficsim.base.geometry.OtsBounds2d;
24  import org.opentrafficsim.base.geometry.OtsLocatable;
25  
26  import nl.tudelft.simulation.dsol.animation.d2.Renderable2d;
27  import nl.tudelft.simulation.language.d2.Angle;
28  import nl.tudelft.simulation.naming.context.Contextualized;
29  
30  /**
31   * Display a text for another Locatable object.
32   * <p>
33   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
34   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
35   * </p>
36   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
37   * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
38   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
39   * @param <L> locatable type
40   * @param <T> text animation type
41   */
42  public abstract class TextAnimation<L extends OtsLocatable, T extends TextAnimation<L, T>> implements OtsLocatable, Serializable
43  {
44      /** */
45      private static final long serialVersionUID = 20161211L;
46  
47      /** The object for which the text is displayed. */
48      private final L source;
49  
50      /** The text to display. */
51      private Supplier<String> text;
52  
53      /** The horizontal movement of the text, in meters. */
54      private float dx;
55  
56      /** The vertical movement of the text, in meters. */
57      private float dy;
58  
59      /** Whether to center or not. */
60      private final TextAlignment textAlignment;
61  
62      /** The color of the text. */
63      private Color color;
64  
65      /** FontSize the size of the font; default = 2.0 (meters). */
66      private final float fontSize;
67  
68      /** Minimum font size to trigger scaling. */
69      private final float minFontSize;
70  
71      /** Maximum font size to trigger scaling. */
72      private final float maxFontSize;
73  
74      /** The animation implementation. */
75      private final AnimationImpl animationImpl;
76  
77      /** The font. */
78      private Font font;
79  
80      /** Access to the current background color. */
81      private final ContrastToBackground background;
82  
83      /** Render dependent on font scale. */
84      private final ScaleDependentRendering scaleDependentRendering;
85  
86      /** Whether the location is dynamic. */
87      private boolean dynamic = false;
88  
89      /** Location of this text. */
90      private OrientedPoint2d location;
91  
92      /**
93       * Construct a new TextAnimation.
94       * @param source L; the object for which the text is displayed
95       * @param text Supplier&lt;String&gt;; the text to display
96       * @param dx float; the horizontal movement of the text, in meters
97       * @param dy float; the vertical movement of the text, in meters
98       * @param textAlignment TextAlignment; where to place the text
99       * @param color Color; the color of the text
100      * @param fontSize float; the size of the font; default = 2.0 (meters)
101      * @param minFontSize float; minimum font size resulting from scaling
102      * @param maxFontSize float; maximum font size resulting from scaling
103      * @param contextualized Contextualized; context provider.
104      * @param background ContrastToBackground; allows querying the background color and adaptation of the actual color of the
105      *            text to ensure contrast
106      * @param scaleDependentRendering ScaleDependentRendering; suppress rendering when font scale is too small
107      * @throws NamingException when animation context cannot be created or retrieved
108      * @throws RemoteException when remote context cannot be found
109      */
110     @SuppressWarnings("checkstyle:parameternumber")
111     public TextAnimation(final L source, final Supplier<String> text, final float dx, final float dy,
112             final TextAlignment textAlignment, final Color color, final float fontSize, final float minFontSize,
113             final float maxFontSize, final Contextualized contextualized, final ContrastToBackground background,
114             final ScaleDependentRendering scaleDependentRendering) throws RemoteException, NamingException
115     {
116         this.source = source;
117         this.text = text;
118         this.dx = dx;
119         this.dy = dy;
120         this.textAlignment = textAlignment;
121         this.color = color;
122         this.fontSize = fontSize;
123         this.minFontSize = minFontSize;
124         this.maxFontSize = maxFontSize;
125         this.background = background;
126         this.scaleDependentRendering = scaleDependentRendering;
127 
128         this.font = new Font("SansSerif", Font.PLAIN, 2);
129         if (this.fontSize != 2.0f)
130         {
131             this.font = this.font.deriveFont(this.fontSize);
132         }
133 
134         this.animationImpl = new AnimationImpl(this, contextualized);
135     }
136 
137     /**
138      * Construct a new TextAnimation without contrast to background protection and no minimum font scale.
139      * @param source L; the object for which the text is displayed
140      * @param text Supplier&lt;String&gt;; the text to display
141      * @param dx float; the horizontal movement of the text, in meters
142      * @param dy float; the vertical movement of the text, in meters
143      * @param textAlignment TextAlignment; where to place the text
144      * @param color Color; the color of the text
145      * @param fontSize float; the size of the font; default = 2.0 (meters)
146      * @param minFontSize float; minimum font size resulting from scaling
147      * @param maxFontSize float; maximum font size resulting from scaling
148      * @param contextualized Contextualized; context provider
149      * @param scaleDependentRendering ScaleDependentRendering; render text only when bigger than minimum scale
150      * @throws NamingException when animation context cannot be created or retrieved
151      * @throws RemoteException when remote context cannot be found
152      */
153     @SuppressWarnings("checkstyle:parameternumber")
154     public TextAnimation(final L source, final Supplier<String> text, final float dx, final float dy,
155             final TextAlignment textAlignment, final Color color, final float fontSize, final float minFontSize,
156             final float maxFontSize, final Contextualized contextualized, final ScaleDependentRendering scaleDependentRendering)
157             throws RemoteException, NamingException
158     {
159         this(source, text, dx, dy, textAlignment, color, fontSize, minFontSize, maxFontSize, contextualized, null,
160                 scaleDependentRendering);
161     }
162 
163     /**
164      * @param source L; the object for which the text is displayed
165      * @param text Supplier&lt;String&gt;; the text to display
166      * @param dx float; the horizontal movement of the text, in meters
167      * @param dy float; the vertical movement of the text, in meters
168      * @param textAlignment TextAlignment; where to place the text
169      * @param color Color; the color of the text
170      * @param contextualized Contextualized; context provider
171      * @param scaleDependentRendering ScaleDependentRendering; render text only when bigger than minimum scale
172      * @throws NamingException when animation context cannot be created or retrieved
173      * @throws RemoteException when remote context cannot be found
174      */
175     public TextAnimation(final L source, final Supplier<String> text, final float dx, final float dy,
176             final TextAlignment textAlignment, final Color color, final Contextualized contextualized,
177             final ScaleDependentRendering scaleDependentRendering) throws RemoteException, NamingException
178     {
179         this(source, text, dx, dy, textAlignment, color, 2.0f, 12.0f, 50f, contextualized, scaleDependentRendering);
180     }
181 
182     /**
183      * Sets whether the location of this text is dynamic.
184      * @param dynamic boolean; whether the location of this text is dynamic.
185      * @return T; for method chaining.
186      */
187     @SuppressWarnings("unchecked")
188     public T setDynamic(final boolean dynamic)
189     {
190         this.dynamic = dynamic;
191         return (T) this;
192     }
193 
194     /** {@inheritDoc} */
195     @Override
196     public OrientedPoint2d getLocation()
197     {
198         if (this.location == null || this.dynamic)
199         {
200             Point2d p = this.source.getLocation();
201             if (p instanceof Oriented)
202             {
203                 // draw not upside down.
204                 double a = Angle.normalizePi(((Oriented<?>) p).getDirZ());
205                 if (a > Math.PI / 2.0 || a < -0.99 * Math.PI / 2.0)
206                 {
207                     a += Math.PI;
208                 }
209                 this.location = new OrientedPoint2d(p, a);
210             }
211             else
212             {
213                 this.location = new OrientedPoint2d(p, 0.0);
214             }
215         }
216         return this.location;
217     }
218 
219     /** {@inheritDoc} */
220     @Override
221     public final OtsBounds2d getBounds()
222     {
223         return new BoundingCircle(1.0);
224     }
225 
226     /**
227      * paint() method so it can be overridden or extended.
228      * @param graphics Graphics2D; the graphics object
229      * @param observer ImageObserver; the observer
230      */
231     @SuppressWarnings("checkstyle:designforextension")
232     public void paint(final Graphics2D graphics, final ImageObserver observer)
233     {
234         double scale = Math.sqrt(graphics.getTransform().getDeterminant());
235         Rectangle2D scaledFontRectangle;
236         String str = this.text.get();
237         synchronized (this.font)
238         {
239             if (!this.scaleDependentRendering.isRendered(scale))
240             {
241                 return;
242             }
243             if (scale < this.minFontSize / this.fontSize)
244             {
245                 graphics.setFont(this.font.deriveFont((float) (this.minFontSize / scale)));
246                 FontMetrics fm = graphics.getFontMetrics();
247                 scaledFontRectangle = fm.getStringBounds(str, graphics);
248             }
249             else if (scale > this.maxFontSize / this.fontSize)
250             {
251                 graphics.setFont(this.font.deriveFont((float) (this.maxFontSize / scale)));
252                 FontMetrics fm = graphics.getFontMetrics();
253                 scaledFontRectangle = fm.getStringBounds(str, graphics);
254             }
255             else
256             {
257                 graphics.setFont(this.font);
258                 FontMetrics fm = graphics.getFontMetrics();
259                 scaledFontRectangle = fm.getStringBounds(str, graphics);
260             }
261             Color useColor = this.color;
262             if (null != this.background && isSimilar(useColor, this.background.getBackgroundColor()))
263             {
264                 // Construct an alternative color
265                 if (Color.BLACK.equals(useColor))
266                 {
267                     useColor = Color.WHITE;
268                 }
269                 else
270                 {
271                     useColor = Color.BLACK;
272                 }
273             }
274 
275             float dxText =
276                     this.textAlignment.equals(TextAlignment.LEFT) ? 0.0f : this.textAlignment.equals(TextAlignment.CENTER)
277                             ? (float) -scaledFontRectangle.getWidth() / 2.0f : (float) -scaledFontRectangle.getWidth();
278             Object antialias = graphics.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
279             graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
280             if (null != this.background)
281             {
282                 // Draw transparent rectangle with background color to makes sure all of the text is visible, even when it is
283                 // drawn outside of the bounds of the object that supplies the background color, or on parts of the object that
284                 // have a different color (e.g. driver dot, brake lights, etc.).
285                 double r = scaledFontRectangle.getHeight() / 2.0; // rounding
286                 double dh = scaledFontRectangle.getHeight() / 5.0; // baseline shift
287                 Shape s = new RoundRectangle2D.Double(this.dx - scaledFontRectangle.getWidth() - dxText,
288                         this.dy + dh - scaledFontRectangle.getHeight(), scaledFontRectangle.getWidth(),
289                         scaledFontRectangle.getHeight(), r, r);
290                 Color bg = this.background.getBackgroundColor();
291                 graphics.setColor(new Color(bg.getRed(), bg.getGreen(), bg.getBlue(), 92));
292                 graphics.fill(s);
293             }
294             graphics.setColor(useColor);
295             graphics.drawString(str, dxText + this.dx, -this.dy);
296 
297             graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialias);
298         }
299     }
300 
301     /**
302      * Returns whether two colors are similar.
303      * @param color1 Color; color 1.
304      * @param color2 Color; color 2.
305      * @return boolean; whether two colors are similar.
306      */
307     private boolean isSimilar(final Color color1, final Color color2)
308     {
309         int r = color1.getRed() - color2.getRed();
310         int g = color1.getGreen() - color2.getGreen();
311         int b = color1.getBlue() - color2.getBlue();
312         return r * r + g * g + b * b < 2000;
313         // this threshold may need to be tweaked, it used to be color.equals(color) which is too narrow
314     }
315 
316     /**
317      * Destroy the text animation.
318      * @param contextProvider Contextualized; the object with a Context
319      */
320     public final void destroy(final Contextualized contextProvider)
321     {
322         this.animationImpl.destroy(contextProvider);
323     }
324 
325     /**
326      * Retrieve the source.
327      * @return L; the source
328      */
329     protected final L getSource()
330     {
331         return this.source;
332     }
333 
334     /**
335      * Retrieve dx.
336      * @return float; the value of dx
337      */
338     protected final float getDx()
339     {
340         return this.dx;
341     }
342 
343     /**
344      * Retrieve dy.
345      * @return float; the value of dy
346      */
347     protected final float getDy()
348     {
349         return this.dy;
350     }
351 
352     /**
353      * Sets a new offset.
354      * @param x float; dx
355      * @param y float; dy
356      */
357     protected final void setXY(final float x, final float y)
358     {
359         this.dx = x;
360         this.dy = y;
361     }
362 
363     /** {@inheritDoc} */
364     @Override
365     public double getZ() throws RemoteException
366     {
367         return DrawLevel.LABEL.getZ();
368     }
369 
370     /**
371      * Retrieve the text alignment.
372      * @return TextAlignment; the text alignment
373      */
374     protected final TextAlignment getTextAlignment()
375     {
376         return this.textAlignment;
377     }
378 
379     /**
380      * Retrieve the font size.
381      * @return float; the font size
382      */
383     protected final float getFontSize()
384     {
385         return this.fontSize;
386     }
387 
388     /**
389      * Retrieve the font.
390      * @return Font; the font
391      */
392     protected final Font getFont()
393     {
394         return this.font;
395     }
396 
397     /**
398      * Retrieve the current text.
399      * @return String; the current text
400      */
401     protected final String getText()
402     {
403         return this.text.get();
404     }
405 
406     /**
407      * Update the text.
408      * @param text Supplier&lt;String&gt;; the new text
409      */
410     public final void setText(final Supplier<String> text)
411     {
412         this.text = text;
413     }
414 
415     /**
416      * Retrieve the current color.
417      * @return Color; the current color
418      */
419     protected final Color getColor()
420     {
421         return this.color;
422     }
423 
424     /**
425      * Update the color.
426      * @param color Color; the new color
427      */
428     protected final void setColor(final Color color)
429     {
430         this.color = color;
431     }
432 
433     /**
434      * Retrieve the current flip status.
435      * @return boolean; the current flip status
436      */
437     public final boolean isFlip()
438     {
439         return this.animationImpl.isFlip();
440     }
441 
442     /**
443      * Update the flip status.
444      * @param flip boolean; the new flip status
445      */
446     public final void setFlip(final boolean flip)
447     {
448         this.animationImpl.setFlip(flip);
449     }
450 
451     /**
452      * Retrieve the current rotation status.
453      * @return boolean; the current rotation status
454      */
455     public final boolean isRotate()
456     {
457         return this.animationImpl.isRotate();
458     }
459 
460     /**
461      * Update the rotation status.
462      * @param rotate boolean; the new rotation status
463      */
464     public final void setRotate(final boolean rotate)
465     {
466         this.animationImpl.setRotate(rotate);
467 
468     }
469 
470     /**
471      * Retrieve the current scale status.
472      * @return boolean; the current scale status
473      */
474     public final boolean isScale()
475     {
476         return this.animationImpl.isScale();
477     }
478 
479     /**
480      * Update the scale status.
481      * @param scale boolean; the new scale status
482      */
483     public final void setScale(final boolean scale)
484     {
485         this.animationImpl.setScale(scale);
486     }
487 
488     /**
489      * Retrieve the current translate status.
490      * @return boolean; the current translate status
491      */
492     public final boolean isTranslate()
493     {
494         return this.animationImpl.isTranslate();
495     }
496 
497     /**
498      * Update the translate status.
499      * @param translate boolean; the new translate status
500      */
501     public final void setTranslate(final boolean translate)
502     {
503         this.animationImpl.setTranslate(translate);
504     }
505 
506     /**
507      * The implementation of the text animation.
508      * <p>
509      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
510      * <br>
511      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
512      * </p>
513      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
514      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
515      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
516      */
517     private static class AnimationImpl extends Renderable2d<TextAnimation<?, ?>>
518     {
519         /** */
520         private static final long serialVersionUID = 20170400L;
521 
522         /**
523          * Construct a new AnimationImpl.
524          * @param source TextAnimation; the source
525          * @param contextualized Contextualized; context provider.
526          * @throws NamingException when animation context cannot be created or retrieved
527          * @throws RemoteException when remote context cannot be found
528          */
529         AnimationImpl(final TextAnimation<?, ?> source, final Contextualized contextualized)
530                 throws NamingException, RemoteException
531         {
532             super(source, contextualized);
533         }
534 
535         /** {@inheritDoc} */
536         @Override
537         public final void paint(final Graphics2D graphics, final ImageObserver observer)
538         {
539             getSource().paint(graphics, observer);
540         }
541 
542         /** {@inheritDoc} */
543         @Override
544         public boolean contains(final Point2d pointWorldCoordinates, final Bounds2d extent)
545         {
546             return false;
547         }
548 
549         /** {@inheritDoc} */
550         @Override
551         public final String toString()
552         {
553             return "TextAnimation.AnimationImpl []";
554         }
555     }
556 
557     /**
558      * Retrieve the scale dependent rendering qualifier (used in cloning).
559      * @return ScaleDependentRendering; the rendering qualifier of this TextAnimation
560      */
561     protected ScaleDependentRendering getScaleDependentRendering()
562     {
563         return this.scaleDependentRendering;
564     }
565 
566     /**
567      * Interface to obtain the color of the background.
568      */
569     public interface ContrastToBackground
570     {
571         /**
572          * Retrieve the color of the background.
573          * @return Color; the (current) color of the background
574          */
575         Color getBackgroundColor();
576     }
577 
578     /**
579      * Determine if a Feature object should be rendered.
580      */
581     public interface ScaleDependentRendering
582     {
583         /**
584          * Determine if a Text should be rendered, depending on the scale.
585          * @param scale double; the current font scale
586          * @return boolean; true if the text should be rendered at the scale; false if the text should not be rendered at the
587          *         scale
588          */
589         boolean isRendered(double scale);
590     }
591 
592     /** Always render the Text. */
593     public static final ScaleDependentRendering RENDERALWAYS = new ScaleDependentRendering()
594     {
595         /** {@inheritDoc} */
596         @Override
597         public boolean isRendered(final double scale)
598         {
599             return true;
600         }
601     };
602 
603     /** Don't render texts when smaller than 1. */
604     public static final ScaleDependentRendering RENDERWHEN1 = new ScaleDependentRendering()
605     {
606         /** {@inheritDoc} */
607         @Override
608         public boolean isRendered(final double scale)
609         {
610             return scale >= 1.0;
611         }
612     };
613 
614     /** Don't render texts when smaller than 2. */
615     public static final ScaleDependentRendering RENDERWHEN10 = new ScaleDependentRendering()
616     {
617         /** {@inheritDoc} */
618         @Override
619         public boolean isRendered(final double scale)
620         {
621             return scale >= 0.1;
622         }
623     };
624 
625     /** Don't render texts when smaller than 2. */
626     public static final ScaleDependentRendering RENDERWHEN100 = new ScaleDependentRendering()
627     {
628         /** {@inheritDoc} */
629         @Override
630         public boolean isRendered(final double scale)
631         {
632             return scale >= 0.01;
633         }
634     };
635 
636 }