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 }