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