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