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