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
42
43
44
45
46
47
48 public class OtsWebModel implements EventListener
49 {
50
51 private final String title;
52
53
54 private final OtsSimulatorInterface simulator;
55
56
57 private boolean dirtyControls = false;
58
59
60 private HtmlAnimationPanel animationPanel;
61
62
63 private long lastWallTIme = -1;
64
65
66 private double prevSimTime = 0;
67
68
69 private boolean killed = false;
70
71
72
73
74
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
96 this.animationPanel
97 .notify(new TimedEvent<>(Replication.START_REPLICATION_EVENT, null, this.simulator.getSimulatorTime()));
98 }
99 }
100
101
102
103
104 public final String getTitle()
105 {
106 return this.title;
107 }
108
109
110
111
112 public final OtsSimulatorInterface getSimulator()
113 {
114 return this.simulator;
115 }
116
117
118
119
120 public final HtmlAnimationPanel getAnimationPanel()
121 {
122 return this.animationPanel;
123 }
124
125
126
127
128 public final boolean isKilled()
129 {
130 return this.killed;
131 }
132
133
134
135
136 public final void setKilled(final boolean killed)
137 {
138 this.killed = killed;
139 }
140
141
142
143
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;
166 return false;
167 }
168
169
170
171
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;
193 return false;
194 }
195
196
197
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
222
223
224
225
226
227
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
234
235
236
237 if (this.killed)
238 {
239 return;
240 }
241
242 Map<String, String[]> params = request.getParameterMap();
243
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
298 boolean started = false;
299 answer = controlButtonResponse(getSimulator() != null, started);
300 break;
301 }
302
303 case "allEvents":
304 {
305
306 boolean started = false;
307 answer = controlButtonResponse(getSimulator() != null, started);
308 break;
309 }
310
311 case "reset":
312 {
313
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
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
531 try
532 {
533 int value = Integer.parseInt(request.getParameter("slider"));
534
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
542 }
543 catch (NumberFormatException exception)
544 {
545 answer = "<message>Error: " + exception.getMessage() + "</message>";
546 }
547 }
548
549
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
562
563
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
589
590
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
617
618
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 }