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