View Javadoc
1   package org.opentrafficsim.draw.core;
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.geom.Rectangle2D;
8   import java.awt.image.ImageObserver;
9   import java.io.Serializable;
10  import java.rmi.RemoteException;
11  
12  import javax.naming.NamingException;
13  
14  import org.djutils.draw.Oriented;
15  import org.djutils.draw.bounds.Bounds2d;
16  import org.djutils.draw.point.Point;
17  import org.djutils.draw.point.Point2d;
18  import org.djutils.logger.CategoryLogger;
19  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
20  import org.opentrafficsim.core.geometry.Bounds;
21  import org.opentrafficsim.core.geometry.DirectedPoint;
22  
23  import nl.tudelft.simulation.dsol.animation.Locatable;
24  import nl.tudelft.simulation.dsol.animation.D2.Renderable2D;
25  import nl.tudelft.simulation.naming.context.Contextualized;
26  
27  /**
28   * Display a text for another Locatable object.
29   * <p>
30   * Copyright (c) 2013-2023 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://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
35   * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
36   */
37  public abstract class TextAnimation implements Locatable, Serializable
38  {
39      /** */
40      private static final long serialVersionUID = 20161211L;
41  
42      /** The object for which the text is displayed. */
43      private final Locatable source;
44  
45      /** The text to display. */
46      private 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 AnimationImpl animationImpl;
71  
72      /** The font. */
73      private Font font;
74  
75      /** Access to the current background color. */
76      private final ContrastToBackground background;
77  
78      /** The font rectangle. */
79      private Rectangle2D fontRectangle = null;
80  
81      /** Render dependent on font scale. */
82      private final ScaleDependentRendering scaleDependentRendering;
83  
84      /**
85       * Construct a new TextAnimation.
86       * @param source Locatable; the object for which the text is displayed
87       * @param text String; the text to display
88       * @param dx float; the horizontal movement of the text, in meters
89       * @param dy float; the vertical movement of the text, in meters
90       * @param textAlignment TextAlignment; where to place the text
91       * @param color Color; the color of the text
92       * @param fontSize float; the size of the font; default = 2.0 (meters)
93       * @param minFontSize float; minimum font size resulting from scaling
94       * @param maxFontSize float; maximum font size resulting from scaling
95       * @param simulator OtsSimulatorInterface; the simulator
96       * @param background ContrastToBackground; allows querying the background color and adaptation of the actual color of the
97       *            text to ensure contrast
98       * @param scaleDependentRendering ScaleDependentRendering; suppress rendering when font scale is too small
99       * @throws NamingException when animation context cannot be created or retrieved
100      * @throws RemoteException when remote context cannot be found
101      */
102     @SuppressWarnings("checkstyle:parameternumber")
103     public TextAnimation(final Locatable source, final 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 OtsSimulatorInterface simulator, final ContrastToBackground background,
106             final ScaleDependentRendering scaleDependentRendering) throws RemoteException, NamingException
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 AnimationImpl(this, simulator);
127     }
128 
129     /**
130      * Construct a new TextAnimation without contrast to background protection and no minimum font scale.
131      * @param source Locatable; the object for which the text is displayed
132      * @param text String; the text to display
133      * @param dx float; the horizontal movement of the text, in meters
134      * @param dy float; the vertical movement of the text, in meters
135      * @param textAlignment TextAlignment; where to place the text
136      * @param color Color; the color of the text
137      * @param fontSize float; the size of the font; default = 2.0 (meters)
138      * @param minFontSize float; minimum font size resulting from scaling
139      * @param maxFontSize float; maximum font size resulting from scaling
140      * @param simulator OtsSimulatorInterface; the simulator
141      * @param scaleDependentRendering ScaleDependentRendering; render text only when bigger than minimum scale
142      * @throws NamingException when animation context cannot be created or retrieved
143      * @throws RemoteException when remote context cannot be found
144      */
145     @SuppressWarnings("checkstyle:parameternumber")
146     public TextAnimation(final Locatable source, final 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 OtsSimulatorInterface simulator,
149             final ScaleDependentRendering scaleDependentRendering) throws RemoteException, NamingException
150     {
151         this(source, text, dx, dy, textAlignment, color, fontSize, minFontSize, maxFontSize, simulator, null,
152                 scaleDependentRendering);
153     }
154 
155     /**
156      * @param source Locatable; the object for which the text is displayed
157      * @param text String; the text to display
158      * @param dx float; the horizontal movement of the text, in meters
159      * @param dy float; the vertical movement of the text, in meters
160      * @param textAlignment TextAlignment; where to place the text
161      * @param color Color; the color of the text
162      * @param simulator OtsSimulatorInterface; the simulator
163      * @param scaleDependentRendering ScaleDependentRendering; render text only when bigger than minimum scale
164      * @throws NamingException when animation context cannot be created or retrieved
165      * @throws RemoteException when remote context cannot be found
166      */
167     public TextAnimation(final Locatable source, final String text, final float dx, final float dy,
168             final TextAlignment textAlignment, final Color color, final OtsSimulatorInterface simulator,
169             final ScaleDependentRendering scaleDependentRendering) throws RemoteException, NamingException
170     {
171         this(source, text, dx, dy, textAlignment, color, 2.0f, 12.0f, 50f, simulator, scaleDependentRendering);
172     }
173 
174     /** {@inheritDoc} */
175     @Override
176     public DirectedPoint getLocation()
177     {
178         // draw always on top.
179         try
180         {
181             Point<?> p = this.source.getLocation();
182             return new DirectedPoint(p.getX(), p.getY(), Double.MAX_VALUE, 0.0, 0.0,
183                     p instanceof Oriented ? ((Oriented<?>) p).getDirZ() : 0.0);
184         }
185         catch (RemoteException exception)
186         {
187             CategoryLogger.always().warn(exception);
188             return new DirectedPoint(0, 0, 0);
189         }
190     }
191 
192     /** {@inheritDoc} */
193     @Override
194     public final Bounds getBounds() throws RemoteException
195     {
196         return new Bounds(0.0, 0.0, 0.0);
197     }
198 
199     /**
200      * paint() method so it can be overridden or extended.
201      * @param graphics Graphics2D; the graphics object
202      * @param observer ImageObserver; the observer
203      */
204     @SuppressWarnings("checkstyle:designforextension")
205     public void paint(final Graphics2D graphics, final ImageObserver observer)
206     {
207         double scale = Math.sqrt(graphics.getTransform().getDeterminant());
208         Rectangle2D scaledFontRectangle;
209         synchronized (this.font)
210         {
211             if (!this.scaleDependentRendering.isRendered(scale))
212             {
213                 return;
214             }
215             if (scale < this.minFontSize / this.fontSize)
216             {
217                 graphics.setFont(this.font.deriveFont((float) (this.minFontSize / scale)));
218                 FontMetrics fm = graphics.getFontMetrics();
219                 scaledFontRectangle = fm.getStringBounds(this.text, graphics);
220             }
221             else if (scale > this.maxFontSize / this.fontSize)
222             {
223                 graphics.setFont(this.font.deriveFont((float) (this.maxFontSize / scale)));
224                 FontMetrics fm = graphics.getFontMetrics();
225                 scaledFontRectangle = fm.getStringBounds(this.text, graphics);
226             }
227             else
228             {
229                 graphics.setFont(this.font);
230                 if (this.fontRectangle == null)
231                 {
232                     FontMetrics fm = graphics.getFontMetrics();
233                     this.fontRectangle = fm.getStringBounds(this.text, graphics);
234                 }
235                 scaledFontRectangle = this.fontRectangle;
236             }
237             Color useColor = this.color;
238             if (null != this.background && useColor.equals(this.background.getBackgroundColor()))
239             {
240                 // Construct an alternative color
241                 if (Color.BLACK.equals(useColor))
242                 {
243                     useColor = Color.WHITE;
244                 }
245                 else
246                 {
247                     useColor = Color.BLACK;
248                 }
249             }
250             graphics.setColor(useColor);
251             float dxText =
252                     this.textAlignment.equals(TextAlignment.LEFT) ? 0.0f : this.textAlignment.equals(TextAlignment.CENTER)
253                             ? (float) -scaledFontRectangle.getWidth() / 2.0f : (float) -scaledFontRectangle.getWidth();
254             graphics.drawString(this.text, dxText + this.dx, -this.dy);
255         }
256     }
257 
258     /**
259      * Destroy the text animation.
260      * @param contextProvider Contextualized; the object with a Context
261      */
262     public final void destroy(final Contextualized contextProvider)
263     {
264         this.animationImpl.destroy(contextProvider);
265     }
266 
267     /**
268      * Retrieve the source.
269      * @return Locatable; the source
270      */
271     protected final Locatable getSource()
272     {
273         return this.source;
274     }
275 
276     /**
277      * Retrieve dx.
278      * @return float; the value of dx
279      */
280     protected final float getDx()
281     {
282         return this.dx;
283     }
284 
285     /**
286      * Retrieve dy.
287      * @return float; the value of dy
288      */
289     protected final float getDy()
290     {
291         return this.dy;
292     }
293 
294     /**
295      * Sets a new offset.
296      * @param x float; dx
297      * @param y float; dy
298      */
299     protected final void setXY(final float x, final float y)
300     {
301         this.dx = x;
302         this.dy = y;
303     }
304 
305     /**
306      * Retrieve the text alignment.
307      * @return TextAlignment; the text alignment
308      */
309     protected final TextAlignment getTextAlignment()
310     {
311         return this.textAlignment;
312     }
313 
314     /**
315      * Retrieve the font size.
316      * @return float; the font size
317      */
318     protected final float getFontSize()
319     {
320         return this.fontSize;
321     }
322 
323     /**
324      * Retrieve the font.
325      * @return Font; the font
326      */
327     protected final Font getFont()
328     {
329         return this.font;
330     }
331 
332     /**
333      * Retrieve the current text.
334      * @return String; the current text
335      */
336     protected final String getText()
337     {
338         return this.text;
339     }
340 
341     /**
342      * Update the text.
343      * @param text String; the new text
344      */
345     protected final void setText(final String text)
346     {
347         this.text = text;
348         synchronized (this.font)
349         {
350             this.fontRectangle = null;
351         }
352     }
353 
354     /**
355      * Retrieve the current color.
356      * @return Color; the current color
357      */
358     protected final Color getColor()
359     {
360         return this.color;
361     }
362 
363     /**
364      * Update the color.
365      * @param color Color; the new color
366      */
367     protected final void setColor(final Color color)
368     {
369         this.color = color;
370     }
371 
372     /**
373      * Retrieve the current flip status.
374      * @return boolean; the current flip status
375      */
376     public final boolean isFlip()
377     {
378         return this.animationImpl.isFlip();
379     }
380 
381     /**
382      * Update the flip status.
383      * @param flip boolean; the new flip status
384      */
385     public final void setFlip(final boolean flip)
386     {
387         this.animationImpl.setFlip(flip);
388     }
389 
390     /**
391      * Retrieve the current rotation status.
392      * @return boolean; the current rotation status
393      */
394     public final boolean isRotate()
395     {
396         return this.animationImpl.isRotate();
397     }
398 
399     /**
400      * Update the rotation status.
401      * @param rotate boolean; the new rotation status
402      */
403     public final void setRotate(final boolean rotate)
404     {
405         this.animationImpl.setRotate(rotate);
406 
407     }
408 
409     /**
410      * Retrieve the current scale status.
411      * @return boolean; the current scale status
412      */
413     public final boolean isScale()
414     {
415         return this.animationImpl.isScale();
416     }
417 
418     /**
419      * Update the scale status.
420      * @param scale boolean; the new scale status
421      */
422     public final void setScale(final boolean scale)
423     {
424         this.animationImpl.setScale(scale);
425     }
426 
427     /**
428      * Retrieve the current translate status.
429      * @return boolean; the current translate status
430      */
431     public final boolean isTranslate()
432     {
433         return this.animationImpl.isTranslate();
434     }
435 
436     /**
437      * Update the translate status.
438      * @param translate boolean; the new translate status
439      */
440     public final void setTranslate(final boolean translate)
441     {
442         this.animationImpl.setTranslate(translate);
443     }
444 
445     /**
446      * The implementation of the text animation. Cloning will be taken care of by the overarching TextAnimation-derived class.
447      * <p>
448      * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
449      * <br>
450      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
451      * </p>
452      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
453      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
454      * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
455      */
456     private static class AnimationImpl extends Renderable2D<Locatable> implements Serializable
457     {
458         /** */
459         private static final long serialVersionUID = 20170400L;
460 
461         /**
462          * Construct a new AnimationImpl.
463          * @param source Locatable; the source
464          * @param simulator OtsSimulatorInterface; the simulator
465          * @throws NamingException when animation context cannot be created or retrieved
466          * @throws RemoteException when remote context cannot be found
467          */
468         AnimationImpl(final Locatable source, final OtsSimulatorInterface simulator) throws NamingException, RemoteException
469         {
470             super(source, simulator);
471         }
472 
473         /** {@inheritDoc} */
474         @Override
475         public final void paint(final Graphics2D graphics, final ImageObserver observer)
476         {
477             TextAnimation ta = ((TextAnimation) getSource());
478             ta.paint(graphics, observer);
479         }
480 
481         /** {@inheritDoc} */
482         @Override
483         public boolean contains(final Point2d pointWorldCoordinates, final Bounds2d extent)
484         {
485             return false;
486         }
487 
488         /** {@inheritDoc} */
489         @Override
490         public final String toString()
491         {
492             return "TextAnimation.AnimationImpl []";
493         }
494 
495     }
496 
497     /**
498      * Retrieve the scale dependent rendering qualifier (used in cloning).
499      * @return ScaleDependentRendering; the rendering qualifier of this TextAnimation
500      */
501     protected ScaleDependentRendering getScaleDependentRendering()
502     {
503         return this.scaleDependentRendering;
504     }
505 
506     /**
507      * Interface to obtain the color of the background.
508      */
509     public interface ContrastToBackground
510     {
511         /**
512          * Retrieve the color of the background.
513          * @return Color; the (current) color of the background
514          */
515         Color getBackgroundColor();
516     }
517 
518     /**
519      * Determine if a Feature object should be rendered.
520      */
521     public interface ScaleDependentRendering
522     {
523         /**
524          * Determine if a Text should be rendered, depending on the scale.
525          * @param scale double; the current font scale
526          * @return boolean; true if the text should be rendered at the scale; false if the text should not be rendered at the
527          *         scale
528          */
529         boolean isRendered(double scale);
530     }
531 
532     /** Always render the Text. */
533     public static final ScaleDependentRendering RENDERALWAYS = new ScaleDependentRendering()
534     {
535 
536         @Override
537         public boolean isRendered(final double scale)
538         {
539             return true;
540         }
541     };
542 
543     /** Don't render texts when smaller than 1. */
544     public static final ScaleDependentRendering RENDERWHEN1 = new ScaleDependentRendering()
545     {
546 
547         @Override
548         public boolean isRendered(final double scale)
549         {
550             return scale >= 1.0;
551         }
552     };
553 
554     /** Don't render texts when smaller than 2. */
555     public static final ScaleDependentRendering RENDERWHEN10 = new ScaleDependentRendering()
556     {
557 
558         @Override
559         public boolean isRendered(final double scale)
560         {
561             return scale >= 0.1;
562         }
563     };
564 
565     /** Don't render texts when smaller than 2. */
566     public static final ScaleDependentRendering RENDERWHEN100 = new ScaleDependentRendering()
567     {
568 
569         @Override
570         public boolean isRendered(final double scale)
571         {
572             return scale >= 0.01;
573         }
574     };
575 
576 }