View Javadoc
1   package org.opentrafficsim.draw;
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-2024 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 <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
35   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
36   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
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             if (null != this.background && isSimilar(useColor, this.background.getBackgroundColor()))
271             {
272                 // Construct an alternative color
273                 if (Color.BLACK.equals(useColor))
274                 {
275                     useColor = Color.WHITE;
276                 }
277                 else
278                 {
279                     useColor = Color.BLACK;
280                 }
281             }
282 
283             float dxText =
284                     this.textAlignment.equals(TextAlignment.LEFT) ? 0.0f : this.textAlignment.equals(TextAlignment.CENTER)
285                             ? (float) -scaledFontRectangle.getWidth() / 2.0f : (float) -scaledFontRectangle.getWidth();
286             Object antialias = graphics.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
287             graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
288             if (null != this.background)
289             {
290                 // Draw transparent rectangle with background color to makes sure all of the text is visible, even when it is
291                 // drawn outside of the bounds of the object that supplies the background color, or on parts of the object that
292                 // have a different color (e.g. driver dot, brake lights, etc.).
293                 double r = scaledFontRectangle.getHeight() / 2.0; // rounding
294                 double dh = scaledFontRectangle.getHeight() / 5.0; // baseline shift
295                 Shape s = new RoundRectangle2D.Double(this.dx - scaledFontRectangle.getWidth() - dxText,
296                         this.dy + dh - scaledFontRectangle.getHeight(), scaledFontRectangle.getWidth(),
297                         scaledFontRectangle.getHeight(), r, r);
298                 Color bg = this.background.getBackgroundColor();
299                 graphics.setColor(new Color(bg.getRed(), bg.getGreen(), bg.getBlue(), 92));
300                 graphics.fill(s);
301             }
302             graphics.setColor(useColor);
303             graphics.drawString(str, dxText + this.dx, -this.dy);
304 
305             graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialias);
306         }
307     }
308 
309     /**
310      * Returns whether two colors are similar.
311      * @param color1 color 1.
312      * @param color2 color 2.
313      * @return whether two colors are similar.
314      */
315     private boolean isSimilar(final Color color1, final Color color2)
316     {
317         int r = color1.getRed() - color2.getRed();
318         int g = color1.getGreen() - color2.getGreen();
319         int b = color1.getBlue() - color2.getBlue();
320         return r * r + g * g + b * b < 2000;
321         // this threshold may need to be tweaked, it used to be color.equals(color) which is too narrow
322     }
323 
324     /**
325      * Destroy the text animation.
326      * @param contextProvider the object with a Context
327      */
328     public final void destroy(final Contextualized contextProvider)
329     {
330         this.animationImpl.destroy(contextProvider);
331     }
332 
333     /**
334      * Retrieve dx.
335      * @return the value of dx
336      */
337     protected final float getDx()
338     {
339         return this.dx;
340     }
341 
342     /**
343      * Retrieve dy.
344      * @return the value of dy
345      */
346     protected final float getDy()
347     {
348         return this.dy;
349     }
350 
351     /**
352      * Sets a new offset.
353      * @param x dx
354      * @param y dy
355      */
356     protected final void setXY(final float x, final float y)
357     {
358         this.dx = x;
359         this.dy = y;
360     }
361 
362     @Override
363     public double getZ()
364     {
365         return DrawLevel.LABEL.getZ();
366     }
367 
368     /**
369      * Retrieve the text alignment.
370      * @return the text alignment
371      */
372     protected TextAlignment getTextAlignment()
373     {
374         return this.textAlignment;
375     }
376 
377     /**
378      * Retrieve the font size.
379      * @return the font size
380      */
381     protected float getFontSize()
382     {
383         return this.fontSize;
384     }
385 
386     /**
387      * Retrieve the font.
388      * @return the font
389      */
390     protected Font getFont()
391     {
392         return this.font;
393     }
394 
395     /**
396      * Retrieve the current text.
397      * @return the current text
398      */
399     protected String getText()
400     {
401         return this.text.get();
402     }
403 
404     /**
405      * Update the text.
406      * @param text the new text
407      */
408     public void setText(final Supplier<String> text)
409     {
410         this.text = text;
411     }
412 
413     /**
414      * Retrieve the current color.
415      * @return the current color
416      */
417     protected Color getColor()
418     {
419         return this.color;
420     }
421 
422     /**
423      * Update the color.
424      * @param color the new color
425      */
426     protected void setColor(final Color color)
427     {
428         this.color = color;
429     }
430 
431     /**
432      * Retrieve the current flip status.
433      * @return the current flip status
434      */
435     public boolean isFlip()
436     {
437         return this.animationImpl.isFlip();
438     }
439 
440     /**
441      * Update the flip status.
442      * @param flip the new flip status
443      */
444     public void setFlip(final boolean flip)
445     {
446         this.animationImpl.setFlip(flip);
447     }
448 
449     /**
450      * Retrieve the current rotation status.
451      * @return the current rotation status
452      */
453     public boolean isRotate()
454     {
455         return this.animationImpl.isRotate();
456     }
457 
458     /**
459      * Update the rotation status.
460      * @param rotate the new rotation status
461      */
462     public void setRotate(final boolean rotate)
463     {
464         this.animationImpl.setRotate(rotate);
465 
466     }
467 
468     /**
469      * Retrieve the current scale status.
470      * @return the current scale status
471      */
472     public boolean isScale()
473     {
474         return this.animationImpl.isScale();
475     }
476 
477     /**
478      * Update the scale status.
479      * @param scale the new scale status
480      */
481     public void setScale(final boolean scale)
482     {
483         this.animationImpl.setScale(scale);
484     }
485 
486     /**
487      * Retrieve the current translate status.
488      * @return the current translate status
489      */
490     public boolean isTranslate()
491     {
492         return this.animationImpl.isTranslate();
493     }
494 
495     /**
496      * Update the translate status.
497      * @param translate the new translate status
498      */
499     public void setTranslate(final boolean translate)
500     {
501         this.animationImpl.setTranslate(translate);
502     }
503 
504     /**
505      * Return whether to scale the renderable in the Y-direction when there is a compressed Y-axis or not.
506      * @return whether to scale the renderable in the Y-direction when there is a compressed Y-axis or not
507      */
508     public boolean isScaleY()
509     {
510         return this.animationImpl.isScaleY();
511     }
512 
513     /**
514      * Set whether to scale the renderable in the Y-direction when there is a compressed Y-axis or not.
515      * @param scaleY whether to scale the renderable in the Y-direction when there is a compressed Y-axis or not
516      */
517     public void setScaleY(final boolean scaleY)
518     {
519         this.animationImpl.setScaleY(scaleY);
520     }
521 
522     /**
523      * Return whether to scale the renderable in the X/Y-direction with the value of RenderableScale.objectScaleFactor or not.
524      * @return whether to scale the renderable in the X/Y-direction with the value of RenderableScale.objectScaleFactor or not
525      */
526     public boolean isScaleObject()
527     {
528         return this.animationImpl.isScaleObject();
529     }
530 
531     /**
532      * Set whether to scale the renderable in the X/Y-direction with the value of RenderableScale.objectScaleFactor or not.
533      * @param scaleObject whether to scale the renderable in the X/Y-direction with the value of
534      *            RenderableScale.objectScaleFactor or not
535      */
536     public void setScaleObject(final boolean scaleObject)
537     {
538         this.animationImpl.setScaleObject(scaleObject);
539     }
540 
541     /**
542      * Renderable text.
543      * <p>
544      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
545      * <br>
546      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
547      * </p>
548      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
549      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
550      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
551      */
552     private static class RenderableText extends Renderable2d<RenderableTextSource<?, ?>>
553     {
554         /**
555          * Construct a new AnimationImpl.
556          * @param source the source
557          * @param contextualized context provider.
558          */
559         RenderableText(final RenderableTextSource<?, ?> source, final Contextualized contextualized)
560         {
561             super(source, contextualized);
562         }
563 
564         @Override
565         public final void paint(final Graphics2D graphics, final ImageObserver observer)
566         {
567             getSource().paint(graphics, observer);
568         }
569 
570         @Override
571         public boolean contains(final Point2D pointScreenCoordinates, final Bounds2d extent, final Dimension screenSize,
572                 final RenderableScale scale, final double worldMargin, final double pixelMargin)
573         {
574             return false;
575         }
576 
577         @Override
578         public final String toString()
579         {
580             return "RenderableText []";
581         }
582     }
583 
584     /**
585      * Retrieve the scale dependent rendering qualifier (used in cloning).
586      * @return the rendering qualifier of this TextAnimation
587      */
588     protected ScaleDependentRendering getScaleDependentRendering()
589     {
590         return this.scaleDependentRendering;
591     }
592 
593     /**
594      * Interface to obtain the color of the background.
595      */
596     public interface ContrastToBackground
597     {
598         /**
599          * Retrieve the color of the background.
600          * @return the (current) color of the background
601          */
602         Color getBackgroundColor();
603     }
604 
605     /**
606      * Determine if a Feature object should be rendered.
607      */
608     public interface ScaleDependentRendering
609     {
610         /**
611          * Determine if a Text should be rendered, depending on the scale.
612          * @param scale the current font scale
613          * @return true if the text should be rendered at the scale; false if the text should not be rendered at the scale
614          */
615         boolean isRendered(double scale);
616     }
617 
618     /** Always render the Text. */
619     public static final ScaleDependentRendering RENDERALWAYS = new ScaleDependentRendering()
620     {
621         @Override
622         public boolean isRendered(final double scale)
623         {
624             return true;
625         }
626     };
627 
628     /** Don't render texts when smaller than 1. */
629     public static final ScaleDependentRendering RENDERWHEN1 = new ScaleDependentRendering()
630     {
631         @Override
632         public boolean isRendered(final double scale)
633         {
634             return scale >= 1.0;
635         }
636     };
637 
638     /** Don't render texts when smaller than 2. */
639     public static final ScaleDependentRendering RENDERWHEN10 = new ScaleDependentRendering()
640     {
641         @Override
642         public boolean isRendered(final double scale)
643         {
644             return scale >= 0.1;
645         }
646     };
647 
648     /** Don't render texts when smaller than 2. */
649     public static final ScaleDependentRendering RENDERWHEN100 = new ScaleDependentRendering()
650     {
651         @Override
652         public boolean isRendered(final double scale)
653         {
654             return scale >= 0.01;
655         }
656     };
657 
658 }