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.Event;
20  import org.djutils.event.EventInterface;
21  import org.djutils.event.EventListenerInterface;
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.simulators.AnimatorInterface;
37  import nl.tudelft.simulation.dsol.simulators.DEVSRealTimeClock;
38  import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
39  import nl.tudelft.simulation.dsol.web.animation.D2.HTMLAnimationPanel;
40  import nl.tudelft.simulation.dsol.web.animation.D2.HTMLGridPanel;
41  import nl.tudelft.simulation.dsol.web.animation.D2.ToggleButtonInfo;
42  import nl.tudelft.simulation.introspection.Property;
43  import nl.tudelft.simulation.introspection.beans.BeanIntrospector;
44  
45  /**
46   * DSOLWebServer.java. <br>
47   * <br>
48   * Copyright (c) 2003-2020 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
49   * for project information <a href="https://www.simulation.tudelft.nl/" target="_blank">www.simulation.tudelft.nl</a>. The
50   * source code and binary code of this software is proprietary information of Delft University of Technology.
51   * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
52   */
53  public abstract class OTSWebServer implements EventListenerInterface
54  {
55      /** the title for the model window. */
56      private final String title;
57  
58      /** the simulator. */
59      private final OTSSimulatorInterface simulator;
60  
61      /** dirty flag for the controls: when the model e.g. stops, the status needs to be changed. */
62      private boolean dirtyControls = false;
63  
64      /** the animation panel. */
65      private HTMLAnimationPanel animationPanel;
66  
67      /**
68       * @param title String; the title for the model window
69       * @param simulator SimulatorInterface&lt;?,?,?&gt;; the simulator
70       * @param extent Rectangle2D.Double; the extent to use for the graphics (min/max coordinates)
71       * @throws Exception in case jetty crashes
72       */
73      public OTSWebServer(final String title, final OTSSimulatorInterface simulator, final Rectangle2D.Double extent)
74              throws Exception
75      {
76          this.title = title;
77  
78          this.simulator = simulator;
79          try
80          {
81              simulator.addListener(this, SimulatorInterface.START_EVENT);
82              simulator.addListener(this, SimulatorInterface.STOP_EVENT);
83          }
84          catch (RemoteException re)
85          {
86              simulator.getLogger().always().warn(re, "Problem adding listeners to Simulator");
87          }
88  
89          if (this.simulator instanceof AnimatorInterface)
90          {
91              this.animationPanel = new HTMLAnimationPanel(extent, new Dimension(800, 600), this.simulator);
92              WebAnimationToggles.setTextAnimationTogglesStandard(this.animationPanel);
93              // get the already created elements in context(/animation/D2)
94              this.animationPanel.notify(
95                      new Event(SimulatorInterface.START_REPLICATION_EVENT, this.simulator, this.simulator.getSimulatorTime()));
96          }
97  
98          new ServerThread().start();
99      }
100 
101     /** Handle in separate thread to avoid 'lock' of the main application. */
102     class ServerThread extends Thread
103     {
104         @Override
105         public void run()
106         {
107             Server server = new Server(8080);
108             ResourceHandler resourceHandler = new ResourceHandler();
109 
110             // root folder; to work in Eclipse, as an external jar, and in an embedded jar
111             URL homeFolder = URLResource.getResource("/home");
112             String webRoot = homeFolder.toExternalForm();
113             System.out.println("webRoot is " + webRoot);
114 
115             resourceHandler.setDirectoriesListed(true);
116             resourceHandler.setWelcomeFiles(new String[] {"index.html"});
117             resourceHandler.setResourceBase(webRoot);
118 
119             HandlerList handlers = new HandlerList();
120             handlers.setHandlers(new Handler[] {resourceHandler, new XHRHandler(OTSWebServer.this)});
121             server.setHandler(handlers);
122 
123             try
124             {
125                 server.start();
126                 server.join();
127             }
128             catch (Exception exception)
129             {
130                 exception.printStackTrace();
131             }
132         }
133     }
134 
135     /**
136      * @return title
137      */
138     public final String getTitle()
139     {
140         return this.title;
141     }
142 
143     /**
144      * @return simulator
145      */
146     public final OTSSimulatorInterface getSimulator()
147     {
148         return this.simulator;
149     }
150 
151     /**
152      * @return animationPanel
153      */
154     public final HTMLAnimationPanel getAnimationPanel()
155     {
156         return this.animationPanel;
157     }
158 
159     /**
160      * Try to start the simulator, and return whether the simulator has been started.
161      * @return whether the simulator has been started or not
162      */
163     protected boolean startSimulator()
164     {
165         if (getSimulator() == null)
166         {
167             System.out.println("SIMULATOR == NULL");
168             return false;
169         }
170         try
171         {
172             System.out.println("START THE SIMULATOR");
173             getSimulator().start();
174         }
175         catch (SimRuntimeException exception)
176         {
177             getSimulator().getLogger().always().warn(exception, "Problem starting Simulator");
178         }
179         if (getSimulator().isRunning())
180         {
181             return true;
182         }
183         this.dirtyControls = false; // undo the notification
184         return false;
185     }
186 
187     /**
188      * Try to stop the simulator, and return whether the simulator has been stopped.
189      * @return whether the simulator has been stopped or not
190      */
191     protected boolean stopSimulator()
192     {
193         if (getSimulator() == null)
194         {
195             return true;
196         }
197         try
198         {
199             System.out.println("STOP THE SIMULATOR");
200             getSimulator().stop();
201         }
202         catch (SimRuntimeException exception)
203         {
204             getSimulator().getLogger().always().warn(exception, "Problem stopping Simulator");
205         }
206         if (!getSimulator().isRunning())
207         {
208             return true;
209         }
210         this.dirtyControls = false; // undo the notification
211         return false;
212     }
213 
214     /**
215      * @param speedFactor double; the new speed factor
216      */
217     protected void setSpeedFactor(final double speedFactor)
218     {
219         if (this.simulator instanceof DEVSRealTimeClock)
220         {
221             ((DEVSRealTimeClock<?, ?, ?>) this.simulator).setSpeedFactor(speedFactor);
222         }
223     }
224 
225     /** {@inheritDoc} */
226     @Override
227     public void notify(EventInterface 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      * <br>
242      * Copyright (c) 2003-2020 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
243      * See for project information <a href="https://www.simulation.tudelft.nl/" target="_blank">www.simulation.tudelft.nl</a>.
244      * The source code and binary code of this software is proprietary information of Delft University of Technology.
245      * @author <a href="https://www.tudelft.nl/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 DSOLWebServer; 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(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
269                 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().isRunning() : 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().isRunning() : 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 scale =
389                                     Renderable2DInterface.Util.getScale(animationPanel.getExtent(), animationPanel.getSize());
390                             Rectangle2D.Double extent = (Rectangle2D.Double) animationPanel.getExtent();
391                             extent.setRect((extent.getMinX() - dx * scale), (extent.getMinY() + dy * scale), extent.getWidth(),
392                                     extent.getHeight());
393                         }
394                         break;
395                     }
396 
397                     case "introspect":
398                     {
399                         if (parts.length == 3)
400                         {
401                             int x = Integer.parseInt(parts[1]);
402                             int y = Integer.parseInt(parts[2]);
403                             List<Locatable> targets = new ArrayList<Locatable>();
404                             try
405                             {
406                                 Point2D point = Renderable2DInterface.Util.getWorldCoordinates(new Point2D.Double(x, y),
407                                         animationPanel.getExtent(), animationPanel.getSize());
408                                 for (Renderable2DInterface<?> renderable : animationPanel.getElements())
409                                 {
410                                     if (animationPanel.isShowElement(renderable)
411                                             && renderable.contains(point, animationPanel.getExtent(), animationPanel.getSize()))
412                                     {
413                                         if (renderable.getSource() instanceof GTU)
414                                         {
415                                             targets.add(renderable.getSource());
416                                         }
417                                     }
418                                 }
419                             }
420                             catch (Exception exception)
421                             {
422                                 this.webServer.getSimulator().getLogger().always().warn(exception, "getSelectedObjects");
423                             }
424                             if (targets.size() > 0)
425                             {
426                                 Object introspectedObject = targets.get(0);
427                                 Property[] properties = new BeanIntrospector().getProperties(introspectedObject);
428                                 SortedMap<String, Property> propertyMap = new TreeMap<>();
429                                 for (Property property : properties)
430                                     propertyMap.put(property.getName(), property);
431                                 answer = "<introspection>\n";
432                                 for (Property property : propertyMap.values())
433                                 {
434                                     answer += "<property><field>" + property.getName() + "</field><value>" + property.getValue()
435                                             + "</value></property>\n";
436                                 }
437                                 answer += "<introspection>\n";
438                             }
439                             else
440                             {
441                                 answer = "<none />";
442                             }
443                         }
444                         break;
445                     }
446 
447                     case "zoomIn":
448                     {
449                         if (parts.length == 1)
450                             animationPanel.zoom(0.9);
451                         else
452                         {
453                             int x = Integer.parseInt(parts[1]);
454                             int y = Integer.parseInt(parts[2]);
455                             animationPanel.zoom(0.9, x, y);
456                         }
457                         break;
458                     }
459 
460                     case "zoomOut":
461                     {
462                         if (parts.length == 1)
463                             animationPanel.zoom(1.1);
464                         else
465                         {
466                             int x = Integer.parseInt(parts[1]);
467                             int y = Integer.parseInt(parts[2]);
468                             animationPanel.zoom(1.1, x, y);
469                         }
470                         break;
471                     }
472 
473                     case "zoomAll":
474                     {
475                         animationPanel.zoomAll();
476                         break;
477                     }
478 
479                     case "home":
480                     {
481                         animationPanel.home();
482                         break;
483                     }
484 
485                     case "toggleGrid":
486                     {
487                         animationPanel.setShowGrid(!animationPanel.isShowGrid());
488                         break;
489                     }
490 
491                     case "getTime":
492                     {
493                         double now = Math.round(this.webServer.getSimulator().getSimulatorTime().si * 1000) / 1000d;
494                         int seconds = (int) Math.floor(now);
495                         int fractionalSeconds = (int) Math.floor(1000 * (now - seconds));
496                         String timeText = String.format("  %02d:%02d:%02d.%03d  ", seconds / 3600, seconds / 60 % 60,
497                                 seconds % 60, fractionalSeconds);
498                         answer = timeText;
499                         break;
500                     }
501 
502                     case "getSpeed":
503                     {
504                         double simTime = this.webServer.getSimulator().getSimulatorTime().si;
505                         double speed = getSimulationSpeed(simTime);
506                         String speedText = "";
507                         if (!Double.isNaN(speed))
508                         {
509                             speedText = String.format("% 5.2fx  ", speed);
510                         }
511                         answer = speedText;
512                         break;
513                     }
514 
515                     case "getToggles":
516                     {
517                         answer = getToggles(animationPanel);
518                         break;
519                     }
520 
521                     // we expect something of the form toggle|class|Node|true or toggle|gis|streets|false
522                     case "toggle":
523                     {
524                         if (parts.length != 4)
525                             System.err.println("wrong toggle commmand: " + message);
526                         else
527                         {
528                             String toggleName = parts[1];
529                             boolean gis = parts[2].equals("gis");
530                             boolean show = parts[3].equals("true");
531                             if (gis)
532                             {
533                                 if (show)
534                                     animationPanel.showGISLayer(toggleName);
535                                 else
536                                     animationPanel.hideGISLayer(toggleName);
537                             }
538                             else
539                             {
540                                 if (show)
541                                     animationPanel.showClass(toggleName);
542                                 else
543                                     animationPanel.hideClass(toggleName);
544                             }
545                         }
546                         break;
547                     }
548 
549                     default:
550                     {
551                         System.err.println("Got unknown message from client: " + command);
552                         answer = "<message>" + request.getParameter("message") + "</message>";
553                         break;
554                     }
555                 }
556             }
557 
558             if (request.getParameter("slider") != null)
559             {
560                 // System.out.println(request.getParameter("slider") + "\n");
561                 try
562                 {
563                     int value = Integer.parseInt(request.getParameter("slider"));
564                     // values range from 100 to 1400. 100 = 0.1, 400 = 1, 1399 = infinite
565                     double speedFactor = 1.0;
566                     if (value > 1398)
567                         speedFactor = Double.MAX_VALUE;
568                     else
569                         speedFactor = Math.pow(2.15444, value / 100.0) / 21.5444;
570                     this.webServer.setSpeedFactor(speedFactor);
571                     // System.out.println("speed factor changed to " + speedFactor);
572                 }
573                 catch (NumberFormatException exception)
574                 {
575                     answer = "<message>Error: " + exception.getMessage() + "</message>";
576                 }
577             }
578 
579             // System.out.println(answer);
580 
581             response.setContentType("text/xml");
582             response.setHeader("Cache-Control", "no-cache");
583             response.setContentLength(answer.length());
584             response.setStatus(HttpServletResponse.SC_OK);
585             response.getWriter().write(answer);
586             response.flushBuffer();
587             baseRequest.setHandled(true);
588         }
589 
590         /**
591          * @param active boolean; is the simulation active?
592          * @param started boolean; has the simulation been started?
593          * @return XML message to send to the server
594          */
595         private String controlButtonResponse(final boolean active, final boolean started)
596         {
597             if (!active)
598             {
599                 return "<controls>\n" + "<oneEventActive>false</oneEventActive>\n"
600                         + "<allEventsActive>false</allEventsActive>\n" + "<startStop>start</startStop>\n"
601                         + "<startStopActive>false</startStopActive>\n" + "<resetActive>false</resetActive>\n" + "</controls>\n";
602             }
603             if (started)
604             {
605                 return "<controls>\n" + "<oneEventActive>false</oneEventActive>\n"
606                         + "<allEventsActive>false</allEventsActive>\n" + "<startStop>stop</startStop>\n"
607                         + "<startStopActive>true</startStopActive>\n" + "<resetActive>false</resetActive>\n" + "</controls>\n";
608             }
609             else
610             {
611                 return "<controls>\n" + "<oneEventActive>true</oneEventActive>\n" + "<allEventsActive>true</allEventsActive>\n"
612                         + "<startStop>start</startStop>\n" + "<startStopActive>true</startStopActive>\n"
613                         + "<resetActive>false</resetActive>\n" + "</controls>\n";
614             }
615         }
616 
617         /**
618          * Return the toggle button info for the toggle panel.
619          * @param panel the HTMLAnimationPanel
620          * @return the String that can be parsed by the select.html iframe
621          */
622         private String getToggles(final HTMLAnimationPanel panel)
623         {
624             String ret = "<toggles>\n";
625             for (ToggleButtonInfo toggle : panel.getToggleButtons())
626             {
627                 if (toggle instanceof ToggleButtonInfo.Text)
628                 {
629                     ret += "<text>" + toggle.getName() + "</text>\n";
630                 }
631                 else if (toggle instanceof ToggleButtonInfo.LocatableClass)
632                 {
633                     ret += "<class>" + toggle.getName() + "," + toggle.isVisible() + "</class>\n";
634                 }
635                 else if (toggle instanceof ToggleButtonInfo.Gis)
636                 {
637                     ret += "<gis>" + toggle.getName() + "," + ((ToggleButtonInfo.Gis) toggle).getLayerName() + ","
638                             + toggle.isVisible() + "</gis>\n";
639                 }
640             }
641             ret += "</toggles>\n";
642             return ret;
643         }
644 
645         /**
646          * Returns the simulation speed.
647          * @param simTime double; simulation time
648          * @return simulation speed
649          */
650         private double getSimulationSpeed(final double simTime)
651         {
652             long now = System.currentTimeMillis();
653             if (this.lastWallTIme < 0 || this.lastWallTIme == now)
654             {
655                 this.lastWallTIme = now;
656                 this.prevSimTime = simTime;
657                 return Double.NaN;
658             }
659             double speed = (simTime - this.prevSimTime) / (0.001 * (now - this.lastWallTIme));
660             this.prevSimTime = simTime;
661             this.lastWallTIme = now;
662             return speed;
663         }
664 
665     }
666 
667 }