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