PriorityAnimation.java
package org.opentrafficsim.draw.road;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.font.TextAttribute;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.ImageObserver;
import java.rmi.RemoteException;
import java.util.Map;
import org.opentrafficsim.draw.ClickableLocatable;
import org.opentrafficsim.draw.DrawLevel;
import org.opentrafficsim.draw.OtsRenderable;
import org.opentrafficsim.draw.road.PriorityAnimation.PriorityData;
import nl.tudelft.simulation.naming.context.Contextualized;
/**
* Animation of conflict priority (which is a link property).
* <p>
* Copyright (c) 2024-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
* </p>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
*/
public class PriorityAnimation extends OtsRenderable<PriorityData>
{
/** */
private static final long serialVersionUID = 20240228L;
/** Shadow. */
private static final Color SHADOW = new Color(0, 0, 0, 128);
/** Shadow x translation. */
private static final double SHADOW_DX = 0.1;
/** Shadow y translation. */
private static final double SHADOW_DY = 0.05;
/**
* Constructor.
* @param source source.
* @param contextProvider contextualized.
*/
public PriorityAnimation(final PriorityData source, final Contextualized contextProvider)
{
super(source, contextProvider);
}
@Override
public boolean isRotate()
{
return false;
}
@Override
public void paint(final Graphics2D graphics, final ImageObserver observer)
{
if (getSource().isNone())
{
return;
}
setRendering(graphics);
if (getSource().isAllStop() || getSource().isStop())
{
paintOctagon(graphics, 1.0, SHADOW, true, true);
paintOctagon(graphics, 1.0, Color.WHITE, true, false);
paintOctagon(graphics, 1.0, Color.BLACK, false, false);
paintOctagon(graphics, 0.868, new Color(230, 0, 0), true, false);
paintString(graphics, "STOP", Color.WHITE, 0.9f, getSource().isAllStop() ? -0.1f : 0.0f);
if (getSource().isAllStop())
{
paintString(graphics, "ALL WAY", Color.WHITE, 0.4f, 0.45f);
}
}
else if (getSource().isBusStop())
{
graphics.setColor(SHADOW);
graphics.fill(new Ellipse2D.Double(-1.0 + SHADOW_DX, -1.0 + SHADOW_DY, 2.0, 2.0));
Color blue = new Color(20, 94, 169);
graphics.setColor(blue);
graphics.fill(new Ellipse2D.Double(-1.0, -1.0, 2.0, 2.0));
graphics.setColor(Color.WHITE);
graphics.setStroke(new BasicStroke(0.04f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
graphics.draw(new Ellipse2D.Double(-0.94, -0.94, 1.88, 1.88));
paintBus(graphics, blue);
}
else if (getSource().isPriority())
{
paintDiamond(graphics, 1.0, SHADOW, true, true);
paintDiamond(graphics, 1.0, Color.WHITE, true, false);
paintDiamond(graphics, 11.0 / 12.0, Color.BLACK, false, false);
paintDiamond(graphics, 11.0 / 18.0, new Color(255, 204, 0), true, false);
}
else if (getSource().isYield())
{
paintTriangle(graphics, 1.0, SHADOW, true, true);
paintTriangle(graphics, 1.0, new Color(230, 0, 0), true, false);
paintTriangle(graphics, 0.9, Color.WHITE, false, false);
paintTriangle(graphics, 0.55, Color.WHITE, true, false);
}
resetRendering(graphics);
}
/**
* Paint octagon.
* @param graphics graphics.
* @param radius radius (half width).
* @param color color.
* @param fill fill (or draw line).
* @param shadow whether this is shadow.
*/
private void paintOctagon(final Graphics2D graphics, final double radius, final Color color, final boolean fill,
final boolean shadow)
{
double k = Math.tan(Math.PI / 8.0) * radius;
double dx = shadow ? SHADOW_DX : 0.0;
double dy = shadow ? SHADOW_DY : 0.0;
Path2D.Float path = new Path2D.Float();
path.moveTo(dx + radius, dy);
path.lineTo(dx + radius, dy + k);
path.lineTo(dx + k, dy + radius);
path.lineTo(dx - k, dy + radius);
path.lineTo(dx - radius, dy + k);
path.lineTo(dx - radius, dy - k);
path.lineTo(dx - k, dy - radius);
path.lineTo(dx + k, dy - radius);
path.lineTo(dx + radius, dy - k);
path.lineTo(dx + radius, dy);
graphics.setColor(color);
if (fill)
{
graphics.fill(path);
}
else
{
graphics.setStroke(new BasicStroke(0.02f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER));
graphics.draw(path);
}
}
/**
* Paints a bus.
* @param graphics graphics.
* @param blue color used for blue background.
*/
private void paintBus(final Graphics2D graphics, final Color blue)
{
// bus
Path2D.Double path = new Path2D.Double();
path.moveTo(0.77, -0.07);
path.lineTo(0.74, -0.36);
path.lineTo(-0.69, -0.36);
path.lineTo(-0.77, -0.07);
path.lineTo(-0.77, 0.22);
path.lineTo(0.43, 0.22);
path.lineTo(0.77, 0.17);
path.lineTo(0.77, -0.07);
graphics.fill(path);
// wheels
graphics.fill(new Ellipse2D.Double(-0.43 - 0.125, 0.22 - 0.125, 0.25, 0.25));
graphics.fill(new Ellipse2D.Double(0.43 - 0.125, 0.22 - 0.125, 0.25, 0.25));
graphics.setColor(blue);
graphics.setStroke(new BasicStroke(0.015f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
graphics.draw(new Ellipse2D.Double(-0.43 - 0.125, 0.22 - 0.125, 0.25, 0.25));
graphics.draw(new Ellipse2D.Double(0.43 - 0.125, 0.22 - 0.125, 0.25, 0.25));
// windows
graphics.setColor(blue);
path = new Path2D.Double();
path.moveTo(-0.52, -0.32);
path.lineTo(-0.66, -0.32);
path.lineTo(-0.73, -0.07);
path.lineTo(-0.52, -0.07);
path.lineTo(-0.52, -0.32);
graphics.fill(path);
for (double x : new double[] {-0.48, -0.23, 0.02, 0.27})
{
graphics.fill(new Rectangle.Double(x, -0.32, 0.21, 0.21));
}
path = new Path2D.Double();
path.moveTo(0.71, -0.32);
path.lineTo(0.52, -0.32);
path.lineTo(0.52, -0.11);
path.lineTo(0.73, -0.11);
path.lineTo(0.71, -0.32);
graphics.fill(path);
}
/**
* Paint diamond.
* @param graphics graphics.
* @param radius radius (half width).
* @param color color.
* @param fill fill (or draw line).
* @param shadow whether this is shadow.
*/
private void paintDiamond(final Graphics2D graphics, final double radius, final Color color, final boolean fill,
final boolean shadow)
{
double dx = shadow ? SHADOW_DX : 0.0;
double dy = shadow ? SHADOW_DY : 0.0;
graphics.setColor(color);
if (fill)
{
Path2D.Float path = new Path2D.Float();
path.moveTo(dx + radius, dy);
path.lineTo(dx, dy + radius);
path.lineTo(dx - radius, dy);
path.lineTo(dx, dy - radius);
path.lineTo(dx + radius, dy);
graphics.fill(path);
}
else
{
// to assist rounded corners, we rotate by 1/8th circle and use RoundRectangle2D
graphics.rotate(Math.PI / 4);
graphics.setStroke(new BasicStroke(0.04f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER));
double r = radius / Math.sqrt(2.0); // diagonal vs. base
RoundRectangle2D.Double shape = new RoundRectangle2D.Double(-r, -r, 2.0 * r, 2.0 * r, 0.15 * r, 0.15 * r);
graphics.draw(shape);
graphics.rotate(-Math.PI / 4);
}
}
/**
* Paint triangle.
* @param graphics graphics.
* @param radius radius (half width).
* @param color color.
* @param fill fill (or draw line).
* @param shadow whether this is shadow.
*/
private void paintTriangle(final Graphics2D graphics, final double radius, final Color color, final boolean fill,
final boolean shadow)
{
double k = radius * Math.sqrt(3.0) / 3.0;
double g = (radius * Math.sqrt(3.0)) - k;
double dx = shadow ? SHADOW_DX : 0.0;
double dy = shadow ? SHADOW_DY : 0.0;
Path2D.Float path = new Path2D.Float();
path.moveTo(dx + 0.0, dy - k);
path.lineTo(dx + -radius, dy - k);
path.lineTo(dx + 0.0, dy + g);
path.lineTo(dx + radius, dy - k);
path.lineTo(dx + 0.0, dy - k);
graphics.setColor(color);
if (fill)
{
graphics.fill(path);
}
else
{
graphics.setStroke(new BasicStroke(0.04f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER));
graphics.draw(path);
}
}
/**
* Paint string.
* @param graphics graphics.
* @param text text.
* @param color color.
* @param fontSize font size.
* @param dy distance down from object location.
*/
private void paintString(final Graphics2D graphics, final String text, final Color color, final float fontSize,
final float dy)
{
if (graphics.getTransform().getDeterminant() > 400000)
{
// TODO
/*
* If we are very zoomed in, the font gets huge on screen. FontMetrics somehow uses this actual image size in Java
* 11, and this gives a bug for fonts above a certain size. Dimensions become 0, and this does not recover after we
* zoom out again. The text never shows anymore. A later java version may not require skipping painting the font.
* See more at: https://bugs.openjdk.org/browse/JDK-8233097
*/
return;
}
graphics.setColor(color);
int fontSizeMetrics = 100;
float factor = fontSize / fontSizeMetrics;
Font font = new Font("Arial", Font.BOLD, fontSizeMetrics).deriveFont(Map.of(TextAttribute.WIDTH, 0.67f));
graphics.setFont(font.deriveFont(fontSize));
FontMetrics metrics = graphics.getFontMetrics(font);
float w = metrics.stringWidth(text) * factor;
float d = metrics.getDescent() * factor;
float h = metrics.getHeight() * factor;
graphics.drawString(text, -w / 2.0f, dy + h / 2.0f - d);
}
/**
* Data for priority animation.
* <p>
* Copyright (c) 2024-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* <br>
* BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
* </p>
* @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
* @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
*/
public interface PriorityData extends ClickableLocatable
{
@Override
default double getZ() throws RemoteException
{
return DrawLevel.NODE.getZ();
}
/**
* Returns whether the priority is all stop.
* @return whether the priority is all stop.
*/
boolean isAllStop();
/**
* Returns whether the priority is bus stop.
* @return whether the priority is bus stop.
*/
boolean isBusStop();
/**
* Returns whether the priority is none.
* @return whether the priority is none.
*/
boolean isNone();
/**
* Returns whether the priority is priority.
* @return whether the priority is priority.
*/
boolean isPriority();
/**
* Returns whether the priority is stop.
* @return whether the priority is stop.
*/
boolean isStop();
/**
* Returns whether the priority is yield.
* @return whether the priority is yield.
*/
boolean isYield();
}
}