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.djutils.draw.bounds.Bounds2d;
19  import org.djutils.draw.point.Point2d;
20  import org.djutils.event.EventInterface;
21  import org.djutils.event.EventListenerInterface;
22  import org.djutils.event.TimedEvent;
23  import org.djutils.io.URLResource;
24  import org.eclipse.jetty.server.Handler;
25  import org.eclipse.jetty.server.Request;
26  import org.eclipse.jetty.server.Server;
27  import org.eclipse.jetty.server.handler.AbstractHandler;
28  import org.eclipse.jetty.server.handler.HandlerList;
29  import org.eclipse.jetty.server.handler.ResourceHandler;
30  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
31  import org.opentrafficsim.core.gtu.GTU;
32  import org.opentrafficsim.web.animation.WebAnimationToggles;
33  
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.ReplicationInterface;
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-2022 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://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
54   */
55  public abstract class OTSWebServer implements EventListenerInterface
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 String; the title for the model window
71       * @param simulator OTSSimulatorInterface; the simulator
72       * @param extent Bounds2d; 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.notify(new TimedEvent(ReplicationInterface.START_REPLICATION_EVENT,
96                      this.simulator.getSourceId(), 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("/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 double; the new speed factor
217      */
218     protected void setSpeedFactor(final double speedFactor)
219     {
220         if (this.simulator instanceof DEVSRealTimeAnimator)
221         {
222             ((DEVSRealTimeAnimator<?, ?, ?>) this.simulator).setSpeedFactor(speedFactor);
223         }
224     }
225 
226     /** {@inheritDoc} */
227     @Override
228     public void notify(final EventInterface event) throws RemoteException
229     {
230         if (event.getType().equals(SimulatorInterface.START_EVENT))
231         {
232             this.dirtyControls = true;
233         }
234         else if (event.getType().equals(SimulatorInterface.STOP_EVENT))
235         {
236             this.dirtyControls = true;
237         }
238     }
239 
240     /**
241      * Answer handles the events from the web-based user interface. <br>
242      * <p>
243      * Copyright (c) 2003-2022 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
244      * BSD-style license. See <a href="https://opentrafficsim.org/docs/v2/license.html">OpenTrafficSim License</a>.
245      * </p>
246      * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
247      */
248     public static class XHRHandler extends AbstractHandler
249     {
250         /** web server for callback of actions. */
251         final OTSWebServer webServer;
252 
253         /** Timer update interval in msec. */
254         private long lastWallTIme = -1;
255 
256         /** Simulation time time. */
257         private double prevSimTime = 0;
258 
259         /**
260          * Create the handler for Servlet requests.
261          * @param webServer OTSWebServer; web server for callback of actions
262          */
263         public XHRHandler(final OTSWebServer webServer)
264         {
265             this.webServer = webServer;
266         }
267 
268         @Override
269         public void handle(final String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response)
270                 throws IOException, ServletException
271         {
272             // System.out.println("target=" + target);
273             // System.out.println("baseRequest=" + baseRequest);
274             // System.out.println("request=" + request);
275 
276             Map<String, String[]> params = request.getParameterMap();
277             // System.out.println(params);
278 
279             String answer = "<message>ok</message>";
280 
281             if (request.getParameter("message") != null)
282             {
283                 String message = request.getParameter("message");
284                 String[] parts = message.split("\\|");
285                 String command = parts[0];
286                 HTMLAnimationPanel animationPanel = this.webServer.getAnimationPanel();
287 
288                 switch (command)
289                 {
290                     case "getTitle":
291                     {
292                         answer = "<title>" + this.webServer.getTitle() + "</title>";
293                         break;
294                     }
295 
296                     case "init":
297                     {
298                         boolean simOk = this.webServer.getSimulator() != null;
299                         boolean started = simOk ? this.webServer.getSimulator().isStartingOrRunning() : false;
300                         answer = controlButtonResponse(simOk, started);
301                         break;
302                     }
303 
304                     case "windowSize":
305                     {
306                         if (parts.length != 3)
307                             System.err.println("wrong windowSize commmand: " + message);
308                         else
309                         {
310                             int width = Integer.parseInt(parts[1]);
311                             int height = Integer.parseInt(parts[2]);
312                             animationPanel.setSize(new Dimension(width, height));
313                         }
314                         break;
315                     }
316 
317                     case "startStop":
318                     {
319                         boolean simOk = this.webServer.getSimulator() != null;
320                         boolean started = simOk ? this.webServer.getSimulator().isStartingOrRunning() : false;
321                         if (simOk && started)
322                             started = !this.webServer.stopSimulator();
323                         else if (simOk && !started)
324                             started = this.webServer.startSimulator();
325                         answer = controlButtonResponse(simOk, started);
326                         break;
327                     }
328 
329                     case "oneEvent":
330                     {
331                         // TODO
332                         boolean started = false;
333                         answer = controlButtonResponse(this.webServer.getSimulator() != null, started);
334                         break;
335                     }
336 
337                     case "allEvents":
338                     {
339                         // TODO
340                         boolean started = false;
341                         answer = controlButtonResponse(this.webServer.getSimulator() != null, started);
342                         break;
343                     }
344 
345                     case "reset":
346                     {
347                         // TODO
348                         boolean started = false;
349                         answer = controlButtonResponse(this.webServer.getSimulator() != null, started);
350                         break;
351                     }
352 
353                     case "animate":
354                     {
355                         answer = animationPanel.getDrawingCommands();
356                         break;
357                     }
358 
359                     case "arrowDown":
360                     {
361                         animationPanel.pan(HTMLGridPanel.DOWN, 0.1);
362                         break;
363                     }
364 
365                     case "arrowUp":
366                     {
367                         animationPanel.pan(HTMLGridPanel.UP, 0.1);
368                         break;
369                     }
370 
371                     case "arrowLeft":
372                     {
373                         animationPanel.pan(HTMLGridPanel.LEFT, 0.1);
374                         break;
375                     }
376 
377                     case "arrowRight":
378                     {
379                         animationPanel.pan(HTMLGridPanel.RIGHT, 0.1);
380                         break;
381                     }
382 
383                     case "pan":
384                     {
385                         if (parts.length == 3)
386                         {
387                             int dx = Integer.parseInt(parts[1]);
388                             int dy = Integer.parseInt(parts[2]);
389                             double scaleX = animationPanel.getRenderableScale().getXScale(animationPanel.getExtent(),
390                                     animationPanel.getSize());
391                             double scaleY = animationPanel.getRenderableScale().getYScale(animationPanel.getExtent(),
392                                     animationPanel.getSize());
393                             Bounds2d extent = animationPanel.getExtent();
394                             animationPanel.setExtent(new Bounds2d(extent.getMinX() - dx * scaleX,
395                                     extent.getMinX() - dx * scaleX + extent.getDeltaX(), extent.getMinY() + dy * scaleY,
396                                     extent.getMinY() + dy * scaleY + extent.getDeltaY()));
397                         }
398                         break;
399                     }
400 
401                     case "introspect":
402                     {
403                         if (parts.length == 3)
404                         {
405                             int x = Integer.parseInt(parts[1]);
406                             int y = Integer.parseInt(parts[2]);
407                             List<Locatable> targets = new ArrayList<Locatable>();
408                             try
409                             {
410                                 Point2d point = animationPanel.getRenderableScale().getWorldCoordinates(
411                                         new Point2D.Double(x, y), animationPanel.getExtent(), animationPanel.getSize());
412                                 for (Renderable2DInterface<?> renderable : animationPanel.getElements())
413                                 {
414                                     if (animationPanel.isShowElement(renderable)
415                                             && renderable.contains(point, animationPanel.getExtent()))
416                                     {
417                                         if (renderable.getSource() instanceof GTU)
418                                         {
419                                             targets.add(renderable.getSource());
420                                         }
421                                     }
422                                 }
423                             }
424                             catch (Exception exception)
425                             {
426                                 this.webServer.getSimulator().getLogger().always().warn(exception, "getSelectedObjects");
427                             }
428                             if (targets.size() > 0)
429                             {
430                                 Object introspectedObject = targets.get(0);
431                                 Property[] properties = new BeanIntrospector().getProperties(introspectedObject);
432                                 SortedMap<String, Property> propertyMap = new TreeMap<>();
433                                 for (Property property : properties)
434                                     propertyMap.put(property.getName(), property);
435                                 answer = "<introspection>\n";
436                                 for (Property property : propertyMap.values())
437                                 {
438                                     answer += "<property><field>" + property.getName() + "</field><value>" + property.getValue()
439                                             + "</value></property>\n";
440                                 }
441                                 answer += "<introspection>\n";
442                             }
443                             else
444                             {
445                                 answer = "<none />";
446                             }
447                         }
448                         break;
449                     }
450 
451                     case "zoomIn":
452                     {
453                         if (parts.length == 1)
454                             animationPanel.zoom(0.9);
455                         else
456                         {
457                             int x = Integer.parseInt(parts[1]);
458                             int y = Integer.parseInt(parts[2]);
459                             animationPanel.zoom(0.9, x, y);
460                         }
461                         break;
462                     }
463 
464                     case "zoomOut":
465                     {
466                         if (parts.length == 1)
467                             animationPanel.zoom(1.1);
468                         else
469                         {
470                             int x = Integer.parseInt(parts[1]);
471                             int y = Integer.parseInt(parts[2]);
472                             animationPanel.zoom(1.1, x, y);
473                         }
474                         break;
475                     }
476 
477                     case "zoomAll":
478                     {
479                         animationPanel.zoomAll();
480                         break;
481                     }
482 
483                     case "home":
484                     {
485                         animationPanel.home();
486                         break;
487                     }
488 
489                     case "toggleGrid":
490                     {
491                         animationPanel.setShowGrid(!animationPanel.isShowGrid());
492                         break;
493                     }
494 
495                     case "getTime":
496                     {
497                         double now = Math.round(this.webServer.getSimulator().getSimulatorTime().si * 1000) / 1000d;
498                         int seconds = (int) Math.floor(now);
499                         int fractionalSeconds = (int) Math.floor(1000 * (now - seconds));
500                         String timeText = String.format("  %02d:%02d:%02d.%03d  ", seconds / 3600, seconds / 60 % 60,
501                                 seconds % 60, fractionalSeconds);
502                         answer = timeText;
503                         break;
504                     }
505 
506                     case "getSpeed":
507                     {
508                         double simTime = this.webServer.getSimulator().getSimulatorTime().si;
509                         double speed = getSimulationSpeed(simTime);
510                         String speedText = "";
511                         if (!Double.isNaN(speed))
512                         {
513                             speedText = String.format("% 5.2fx  ", speed);
514                         }
515                         answer = speedText;
516                         break;
517                     }
518 
519                     case "getToggles":
520                     {
521                         answer = getToggles(animationPanel);
522                         break;
523                     }
524 
525                     // we expect something of the form toggle|class|Node|true or toggle|gis|streets|false
526                     case "toggle":
527                     {
528                         if (parts.length != 4)
529                             System.err.println("wrong toggle commmand: " + message);
530                         else
531                         {
532                             String toggleName = parts[1];
533                             boolean gis = parts[2].equals("gis");
534                             boolean show = parts[3].equals("true");
535                             if (gis)
536                             {
537                                 if (show)
538                                     animationPanel.showGISLayer(toggleName);
539                                 else
540                                     animationPanel.hideGISLayer(toggleName);
541                             }
542                             else
543                             {
544                                 if (show)
545                                     animationPanel.showClass(toggleName);
546                                 else
547                                     animationPanel.hideClass(toggleName);
548                             }
549                         }
550                         break;
551                     }
552 
553                     default:
554                     {
555                         System.err.println("Got unknown message from client: " + command);
556                         answer = "<message>" + request.getParameter("message") + "</message>";
557                         break;
558                     }
559                 }
560             }
561 
562             if (request.getParameter("slider") != null)
563             {
564                 // System.out.println(request.getParameter("slider") + "\n");
565                 try
566                 {
567                     int value = Integer.parseInt(request.getParameter("slider"));
568                     // values range from 100 to 1400. 100 = 0.1, 400 = 1, 1399 = infinite
569                     double speedFactor = 1.0;
570                     if (value > 1398)
571                         speedFactor = Double.MAX_VALUE;
572                     else
573                         speedFactor = Math.pow(2.15444, value / 100.0) / 21.5444;
574                     this.webServer.setSpeedFactor(speedFactor);
575                     // System.out.println("speed factor changed to " + speedFactor);
576                 }
577                 catch (NumberFormatException exception)
578                 {
579                     answer = "<message>Error: " + exception.getMessage() + "</message>";
580                 }
581             }
582 
583             // System.out.println(answer);
584 
585             response.setContentType("text/xml");
586             response.setHeader("Cache-Control", "no-cache");
587             response.setContentLength(answer.length());
588             response.setStatus(HttpServletResponse.SC_OK);
589             response.getWriter().write(answer);
590             response.flushBuffer();
591             baseRequest.setHandled(true);
592         }
593 
594         /**
595          * @param active boolean; is the simulation active?
596          * @param started boolean; has the simulation been started?
597          * @return XML message to send to the server
598          */
599         private String controlButtonResponse(final boolean active, final boolean started)
600         {
601             if (!active)
602             {
603                 return "<controls>\n" + "<oneEventActive>false</oneEventActive>\n"
604                         + "<allEventsActive>false</allEventsActive>\n" + "<startStop>start</startStop>\n"
605                         + "<startStopActive>false</startStopActive>\n" + "<resetActive>false</resetActive>\n" + "</controls>\n";
606             }
607             if (started)
608             {
609                 return "<controls>\n" + "<oneEventActive>false</oneEventActive>\n"
610                         + "<allEventsActive>false</allEventsActive>\n" + "<startStop>stop</startStop>\n"
611                         + "<startStopActive>true</startStopActive>\n" + "<resetActive>false</resetActive>\n" + "</controls>\n";
612             }
613             else
614             {
615                 return "<controls>\n" + "<oneEventActive>true</oneEventActive>\n" + "<allEventsActive>true</allEventsActive>\n"
616                         + "<startStop>start</startStop>\n" + "<startStopActive>true</startStopActive>\n"
617                         + "<resetActive>false</resetActive>\n" + "</controls>\n";
618             }
619         }
620 
621         /**
622          * Return the toggle button info for the toggle panel.
623          * @param panel HTMLAnimationPanel; the HTMLAnimationPanel
624          * @return the String that can be parsed by the select.html iframe
625          */
626         private String getToggles(final HTMLAnimationPanel panel)
627         {
628             String ret = "<toggles>\n";
629             for (ToggleButtonInfo toggle : panel.getToggleButtons())
630             {
631                 if (toggle instanceof ToggleButtonInfo.Text)
632                 {
633                     ret += "<text>" + toggle.getName() + "</text>\n";
634                 }
635                 else if (toggle instanceof ToggleButtonInfo.LocatableClass)
636                 {
637                     ret += "<class>" + toggle.getName() + "," + toggle.isVisible() + "</class>\n";
638                 }
639                 else if (toggle instanceof ToggleButtonInfo.Gis)
640                 {
641                     ret += "<gis>" + toggle.getName() + "," + ((ToggleButtonInfo.Gis) toggle).getLayerName() + ","
642                             + toggle.isVisible() + "</gis>\n";
643                 }
644             }
645             ret += "</toggles>\n";
646             return ret;
647         }
648 
649         /**
650          * Returns the simulation speed.
651          * @param simTime double; simulation time
652          * @return simulation speed
653          */
654         private double getSimulationSpeed(final double simTime)
655         {
656             long now = System.currentTimeMillis();
657             if (this.lastWallTIme < 0 || this.lastWallTIme == now)
658             {
659                 this.lastWallTIme = now;
660                 this.prevSimTime = simTime;
661                 return Double.NaN;
662             }
663             double speed = (simTime - this.prevSimTime) / (0.001 * (now - this.lastWallTIme));
664             this.prevSimTime = simTime;
665             this.lastWallTIme = now;
666             return speed;
667         }
668 
669     }
670 
671 }