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