Renderable2d.java
package nl.tudelft.simulation.dsol.animation.d2;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.ImageObserver;
import java.rmi.RemoteException;
import java.util.concurrent.atomic.AtomicInteger;
import javax.naming.NamingException;
import org.djutils.draw.bounds.Bounds2d;
import org.djutils.draw.point.Point;
import org.djutils.draw.point.Point2d;
import org.djutils.logger.CategoryLogger;
import nl.tudelft.simulation.dsol.animation.Locatable;
import nl.tudelft.simulation.language.d2.Shape2d;
import nl.tudelft.simulation.naming.context.Contextualized;
import nl.tudelft.simulation.naming.context.util.ContextUtil;
/**
* The Renderable2d provides an easy accessible renderable object that can be drawn on an absolute or relative position, scaled,
* flipped, and rotated. For scaling, several options exist:<br>
* - scale: whether to scale the drawing at all; e.g. for a legend, absolute coordinates might be used (scale = false);<br>
* - scaleY: whether to scale differently in X and Y direction, e.g. for a map at higher latitudes (scaleY = true);<br>
* - scaleObject: whether to scale the drawing larger or smaller than the scale factor of the extent (e.g., to draw an object on
* a map where the units of the object are in meters, while the map is in lat / lon degrees).<br>
* The default values are: translate = true; scale = true; flip = false; rotate = true; scaleY = false; scaleObject = false.
* <p>
* Copyright (c) 2002-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
* for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>. The DSOL
* project is distributed under a three-clause BSD-style license, which can be found at
* <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">
* https://https://simulation.tudelft.nl/dsol/docs/latest/license.html</a>.
* </p>
* @author <a href="https://www.linkedin.com/in/peterhmjacobs">Peter Jacobs </a>
* @param <L> the Locatable class of the source that can return the location of the Renderable on the screen
*/
public abstract class Renderable2d<L extends Locatable> implements Renderable2dInterface<L>
{
/** */
private static final long serialVersionUID = 20200108L;
/**
* Storage of the boolean flags, to prevent each flag from taking 32 bits... The initial value is binary 1011 = 0B: rotate =
* true, flip = false, scale = true, translate = true, scaleY = false; scaleObject = false.
*/
private byte flags = 0x0B;
/** whether to scale the X/Y-value with the value of RenderableScale.OnjectScaleFactor. Flag is 00100000 */
private static final byte SCALE_OBJECT_FLAG = 0x20;
/** whether to scale the Y-value in case of a compressed Y-axis. Flag is 00010000 */
private static final byte SCALE_Y_FLAG = 0x10;
/** whether to rotate the renderable. Flag is 1000 */
private static final byte ROTATE_FLAG = 0x08;
/** whether to flip the renderable after rotating 180 degrees. Flag is 0100 */
private static final byte FLIP_FLAG = 0x04;
/** whether to scale the renderable when zooming in or out. Flag is 0010 */
private static final byte SCALE_FLAG = 0x02;
/** whether to translate the renderable when panning. Flag is 0001 */
private static final byte TRANSLATE_FLAG = 0x01;
/** the source of the renderable. */
private L source;
/** the object number counter for a unique id. */
private static AtomicInteger animationObjectCounter = new AtomicInteger(0);
/** the unique id of this animation object. */
private int id;
/**
* Constructs a new Renderable2d.
* @param source T; the source
* @param contextProvider Contextualized; the object that can provide the context to store the animation objects
*/
public Renderable2d(final L source, final Contextualized contextProvider)
{
this.source = source;
this.bind2Context(contextProvider);
}
/**
* Bind a renderable2D to the context. The reason for specifying this in an independent method instead of adding the code in
* the constructor is related to the RFE submitted by van Houten that in specific distributed context, such binding must be
* overwritten.
* @param contextProvider Contextualized; the object that can provide the context to store the animation objects
*/
public void bind2Context(final Contextualized contextProvider)
{
try
{
this.id = animationObjectCounter.incrementAndGet();
ContextUtil.lookupOrCreateSubContext(contextProvider.getContext(), "animation/2D")
.bind(Integer.toString(this.id), this);
}
catch (NamingException | RemoteException exception)
{
CategoryLogger.always().warn(exception);
}
}
/**
* Return whether to flip the renderable, if the direction is 'left' or not.
* @return boolean; whether to flip the renderable, if the direction is 'left' or not
*/
public boolean isFlip()
{
return (this.flags & FLIP_FLAG) != 0;
}
/**
* Set whether to flip the renderable, if the direction is 'left' or not.
* @param flip boolean; whether to flip the renderable, if the direction is 'left' or not
*/
@SuppressWarnings("checkstyle:needbraces")
public void setFlip(final boolean flip)
{
if (flip)
this.flags |= FLIP_FLAG;
else
this.flags &= (~FLIP_FLAG);
}
/**
* Return whether to rotate the renderable or not.
* @return boolean; whether to rotate the renderable or not
*/
public boolean isRotate()
{
return (this.flags & ROTATE_FLAG) != 0;
}
/**
* Set whether to rotate the renderable or not.
* @param rotate boolean; whether to rotate the renderable or not
*/
@SuppressWarnings("checkstyle:needbraces")
public void setRotate(final boolean rotate)
{
if (rotate)
this.flags |= ROTATE_FLAG;
else
this.flags &= (~ROTATE_FLAG);
}
/**
* Return whether to scale the renderable or not.
* @return boolean; whether to scale the renderable or not
*/
public boolean isScale()
{
return (this.flags & SCALE_FLAG) != 0;
}
/**
* Set whether to scale the renderable or not.
* @param scale boolean; whether to scale the renderable or not
*/
@SuppressWarnings("checkstyle:needbraces")
public void setScale(final boolean scale)
{
if (scale)
this.flags |= SCALE_FLAG;
else
this.flags &= (~SCALE_FLAG);
}
/**
* Return whether to scale the renderable in the Y-direction when there is a compressed Y-axis or not.
* @return boolean; whether to scale the renderable in the Y-direction when there is a compressed Y-axis or not
*/
public boolean isScaleY()
{
return (this.flags & SCALE_Y_FLAG) != 0;
}
/**
* Set whether to scale the renderable in the X/Y-direction with the value of RenderableScale.objectScaleFactor or not.
* @param scaleY boolean; whether to scale the renderable in the X/Y-direction with the value of
* RenderableScale.objectScaleFactor or not
*/
@SuppressWarnings("checkstyle:needbraces")
public void setScaleObject(final boolean scaleY)
{
if (scaleY)
this.flags |= SCALE_OBJECT_FLAG;
else
this.flags &= (~SCALE_OBJECT_FLAG);
}
/**
* Return whether to scale the renderable in the X/Y-direction with the value of RenderableScale.objectScaleFactor or not.
* @return boolean; whether to scale the renderable in the X/Y-direction with the value of RenderableScale.objectScaleFactor
* or not
*/
public boolean isScaleObject()
{
return (this.flags & SCALE_OBJECT_FLAG) != 0;
}
/**
* Set whether to scale the renderable in the Y-direction when there is a compressed Y-axis or not.
* @param scaleY boolean; whether to scale the renderable in the Y-direction when there is a compressed Y-axis or not
*/
@SuppressWarnings("checkstyle:needbraces")
public void setScaleY(final boolean scaleY)
{
if (scaleY)
this.flags |= SCALE_Y_FLAG;
else
this.flags &= (~SCALE_Y_FLAG);
}
/**
* Return whether to translate the renderable to its position or not (false means absolute position).
* @return boolean; whether to translate the renderable to its position or not (false means absolute position)
*/
public boolean isTranslate()
{
return (this.flags & TRANSLATE_FLAG) != 0;
}
/**
* Set whether to translate the renderable to its position or not (false means absolute position).
* @param translate boolean; whether to translate the renderable to its position or not (false means absolute position)
*/
@SuppressWarnings("checkstyle:needbraces")
public void setTranslate(final boolean translate)
{
if (translate)
this.flags |= TRANSLATE_FLAG;
else
this.flags &= (~TRANSLATE_FLAG);
}
/** {@inheritDoc} */
@Override
public L getSource()
{
return this.source;
}
/** {@inheritDoc} */
@Override
public void paintComponent(final Graphics2D graphics, final Bounds2d extent, final Dimension screenSize,
final RenderableScale renderableScale, final ImageObserver observer)
{
// by default: delegate to the paint() method.
paint(graphics, extent, screenSize, renderableScale, observer);
}
/**
* The methods that actually paints the object at the right scale, rotation, and position on the screen using the
* user-implemented <code>paint(graphics, observer)</code> method to do the actual work.
* @param graphics Graphics2D; the graphics object
* @param extent Bounds2d; the extent of the panel
* @param screenSize Dimension; the screen of the panel
* @param renderableScale RenderableScale; the scale to use (usually RenderableScaleDefault where X/Y ratio is 1)
* @param observer ImageObserver; the observer of the renderableInterface
*/
protected synchronized void paint(final Graphics2D graphics, final Bounds2d extent, final Dimension screenSize,
final RenderableScale renderableScale, final ImageObserver observer)
{
if (this.source == null)
{
return;
}
// save the transform -- clone because transform is a volatile object
AffineTransform transform = (AffineTransform) graphics.getTransform().clone();
try
{
Point<?> center = this.source.getLocation();
if (center == null)
{
return;
}
Bounds2d rectangle = BoundsUtil.projectBounds(center, this.source.getBounds());
if (rectangle == null || (!Shape2d.overlaps(extent, rectangle) && isTranslate()))
{
return;
}
// Let's transform
if (isTranslate())
{
Point2D screenCoordinates = renderableScale.getScreenCoordinates(center, extent, screenSize);
graphics.translate(screenCoordinates.getX(), screenCoordinates.getY());
}
if (isScale())
{
double objectScaleFactor = isScaleObject() ? renderableScale.getObjectScaleFactor() : 1.0;
if (isScaleY())
{
graphics.scale(objectScaleFactor / renderableScale.getXScale(extent, screenSize),
objectScaleFactor / renderableScale.getYScale(extent, screenSize));
}
else
{
graphics.scale(objectScaleFactor / renderableScale.getXScale(extent, screenSize), objectScaleFactor
* renderableScale.getYScaleRatio() / renderableScale.getYScale(extent, screenSize));
}
}
double angle = -this.source.getDirZ();
if (angle != 0.0)
{
if (isFlip() && angle < -Math.PI)
{
angle = angle + Math.PI;
}
if (isRotate())
{
graphics.rotate(angle);
}
}
// Now we paint
this.paint(graphics, observer);
}
catch (Exception exception)
{
CategoryLogger.always().warn(exception, "paint");
}
finally
{
// Let's untransform
graphics.setTransform(transform);
}
}
/** {@inheritDoc} */
@Override
public boolean contains(final Point2d pointWorldCoordinates, final Bounds2d extent)
{
try
{
if (pointWorldCoordinates == null || this.source == null || this.source.getLocation() == null)
{
return false;
}
Bounds2d intersect = BoundsUtil.projectBounds(this.source.getLocation(), this.source.getBounds());
return intersect.contains(pointWorldCoordinates);
}
catch (RemoteException exception)
{
CategoryLogger.always().warn(exception, "contains");
return false;
}
}
/** {@inheritDoc} */
@Override
public void destroy(final Contextualized contextProvider)
{
try
{
ContextUtil.lookupOrCreateSubContext(contextProvider.getContext(), "animation/2D")
.unbind(Integer.toString(this.id));
this.source = null; // to indicate the animation is destroyed. Remove pointer to source for GC.
}
catch (NamingException | RemoteException exception)
{
CategoryLogger.always().warn(exception);
}
}
/** {@inheritDoc} */
@Override
public long getId()
{
return this.id;
}
/** {@inheritDoc} */
@Override
public String toString()
{
return "Renderable2d [source=" + this.source + "]";
}
/**
* Draws an animation on a world coordinate around [x,y] = [0,0].
* @param graphics Graphics2D; the graphics object
* @param observer ImageObserver; the observer
*/
public abstract void paint(Graphics2D graphics, ImageObserver observer);
}