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