View Javadoc
1   package nl.tudelft.simulation.dsol.jetty.sse;
2   
3   import java.awt.Dimension;
4   import java.awt.geom.Point2D;
5   import java.io.IOException;
6   import java.net.URL;
7   import java.rmi.RemoteException;
8   import java.util.ArrayList;
9   import java.util.List;
10  import java.util.Map;
11  import java.util.SortedMap;
12  import java.util.TreeMap;
13  
14  import javax.servlet.ServletException;
15  import javax.servlet.http.HttpServletRequest;
16  import javax.servlet.http.HttpServletResponse;
17  
18  import org.djunits.value.vdouble.scalar.Duration;
19  import org.djutils.draw.bounds.Bounds2d;
20  import org.djutils.draw.point.Point2d;
21  import org.djutils.event.Event;
22  import org.djutils.event.EventListener;
23  import org.djutils.event.TimedEvent;
24  import org.djutils.io.URLResource;
25  import org.eclipse.jetty.server.Handler;
26  import org.eclipse.jetty.server.Request;
27  import org.eclipse.jetty.server.Server;
28  import org.eclipse.jetty.server.handler.AbstractHandler;
29  import org.eclipse.jetty.server.handler.HandlerList;
30  import org.eclipse.jetty.server.handler.ResourceHandler;
31  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
32  import org.opentrafficsim.core.gtu.Gtu;
33  import org.opentrafficsim.web.animation.WebAnimationToggles;
34  
35  import nl.tudelft.simulation.dsol.SimRuntimeException;
36  import nl.tudelft.simulation.dsol.animation.Locatable;
37  import nl.tudelft.simulation.dsol.animation.d2.Renderable2dInterface;
38  import nl.tudelft.simulation.dsol.experiment.Replication;
39  import nl.tudelft.simulation.dsol.simulators.AnimatorInterface;
40  import nl.tudelft.simulation.dsol.simulators.DevsRealTimeAnimator;
41  import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
42  import nl.tudelft.simulation.dsol.web.animation.d2.HtmlAnimationPanel;
43  import nl.tudelft.simulation.dsol.web.animation.d2.HtmlGridPanel;
44  import nl.tudelft.simulation.dsol.web.animation.d2.ToggleButtonInfo;
45  import nl.tudelft.simulation.introspection.Property;
46  import nl.tudelft.simulation.introspection.beans.BeanIntrospector;
47  
48  /**
49   * DSOLWebServer.java.
50   * <p>
51   * Copyright (c) 2003-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
52   * BSD-style license. See <a href="https://opentrafficsim.org/docs/v2/license.html">OpenTrafficSim License</a>.
53   * </p>
54   * @author <a href="https://github.com/averbraeck" target="_blank">Alexander Verbraeck</a>
55   */
56  public abstract class OtsWebServer implements EventListener
57  {
58      /** the title for the model window. */
59      private final String title;
60  
61      /** the simulator. */
62      private final OtsSimulatorInterface simulator;
63  
64      /** dirty flag for the controls: when the model e.g. stops, the status needs to be changed. */
65      private boolean dirtyControls = false;
66  
67      /** the animation panel. */
68      private HtmlAnimationPanel animationPanel;
69  
70      /**
71       * @param title String; the title for the model window
72       * @param simulator OtsSimulatorInterface; the simulator
73       * @param extent Bounds2d; the extent to use for the graphics (min/max coordinates)
74       * @throws Exception in case jetty crashes
75       */
76      public OtsWebServer(final String title, final OtsSimulatorInterface simulator, final Bounds2d extent) throws Exception
77      {
78          this.title = title;
79  
80          this.simulator = simulator;
81          try
82          {
83              simulator.addListener(this, SimulatorInterface.START_EVENT);
84              simulator.addListener(this, SimulatorInterface.STOP_EVENT);
85          }
86          catch (RemoteException re)
87          {
88              simulator.getLogger().always().warn(re, "Problem adding listeners to Simulator");
89          }
90  
91          if (this.simulator instanceof AnimatorInterface)
92          {
93              this.animationPanel = new HtmlAnimationPanel(extent, this.simulator);
94              WebAnimationToggles.setTextAnimationTogglesStandard(this.animationPanel);
95              // get the already created elements in context(/animation/D2)
96              this.animationPanel.notify(new TimedEvent(Replication.START_REPLICATION_EVENT,
97                      null, this.simulator.getSimulatorTime()));
98          }
99  
100         new ServerThread().start();
101     }
102 
103     /** Handle in separate thread to avoid 'lock' of the main application. */
104     class ServerThread extends Thread
105     {
106         @Override
107         public void run()
108         {
109             Server server = new Server(8080);
110             ResourceHandler resourceHandler = new ResourceHandler();
111 
112             // root folder; to work in Eclipse, as an external jar, and in an embedded jar
113             URL homeFolder = URLResource.getResource("/resources/home");
114             String webRoot = homeFolder.toExternalForm();
115             System.out.println("webRoot is " + webRoot);
116 
117             resourceHandler.setDirectoriesListed(true);
118             resourceHandler.setWelcomeFiles(new String[] {"index.html"});
119             resourceHandler.setResourceBase(webRoot);
120 
121             HandlerList handlers = new HandlerList();
122             handlers.setHandlers(new Handler[] {resourceHandler, new XHRHandler(OtsWebServer.this)});
123             server.setHandler(handlers);
124 
125             try
126             {
127                 server.start();
128                 server.join();
129             }
130             catch (Exception exception)
131             {
132                 exception.printStackTrace();
133             }
134         }
135     }
136 
137     /**
138      * @return title
139      */
140     public final String getTitle()
141     {
142         return this.title;
143     }
144 
145     /**
146      * @return simulator
147      */
148     public final OtsSimulatorInterface getSimulator()
149     {
150         return this.simulator;
151     }
152 
153     /**
154      * @return animationPanel
155      */
156     public final HtmlAnimationPanel getAnimationPanel()
157     {
158         return this.animationPanel;
159     }
160 
161     /**
162      * Try to start the simulator, and return whether the simulator has been started.
163      * @return whether the simulator has been started or not
164      */
165     protected boolean startSimulator()
166     {
167         if (getSimulator() == null)
168         {
169             System.out.println("SIMULATOR == NULL");
170             return false;
171         }
172         try
173         {
174             System.out.println("START THE SIMULATOR");
175             getSimulator().start();
176         }
177         catch (SimRuntimeException exception)
178         {
179             getSimulator().getLogger().always().warn(exception, "Problem starting Simulator");
180         }
181         if (getSimulator().isStartingOrRunning())
182         {
183             return true;
184         }
185         this.dirtyControls = false; // undo the notification
186         return false;
187     }
188 
189     /**
190      * Try to stop the simulator, and return whether the simulator has been stopped.
191      * @return whether the simulator has been stopped or not
192      */
193     protected boolean stopSimulator()
194     {
195         if (getSimulator() == null)
196         {
197             return true;
198         }
199         try
200         {
201             System.out.println("STOP THE SIMULATOR");
202             getSimulator().stop();
203         }
204         catch (SimRuntimeException exception)
205         {
206             getSimulator().getLogger().always().warn(exception, "Problem stopping Simulator");
207         }
208         if (!getSimulator().isStartingOrRunning())
209         {
210             return true;
211         }
212         this.dirtyControls = false; // undo the notification
213         return false;
214     }
215 
216     /**
217      * @param speedFactor double; the new speed factor
218      */
219     protected void setSpeedFactor(final double speedFactor)
220     {
221         if (this.simulator instanceof DevsRealTimeAnimator)
222         {
223             ((DevsRealTimeAnimator<Duration>) this.simulator).setSpeedFactor(speedFactor);
224         }
225     }
226 
227     /** {@inheritDoc} */
228     @Override
229     public void notify(final Event event) throws RemoteException
230     {
231         if (event.getType().equals(SimulatorInterface.START_EVENT))
232         {
233             this.dirtyControls = true;
234         }
235         else if (event.getType().equals(SimulatorInterface.STOP_EVENT))
236         {
237             this.dirtyControls = true;
238         }
239     }
240 
241     /**
242      * Answer handles the events from the web-based user interface. <br>
243      * <p>
244      * Copyright (c) 2003-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
245      * BSD-style license. See <a href="https://opentrafficsim.org/docs/v2/license.html">OpenTrafficSim License</a>.
246      * </p>
247      * @author <a href="https://github.com/averbraeck" target="_blank">Alexander Verbraeck</a>
248      */
249     public static class XHRHandler extends AbstractHandler
250     {
251         /** web server for callback of actions. */
252         final OtsWebServer webServer;
253 
254         /** Timer update interval in msec. */
255         private long lastWallTIme = -1;
256 
257         /** Simulation time time. */
258         private double prevSimTime = 0;
259 
260         /**
261          * Create the handler for Servlet requests.
262          * @param webServer OtsWebServer; web server for callback of actions
263          */
264         public XHRHandler(final OtsWebServer webServer)
265         {
266             this.webServer = webServer;
267         }
268 
269         @Override
270         public void handle(final String target, final Request baseRequest, final HttpServletRequest request,
271                 final HttpServletResponse response) throws IOException, ServletException
272         {
273             // System.out.println("target=" + target);
274             // System.out.println("baseRequest=" + baseRequest);
275             // System.out.println("request=" + request);
276 
277             Map<String, String[]> params = request.getParameterMap();
278             // System.out.println(params);
279 
280             String answer = "<message>ok</message>";
281 
282             if (request.getParameter("message") != null)
283             {
284                 String message = request.getParameter("message");
285                 String[] parts = message.split("\\|");
286                 String command = parts[0];
287                 HtmlAnimationPanel animationPanel = this.webServer.getAnimationPanel();
288 
289                 switch (command)
290                 {
291                     case "getTitle":
292                     {
293                         answer = "<title>" + this.webServer.getTitle() + "</title>";
294                         break;
295                     }
296 
297                     case "init":
298                     {
299                         boolean simOk = this.webServer.getSimulator() != null;
300                         boolean started = simOk ? this.webServer.getSimulator().isStartingOrRunning() : false;
301                         answer = controlButtonResponse(simOk, started);
302                         break;
303                     }
304 
305                     case "windowSize":
306                     {
307                         if (parts.length != 3)
308                             System.err.println("wrong windowSize commmand: " + message);
309                         else
310                         {
311                             int width = Integer.parseInt(parts[1]);
312                             int height = Integer.parseInt(parts[2]);
313                             animationPanel.setSize(new Dimension(width, height));
314                         }
315                         break;
316                     }
317 
318                     case "startStop":
319                     {
320                         boolean simOk = this.webServer.getSimulator() != null;
321                         boolean started = simOk ? this.webServer.getSimulator().isStartingOrRunning() : false;
322                         if (simOk && started)
323                             started = !this.webServer.stopSimulator();
324                         else if (simOk && !started)
325                             started = this.webServer.startSimulator();
326                         answer = controlButtonResponse(simOk, started);
327                         break;
328                     }
329 
330                     case "oneEvent":
331                     {
332                         // TODO
333                         boolean started = false;
334                         answer = controlButtonResponse(this.webServer.getSimulator() != null, started);
335                         break;
336                     }
337 
338                     case "allEvents":
339                     {
340                         // TODO
341                         boolean started = false;
342                         answer = controlButtonResponse(this.webServer.getSimulator() != null, started);
343                         break;
344                     }
345 
346                     case "reset":
347                     {
348                         // TODO
349                         boolean started = false;
350                         answer = controlButtonResponse(this.webServer.getSimulator() != null, started);
351                         break;
352                     }
353 
354                     case "animate":
355                     {
356                         answer = animationPanel.getDrawingCommands();
357                         break;
358                     }
359 
360                     case "arrowDown":
361                     {
362                         animationPanel.pan(HtmlGridPanel.DOWN, 0.1);
363                         break;
364                     }
365 
366                     case "arrowUp":
367                     {
368                         animationPanel.pan(HtmlGridPanel.UP, 0.1);
369                         break;
370                     }
371 
372                     case "arrowLeft":
373                     {
374                         animationPanel.pan(HtmlGridPanel.LEFT, 0.1);
375                         break;
376                     }
377 
378                     case "arrowRight":
379                     {
380                         animationPanel.pan(HtmlGridPanel.RIGHT, 0.1);
381                         break;
382                     }
383 
384                     case "pan":
385                     {
386                         if (parts.length == 3)
387                         {
388                             int dx = Integer.parseInt(parts[1]);
389                             int dy = Integer.parseInt(parts[2]);
390                             double scaleX = animationPanel.getRenderableScale().getXScale(animationPanel.getExtent(),
391                                     animationPanel.getSize());
392                             double scaleY = animationPanel.getRenderableScale().getYScale(animationPanel.getExtent(),
393                                     animationPanel.getSize());
394                             Bounds2d extent = animationPanel.getExtent();
395                             animationPanel.setExtent(new Bounds2d(extent.getMinX() - dx * scaleX,
396                                     extent.getMinX() - dx * scaleX + extent.getDeltaX(), extent.getMinY() + dy * scaleY,
397                                     extent.getMinY() + dy * scaleY + extent.getDeltaY()));
398                         }
399                         break;
400                     }
401 
402                     case "introspect":
403                     {
404                         if (parts.length == 3)
405                         {
406                             int x = Integer.parseInt(parts[1]);
407                             int y = Integer.parseInt(parts[2]);
408                             List<Locatable> targets = new ArrayList<Locatable>();
409                             try
410                             {
411                                 Point2d point = animationPanel.getRenderableScale().getWorldCoordinates(
412                                         new Point2D.Double(x, y), animationPanel.getExtent(), animationPanel.getSize());
413                                 for (Renderable2dInterface<?> renderable : animationPanel.getElements())
414                                 {
415                                     if (animationPanel.isShowElement(renderable)
416                                             && renderable.contains(point, animationPanel.getExtent()))
417                                     {
418                                         if (renderable.getSource() instanceof Gtu)
419                                         {
420                                             targets.add(renderable.getSource());
421                                         }
422                                     }
423                                 }
424                             }
425                             catch (Exception exception)
426                             {
427                                 this.webServer.getSimulator().getLogger().always().warn(exception, "getSelectedObjects");
428                             }
429                             if (targets.size() > 0)
430                             {
431                                 Object introspectedObject = targets.get(0);
432                                 Property[] properties = new BeanIntrospector().getProperties(introspectedObject);
433                                 SortedMap<String, Property> propertyMap = new TreeMap<>();
434                                 for (Property property : properties)
435                                     propertyMap.put(property.getName(), property);
436                                 answer = "<introspection>\n";
437                                 for (Property property : propertyMap.values())
438                                 {
439                                     answer += "<property><field>" + property.getName() + "</field><value>" + property.getValue()
440                                             + "</value></property>\n";
441                                 }
442                                 answer += "<introspection>\n";
443                             }
444                             else
445                             {
446                                 answer = "<none />";
447                             }
448                         }
449                         break;
450                     }
451 
452                     case "zoomIn":
453                     {
454                         if (parts.length == 1)
455                             animationPanel.zoom(0.9);
456                         else
457                         {
458                             int x = Integer.parseInt(parts[1]);
459                             int y = Integer.parseInt(parts[2]);
460                             animationPanel.zoom(0.9, x, y);
461                         }
462                         break;
463                     }
464 
465                     case "zoomOut":
466                     {
467                         if (parts.length == 1)
468                             animationPanel.zoom(1.1);
469                         else
470                         {
471                             int x = Integer.parseInt(parts[1]);
472                             int y = Integer.parseInt(parts[2]);
473                             animationPanel.zoom(1.1, x, y);
474                         }
475                         break;
476                     }
477 
478                     case "zoomAll":
479                     {
480                         animationPanel.zoomAll();
481                         break;
482                     }
483 
484                     case "home":
485                     {
486                         animationPanel.home();
487                         break;
488                     }
489 
490                     case "toggleGrid":
491                     {
492                         animationPanel.setShowGrid(!animationPanel.isShowGrid());
493                         break;
494                     }
495 
496                     case "getTime":
497                     {
498                         double now = Math.round(this.webServer.getSimulator().getSimulatorTime().si * 1000) / 1000d;
499                         int seconds = (int) Math.floor(now);
500                         int fractionalSeconds = (int) Math.floor(1000 * (now - seconds));
501                         String timeText = String.format("  %02d:%02d:%02d.%03d  ", seconds / 3600, seconds / 60 % 60,
502                                 seconds % 60, fractionalSeconds);
503                         answer = timeText;
504                         break;
505                     }
506 
507                     case "getSpeed":
508                     {
509                         double simTime = this.webServer.getSimulator().getSimulatorTime().si;
510                         double speed = getSimulationSpeed(simTime);
511                         String speedText = "";
512                         if (!Double.isNaN(speed))
513                         {
514                             speedText = String.format("% 5.2fx  ", speed);
515                         }
516                         answer = speedText;
517                         break;
518                     }
519 
520                     case "getToggles":
521                     {
522                         answer = getToggles(animationPanel);
523                         break;
524                     }
525 
526                     // we expect something of the form toggle|class|Node|true or toggle|gis|streets|false
527                     case "toggle":
528                     {
529                         if (parts.length != 4)
530                             System.err.println("wrong toggle commmand: " + message);
531                         else
532                         {
533                             String toggleName = parts[1];
534                             boolean gis = parts[2].equals("gis");
535                             boolean show = parts[3].equals("true");
536                             if (gis)
537                             {
538                                 if (show)
539                                     animationPanel.showGISLayer(toggleName);
540                                 else
541                                     animationPanel.hideGISLayer(toggleName);
542                             }
543                             else
544                             {
545                                 if (show)
546                                     animationPanel.showClass(toggleName);
547                                 else
548                                     animationPanel.hideClass(toggleName);
549                             }
550                         }
551                         break;
552                     }
553 
554                     default:
555                     {
556                         System.err.println("Got unknown message from client: " + command);
557                         answer = "<message>" + request.getParameter("message") + "</message>";
558                         break;
559                     }
560                 }
561             }
562 
563             if (request.getParameter("slider") != null)
564             {
565                 // System.out.println(request.getParameter("slider") + "\n");
566                 try
567                 {
568                     int value = Integer.parseInt(request.getParameter("slider"));
569                     // values range from 100 to 1400. 100 = 0.1, 400 = 1, 1399 = infinite
570                     double speedFactor = 1.0;
571                     if (value > 1398)
572                         speedFactor = Double.MAX_VALUE;
573                     else
574                         speedFactor = Math.pow(2.15444, value / 100.0) / 21.5444;
575                     this.webServer.setSpeedFactor(speedFactor);
576                     // System.out.println("speed factor changed to " + speedFactor);
577                 }
578                 catch (NumberFormatException exception)
579                 {
580                     answer = "<message>Error: " + exception.getMessage() + "</message>";
581                 }
582             }
583 
584             // System.out.println(answer);
585 
586             response.setContentType("text/xml");
587             response.setHeader("Cache-Control", "no-cache");
588             response.setContentLength(answer.length());
589             response.setStatus(HttpServletResponse.SC_OK);
590             response.getWriter().write(answer);
591             response.flushBuffer();
592             baseRequest.setHandled(true);
593         }
594 
595         /**
596          * @param active boolean; is the simulation active?
597          * @param started boolean; has the simulation been started?
598          * @return XML message to send to the server
599          */
600         private String controlButtonResponse(final boolean active, final boolean started)
601         {
602             if (!active)
603             {
604                 return "<controls>\n" + "<oneEventActive>false</oneEventActive>\n"
605                         + "<allEventsActive>false</allEventsActive>\n" + "<startStop>start</startStop>\n"
606                         + "<startStopActive>false</startStopActive>\n" + "<resetActive>false</resetActive>\n" + "</controls>\n";
607             }
608             if (started)
609             {
610                 return "<controls>\n" + "<oneEventActive>false</oneEventActive>\n"
611                         + "<allEventsActive>false</allEventsActive>\n" + "<startStop>stop</startStop>\n"
612                         + "<startStopActive>true</startStopActive>\n" + "<resetActive>false</resetActive>\n" + "</controls>\n";
613             }
614             else
615             {
616                 return "<controls>\n" + "<oneEventActive>true</oneEventActive>\n" + "<allEventsActive>true</allEventsActive>\n"
617                         + "<startStop>start</startStop>\n" + "<startStopActive>true</startStopActive>\n"
618                         + "<resetActive>false</resetActive>\n" + "</controls>\n";
619             }
620         }
621 
622         /**
623          * Return the toggle button info for the toggle panel.
624          * @param panel HTMLAnimationPanel; the HTMLAnimationPanel
625          * @return the String that can be parsed by the select.html iframe
626          */
627         private String getToggles(final HtmlAnimationPanel panel)
628         {
629             String ret = "<toggles>\n";
630             for (ToggleButtonInfo toggle : panel.getToggleButtons())
631             {
632                 if (toggle instanceof ToggleButtonInfo.Text)
633                 {
634                     ret += "<text>" + toggle.getName() + "</text>\n";
635                 }
636                 else if (toggle instanceof ToggleButtonInfo.LocatableClass)
637                 {
638                     ret += "<class>" + toggle.getName() + "," + toggle.isVisible() + "</class>\n";
639                 }
640                 else if (toggle instanceof ToggleButtonInfo.Gis)
641                 {
642                     ret += "<gis>" + toggle.getName() + "," + ((ToggleButtonInfo.Gis) toggle).getLayerName() + ","
643                             + toggle.isVisible() + "</gis>\n";
644                 }
645             }
646             ret += "</toggles>\n";
647             return ret;
648         }
649 
650         /**
651          * Returns the simulation speed.
652          * @param simTime double; simulation time
653          * @return simulation speed
654          */
655         private double getSimulationSpeed(final double simTime)
656         {
657             long now = System.currentTimeMillis();
658             if (this.lastWallTIme < 0 || this.lastWallTIme == now)
659             {
660                 this.lastWallTIme = now;
661                 this.prevSimTime = simTime;
662                 return Double.NaN;
663             }
664             double speed = (simTime - this.prevSimTime) / (0.001 * (now - this.lastWallTIme));
665             this.prevSimTime = simTime;
666             this.lastWallTIme = now;
667             return speed;
668         }
669 
670     }
671 
672 }