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.awt.geom.Rectangle2D;
6   import java.io.IOException;
7   import java.net.URL;
8   import java.rmi.RemoteException;
9   import java.util.ArrayList;
10  import java.util.List;
11  import java.util.Map;
12  import java.util.SortedMap;
13  import java.util.TreeMap;
14  
15  import javax.servlet.ServletException;
16  import javax.servlet.http.HttpServletRequest;
17  import javax.servlet.http.HttpServletResponse;
18  
19  import org.djutils.event.EventInterface;
20  import org.djutils.event.EventListenerInterface;
21  import org.djutils.event.TimedEvent;
22  import org.djutils.io.URLResource;
23  import org.eclipse.jetty.server.Handler;
24  import org.eclipse.jetty.server.Request;
25  import org.eclipse.jetty.server.Server;
26  import org.eclipse.jetty.server.handler.AbstractHandler;
27  import org.eclipse.jetty.server.handler.HandlerList;
28  import org.eclipse.jetty.server.handler.ResourceHandler;
29  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
30  import org.opentrafficsim.core.gtu.GTU;
31  import org.opentrafficsim.web.animation.WebAnimationToggles;
32  
33  import nl.tudelft.simulation.dsol.SimRuntimeException;
34  import nl.tudelft.simulation.dsol.animation.Locatable;
35  import nl.tudelft.simulation.dsol.animation.D2.Renderable2DInterface;
36  import nl.tudelft.simulation.dsol.experiment.Replication;
37  import nl.tudelft.simulation.dsol.simulators.AnimatorInterface;
38  import nl.tudelft.simulation.dsol.simulators.DEVSRealTimeClock;
39  import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
40  import nl.tudelft.simulation.dsol.web.animation.D2.HTMLAnimationPanel;
41  import nl.tudelft.simulation.dsol.web.animation.D2.HTMLGridPanel;
42  import nl.tudelft.simulation.dsol.web.animation.D2.ToggleButtonInfo;
43  import nl.tudelft.simulation.introspection.Property;
44  import nl.tudelft.simulation.introspection.beans.BeanIntrospector;
45  
46  /**
47   * DSOLWebServer.java. <br>
48   * <br>
49   * Copyright (c) 2003-2020 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
50   * for project information <a href="https://www.simulation.tudelft.nl/" target="_blank">www.simulation.tudelft.nl</a>. The
51   * source code and binary code of this software is proprietary information of Delft University of Technology.
52   * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
53   */
54  public abstract class OTSWebServer implements EventListenerInterface
55  {
56      /** the title for the model window. */
57      private final String title;
58  
59      /** the simulator. */
60      private final OTSSimulatorInterface simulator;
61  
62      /** dirty flag for the controls: when the model e.g. stops, the status needs to be changed. */
63      private boolean dirtyControls = false;
64  
65      /** the animation panel. */
66      private HTMLAnimationPanel animationPanel;
67  
68      /**
69       * @param title String; the title for the model window
70       * @param simulator SimulatorInterface&lt;?,?,?&gt;; the simulator
71       * @param extent Rectangle2D.Double; the extent to use for the graphics (min/max coordinates)
72       * @throws Exception in case jetty crashes
73       */
74      public OTSWebServer(final String title, final OTSSimulatorInterface simulator, final Rectangle2D.Double extent)
75              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, new Dimension(800, 600), this.simulator);
93              WebAnimationToggles.setTextAnimationTogglesStandard(this.animationPanel);
94              // get the already created elements in context(/animation/D2)
95              this.animationPanel.notify(new TimedEvent(Replication.START_REPLICATION_EVENT, this.simulator.getSourceId(), null,
96                      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 DEVSRealTimeClock)
221         {
222             ((DEVSRealTimeClock<?, ?, ?>) this.simulator).setSpeedFactor(speedFactor);
223         }
224     }
225 
226     /** {@inheritDoc} */
227     @Override
228     public void notify(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      * <br>
243      * Copyright (c) 2003-2020 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
244      * See for project information <a href="https://www.simulation.tudelft.nl/" target="_blank">www.simulation.tudelft.nl</a>.
245      * The source code and binary code of this software is proprietary information of Delft University of Technology.
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 DSOLWebServer; 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(String target, Request baseRequest, HttpServletRequest request, 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 scale =
390                                     Renderable2DInterface.Util.getScale(animationPanel.getExtent(), animationPanel.getSize());
391                             Rectangle2D.Double extent = (Rectangle2D.Double) animationPanel.getExtent();
392                             extent.setRect((extent.getMinX() - dx * scale), (extent.getMinY() + dy * scale), extent.getWidth(),
393                                     extent.getHeight());
394                         }
395                         break;
396                     }
397 
398                     case "introspect":
399                     {
400                         if (parts.length == 3)
401                         {
402                             int x = Integer.parseInt(parts[1]);
403                             int y = Integer.parseInt(parts[2]);
404                             List<Locatable> targets = new ArrayList<Locatable>();
405                             try
406                             {
407                                 Point2D point = Renderable2DInterface.Util.getWorldCoordinates(new Point2D.Double(x, y),
408                                         animationPanel.getExtent(), animationPanel.getSize());
409                                 for (Renderable2DInterface<?> renderable : animationPanel.getElements())
410                                 {
411                                     if (animationPanel.isShowElement(renderable)
412                                             && renderable.contains(point, animationPanel.getExtent(), animationPanel.getSize()))
413                                     {
414                                         if (renderable.getSource() instanceof GTU)
415                                         {
416                                             targets.add(renderable.getSource());
417                                         }
418                                     }
419                                 }
420                             }
421                             catch (Exception exception)
422                             {
423                                 this.webServer.getSimulator().getLogger().always().warn(exception, "getSelectedObjects");
424                             }
425                             if (targets.size() > 0)
426                             {
427                                 Object introspectedObject = targets.get(0);
428                                 Property[] properties = new BeanIntrospector().getProperties(introspectedObject);
429                                 SortedMap<String, Property> propertyMap = new TreeMap<>();
430                                 for (Property property : properties)
431                                     propertyMap.put(property.getName(), property);
432                                 answer = "<introspection>\n";
433                                 for (Property property : propertyMap.values())
434                                 {
435                                     answer += "<property><field>" + property.getName() + "</field><value>" + property.getValue()
436                                             + "</value></property>\n";
437                                 }
438                                 answer += "<introspection>\n";
439                             }
440                             else
441                             {
442                                 answer = "<none />";
443                             }
444                         }
445                         break;
446                     }
447 
448                     case "zoomIn":
449                     {
450                         if (parts.length == 1)
451                             animationPanel.zoom(0.9);
452                         else
453                         {
454                             int x = Integer.parseInt(parts[1]);
455                             int y = Integer.parseInt(parts[2]);
456                             animationPanel.zoom(0.9, x, y);
457                         }
458                         break;
459                     }
460 
461                     case "zoomOut":
462                     {
463                         if (parts.length == 1)
464                             animationPanel.zoom(1.1);
465                         else
466                         {
467                             int x = Integer.parseInt(parts[1]);
468                             int y = Integer.parseInt(parts[2]);
469                             animationPanel.zoom(1.1, x, y);
470                         }
471                         break;
472                     }
473 
474                     case "zoomAll":
475                     {
476                         animationPanel.zoomAll();
477                         break;
478                     }
479 
480                     case "home":
481                     {
482                         animationPanel.home();
483                         break;
484                     }
485 
486                     case "toggleGrid":
487                     {
488                         animationPanel.setShowGrid(!animationPanel.isShowGrid());
489                         break;
490                     }
491 
492                     case "getTime":
493                     {
494                         double now = Math.round(this.webServer.getSimulator().getSimulatorTime().si * 1000) / 1000d;
495                         int seconds = (int) Math.floor(now);
496                         int fractionalSeconds = (int) Math.floor(1000 * (now - seconds));
497                         String timeText = String.format("  %02d:%02d:%02d.%03d  ", seconds / 3600, seconds / 60 % 60,
498                                 seconds % 60, fractionalSeconds);
499                         answer = timeText;
500                         break;
501                     }
502 
503                     case "getSpeed":
504                     {
505                         double simTime = this.webServer.getSimulator().getSimulatorTime().si;
506                         double speed = getSimulationSpeed(simTime);
507                         String speedText = "";
508                         if (!Double.isNaN(speed))
509                         {
510                             speedText = String.format("% 5.2fx  ", speed);
511                         }
512                         answer = speedText;
513                         break;
514                     }
515 
516                     case "getToggles":
517                     {
518                         answer = getToggles(animationPanel);
519                         break;
520                     }
521 
522                     // we expect something of the form toggle|class|Node|true or toggle|gis|streets|false
523                     case "toggle":
524                     {
525                         if (parts.length != 4)
526                             System.err.println("wrong toggle commmand: " + message);
527                         else
528                         {
529                             String toggleName = parts[1];
530                             boolean gis = parts[2].equals("gis");
531                             boolean show = parts[3].equals("true");
532                             if (gis)
533                             {
534                                 if (show)
535                                     animationPanel.showGISLayer(toggleName);
536                                 else
537                                     animationPanel.hideGISLayer(toggleName);
538                             }
539                             else
540                             {
541                                 if (show)
542                                     animationPanel.showClass(toggleName);
543                                 else
544                                     animationPanel.hideClass(toggleName);
545                             }
546                         }
547                         break;
548                     }
549 
550                     default:
551                     {
552                         System.err.println("Got unknown message from client: " + command);
553                         answer = "<message>" + request.getParameter("message") + "</message>";
554                         break;
555                     }
556                 }
557             }
558 
559             if (request.getParameter("slider") != null)
560             {
561                 // System.out.println(request.getParameter("slider") + "\n");
562                 try
563                 {
564                     int value = Integer.parseInt(request.getParameter("slider"));
565                     // values range from 100 to 1400. 100 = 0.1, 400 = 1, 1399 = infinite
566                     double speedFactor = 1.0;
567                     if (value > 1398)
568                         speedFactor = Double.MAX_VALUE;
569                     else
570                         speedFactor = Math.pow(2.15444, value / 100.0) / 21.5444;
571                     this.webServer.setSpeedFactor(speedFactor);
572                     // System.out.println("speed factor changed to " + speedFactor);
573                 }
574                 catch (NumberFormatException exception)
575                 {
576                     answer = "<message>Error: " + exception.getMessage() + "</message>";
577                 }
578             }
579 
580             // System.out.println(answer);
581 
582             response.setContentType("text/xml");
583             response.setHeader("Cache-Control", "no-cache");
584             response.setContentLength(answer.length());
585             response.setStatus(HttpServletResponse.SC_OK);
586             response.getWriter().write(answer);
587             response.flushBuffer();
588             baseRequest.setHandled(true);
589         }
590 
591         /**
592          * @param active boolean; is the simulation active?
593          * @param started boolean; has the simulation been started?
594          * @return XML message to send to the server
595          */
596         private String controlButtonResponse(final boolean active, final boolean started)
597         {
598             if (!active)
599             {
600                 return "<controls>\n" + "<oneEventActive>false</oneEventActive>\n"
601                         + "<allEventsActive>false</allEventsActive>\n" + "<startStop>start</startStop>\n"
602                         + "<startStopActive>false</startStopActive>\n" + "<resetActive>false</resetActive>\n" + "</controls>\n";
603             }
604             if (started)
605             {
606                 return "<controls>\n" + "<oneEventActive>false</oneEventActive>\n"
607                         + "<allEventsActive>false</allEventsActive>\n" + "<startStop>stop</startStop>\n"
608                         + "<startStopActive>true</startStopActive>\n" + "<resetActive>false</resetActive>\n" + "</controls>\n";
609             }
610             else
611             {
612                 return "<controls>\n" + "<oneEventActive>true</oneEventActive>\n" + "<allEventsActive>true</allEventsActive>\n"
613                         + "<startStop>start</startStop>\n" + "<startStopActive>true</startStopActive>\n"
614                         + "<resetActive>false</resetActive>\n" + "</controls>\n";
615             }
616         }
617 
618         /**
619          * Return the toggle button info for the toggle panel.
620          * @param panel the HTMLAnimationPanel
621          * @return the String that can be parsed by the select.html iframe
622          */
623         private String getToggles(final HTMLAnimationPanel panel)
624         {
625             String ret = "<toggles>\n";
626             for (ToggleButtonInfo toggle : panel.getToggleButtons())
627             {
628                 if (toggle instanceof ToggleButtonInfo.Text)
629                 {
630                     ret += "<text>" + toggle.getName() + "</text>\n";
631                 }
632                 else if (toggle instanceof ToggleButtonInfo.LocatableClass)
633                 {
634                     ret += "<class>" + toggle.getName() + "," + toggle.isVisible() + "</class>\n";
635                 }
636                 else if (toggle instanceof ToggleButtonInfo.Gis)
637                 {
638                     ret += "<gis>" + toggle.getName() + "," + ((ToggleButtonInfo.Gis) toggle).getLayerName() + ","
639                             + toggle.isVisible() + "</gis>\n";
640                 }
641             }
642             ret += "</toggles>\n";
643             return ret;
644         }
645 
646         /**
647          * Returns the simulation speed.
648          * @param simTime double; simulation time
649          * @return simulation speed
650          */
651         private double getSimulationSpeed(final double simTime)
652         {
653             long now = System.currentTimeMillis();
654             if (this.lastWallTIme < 0 || this.lastWallTIme == now)
655             {
656                 this.lastWallTIme = now;
657                 this.prevSimTime = simTime;
658                 return Double.NaN;
659             }
660             double speed = (simTime - this.prevSimTime) / (0.001 * (now - this.lastWallTIme));
661             this.prevSimTime = simTime;
662             this.lastWallTIme = now;
663             return speed;
664         }
665 
666     }
667 
668 }