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