View Javadoc
1   package org.opentrafficsim.web;
2   
3   import java.awt.Dimension;
4   import java.awt.geom.Point2D;
5   import java.net.URL;
6   import java.util.ArrayList;
7   import java.util.List;
8   import java.util.SortedMap;
9   import java.util.TreeMap;
10  
11  import org.djunits.value.vdouble.scalar.Duration;
12  import org.djutils.draw.bounds.Bounds2d;
13  import org.djutils.draw.point.Point2d;
14  import org.djutils.event.Event;
15  import org.djutils.event.EventListener;
16  import org.djutils.event.TimedEvent;
17  import org.djutils.io.ResourceResolver;
18  import org.eclipse.jetty.io.Content;
19  import org.eclipse.jetty.server.Handler;
20  import org.eclipse.jetty.server.Request;
21  import org.eclipse.jetty.server.Response;
22  import org.eclipse.jetty.server.Server;
23  import org.eclipse.jetty.server.handler.ContextHandlerCollection;
24  import org.eclipse.jetty.server.handler.ResourceHandler;
25  import org.eclipse.jetty.util.Callback;
26  import org.eclipse.jetty.util.Fields;
27  import org.opentrafficsim.base.logger.Logger;
28  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
29  import org.opentrafficsim.core.gtu.Gtu;
30  import org.opentrafficsim.web.animation.WebAnimationToggles;
31  import org.opentrafficsim.web.animation.d2.HtmlAnimationPanel;
32  import org.opentrafficsim.web.animation.d2.HtmlGridPanel;
33  import org.opentrafficsim.web.animation.d2.ToggleButtonInfo;
34  
35  import nl.tudelft.simulation.dsol.SimRuntimeException;
36  import nl.tudelft.simulation.dsol.animation.Locatable;
37  import nl.tudelft.simulation.dsol.animation.d2.Renderable2dInterface;
38  import nl.tudelft.simulation.dsol.experiment.Replication;
39  import nl.tudelft.simulation.dsol.simulators.AnimatorInterface;
40  import nl.tudelft.simulation.dsol.simulators.DevsRealTimeAnimator;
41  import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
42  import nl.tudelft.simulation.introspection.Property;
43  import nl.tudelft.simulation.introspection.beans.BeanIntrospector;
44  
45  /**
46   * DSOLWebServer.java.
47   * <p>
48   * Copyright (c) 2003-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
49   * BSD-style license. See <a href="https://opentrafficsim.org/docs/v2/license.html">OpenTrafficSim License</a>.
50   * </p>
51   * @author <a href="https://github.com/averbraeck" target="_blank">Alexander Verbraeck</a>
52   */
53  public abstract class OtsWebServer implements EventListener
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       * Constructor.
69       * @param title the title for the model window
70       * @param simulator the simulator
71       * @param extent 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 Bounds2d extent) throws Exception
75      {
76          this.title = title;
77  
78          this.simulator = simulator;
79          simulator.addListener(this, SimulatorInterface.START_EVENT);
80          simulator.addListener(this, SimulatorInterface.STOP_EVENT);
81  
82          if (this.simulator instanceof AnimatorInterface)
83          {
84              this.animationPanel = new HtmlAnimationPanel(extent, this.simulator);
85              WebAnimationToggles.setTextAnimationTogglesStandard(this.animationPanel);
86              // get the already created elements in context(/animation/D2)
87              this.animationPanel
88                      .notify(new TimedEvent(Replication.START_REPLICATION_EVENT, null, this.simulator.getSimulatorTime()));
89          }
90  
91          new ServerThread().start();
92      }
93  
94      /** Handle in separate thread to avoid 'lock' of the main application. */
95      class ServerThread extends Thread
96      {
97          /**
98           * Constructor.
99           */
100         ServerThread()
101         {
102             //
103         }
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 = ResourceResolver.resolve("/resources/home").asUrl();
113             String webRoot = homeFolder.toExternalForm();
114             Logger.ots().trace("webRoot is " + webRoot);
115 
116             resourceHandler.setDirAllowed(true);
117             resourceHandler.setWelcomeFiles(new String[] {"index.html"});
118             resourceHandler.setBaseResourceAsString(webRoot);
119 
120             ContextHandlerCollection handlers = new ContextHandlerCollection();
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      * Get title.
138      * @return title
139      */
140     public final String getTitle()
141     {
142         return this.title;
143     }
144 
145     /**
146      * Get simulator.
147      * @return simulator
148      */
149     public final OtsSimulatorInterface getSimulator()
150     {
151         return this.simulator;
152     }
153 
154     /**
155      * Get animation panel.
156      * @return animationPanel
157      */
158     public final HtmlAnimationPanel getAnimationPanel()
159     {
160         return this.animationPanel;
161     }
162 
163     /**
164      * Try to start the simulator, and return whether the simulator has been started.
165      * @return whether the simulator has been started or not
166      */
167     protected boolean startSimulator()
168     {
169         if (getSimulator() == null)
170         {
171             Logger.ots().trace("SIMULATOR == NULL");
172             return false;
173         }
174         try
175         {
176             Logger.ots().trace("START THE SIMULATOR");
177             getSimulator().start();
178         }
179         catch (SimRuntimeException exception)
180         {
181             Logger.ots().warn(exception, "Problem starting Simulator");
182         }
183         if (getSimulator().isStartingOrRunning())
184         {
185             return true;
186         }
187         this.dirtyControls = false; // undo the notification
188         return false;
189     }
190 
191     /**
192      * Try to stop the simulator, and return whether the simulator has been stopped.
193      * @return whether the simulator has been stopped or not
194      */
195     protected boolean stopSimulator()
196     {
197         if (getSimulator() == null)
198         {
199             return true;
200         }
201         try
202         {
203             Logger.ots().trace("STOP THE SIMULATOR");
204             getSimulator().stop();
205         }
206         catch (SimRuntimeException exception)
207         {
208             Logger.ots().warn(exception, "Problem stopping Simulator");
209         }
210         if (!getSimulator().isStartingOrRunning())
211         {
212             return true;
213         }
214         this.dirtyControls = false; // undo the notification
215         return false;
216     }
217 
218     /**
219      * Set speed factor.
220      * @param speedFactor the new speed factor
221      */
222     @SuppressWarnings("unchecked")
223     protected void setSpeedFactor(final double speedFactor)
224     {
225         if (this.simulator instanceof DevsRealTimeAnimator)
226         {
227             ((DevsRealTimeAnimator<Duration>) this.simulator).setSpeedFactor(speedFactor);
228         }
229     }
230 
231     @Override
232     public void notify(final Event event)
233     {
234         if (event.getType().equals(SimulatorInterface.START_EVENT))
235         {
236             this.dirtyControls = true;
237         }
238         else if (event.getType().equals(SimulatorInterface.STOP_EVENT))
239         {
240             this.dirtyControls = true;
241         }
242     }
243 
244     /**
245      * Answer handles the events from the web-based user interface. <br>
246      * <p>
247      * Copyright (c) 2003-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
248      * BSD-style license. See <a href="https://opentrafficsim.org/docs/v2/license.html">OpenTrafficSim License</a>.
249      * </p>
250      * @author <a href="https://github.com/averbraeck" target="_blank">Alexander Verbraeck</a>
251      */
252     public static class XHRHandler extends Handler.Abstract
253     {
254         /** web server for callback of actions. */
255         final OtsWebServer webServer;
256 
257         /** Timer update interval in msec. */
258         private long lastWallTIme = -1;
259 
260         /** Simulation time time. */
261         private double prevSimTime = 0;
262 
263         /**
264          * Create the handler for Servlet requests.
265          * @param webServer web server for callback of actions
266          */
267         public XHRHandler(final OtsWebServer webServer)
268         {
269             this.webServer = webServer;
270         }
271 
272         @Override
273         public boolean handle(final Request request, final Response response, final Callback callback) throws Exception
274         {
275 
276             // https://jetty.org/docs/jetty/12.1/programming-guide/migration/11-to-12.html#api-changes
277 
278             Fields fields = Request.getParameters(request);
279             String message = fields.getValue("message");
280             String answer = "<message>ok</message>";
281             if (message != null)
282             {
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                             Logger.ots().error("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                                 Logger.ots().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                             Logger.ots().error("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                         Logger.ots().error("Got unknown message from client: {}", command);
555                         answer = "<message>" + request.getAttribute("message") + "</message>";
556                         break;
557                     }
558                 }
559             }
560 
561             String slider = fields.getValue("slider");
562             if (slider != null)
563             {
564                 Logger.ots().trace("{}", slider);
565                 try
566                 {
567                     int value = Integer.parseInt(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                     Logger.ots().trace("speed factor changed to {}", speedFactor);
576                 }
577                 catch (NumberFormatException exception)
578                 {
579                     answer = "<message>Error: " + exception.getMessage() + "</message>";
580                 }
581             }
582 
583             Content.Sink.write(response, true, answer, callback);
584             return true; // handled
585         }
586 
587         /**
588          * @param active is the simulation active?
589          * @param started has the simulation been started?
590          * @return XML message to send to the server
591          */
592         private String controlButtonResponse(final boolean active, final boolean started)
593         {
594             if (!active)
595             {
596                 return "<controls>\n" + "<oneEventActive>false</oneEventActive>\n"
597                         + "<allEventsActive>false</allEventsActive>\n" + "<startStop>start</startStop>\n"
598                         + "<startStopActive>false</startStopActive>\n" + "<resetActive>false</resetActive>\n" + "</controls>\n";
599             }
600             if (started)
601             {
602                 return "<controls>\n" + "<oneEventActive>false</oneEventActive>\n"
603                         + "<allEventsActive>false</allEventsActive>\n" + "<startStop>stop</startStop>\n"
604                         + "<startStopActive>true</startStopActive>\n" + "<resetActive>false</resetActive>\n" + "</controls>\n";
605             }
606             else
607             {
608                 return "<controls>\n" + "<oneEventActive>true</oneEventActive>\n" + "<allEventsActive>true</allEventsActive>\n"
609                         + "<startStop>start</startStop>\n" + "<startStopActive>true</startStopActive>\n"
610                         + "<resetActive>false</resetActive>\n" + "</controls>\n";
611             }
612         }
613 
614         /**
615          * Return the toggle button info for the toggle panel.
616          * @param panel the HTMLAnimationPanel
617          * @return the String that can be parsed by the select.html iframe
618          */
619         private String getToggles(final HtmlAnimationPanel panel)
620         {
621             String ret = "<toggles>\n";
622             for (ToggleButtonInfo toggle : panel.getToggleButtons())
623             {
624                 if (toggle instanceof ToggleButtonInfo.Text)
625                 {
626                     ret += "<text>" + toggle.getName() + "</text>\n";
627                 }
628                 else if (toggle instanceof ToggleButtonInfo.LocatableClass)
629                 {
630                     ret += "<class>" + toggle.getName() + "," + toggle.isVisible() + "</class>\n";
631                 }
632                 else if (toggle instanceof ToggleButtonInfo.Gis)
633                 {
634                     ret += "<gis>" + toggle.getName() + "," + ((ToggleButtonInfo.Gis) toggle).getLayerName() + ","
635                             + toggle.isVisible() + "</gis>\n";
636                 }
637             }
638             ret += "</toggles>\n";
639             return ret;
640         }
641 
642         /**
643          * Returns the simulation speed.
644          * @param simTime simulation time
645          * @return simulation speed
646          */
647         private double getSimulationSpeed(final double simTime)
648         {
649             long now = System.currentTimeMillis();
650             if (this.lastWallTIme < 0 || this.lastWallTIme == now)
651             {
652                 this.lastWallTIme = now;
653                 this.prevSimTime = simTime;
654                 return Double.NaN;
655             }
656             double speed = (simTime - this.prevSimTime) / (0.001 * (now - this.lastWallTIme));
657             this.prevSimTime = simTime;
658             this.lastWallTIme = now;
659             return speed;
660         }
661 
662     }
663 
664 }