package traffic3.simulator;
import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.Insets;
import java.awt.Shape;
import java.awt.Point;
import java.awt.Stroke;
import java.awt.BasicStroke;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.Timer;
import javax.swing.SwingUtilities;
import javax.swing.Box;
import javax.swing.BorderFactory;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import traffic3.objects.TrafficArea;
import traffic3.objects.TrafficAgent;
import traffic3.objects.TrafficBlockade;
import traffic3.manager.TrafficManager;
import rescuecore2.misc.gui.ScreenTransform;
import rescuecore2.misc.gui.PanZoomListener;
import rescuecore2.misc.geometry.Line2D;
import rescuecore2.misc.geometry.Point2D;
import rescuecore2.log.Logger;
import rescuecore2.standard.entities.Edge;
/**
A GUI for watching the traffic simulator.
*/
public class TrafficSimulatorGUI extends JPanel {
private static final Color SELECTED_AREA_COLOUR = new Color(0, 0, 255, 128);
private static final Color AREA_OUTLINE_COLOUR = new Color(0, 0, 0);
private static final Color BLOCKADE_OUTLINE_COLOUR = new Color(128, 0, 0);
// private static final Color BLOCKADE_FILL_COLOUR = new Color(255, 0, 0, 128);
private static final Stroke PASSABLE_EDGE_STROKE = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
private static final Stroke IMPASSABLE_EDGE_STROKE = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
private static final Stroke SELECTED_AREA_OUTLINE_STROKE = new BasicStroke(3, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
private static final Stroke BLOCKADE_STROKE = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
private static final int PATH_NODE_SIZE = 5;
private static final int PATH_SPECIAL_NODE_SIZE = 9;
private static final int TICK_TIME_MS = 10;
private static final double FORCE_GUI_FACTOR = 1000;
private TrafficManager manager;
private volatile boolean waitOnRefresh;
private final Object lock = new Object();
private CountDownLatch latch;
private WorldView view;
private JButton cont;
private JCheckBox wait;
private JCheckBox animate;
private Timer timer;
private Box verboseBox;
/**
Construct a TrafficSimulatorGUI.
@param manager The traffic manager.
*/
public TrafficSimulatorGUI(TrafficManager manager) {
super(new BorderLayout());
this.manager = manager;
waitOnRefresh = false;
view = new WorldView();
cont = new JButton("Continue");
wait = new JCheckBox("Wait on refresh", waitOnRefresh);
animate = new JCheckBox("Animate", false);
verboseBox = Box.createVerticalBox();
verboseBox.setBorder(BorderFactory.createTitledBorder("Verbose agents"));
cont.setEnabled(false);
cont.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
synchronized (lock) {
if (latch != null) {
latch.countDown();
}
}
cont.setEnabled(false);
}
});
wait.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
waitOnRefresh = wait.isSelected();
}
});
animate.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (animate.isSelected()) {
timer.start();
}
else {
timer.stop();
}
cont.setEnabled(false);
}
});
Box buttons = Box.createHorizontalBox();
buttons.add(wait);
buttons.add(cont);
buttons.add(animate);
add(view, BorderLayout.CENTER);
add(buttons, BorderLayout.SOUTH);
add(new JScrollPane(verboseBox, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), BorderLayout.EAST);
timer = new Timer(TICK_TIME_MS, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
synchronized (lock) {
if (latch != null) {
latch.countDown();
}
}
}
});
}
/**
Initialise the GUI.
*/
public void initialise() {
view.initialise();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
verboseBox.removeAll();
for (TrafficAgent next : manager.getAgents()) {
final TrafficAgent ta = next;
final JCheckBox check = new JCheckBox("Agent " + ta.getHuman(), false);
check.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
ta.setVerbose(check.isSelected());
}
});
verboseBox.add(check);
}
verboseBox.revalidate();
}
});
}
/**
Refresh the view and wait for user input if required.
@see #setWaitOnRefresh(boolean).
*/
public void refresh() {
repaint();
if (waitOnRefresh) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (!timer.isRunning()) {
cont.setEnabled(true);
}
}
});
synchronized (lock) {
latch = new CountDownLatch(1);
}
try {
latch.await();
}
catch (InterruptedException e) {
Logger.error("Error waiting for continue", e);
}
}
}
/**
Set whether to wait for the user before returning from a call to {@link #refresh()}.
@param b Whether to wait on future calls to refresh.
*/
public void setWaitOnRefresh(boolean b) {
waitOnRefresh = b;
}
private class WorldView extends JComponent {
private ScreenTransform transform;
private TrafficArea selectedArea;
private TrafficAgent selectedAgent;
private Map<Shape, TrafficArea> areas;
private Map<Shape, TrafficAgent> agents;
public WorldView() {
}
public void initialise() {
Rectangle2D bounds = null;
for (TrafficArea area : manager.getAreas()) {
Rectangle2D r = area.getArea().getShape().getBounds2D();
if (bounds == null) {
bounds = new Rectangle2D.Double(r.getX(), r.getY(), r.getWidth(), r.getHeight());
}
else {
Rectangle2D.union(bounds, r, bounds);
}
}
transform = new ScreenTransform(bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY());
new PanZoomListener(this).setScreenTransform(transform);
selectedArea = null;
areas = new HashMap<Shape, TrafficArea>();
agents = new HashMap<Shape, TrafficAgent>();
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
Point p = e.getPoint();
selectedArea = null;
selectedAgent = null;
for (Map.Entry<Shape, TrafficArea> next : areas.entrySet()) {
if (next.getKey().contains(p)) {
selectedArea = next.getValue();
}
}
for (Map.Entry<Shape, TrafficAgent> next : agents.entrySet()) {
if (next.getKey().contains(p)) {
selectedAgent = next.getValue();
}
}
repaint();
}
});
}
@Override
public void paintComponent(Graphics g) {
Logger.pushLogContext("traffic3");
try {
int width = getWidth();
int height = getHeight();
Insets insets = getInsets();
width -= insets.left + insets.right;
height -= insets.top + insets.bottom;
transform.rescale(width, height);
Graphics2D copy = (Graphics2D)g.create(insets.left, insets.top, width, height);
drawObjects(copy);
}
finally {
Logger.popLogContext();
}
}
private void drawObjects(Graphics2D g) {
drawAreas((Graphics2D)g.create());
drawAgents((Graphics2D)g.create());
drawBlockades((Graphics2D)g.create());
}
private void drawAreas(Graphics2D g) {
areas.clear();
for (TrafficArea area : manager.getAreas()) {
Path2D shape = new Path2D.Double();
List<Edge> edges = area.getArea().getEdges();
Edge e = edges.get(0);
shape.moveTo(transform.xToScreen(e.getStartX()), transform.yToScreen(e.getStartY()));
for (Edge edge : edges) {
shape.lineTo(transform.xToScreen(edge.getEndX()), transform.yToScreen(edge.getEndY()));
}
if (area == selectedArea) {
g.setColor(SELECTED_AREA_COLOUR);
g.fill(shape);
g.setColor(AREA_OUTLINE_COLOUR);
paintEdges(edges, g);
}
else {
g.setColor(AREA_OUTLINE_COLOUR);
paintEdges(edges, g);
}
areas.put(shape, area);
}
}
private void paintEdges(List<Edge> edges, Graphics2D g) {
for (Edge edge : edges) {
if (edge.isPassable()) {
g.setStroke(PASSABLE_EDGE_STROKE);
}
else {
g.setStroke(IMPASSABLE_EDGE_STROKE);
}
Line2D line = edge.getLine();
g.drawLine(transform.xToScreen(line.getOrigin().getX()),
transform.yToScreen(line.getOrigin().getY()),
transform.xToScreen(line.getEndPoint().getX()),
transform.yToScreen(line.getEndPoint().getY()));
}
}
private void drawBlockades(Graphics2D g) {
g.setStroke(BLOCKADE_STROKE);
g.setColor(BLOCKADE_OUTLINE_COLOUR);
for (TrafficBlockade b : manager.getBlockades()) {
for (Line2D line : b.getLines()) {
int x1 = transform.xToScreen(line.getOrigin().getX());
int y1 = transform.yToScreen(line.getOrigin().getY());
int x2 = transform.xToScreen(line.getEndPoint().getX());
int y2 = transform.yToScreen(line.getEndPoint().getY());
g.drawLine(x1, y1, x2, y2);
}
}
}
private void drawAgents(Graphics2D g) {
for (TrafficAgent agent : manager.getAgents()) {
double agentX = agent.getX();
double agentY = agent.getY();
double ellipseX1 = agentX - agent.getRadius();
double ellipseY1 = agentY - agent.getRadius();
double ellipseX2 = agentX + agent.getRadius();
double ellipseY2 = agentY + agent.getRadius();
double velocityX = agentX + (agent.getVX() * 1000);
double velocityY = agentY + (agent.getVY() * 1000);
double forceX = agentX + (agent.getFX() * FORCE_GUI_FACTOR);
double forceY = agentY + (agent.getFY() * FORCE_GUI_FACTOR);
int x = transform.xToScreen(agentX);
int y = transform.yToScreen(agentY);
int x1 = transform.xToScreen(ellipseX1);
int y1 = transform.yToScreen(ellipseY1);
int x2 = transform.xToScreen(ellipseX2);
int y2 = transform.yToScreen(ellipseY2);
int vx = transform.xToScreen(velocityX);
int vy = transform.yToScreen(velocityY);
int fx = transform.xToScreen(forceX);
int fy = transform.yToScreen(forceY);
int ellipseWidth = x2 - x1;
int ellipseHeight = y1 - y2;
/*
Logger.debug("Agent " + agent);
Logger.debug("Position: " + agentX + ", " + agentY + " -> " + x + ", " + y);
Logger.debug("Ellipse bounds: " + ellipseX1 + ", " + ellipseY1 + " -> " + ellipseX2 + ", " + ellipseY2);
Logger.debug(" " + x1 + ", " + y1 + " -> " + x2 + ", " + y2);
Logger.debug(" Width: " + ellipseWidth + ", height: " + ellipseHeight);
Logger.debug("Velocity: " + velocityX + ", " + velocityY + " -> " + vx + ", " + vy);
Logger.debug("Force: " + forceX + ", " + forceY + " -> " + fx + ", " + fy);
*/
g.setColor(agent == selectedAgent ? Color.orange : Color.red);
Shape shape = new Ellipse2D.Double(x1, y2, ellipseWidth, ellipseHeight);
g.fill(shape);
agents.put(shape, agent);
// Draw the path of the selected agent
if (agent == selectedAgent) {
List<PathElement> path = new ArrayList<PathElement>(selectedAgent.getPath());
if (selectedAgent.getCurrentElement() != null) {
path.add(0, selectedAgent.getCurrentElement());
}
if (path != null) {
Point2D goal = selectedAgent.getFinalDestination();
Point2D current = selectedAgent.getCurrentDestination();
g.setColor(Color.gray);
int lastX = x;
int lastY = y;
for (PathElement next : path) {
List<Point2D> waypoints = new ArrayList<Point2D>(next.getWaypoints());
Collections.reverse(waypoints);
for (Point2D p : waypoints) {
int nodeX = transform.xToScreen(p.getX());
int nodeY = transform.yToScreen(p.getY());
g.fillOval(nodeX - (PATH_NODE_SIZE / 2), nodeY - (PATH_NODE_SIZE / 2), PATH_NODE_SIZE, PATH_NODE_SIZE);
g.drawLine(lastX, lastY, nodeX, nodeY);
lastX = nodeX;
lastY = nodeY;
}
}
if (current != null) {
g.setColor(Color.YELLOW);
int nodeX = transform.xToScreen(current.getX());
int nodeY = transform.yToScreen(current.getY());
g.fillOval(nodeX - (PATH_SPECIAL_NODE_SIZE / 2), nodeY - (PATH_SPECIAL_NODE_SIZE / 2), PATH_SPECIAL_NODE_SIZE, PATH_SPECIAL_NODE_SIZE);
g.drawLine(x, y, nodeX, nodeY);
}
if (goal != null) {
g.setColor(Color.WHITE);
int nodeX = transform.xToScreen(goal.getX());
int nodeY = transform.yToScreen(goal.getY());
g.fillOval(nodeX - (PATH_SPECIAL_NODE_SIZE / 2), nodeY - (PATH_SPECIAL_NODE_SIZE / 2), PATH_SPECIAL_NODE_SIZE, PATH_SPECIAL_NODE_SIZE);
}
}
}
// Draw force and velocity lines
g.setColor(Color.blue);
g.drawLine(x, y, vx, vy);
g.setColor(Color.green);
g.drawLine(x, y, fx, fy);
}
}
}
}