package nl.tudelft.simulation.dsol.jetty.sse;
import java.awt.Dimension;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.djutils.draw.bounds.Bounds2d;
import org.djutils.draw.point.Point2d;
import org.djutils.event.EventInterface;
import org.djutils.event.EventListenerInterface;
import org.djutils.event.TimedEvent;
import org.eclipse.jetty.server.Request;
import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
import org.opentrafficsim.core.gtu.GTU;
import org.opentrafficsim.web.animation.WebAnimationToggles;
import nl.tudelft.simulation.dsol.SimRuntimeException;
import nl.tudelft.simulation.dsol.animation.Locatable;
import nl.tudelft.simulation.dsol.animation.D2.Renderable2DInterface;
import nl.tudelft.simulation.dsol.experiment.ReplicationInterface;
import nl.tudelft.simulation.dsol.simulators.AnimatorInterface;
import nl.tudelft.simulation.dsol.simulators.DEVSRealTimeAnimator;
import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
import nl.tudelft.simulation.dsol.web.animation.D2.HTMLAnimationPanel;
import nl.tudelft.simulation.dsol.web.animation.D2.HTMLGridPanel;
import nl.tudelft.simulation.dsol.web.animation.D2.ToggleButtonInfo;
import nl.tudelft.simulation.introspection.Property;
import nl.tudelft.simulation.introspection.beans.BeanIntrospector;
/**
* OTSWebModel.java.
*
* Copyright (c) 2003-2022 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License.
*
* @author Alexander Verbraeck
*/
public class OTSWebModel implements EventListenerInterface
{
/** the title for the model window. */
private final String title;
/** the simulator. */
private final OTSSimulatorInterface simulator;
/** dirty flag for the controls: when the model e.g. stops, the status needs to be changed. */
private boolean dirtyControls = false;
/** the animation panel. */
private HTMLAnimationPanel animationPanel;
/** Timer update interval in msec. */
private long lastWallTIme = -1;
/** Simulation time time. */
private double prevSimTime = 0;
/** has the model been killed? */
private boolean killed = false;
/**
* @param title String; the title for the model window
* @param simulator OTSSimulatorInterface; the simulator
* @throws Exception in case jetty crashes
*/
public OTSWebModel(final String title, final OTSSimulatorInterface simulator) throws Exception
{
this.title = title;
this.simulator = simulator;
Bounds2d extent = new Bounds2d(-200, 200, -200, 200);
try
{
simulator.addListener(this, SimulatorInterface.START_EVENT);
simulator.addListener(this, SimulatorInterface.STOP_EVENT);
}
catch (RemoteException re)
{
getSimulator().getLogger().always().warn(re, "Problem adding listeners to Simulator");
}
if (this.simulator instanceof AnimatorInterface)
{
this.animationPanel = new HTMLAnimationPanel(extent, this.simulator);
WebAnimationToggles.setTextAnimationTogglesStandard(this.animationPanel);
// get the already created elements in context(/animation/D2)
this.animationPanel.notify(new TimedEvent(ReplicationInterface.START_REPLICATION_EVENT,
this.simulator.getSourceId(), null, this.simulator.getSimulatorTime()));
}
}
/**
* @return title
*/
public final String getTitle()
{
return this.title;
}
/**
* @return simulator
*/
public final OTSSimulatorInterface getSimulator()
{
return this.simulator;
}
/**
* @return animationPanel
*/
public final HTMLAnimationPanel getAnimationPanel()
{
return this.animationPanel;
}
/**
* @return killed
*/
public final boolean isKilled()
{
return this.killed;
}
/**
* @param killed boolean; set killed
*/
public final void setKilled(final boolean killed)
{
this.killed = killed;
}
/**
* Try to start the simulator, and return whether the simulator has been started.
* @return whether the simulator has been started or not
*/
protected boolean startSimulator()
{
if (getSimulator() == null)
{
System.out.println("SIMULATOR == NULL");
return false;
}
try
{
System.out.println("START THE SIMULATOR");
getSimulator().start();
}
catch (SimRuntimeException exception)
{
getSimulator().getLogger().always().warn(exception, "Problem starting Simulator");
}
if (getSimulator().isStartingOrRunning())
{
return true;
}
this.dirtyControls = false; // undo the notification
return false;
}
/**
* Try to stop the simulator, and return whether the simulator has been stopped.
* @return whether the simulator has been stopped or not
*/
protected boolean stopSimulator()
{
if (getSimulator() == null)
{
return true;
}
try
{
System.out.println("STOP THE SIMULATOR");
getSimulator().stop();
}
catch (SimRuntimeException exception)
{
getSimulator().getLogger().always().warn(exception, "Problem stopping Simulator");
}
if (!getSimulator().isStartingOrRunning())
{
return true;
}
this.dirtyControls = false; // undo the notification
return false;
}
/**
* @param speedFactor double; the new speed factor
*/
protected void setSpeedFactor(final double speedFactor)
{
if (this.simulator instanceof DEVSRealTimeAnimator)
{
((DEVSRealTimeAnimator, ?, ?>) this.simulator).setSpeedFactor(speedFactor);
}
}
/** {@inheritDoc} */
@Override
public void notify(final EventInterface event) throws RemoteException
{
if (event.getType().equals(SimulatorInterface.START_EVENT))
{
this.dirtyControls = true;
}
else if (event.getType().equals(SimulatorInterface.STOP_EVENT))
{
this.dirtyControls = true;
}
}
/**
* Delegate handle method from the main web server for this particular model.
* @param target String; t
* @param baseRequest Request; br
* @param request HttpServletRequest; r
* @param response HttpServletResponse; re
* @throws IOException on error
* @throws ServletException on error
*/
@SuppressWarnings({"checkstyle:needbraces", "checkstyle:methodlength"})
public void handle(final String target, final Request baseRequest, final HttpServletRequest request,
final HttpServletResponse response) throws IOException, ServletException
{
// System.out.println("target=" + target);
// System.out.println("baseRequest=" + baseRequest);
// System.out.println("request=" + request);
if (this.killed)
{
return;
}
Map params = request.getParameterMap();
// System.out.println(params);
String answer = "ok";
if (request.getParameter("message") != null)
{
String message = request.getParameter("message");
String[] parts = message.split("\\|");
String command = parts[0];
HTMLAnimationPanel animationPanel = getAnimationPanel();
switch (command)
{
case "getTitle":
{
answer = "" + getTitle() + "";
break;
}
case "init":
{
boolean simOk = getSimulator() != null;
boolean started = simOk ? getSimulator().isStartingOrRunning() : false;
answer = controlButtonResponse(simOk, started);
break;
}
case "windowSize":
{
if (parts.length != 3)
System.err.println("wrong windowSize commmand: " + message);
else
{
int width = Integer.parseInt(parts[1]);
int height = Integer.parseInt(parts[2]);
animationPanel.setSize(new Dimension(width, height));
}
break;
}
case "startStop":
{
boolean simOk = getSimulator() != null;
boolean started = simOk ? getSimulator().isStartingOrRunning() : false;
if (simOk && started)
started = !stopSimulator();
else if (simOk && !started)
started = startSimulator();
answer = controlButtonResponse(simOk, started);
break;
}
case "oneEvent":
{
// TODO
boolean started = false;
answer = controlButtonResponse(getSimulator() != null, started);
break;
}
case "allEvents":
{
// TODO
boolean started = false;
answer = controlButtonResponse(getSimulator() != null, started);
break;
}
case "reset":
{
// TODO
boolean started = false;
answer = controlButtonResponse(getSimulator() != null, started);
break;
}
case "animate":
{
answer = animationPanel.getDrawingCommands();
break;
}
case "arrowDown":
{
animationPanel.pan(HTMLGridPanel.DOWN, 0.1);
break;
}
case "arrowUp":
{
animationPanel.pan(HTMLGridPanel.UP, 0.1);
break;
}
case "arrowLeft":
{
animationPanel.pan(HTMLGridPanel.LEFT, 0.1);
break;
}
case "arrowRight":
{
animationPanel.pan(HTMLGridPanel.RIGHT, 0.1);
break;
}
case "pan":
{
if (parts.length == 3)
{
int dx = Integer.parseInt(parts[1]);
int dy = Integer.parseInt(parts[2]);
double scaleX = animationPanel.getRenderableScale().getXScale(animationPanel.getExtent(),
animationPanel.getSize());
double scaleY = animationPanel.getRenderableScale().getYScale(animationPanel.getExtent(),
animationPanel.getSize());
Bounds2d extent = animationPanel.getExtent();
animationPanel.setExtent(new Bounds2d(extent.getMinX() - dx * scaleX,
extent.getMinX() - dx * scaleX + extent.getDeltaX(), extent.getMinY() + dy * scaleY,
extent.getMinY() + dy * scaleY + extent.getDeltaY()));
}
break;
}
case "introspect":
{
if (parts.length == 3)
{
int x = Integer.parseInt(parts[1]);
int y = Integer.parseInt(parts[2]);
List targets = new ArrayList();
try
{
Point2d point = animationPanel.getRenderableScale().getWorldCoordinates(new Point2D.Double(x, y),
animationPanel.getExtent(), animationPanel.getSize());
for (Renderable2DInterface> renderable : animationPanel.getElements())
{
if (animationPanel.isShowElement(renderable)
&& renderable.contains(point, animationPanel.getExtent()))
{
if (renderable.getSource() instanceof GTU)
{
targets.add(renderable.getSource());
}
}
}
}
catch (Exception exception)
{
this.simulator.getLogger().always().warn(exception, "getSelectedObjects");
}
if (targets.size() > 0)
{
Object introspectedObject = targets.get(0);
Property[] properties = new BeanIntrospector().getProperties(introspectedObject);
SortedMap propertyMap = new TreeMap<>();
for (Property property : properties)
propertyMap.put(property.getName(), property);
answer = "\n";
for (Property property : propertyMap.values())
{
answer += "" + property.getName() + "" + property.getValue()
+ "\n";
}
answer += "\n";
}
else
{
answer = "";
}
}
break;
}
case "zoomIn":
{
if (parts.length == 1)
animationPanel.zoom(0.9);
else
{
int x = Integer.parseInt(parts[1]);
int y = Integer.parseInt(parts[2]);
animationPanel.zoom(0.9, x, y);
}
break;
}
case "zoomOut":
{
if (parts.length == 1)
animationPanel.zoom(1.1);
else
{
int x = Integer.parseInt(parts[1]);
int y = Integer.parseInt(parts[2]);
animationPanel.zoom(1.1, x, y);
}
break;
}
case "zoomAll":
{
animationPanel.zoomAll();
break;
}
case "home":
{
animationPanel.home();
break;
}
case "toggleGrid":
{
animationPanel.setShowGrid(!animationPanel.isShowGrid());
break;
}
case "getTime":
{
double now = Math.round(getSimulator().getSimulatorTime().si * 1000) / 1000d;
int seconds = (int) Math.floor(now);
int fractionalSeconds = (int) Math.floor(1000 * (now - seconds));
String timeText = String.format(" %02d:%02d:%02d.%03d ", seconds / 3600, seconds / 60 % 60, seconds % 60,
fractionalSeconds);
answer = timeText;
break;
}
case "getSpeed":
{
double simTime = getSimulator().getSimulatorTime().si;
double speed = getSimulationSpeed(simTime);
String speedText = "";
if (!Double.isNaN(speed))
{
speedText = String.format("% 5.2fx ", speed);
}
answer = speedText;
break;
}
case "getToggles":
{
answer = getToggles(animationPanel);
break;
}
// we expect something of the form toggle|class|Node|true or toggle|gis|streets|false
case "toggle":
{
if (parts.length != 4)
System.err.println("wrong toggle commmand: " + message);
else
{
String toggleName = parts[1];
boolean gis = parts[2].equals("gis");
boolean show = parts[3].equals("true");
if (gis)
{
if (show)
animationPanel.showGISLayer(toggleName);
else
animationPanel.hideGISLayer(toggleName);
}
else
{
if (show)
animationPanel.showClass(toggleName);
else
animationPanel.hideClass(toggleName);
}
}
break;
}
default:
{
System.err.println("OTSWebModel: Got unknown message from client: " + command);
answer = "" + request.getParameter("message") + "";
break;
}
}
}
if (request.getParameter("slider") != null)
{
// System.out.println(request.getParameter("slider") + "\n");
try
{
int value = Integer.parseInt(request.getParameter("slider"));
// values range from 100 to 1400. 100 = 0.1, 400 = 1, 1399 = infinite
double speedFactor = 1.0;
if (value > 1398)
speedFactor = Double.MAX_VALUE;
else
speedFactor = Math.pow(2.15444, value / 100.0) / 21.5444;
setSpeedFactor(speedFactor);
// System.out.println("speed factor changed to " + speedFactor);
}
catch (NumberFormatException exception)
{
answer = "Error: " + exception.getMessage() + "";
}
}
// System.out.println(answer);
response.setContentType("text/xml");
response.setHeader("Cache-Control", "no-cache");
response.setContentLength(answer.length());
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write(answer);
response.flushBuffer();
baseRequest.setHandled(true);
}
/**
* @param active boolean; is the simulation active?
* @param started boolean; has the simulation been started?
* @return XML message to send to the server
*/
private String controlButtonResponse(final boolean active, final boolean started)
{
if (!active)
{
return "\n" + "false\n" + "false\n"
+ "start\n" + "false\n"
+ "false\n" + "\n";
}
if (started)
{
return "\n" + "false\n" + "false\n"
+ "stop\n" + "true\n"
+ "false\n" + "\n";
}
else
{
return "\n" + "true\n" + "true\n"
+ "start\n" + "true\n"
+ "false\n" + "\n";
}
}
/**
* Return the toggle button info for the toggle panel.
* @param panel HTMLAnimationPanel; the HTMLAnimationPanel
* @return the String that can be parsed by the select.html iframe
*/
private String getToggles(final HTMLAnimationPanel panel)
{
String ret = "\n";
for (ToggleButtonInfo toggle : panel.getToggleButtons())
{
if (toggle instanceof ToggleButtonInfo.Text)
{
ret += "" + toggle.getName() + "\n";
}
else if (toggle instanceof ToggleButtonInfo.LocatableClass)
{
ret += "" + toggle.getName() + "," + toggle.isVisible() + "\n";
}
else if (toggle instanceof ToggleButtonInfo.Gis)
{
ret += "" + toggle.getName() + "," + ((ToggleButtonInfo.Gis) toggle).getLayerName() + ","
+ toggle.isVisible() + "\n";
}
}
ret += "\n";
return ret;
}
/**
* Returns the simulation speed.
* @param simTime double; simulation time
* @return simulation speed
*/
private double getSimulationSpeed(final double simTime)
{
long now = System.currentTimeMillis();
if (this.lastWallTIme < 0 || this.lastWallTIme == now)
{
this.lastWallTIme = now;
this.prevSimTime = simTime;
return Double.NaN;
}
double speed = (simTime - this.prevSimTime) / (0.001 * (now - this.lastWallTIme));
this.prevSimTime = simTime;
this.lastWallTIme = now;
return speed;
}
}