A Discrete-Event Network Simulator
API
Loading...
Searching...
No Matches
core.py
Go to the documentation of this file.
1# -*- Mode: python; coding: utf-8 -*-
2from ctypes import c_double
3
4LAYOUT_ALGORITHM = "neato" # ['neato'|'dot'|'twopi'|'circo'|'fdp'|'nop']
5REPRESENT_CHANNELS_AS_NODES = 1
6DEFAULT_NODE_SIZE = 1.0 # default node size in meters
7DEFAULT_TRANSMISSIONS_MEMORY = (
8 5 # default number of of past intervals whose transmissions are remembered
9)
10BITRATE_FONT_SIZE = 10
11
12# internal constants, normally not meant to be changed
13SAMPLE_PERIOD = 0.1
14PRIORITY_UPDATE_MODEL = -100
15PRIORITY_UPDATE_VIEW = 200
16
17import platform
18import warnings
19
20if platform.system() == "Windows":
21 SHELL_FONT = "Lucida Console 9"
22else:
23 SHELL_FONT = "Luxi Mono 10"
24
25import math
26import os
27import sys
28import threading
29
30try:
31 import pygraphviz
32except ImportError:
33 print("Pygraphviz is required by the visualizer module and could not be found")
34 exit(1)
35
36try:
37 import cairo
38except ImportError:
39 print("Pycairo is required by the visualizer module and could not be found")
40 exit(1)
41
42try:
43 import gi
44except ImportError:
45 print("PyGObject is required by the visualizer module and could not be found")
46 exit(1)
47
48try:
49 import svgitem
50except ImportError:
51 svgitem = None
52
53try:
54 gi.require_version("GooCanvas", "2.0")
55 gi.require_version("Gtk", "3.0")
56 gi.require_version("Gdk", "3.0")
57 gi.require_foreign("cairo")
58 from gi.repository import Gdk, GLib, GObject, GooCanvas, Gtk, Pango
59
60 from . import hud
61except ImportError as e:
62 _import_error = e
63else:
64 _import_error = None
65
66try:
67 from . import ipython_view
68except ImportError:
69 ipython_view = None
70
71from .base import (
72 PIXELS_PER_METER,
73 InformationWindow,
74 Link,
75 PyVizObject,
76 load_plugins,
77 lookup_netdevice_traits,
78 plugins,
79 register_plugin,
80 transform_distance_canvas_to_simulation,
81 transform_distance_simulation_to_canvas,
82 transform_point_canvas_to_simulation,
83 transform_point_simulation_to_canvas,
84)
85
86
87## Node class
89 ## @var visualizer
90 # visualier object
91 ## @var node_index
92 # node index
93 ## @var canvas_item
94 # canvas item
95 ## @var links
96 # links
97 ## @var _has_mobility
98 # has mobility model
99 ## @var _selected
100 # is selected
101 ## @var _highlighted
102 # is highlighted
103 ## @var _color
104 # color
105 ## @var _size
106 # size
107 ## @var menu
108 # menu
109 ## @var svg_item
110 # svg item
111 ## @var svg_align_x
112 # svg align X
113 ## @var svg_align_y
114 # svg align Y
115 ## @var _label
116 # label
117 ## @var _label_canvas_item
118 # label canvas
119 ## @var highlighted
120 # highlighted property
121 ## @var selected
122 # selected property
123 ## @var on_enter_notify_event
124 # on_enter_notify_event function
125 ## @var on_leave_notify_event
126 # on_leave_notify_event function
127
128 ## signal emitted whenever a tooltip is about to be shown for the node
129 ## the first signal parameter is a python list of strings, to which
130 ## information can be appended
131 __gsignals__ = {
132 "query-extra-tooltip-info": (GObject.SignalFlags.RUN_LAST, None, (object,)),
133 }
134
135 def __init__(self, visualizer, node_index):
136 """! Initialize function.
137 @param self The object pointer.
138 @param visualizer visualizer object
139 @param node_index node index
140 """
141 super(Node, self).__init__()
142
143 self.visualizer = visualizer
144 self.node_index = node_index
145 self.canvas_item = GooCanvas.CanvasEllipse()
146 self.canvas_item.pyviz_object = self
147 self.links = []
148 self._has_mobility = None
149 self._selected = False
150 self._highlighted = False
151 self._color = 0x808080FF
152 self._size = DEFAULT_NODE_SIZE
153 self.canvas_item.connect("enter-notify-event", self.on_enter_notify_event)
154 self.canvas_item.connect("leave-notify-event", self.on_leave_notify_event)
155 self.menu = None
156 self.svg_item = None
157 self.svg_align_x = None
158 self.svg_align_y = None
159 self._label = None
161
162 self._update_appearance() # call this last
163
164 def set_svg_icon(self, file_base_name, width=None, height=None, align_x=0.5, align_y=0.5):
165 """!
166 Set a background SVG icon for the node.
167
168 @param file_base_name: base file name, including .svg
169 extension, of the svg file. Place the file in the folder
170 src/contrib/visualizer/resource.
171
172 @param width: scale to the specified width, in meters
173 @param height: scale to the specified height, in meters
174
175 @param align_x: horizontal alignment of the icon relative to
176 the node position, from 0 (icon fully to the left of the node)
177 to 1.0 (icon fully to the right of the node)
178
179 @param align_y: vertical alignment of the icon relative to the
180 node position, from 0 (icon fully to the top of the node) to
181 1.0 (icon fully to the bottom of the node)
182
183 @return a ValueError exception if invalid dimensions.
184
185 """
186 if width is None and height is None:
187 raise ValueError("either width or height must be given")
188 rsvg_handle = svgitem.rsvg_handle_factory(file_base_name)
189 x = self.canvas_item.props.center_x
190 y = self.canvas_item.props.center_y
191 self.svg_item = svgitem.SvgItem(x, y, rsvg_handle)
192 self.svg_item.props.parent = self.visualizer.canvas.get_root_item()
193 self.svg_item.props.pointer_events = GooCanvas.CanvasPointerEvents.NONE
194 self.svg_item.lower(None)
195 self.svg_item.props.visibility = GooCanvas.CanvasItemVisibility.VISIBLE_ABOVE_THRESHOLD
196 if width is not None:
197 self.svg_item.props.width = transform_distance_simulation_to_canvas(width)
198 if height is not None:
199 self.svg_item.props.height = transform_distance_simulation_to_canvas(height)
200
201 # threshold1 = 10.0/self.svg_item.props.height
202 # threshold2 = 10.0/self.svg_item.props.width
203 # self.svg_item.props.visibility_threshold = min(threshold1, threshold2)
204
205 self.svg_align_x = align_x
206 self.svg_align_y = align_y
207 self._update_svg_position(x, y)
208 self._update_appearance()
209
210 def set_label(self, label):
211 """!
212 Set a label for the node.
213
214 @param self: class object.
215 @param label: label to set
216
217 @return: an exception if invalid parameter.
218 """
219 assert isinstance(label, basestring)
220 self._label = label
221 self._update_appearance()
222
223 def _update_svg_position(self, x, y):
224 """!
225 Update svg position.
226
227 @param self: class object.
228 @param x: x position
229 @param y: y position
230 @return none
231 """
232 w = self.svg_item.width
233 h = self.svg_item.height
234 self.svg_item.set_properties(
235 x=(x - (1 - self.svg_align_x) * w), y=(y - (1 - self.svg_align_y) * h)
236 )
237
238 def tooltip_query(self, tooltip):
239 """!
240 Query tooltip.
241
242 @param self: class object.
243 @param tooltip: tooltip
244 @return none
245 """
246 self.visualizer.simulation.lock.acquire()
247 try:
248 ns3_node = ns.NodeList.GetNode(self.node_index)
249 ipv4 = ns3_node.GetObject[ns.Ipv4]().__deref__()
250 ipv6 = ns3_node.GetObject[ns.Ipv6]().__deref__()
251
252 name = "<b><u>Node %i</u></b>" % self.node_index
253 node_name = ns.Names.FindName(ns3_node)
254 if len(node_name) != 0:
255 name += " <b>(" + node_name + ")</b>"
256
257 lines = [name]
258 lines.append("")
259
260 self.emit("query-extra-tooltip-info", lines)
261
262 mob = ns3_node.GetObject[ns.MobilityModel]()
263 if mob:
264 mobility_model_name = mob.__deref__().GetInstanceTypeId().GetName()
265 lines.append(" <b>Mobility Model</b>: %s" % mobility_model_name)
266
267 for devI in range(ns3_node.GetNDevices()):
268 lines.append("")
269 lines.append(" <u>NetDevice %i:</u>" % devI)
270 dev = ns3_node.GetDevice(devI)
271 name = ns.Names.FindName(dev)
272 if name:
273 lines.append(" <b>Name:</b> %s" % name)
274 devname = dev.GetInstanceTypeId().GetName()
275 lines.append(" <b>Type:</b> %s" % devname)
276
277 if ipv4 is not None:
278 ipv4_idx = ipv4.GetInterfaceForDevice(dev)
279 if ipv4_idx != -1:
280 addresses = [
281 "%s/%s"
282 % (
283 ipv4.GetAddress(ipv4_idx, i).GetLocal(),
284 ipv4.GetAddress(ipv4_idx, i).GetMask(),
285 )
286 for i in range(ipv4.GetNAddresses(ipv4_idx))
287 ]
288 lines.append(" <b>IPv4 Addresses:</b> %s" % "; ".join(addresses))
289
290 if ipv6 is not None:
291 ipv6_idx = ipv6.GetInterfaceForDevice(dev)
292 if ipv6_idx != -1:
293 addresses = [
294 "%s/%s"
295 % (
296 ipv6.GetAddress(ipv6_idx, i).GetAddress(),
297 ipv6.GetAddress(ipv6_idx, i).GetPrefix(),
298 )
299 for i in range(ipv6.GetNAddresses(ipv6_idx))
300 ]
301 lines.append(" <b>IPv6 Addresses:</b> %s" % "; ".join(addresses))
302
303 lines.append(" <b>MAC Address:</b> %s" % (dev.GetAddress(),))
304
305 tooltip.set_markup("\n".join(lines))
306 finally:
307 self.visualizer.simulation.lock.release()
308
309 def on_enter_notify_event(self, view, target, event):
310 """!
311 On Enter event handle.
312
313 @param self: class object.
314 @param view: view
315 @param target: target
316 @param event: event
317 @return none
318 """
319
320 ## highlighted property
321 self.highlighted = True
322
323 def on_leave_notify_event(self, view, target, event):
324 """!
325 On Leave event handle.
326
327 @param self: class object.
328 @param view: view
329 @param target: target
330 @param event: event
331 @return none
332 """
333 self.highlighted = False
334
335 def _set_selected(self, value):
336 """!
337 Set selected function.
338
339 @param self: class object.
340 @param value: selected value
341 @return none
342 """
343 self._selected = value
344 self._update_appearance()
345
346 def _get_selected(self):
347 """!
348 Get selected function.
349
350 @param self: class object.
351 @return selected status
352 """
353 return self._selected
354
355 selected = property(_get_selected, _set_selected)
356
357 def _set_highlighted(self, value):
358 """!
359 Set highlighted function.
360
361 @param self: class object.
362 @param value: selected value
363 @return none
364 """
365 self._highlighted = value
366 self._update_appearance()
367
369 """!
370 Get highlighted function.
371
372 @param self: class object.
373 @return highlighted status
374 """
375 return self._highlighted
376
377 highlighted = property(_get_highlighted, _set_highlighted)
378
379 def set_size(self, size):
380 """!
381 Set size function.
382
383 @param self: class object.
384 @param size: selected size
385 @return none
386 """
387 self._size = size
388 self._update_appearance()
389
391 """!
392 Update the node aspect to reflect the selected/highlighted state
393
394 @param self: class object.
395 @return none
396 """
397
398 size = transform_distance_simulation_to_canvas(self._size)
399 if self.svg_item is not None:
400 alpha = 0x80
401 else:
402 alpha = 0xFF
403 fill_color_rgba = (self._color & 0xFFFFFF00) | alpha
404 self.canvas_item.set_properties(
405 radius_x=size, radius_y=size, fill_color_rgba=fill_color_rgba
406 )
407 if self._selected:
408 line_width = size * 0.3
409 else:
410 line_width = size * 0.15
411 if self.highlighted:
412 stroke_color = "yellow"
413 else:
414 stroke_color = "black"
415 self.canvas_item.set_properties(line_width=line_width, stroke_color=stroke_color)
416
417 if self._label is not None:
418 if self._label_canvas_item is None:
419 self._label_canvas_item = GooCanvas.CanvasText(
420 visibility_threshold=0.5,
421 font="Sans Serif 10",
422 fill_color_rgba=0x808080FF,
423 alignment=Pango.Alignment.CENTER,
424 anchor=GooCanvas.CanvasAnchorType.N,
425 parent=self.visualizer.canvas.get_root_item(),
426 pointer_events=GooCanvas.CanvasPointerEvents.NONE,
427 )
428 self._label_canvas_item.lower(None)
429
430 self._label_canvas_item.set_properties(
431 visibility=GooCanvas.CanvasItemVisibility.VISIBLE_ABOVE_THRESHOLD, text=self._label
432 )
433 self._update_position()
434
435 def set_position(self, x, y):
436 """!
437 Set position function.
438
439 @param self: class object.
440 @param x: x position
441 @param y: y position
442 @return none
443 """
444 self.canvas_item.set_property("center_x", x)
445 self.canvas_item.set_property("center_y", y)
446 if self.svg_item is not None:
447 self._update_svg_position(x, y)
448
449 for link in self.links:
450 link.update_points()
451
452 if self._label_canvas_item is not None:
453 self._label_canvas_item.set_properties(x=x, y=(y + self._size * 3))
454
455 # If the location of the point is now beyond the bounds of the
456 # canvas then those bounds now need to be increased
457 try:
458 bounds = self.visualizer.canvas.get_bounds()
459
460 (min_x, min_y, max_x, max_y) = bounds
461
462 min_x = min(x, min_x)
463 min_y = min(y, min_y)
464 max_x = max(x, max_x)
465 max_y = max(y, max_y)
466
467 new_bounds = (min_x, min_y, max_x, max_y)
468
469 if new_bounds != bounds:
470 self.visualizer.canvas.set_bounds(*new_bounds)
471 except TypeError:
472 # bug 2969: GooCanvas.Canvas.get_bounds() inconsistency
473 pass
474
475 def get_position(self):
476 """!
477 Get position function.
478
479 @param self: class object.
480 @return x and y position
481 """
482 return (
483 self.canvas_item.get_property("center_x"),
484 self.canvas_item.get_property("center_y"),
485 )
486
488 """!
489 Update position function.
490
491 @param self: class object.
492 @return none
493 """
494 x, y = self.get_position()
495 self.set_position(x, y)
496
497 def set_color(self, color):
498 """!
499 Set color function.
500
501 @param self: class object.
502 @param color: color to set.
503 @return none
504 """
505 if isinstance(color, str):
506 color = Gdk.color_parse(color)
507 color = (
508 ((color.red >> 8) << 24)
509 | ((color.green >> 8) << 16)
510 | ((color.blue >> 8) << 8)
511 | 0xFF
512 )
513 self._color = color
514 self._update_appearance()
515
516 def add_link(self, link):
517 """!
518 Add link function.
519
520 @param self: class object.
521 @param link: link to add.
522 @return none
523 """
524 assert isinstance(link, Link)
525 self.links.append(link)
526
527 def remove_link(self, link):
528 """!
529 Remove link function.
530
531 @param self: class object.
532 @param link: link to add.
533 @return none
534 """
535 assert isinstance(link, Link)
536 self.links.remove(link)
537
538 @property
539 def has_mobility(self):
540 """!
541 Has mobility function.
542
543 @param self: class object.
544 @return modility option
545 """
546 if self._has_mobility is None:
547 node = ns.NodeList.GetNode(self.node_index)
548 self._has_mobility = node.GetObject[ns.MobilityModel]()
549 return self._has_mobility
550
551
552## Channel
554 ## @var channel
555 # channel
556 ## @var canvas_item
557 # canvas
558 ## @var links
559 # list of links
560 #
561 def __init__(self, channel):
562 """!
563 Initializer function.
564
565 @param self: class object.
566 @param channel: channel.
567 """
568 self.channel = channel
569 self.canvas_item = GooCanvas.CanvasEllipse(
570 radius_x=30,
571 radius_y=30,
572 fill_color="white",
573 stroke_color="grey",
574 line_width=2.0,
575 line_dash=GooCanvas.CanvasLineDash.newv([10.0, 10.0]),
576 visibility=GooCanvas.CanvasItemVisibility.VISIBLE,
577 )
578 self.canvas_item.pyviz_object = self
579 self.links = []
580
581 def set_position(self, x, y):
582 """!
583 Initializer function.
584
585 @param self: class object.
586 @param x: x position.
587 @param y: y position.
588 @return
589 """
590 self.canvas_item.set_property("center_x", x)
591 self.canvas_item.set_property("center_y", y)
592
593 for link in self.links:
594 link.update_points()
595
596 def get_position(self):
597 """!
598 Initializer function.
599
600 @param self: class object.
601 @return x / y position.
602 """
603 return (
604 self.canvas_item.get_property("center_x"),
605 self.canvas_item.get_property("center_y"),
606 )
607
608
609## WiredLink
611 ## @var node1
612 # first node
613 ## @var node2
614 # second node
615 ## @var canvas_item
616 # canvas
617 #
618 def __init__(self, node1, node2):
619 """!
620 Initializer function.
621
622 @param self: class object.
623 @param node1: class object.
624 @param node2: class object.
625 """
626 assert isinstance(node1, Node)
627 assert isinstance(node2, (Node, Channel))
628 self.node1 = node1
629 self.node2 = node2
630 self.canvas_item = GooCanvas.CanvasPath(line_width=1.0, stroke_color="black")
631 self.canvas_item.pyviz_object = self
632 self.node1.links.append(self)
633 self.node2.links.append(self)
634
635 def update_points(self):
636 """!
637 Update points function.
638
639 @param self: class object.
640 @return none
641 """
642 pos1_x, pos1_y = self.node1.get_position()
643 pos2_x, pos2_y = self.node2.get_position()
644 self.canvas_item.set_property("data", "M %r %r L %r %r" % (pos1_x, pos1_y, pos2_x, pos2_y))
645
646
647## SimulationThread
648class SimulationThread(threading.Thread):
649 ## @var viz
650 # Visualizer object
651 ## @var lock
652 # thread lock
653 ## @var go
654 # thread event
655 ## @var target_time
656 # in seconds
657 ## @var quit
658 # quit indicator
659 ## @var sim_helper
660 # helper function
661 ## @var pause_messages
662 # pause messages
663 def __init__(self, viz):
664 """!
665 Initializer function.
666
667 @param self: class object.
668 @param viz: class object.
669 """
670 super(SimulationThread, self).__init__()
671 assert isinstance(viz, Visualizer)
672 self.viz = viz # Visualizer object
673 self.lock = threading.Lock()
674 self.go = threading.Event()
675 self.go.clear()
676 self.target_time = 0 # in seconds
677 self.quit = False
678 self.sim_helper = ns.PyViz()
680
681 def set_nodes_of_interest(self, nodes):
682 """!
683 Set nodes of interest function.
684
685 @param self: class object.
686 @param nodes: class object.
687 @return
688 """
689 self.lock.acquire()
690 try:
691 self.sim_helper.SetNodesOfInterest(nodes)
692 finally:
693 self.lock.release()
694
695 def run(self):
696 """!
697 Initializer function.
698
699 @param self: class object.
700 @return none
701 """
702 while not self.quit:
703 # print "sim: Wait for go"
704 self.go.wait() # wait until the main (view) thread gives us the go signal
705 self.go.clear()
706 if self.quit:
707 break
708 # self.go.clear()
709 # print "sim: Acquire lock"
710 self.lock.acquire()
711 try:
712 # print "sim: Current time is %f; Run until: %f" % (ns3.Simulator.Now ().GetSeconds (), self.target_time)
713 # print "skipping, model is ahead of view!"
714 self.sim_helper.SimulatorRunUntil(ns.Seconds(self.target_time))
715 # print "sim: Run until ended at current time: ", ns3.Simulator.Now ().GetSeconds ()
716 self.pause_messages.extend(self.sim_helper.GetPauseMessages())
717 GLib.idle_add(self.viz.update_model, priority=PRIORITY_UPDATE_MODEL)
718
719 if (
720 ns.Simulator.Now().GetSeconds()
721 >= self.sim_helper.GetSimulatorStopTime().GetSeconds()
722 ):
723 GLib.idle_add(self.viz._on_simulation_finished)
724 break
725 finally:
726 self.lock.release()
727 # print "sim: Release lock, loop."
728
729
730## ShowTransmissionsMode
732 ## @var ALL
733 # all
734 ## @var NONE
735 # none
736 ## @var SELECTED
737 # selected
738
739 ## enumeration
740 __slots__ = []
741
742
743ShowTransmissionsMode.ALL = ShowTransmissionsMode()
744ShowTransmissionsMode.NONE = ShowTransmissionsMode()
745ShowTransmissionsMode.SELECTED = ShowTransmissionsMode()
746
747
748## Visualizer
749class Visualizer(GObject.GObject):
750 ## @var INSTANCE
751 # all
752 INSTANCE = None
753
754 if _import_error is None:
755 __gsignals__ = {
756 # signal emitted whenever a right-click-on-node popup menu is being constructed
757 "populate-node-menu": (
758 GObject.SignalFlags.RUN_LAST,
759 None,
760 (
761 object,
762 Gtk.Menu,
763 ),
764 ),
765 # signal emitted after every simulation period (SAMPLE_PERIOD seconds of simulated time)
766 # the simulation lock is acquired while the signal is emitted
767 "simulation-periodic-update": (GObject.SignalFlags.RUN_LAST, None, ()),
768 # signal emitted right after the topology is scanned
769 "topology-scanned": (GObject.SignalFlags.RUN_LAST, None, ()),
770 # signal emitted when it's time to update the view objects
771 "update-view": (GObject.SignalFlags.RUN_LAST, None, ()),
772 }
773
774 def __init__(self):
775 """!
776 Initializer function.
777
778 @param self: class object.
779 @return none
780 """
781 assert Visualizer.INSTANCE is None
782 Visualizer.INSTANCE = self
783 super(Visualizer, self).__init__()
784 self.nodes = {} # node index -> Node
785 self.channels = {} # id(ns3.Channel) -> Channel
786 self.window = None # toplevel window
787 self.canvas = None # GooCanvas.Canvas
788 self.time_label = None # Gtk.Label
789 self.play_button = None # Gtk.ToggleButton
790 self.zoom = None # Gtk.Adjustment
791 self._scrolled_window = None # Gtk.ScrolledWindow
792
793 self.links_group = GooCanvas.CanvasGroup()
794 self.channels_group = GooCanvas.CanvasGroup()
795 self.nodes_group = GooCanvas.CanvasGroup()
796
797 self._update_timeout_id = None
798 self.simulation = SimulationThread(self)
799 self.selected_node = None # node currently selected
800 self.speed = 1.0
801 self.information_windows = []
802 self._transmission_arrows = []
803 self._last_transmissions = []
804 self._drop_arrows = []
805 self._last_drops = []
806 self._show_transmissions_mode = None
807 self.set_show_transmissions_mode(ShowTransmissionsMode.ALL)
808 self._panning_state = None
809 self.node_size_adjustment = None
810 self.transmissions_smoothing_adjustment = None
811 self.sample_period = SAMPLE_PERIOD
812 self.node_drag_state = None
813 self.follow_node = None
814 self.shell_window = None
815
816 self.create_gui()
817
818 for plugin in plugins:
819 plugin(self)
820
821 def set_show_transmissions_mode(self, mode):
822 """!
823 Set show transmission mode.
824
825 @param self: class object.
826 @param mode: mode to set.
827 @return none
828 """
829 assert isinstance(mode, ShowTransmissionsMode)
830 self._show_transmissions_mode = mode
831 if self._show_transmissions_mode == ShowTransmissionsMode.ALL:
832 self.simulation.set_nodes_of_interest(list(range(ns.NodeList.GetNNodes())))
833 elif self._show_transmissions_mode == ShowTransmissionsMode.NONE:
834 self.simulation.set_nodes_of_interest([])
835 elif self._show_transmissions_mode == ShowTransmissionsMode.SELECTED:
836 if self.selected_node is None:
837 self.simulation.set_nodes_of_interest([])
838 else:
839 self.simulation.set_nodes_of_interest([self.selected_node.node_index])
840
841 def _create_advanced_controls(self):
842 """!
843 Create advanced controls.
844
845 @param self: class object.
846 @return expander
847 """
848 expander = Gtk.Expander.new("Advanced")
849 expander.show()
850
851 main_vbox = GObject.new(Gtk.VBox, border_width=8, visible=True)
852 expander.add(main_vbox)
853
854 main_hbox1 = GObject.new(Gtk.HBox, border_width=8, visible=True)
855 main_vbox.pack_start(main_hbox1, True, True, 0)
856
857 show_transmissions_group = GObject.new(
858 Gtk.HeaderBar, title="Show transmissions", visible=True
859 )
860 main_hbox1.pack_start(show_transmissions_group, False, False, 8)
861
862 vbox = Gtk.VBox(homogeneous=True, spacing=4)
863 vbox.show()
864 show_transmissions_group.add(vbox)
865
866 all_nodes = Gtk.RadioButton.new(None)
867 all_nodes.set_label("All nodes")
868 all_nodes.set_active(True)
869 all_nodes.show()
870 vbox.add(all_nodes)
871
872 selected_node = Gtk.RadioButton.new_from_widget(all_nodes)
873 selected_node.show()
874 selected_node.set_label("Selected node")
875 selected_node.set_active(False)
876 vbox.add(selected_node)
877
878 no_node = Gtk.RadioButton.new_from_widget(all_nodes)
879 no_node.show()
880 no_node.set_label("Disabled")
881 no_node.set_active(False)
882 vbox.add(no_node)
883
884 def toggled(radio):
885 if radio.get_active():
886 self.set_show_transmissions_mode(ShowTransmissionsMode.ALL)
887
888 all_nodes.connect("toggled", toggled)
889
890 def toggled(radio):
891 if radio.get_active():
892 self.set_show_transmissions_mode(ShowTransmissionsMode.NONE)
893
894 no_node.connect("toggled", toggled)
895
896 def toggled(radio):
897 if radio.get_active():
898 self.set_show_transmissions_mode(ShowTransmissionsMode.SELECTED)
899
900 selected_node.connect("toggled", toggled)
901
902 # -- misc settings
903 misc_settings_group = GObject.new(Gtk.HeaderBar, title="Misc Settings", visible=True)
904 main_hbox1.pack_start(misc_settings_group, False, False, 8)
905 settings_hbox = GObject.new(Gtk.HBox, border_width=8, visible=True)
906 misc_settings_group.add(settings_hbox)
907
908 # --> node size
909 vbox = GObject.new(Gtk.VBox, border_width=0, visible=True)
910 scale = GObject.new(Gtk.HScale, visible=True, digits=2)
911 vbox.pack_start(scale, True, True, 0)
912 vbox.pack_start(GObject.new(Gtk.Label, label="Node Size", visible=True), True, True, 0)
913 settings_hbox.pack_start(vbox, False, False, 6)
914 self.node_size_adjustment = scale.get_adjustment()
915
916 def node_size_changed(adj):
917 for node in self.nodes.values():
918 node.set_size(adj.get_value())
919
920 self.node_size_adjustment.connect("value-changed", node_size_changed)
921 self.node_size_adjustment.set_lower(0.01)
922 self.node_size_adjustment.set_upper(20)
923 self.node_size_adjustment.set_step_increment(0.1)
924 self.node_size_adjustment.set_value(DEFAULT_NODE_SIZE)
925
926 # --> transmissions smooth factor
927 vbox = GObject.new(Gtk.VBox, border_width=0, visible=True)
928 scale = GObject.new(Gtk.HScale, visible=True, digits=1)
929 vbox.pack_start(scale, True, True, 0)
930 vbox.pack_start(
931 GObject.new(Gtk.Label, label="Tx. Smooth Factor (s)", visible=True), True, True, 0
932 )
933 settings_hbox.pack_start(vbox, False, False, 6)
934 self.transmissions_smoothing_adjustment = scale.get_adjustment()
935 adj = self.transmissions_smoothing_adjustment
936 adj.set_lower(0.1)
937 adj.set_upper(10)
938 adj.set_step_increment(0.1)
939 adj.set_value(DEFAULT_TRANSMISSIONS_MEMORY * 0.1)
940
941 return expander
942
943 ## PanningState class
944 class _PanningState(object):
945 ## @var __slots__
946 # internal variables
947 __slots__ = ["initial_mouse_pos", "initial_canvas_pos", "motion_signal"]
948
949 def _begin_panning(self, widget, event):
950 """!
951 Set show trnamission mode.
952
953 @param self: class object.
954 @param mode: mode to set.
955 @return none
956 """
957 display = self.canvas.get_window().get_display()
958 cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.FLEUR)
959 self.canvas.get_window().set_cursor(cursor)
960 self._panning_state = self._PanningState()
961 pos = widget.get_window().get_device_position(event.device)
962 self._panning_state.initial_mouse_pos = (pos.x, pos.y)
963 x = self._scrolled_window.get_hadjustment().get_value()
964 y = self._scrolled_window.get_vadjustment().get_value()
965 self._panning_state.initial_canvas_pos = (x, y)
966 self._panning_state.motion_signal = self.canvas.connect(
967 "motion-notify-event", self._panning_motion
968 )
969
970 def _end_panning(self, event):
971 """!
972 End panning function.
973
974 @param self: class object.
975 @param event: active event.
976 @return none
977 """
978 if self._panning_state is None:
979 return
980 self.canvas.get_window().set_cursor(None)
981 self.canvas.disconnect(self._panning_state.motion_signal)
982 self._panning_state = None
983
984 def _panning_motion(self, widget, event):
985 """!
986 Panning motion function.
987
988 @param self: class object.
989 @param widget: widget.
990 @param event: event.
991 @return true if successful
992 """
993 assert self._panning_state is not None
994 if event.is_hint:
995 pos = widget.get_window().get_device_position(event.device)
996 x, y = pos.x, pos.y
997 else:
998 x, y = event.x, event.y
999
1000 hadj = self._scrolled_window.get_hadjustment()
1001 vadj = self._scrolled_window.get_vadjustment()
1002 mx0, my0 = self._panning_state.initial_mouse_pos
1003 cx0, cy0 = self._panning_state.initial_canvas_pos
1004
1005 dx = x - mx0
1006 dy = y - my0
1007 hadj.set_value(cx0 - dx)
1008 vadj.set_value(cy0 - dy)
1009 return True
1010
1011 def _canvas_button_press(self, widget, event):
1012 if event.button == 2:
1013 self._begin_panning(widget, event)
1014 return True
1015 return False
1016
1017 def _canvas_button_release(self, dummy_widget, event):
1018 if event.button == 2:
1019 self._end_panning(event)
1020 return True
1021 return False
1022
1023 def _canvas_scroll_event(self, dummy_widget, event):
1024 if event.direction == Gdk.ScrollDirection.UP:
1025 self.zoom.set_value(self.zoom.get_value() * 1.25)
1026 return True
1027 elif event.direction == Gdk.ScrollDirection.DOWN:
1028 self.zoom.set_value(self.zoom.get_value() / 1.25)
1029 return True
1030 return False
1031
1032 def get_hadjustment(self):
1033 return self._scrolled_window.get_hadjustment()
1034
1035 def get_vadjustment(self):
1036 return self._scrolled_window.get_vadjustment()
1037
1038 def create_gui(self):
1039 self.window = Gtk.Window()
1040 self.window.set_title(sys.argv[0])
1041 vbox = Gtk.VBox()
1042 vbox.show()
1043 self.window.add(vbox)
1044
1045 # canvas
1046 self.canvas = GooCanvas.Canvas()
1047 self.canvas.connect_after("button-press-event", self._canvas_button_press)
1048 self.canvas.connect_after("button-release-event", self._canvas_button_release)
1049 self.canvas.connect("scroll-event", self._canvas_scroll_event)
1050 self.canvas.props.has_tooltip = True
1051 self.canvas.connect("query-tooltip", self._canvas_tooltip_cb)
1052 self.canvas.show()
1053 sw = Gtk.ScrolledWindow()
1054 sw.show()
1055 self._scrolled_window = sw
1056 sw.add(self.canvas)
1057 vbox.pack_start(sw, True, True, 4)
1058 self.canvas.set_size_request(600, 450)
1059 self.canvas.set_bounds(-10000, -10000, 10000, 10000)
1060 self.canvas.scroll_to(0, 0)
1061
1062 self.canvas.get_root_item().add_child(self.links_group, -1)
1063 self.links_group.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1064
1065 self.canvas.get_root_item().add_child(self.channels_group, -1)
1066 self.channels_group.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1067 self.channels_group.raise_(self.links_group)
1068
1069 self.canvas.get_root_item().add_child(self.nodes_group, -1)
1070 self.nodes_group.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1071 self.nodes_group.raise_(self.channels_group)
1072
1073 self.hud = hud.Axes(self)
1074
1075 hbox = Gtk.HBox()
1076 hbox.show()
1077 vbox.pack_start(hbox, False, False, 4)
1078
1079 # zoom
1080 zoom_adj = Gtk.Adjustment(
1081 value=1.0,
1082 lower=0.01,
1083 upper=10.0,
1084 step_increment=0.02,
1085 page_increment=1.0,
1086 page_size=1.0,
1087 )
1088 self.zoom = zoom_adj
1089
1090 def _zoom_changed(adj):
1091 self.canvas.set_scale(adj.get_value())
1092
1093 zoom_adj.connect("value-changed", _zoom_changed)
1094 zoom = Gtk.SpinButton.new(zoom_adj, 0.1, 1)
1095 zoom.set_digits(3)
1096 zoom.show()
1097 hbox.pack_start(GObject.new(Gtk.Label, label=" Zoom:", visible=True), False, False, 4)
1098 hbox.pack_start(zoom, False, False, 4)
1099 _zoom_changed(zoom_adj)
1100
1101 # speed
1102 speed_adj = Gtk.Adjustment(
1103 value=1.0, lower=0.01, upper=10.0, step_increment=0.02, page_increment=1.0, page_size=0
1104 )
1105
1106 def _speed_changed(adj):
1107 self.speed = adj.get_value()
1108 self.sample_period = SAMPLE_PERIOD * adj.get_value()
1109 self._start_update_timer()
1110
1111 speed_adj.connect("value-changed", _speed_changed)
1112 speed = Gtk.SpinButton.new(speed_adj, 1, 0)
1113 speed.set_digits(3)
1114 speed.show()
1115 hbox.pack_start(GObject.new(Gtk.Label, label=" Speed:", visible=True), False, False, 4)
1116 hbox.pack_start(speed, False, False, 4)
1117 _speed_changed(speed_adj)
1118
1119 # Current time
1120 self.time_label = GObject.new(Gtk.Label, label=" Speed:", visible=True)
1121 self.time_label.set_width_chars(20)
1122 hbox.pack_start(self.time_label, False, False, 4)
1123
1124 # Screenshot button
1125 screenshot_button = GObject.new(
1126 Gtk.Button,
1127 label="Snapshot",
1128 relief=Gtk.ReliefStyle.NONE,
1129 focus_on_click=False,
1130 visible=True,
1131 )
1132 hbox.pack_start(screenshot_button, False, False, 4)
1133
1134 def load_button_icon(button, icon_name):
1135 if not Gtk.IconTheme.get_default().has_icon(icon_name):
1136 print(f"Could not load icon {icon_name}", file=sys.stderr)
1137 return
1138 image = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.BUTTON)
1139 button.set_image(image)
1140 button.props.always_show_image = True
1141
1142 load_button_icon(screenshot_button, "applets-screenshooter")
1143 screenshot_button.connect("clicked", self._take_screenshot)
1144
1145 # Shell button
1146 if ipython_view is not None:
1147 shell_button = GObject.new(
1148 Gtk.Button,
1149 label="Shell",
1150 relief=Gtk.ReliefStyle.NONE,
1151 focus_on_click=False,
1152 visible=True,
1153 )
1154 hbox.pack_start(shell_button, False, False, 4)
1155 load_button_icon(shell_button, "gnome-terminal")
1156 shell_button.connect("clicked", self._start_shell)
1157
1158 # Play button
1159 self.play_button = GObject.new(
1160 Gtk.ToggleButton,
1161 label="Simulate (F3)",
1162 relief=Gtk.ReliefStyle.NONE,
1163 focus_on_click=False,
1164 visible=True,
1165 )
1166 load_button_icon(self.play_button, "media-playback-start")
1167 accel_group = Gtk.AccelGroup()
1168 self.window.add_accel_group(accel_group)
1169 self.play_button.add_accelerator(
1170 "clicked", accel_group, Gdk.KEY_F3, 0, Gtk.AccelFlags.VISIBLE
1171 )
1172 self.play_button.connect("toggled", self._on_play_button_toggled)
1173 hbox.pack_start(self.play_button, False, False, 4)
1174
1175 self.canvas.get_root_item().connect("button-press-event", self.on_root_button_press_event)
1176
1177 vbox.pack_start(self._create_advanced_controls(), False, False, 4)
1178
1179 display = Gdk.Display.get_default()
1180 try:
1181 monitor = display.get_primary_monitor()
1182 geometry = monitor.get_geometry()
1183 scale_factor = monitor.get_scale_factor()
1184 except AttributeError:
1185 screen = display.get_default_screen()
1186 monitor_id = screen.get_primary_monitor()
1187 geometry = screen.get_monitor_geometry(monitor_id)
1188 scale_factor = screen.get_monitor_scale_factor(monitor_id)
1189 width = scale_factor * geometry.width
1190 height = scale_factor * geometry.height
1191 self.window.set_default_size(width * 2 / 3, height * 2 / 3)
1192 self.window.show()
1193
1194 def scan_topology(self):
1195 print("scanning topology: %i nodes..." % (ns.NodeList.GetNNodes(),))
1196 graph = pygraphviz.AGraph()
1197 seen_nodes = 0
1198 for nodeI in range(ns.NodeList.GetNNodes()):
1199 seen_nodes += 1
1200 if seen_nodes == 100:
1201 print(
1202 "scan topology... %i nodes visited (%.1f%%)"
1203 % (nodeI, 100 * nodeI / ns.NodeList.GetNNodes())
1204 )
1205 seen_nodes = 0
1206 node = ns.NodeList.GetNode(nodeI)
1207 node_name = "Node %i" % nodeI
1208 node_view = self.get_node(nodeI)
1209
1210 mobility = node.GetObject[ns.MobilityModel]()
1211 if mobility:
1212 node_view.set_color("red")
1213 pos = node.GetObject[ns.MobilityModel]().__deref__().GetPosition()
1214 node_view.set_position(*transform_point_simulation_to_canvas(pos.x, pos.y))
1215 # print "node has mobility position -> ", "%f,%f" % (pos.x, pos.y)
1216 else:
1217 graph.add_node(node_name)
1218
1219 for devI in range(node.GetNDevices()):
1220 device = node.GetDevice(devI)
1221 device_traits = lookup_netdevice_traits(type(device.__deref__()))
1222 if device_traits.is_wireless:
1223 continue
1224 if device_traits.is_virtual:
1225 continue
1226 channel = device.GetChannel()
1227 if channel.GetNDevices() > 2:
1228 if REPRESENT_CHANNELS_AS_NODES:
1229 # represent channels as white nodes
1230 if mobility is None:
1231 channel_name = "Channel %s" % id(channel)
1232 graph.add_edge(node_name, channel_name)
1233 self.get_channel(channel)
1234 self.create_link(self.get_node(nodeI), self.get_channel(channel))
1235 else:
1236 # don't represent channels, just add links between nodes in the same channel
1237 for otherDevI in range(channel.GetNDevices()):
1238 otherDev = channel.GetDevice(otherDevI)
1239 otherNode = otherDev.GetNode()
1240 otherNodeView = self.get_node(otherNode.GetId())
1241 if otherNode is not node:
1242 if mobility is None and not otherNodeView.has_mobility:
1243 other_node_name = "Node %i" % otherNode.GetId()
1244 graph.add_edge(node_name, other_node_name)
1245 self.create_link(self.get_node(nodeI), otherNodeView)
1246 else:
1247 for otherDevI in range(channel.GetNDevices()):
1248 otherDev = channel.GetDevice(otherDevI)
1249 otherNode = otherDev.GetNode()
1250 otherNodeView = self.get_node(otherNode.GetId())
1251 if otherNode is not node:
1252 if mobility is None and not otherNodeView.has_mobility:
1253 other_node_name = "Node %i" % otherNode.GetId()
1254 graph.add_edge(node_name, other_node_name)
1255 self.create_link(self.get_node(nodeI), otherNodeView)
1256
1257 print("scanning topology: calling graphviz layout")
1258 graph.layout(LAYOUT_ALGORITHM)
1259 for node in graph.iternodes():
1260 # print node, "=>", node.attr['pos']
1261 node_type, node_id = node.split(" ")
1262 pos_x, pos_y = [float(s) for s in node.attr["pos"].split(",")]
1263 if node_type == "Node":
1264 obj = self.nodes[int(node_id)]
1265 elif node_type == "Channel":
1266 obj = self.channels[int(node_id)]
1267 obj.set_position(pos_x, pos_y)
1268
1269 print("scanning topology: all done.")
1270 self.emit("topology-scanned")
1271
1272 def get_node(self, index):
1273 try:
1274 return self.nodes[index]
1275 except KeyError:
1276 node = Node(self, index)
1277 self.nodes[index] = node
1278 self.nodes_group.add_child(node.canvas_item, -1)
1279 node.canvas_item.connect("button-press-event", self.on_node_button_press_event, node)
1280 node.canvas_item.connect(
1281 "button-release-event", self.on_node_button_release_event, node
1282 )
1283 return node
1284
1285 def get_channel(self, ns3_channel):
1286 try:
1287 return self.channels[id(ns3_channel)]
1288 except KeyError:
1289 channel = Channel(ns3_channel)
1290 self.channels[id(ns3_channel)] = channel
1291 self.channels_group.add_child(channel.canvas_item, -1)
1292 return channel
1293
1294 def create_link(self, node, node_or_channel):
1295 link = WiredLink(node, node_or_channel)
1296 self.links_group.add_child(link.canvas_item, -1)
1297 link.canvas_item.lower(None)
1298
1299 def update_view(self):
1300 # print "update_view"
1301
1302 self.time_label.set_text("Time: %f s" % ns.Simulator.Now().GetSeconds())
1303
1304 self._update_node_positions()
1305
1306 # Update information
1307 for info_win in self.information_windows:
1308 info_win.update()
1309
1310 self._update_transmissions_view()
1311 self._update_drops_view()
1312
1313 self.emit("update-view")
1314
1315 def _update_node_positions(self):
1316 for node in self.nodes.values():
1317 if node.has_mobility:
1318 ns3_node = ns.NodeList.GetNode(node.node_index)
1319 mobility = ns3_node.GetObject[ns.MobilityModel]()
1320 if mobility:
1321 pos = ns3_node.GetObject[ns.MobilityModel]().__deref__().GetPosition()
1322 x, y = transform_point_simulation_to_canvas(pos.x, pos.y)
1323 node.set_position(x, y)
1324 if node is self.follow_node:
1325 hadj = self._scrolled_window.get_hadjustment()
1326 vadj = self._scrolled_window.get_vadjustment()
1327 px, py = self.canvas.convert_to_pixels(x, y)
1328 hadj.set_value(px - hadj.get_page_size() / 2)
1329 vadj.set_value(py - vadj.get_page_size() / 2)
1330
1331 def center_on_node(self, node):
1332 if isinstance(node, ns.Node):
1333 node = self.nodes[node.GetId()]
1334 elif isinstance(node, int):
1335 node = self.nodes[node]
1336 elif isinstance(node, Node):
1337 pass
1338 else:
1339 raise TypeError("expected int, viz.Node or ns.Node, not %r" % node)
1340
1341 x, y = node.get_position()
1342 hadj = self._scrolled_window.get_hadjustment()
1343 vadj = self._scrolled_window.get_vadjustment()
1344 px, py = self.canvas.convert_to_pixels(x, y)
1345 hadj.set_value(px - hadj.get_page_size() / 2)
1346 vadj.set_value(py - vadj.get_page_size() / 2)
1347
1348 def update_model(self):
1349 self.simulation.lock.acquire()
1350 try:
1351 self.emit("simulation-periodic-update")
1352 finally:
1353 self.simulation.lock.release()
1354
1355 def do_simulation_periodic_update(self):
1356 smooth_factor = int(self.transmissions_smoothing_adjustment.get_value() * 10)
1357
1358 transmissions = self.simulation.sim_helper.GetTransmissionSamples()
1359 self._last_transmissions.append(transmissions)
1360 while len(self._last_transmissions) > smooth_factor:
1361 self._last_transmissions.pop(0)
1362
1363 drops = self.simulation.sim_helper.GetPacketDropSamples()
1364 self._last_drops.append(drops)
1365 while len(self._last_drops) > smooth_factor:
1366 self._last_drops.pop(0)
1367
1368 def _get_label_over_line_position(self, pos1_x, pos1_y, pos2_x, pos2_y):
1369 hadj = self._scrolled_window.get_hadjustment()
1370 vadj = self._scrolled_window.get_vadjustment()
1371 bounds_x1, bounds_y1 = self.canvas.convert_from_pixels(hadj.get_value(), vadj.get_value())
1372 bounds_x2, bounds_y2 = self.canvas.convert_from_pixels(
1373 hadj.get_value() + hadj.get_page_size(), vadj.get_value() + vadj.get_page_size()
1374 )
1375 ns.PyViz.LineClipping(
1376 bounds_x1, bounds_y1, bounds_x2, bounds_y2, pos1_x, pos1_y, pos2_x, pos2_y
1377 )
1378 return (pos1_x.value + pos2_x.value) / 2, (pos1_y.value + pos2_y.value) / 2
1379
1380 def _update_transmissions_view(self):
1381 transmissions_average = {}
1382 for transmission_set in self._last_transmissions:
1383 for transmission in transmission_set:
1384 key = (transmission.transmitter.GetId(), transmission.receiver.GetId())
1385 rx_bytes, count = transmissions_average.get(key, (0, 0))
1386 rx_bytes += transmission.bytes
1387 count += 1
1388 transmissions_average[key] = rx_bytes, count
1389
1390 old_arrows = self._transmission_arrows
1391 for arrow, label in old_arrows:
1392 arrow.set_property("visibility", GooCanvas.CanvasItemVisibility.HIDDEN)
1393 label.set_property("visibility", GooCanvas.CanvasItemVisibility.HIDDEN)
1394 new_arrows = []
1395
1396 k = self.node_size_adjustment.get_value() / 5
1397
1398 for (transmitter_id, receiver_id), (rx_bytes, rx_count) in transmissions_average.items():
1399 transmitter = self.get_node(transmitter_id)
1400 receiver = self.get_node(receiver_id)
1401 try:
1402 arrow, label = old_arrows.pop()
1403 except IndexError:
1404 arrow = GooCanvas.CanvasPolyline(
1405 line_width=2.0,
1406 stroke_color_rgba=0x00C000C0,
1407 close_path=False,
1408 end_arrow=True,
1409 pointer_events=GooCanvas.CanvasPointerEvents.NONE,
1410 )
1411 arrow.set_property("parent", self.canvas.get_root_item())
1412 arrow.raise_(None)
1413
1414 label = GooCanvas.CanvasText(
1415 parent=self.canvas.get_root_item(),
1416 pointer_events=GooCanvas.CanvasPointerEvents.NONE,
1417 )
1418 label.raise_(None)
1419
1420 arrow.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1421 line_width = max(0.1, math.log(float(rx_bytes) / rx_count / self.sample_period) * k)
1422 arrow.set_property("line-width", line_width)
1423
1424 pos1_x, pos1_y = transmitter.get_position()
1425 pos2_x, pos2_y = receiver.get_position()
1426 points = GooCanvas.CanvasPoints.new(2)
1427 points.set_point(0, pos1_x, pos1_y)
1428 points.set_point(1, pos2_x, pos2_y)
1429 arrow.set_property("points", points)
1430
1431 kbps = float(rx_bytes * 8) / 1e3 / rx_count / self.sample_period
1432 label.set_properties(
1433 visibility=GooCanvas.CanvasItemVisibility.VISIBLE_ABOVE_THRESHOLD,
1434 visibility_threshold=0.5,
1435 font=("Sans Serif %f" % int(1 + BITRATE_FONT_SIZE * k)),
1436 )
1437
1438 angle = math.atan2((pos2_y - pos1_y), (pos2_x - pos1_x))
1439 if angle > math.pi / 2 or angle < -math.pi / 2:
1440 # Normalize the angle, in essence, adjust the angle to keep
1441 # it from rotating beyond +/- 90 degrees. In this way, the
1442 # direction of the label remain always readable regardless of
1443 # the angle.
1444 angle += math.pi
1445 label.set_properties(
1446 text=("← %.2f kbit/s" % (kbps,)),
1447 alignment=Pango.Alignment.CENTER,
1448 anchor=GooCanvas.CanvasAnchorType.S,
1449 x=0,
1450 y=-line_width / 2,
1451 )
1452 else:
1453 label.set_properties(
1454 text=("%.2f kbit/s →" % (kbps,)),
1455 alignment=Pango.Alignment.CENTER,
1456 anchor=GooCanvas.CanvasAnchorType.N,
1457 x=0,
1458 y=line_width / 2,
1459 )
1460 M = cairo.Matrix()
1461 lx, ly = self._get_label_over_line_position(
1462 c_double(pos1_x), c_double(pos1_y), c_double(pos2_x), c_double(pos2_y)
1463 )
1464 M.translate(lx, ly)
1465 M.rotate(angle)
1466 try:
1467 label.set_transform(M)
1468 except KeyError:
1469 # https://gitlab.gnome.org/GNOME/pygobject/issues/16
1470 warnings.warn(
1471 "PyGobject bug causing label position error; "
1472 "should be fixed in PyGObject >= 3.29.1"
1473 )
1474 label.set_properties(x=(lx + label.props.x), y=(ly + label.props.y))
1475
1476 new_arrows.append((arrow, label))
1477
1478 self._transmission_arrows = new_arrows + old_arrows
1479
1480 def _update_drops_view(self):
1481 drops_average = {}
1482 for drop_set in self._last_drops:
1483 for drop in drop_set:
1484 key = drop.transmitter.GetId()
1485 drop_bytes, count = drops_average.get(key, (0, 0))
1486 drop_bytes += drop.bytes
1487 count += 1
1488 drops_average[key] = drop_bytes, count
1489
1490 old_arrows = self._drop_arrows
1491 for arrow, label in old_arrows:
1492 arrow.set_property("visibility", GooCanvas.CanvasItemVisibility.HIDDEN)
1493 label.set_property("visibility", GooCanvas.CanvasItemVisibility.HIDDEN)
1494 new_arrows = []
1495
1496 # get the coordinates for the edge of screen
1497 vadjustment = self._scrolled_window.get_vadjustment()
1498 bottom_y = vadjustment.get_value() + vadjustment.get_page_size()
1499 dummy, edge_y = self.canvas.convert_from_pixels(0, bottom_y)
1500
1501 k = self.node_size_adjustment.get_value() / 5
1502
1503 for transmitter_id, (drop_bytes, drop_count) in drops_average.items():
1504 transmitter = self.get_node(transmitter_id)
1505 try:
1506 arrow, label = old_arrows.pop()
1507 except IndexError:
1508 arrow = GooCanvas.CanvasPolyline(
1509 line_width=2.0,
1510 stroke_color_rgba=0xC00000C0,
1511 close_path=False,
1512 end_arrow=True,
1513 pointer_events=GooCanvas.CanvasPointerEvents.NONE,
1514 )
1515 arrow.set_property("parent", self.canvas.get_root_item())
1516 arrow.raise_(None)
1517
1518 label = GooCanvas.CanvasText(
1519 pointer_events=GooCanvas.CanvasPointerEvents.NONE
1520 ) # , fill_color_rgba=0x00C000C0)
1521 label.set_property("parent", self.canvas.get_root_item())
1522 label.raise_(None)
1523
1524 arrow.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1525 arrow.set_property(
1526 "line-width",
1527 max(0.1, math.log(float(drop_bytes) / drop_count / self.sample_period) * k),
1528 )
1529 pos1_x, pos1_y = transmitter.get_position()
1530 pos2_x, pos2_y = pos1_x, edge_y
1531 points = GooCanvas.CanvasPoints.new(2)
1532 points.set_point(0, pos1_x, pos1_y)
1533 points.set_point(1, pos2_x, pos2_y)
1534 arrow.set_property("points", points)
1535
1536 label.set_properties(
1537 visibility=GooCanvas.CanvasItemVisibility.VISIBLE_ABOVE_THRESHOLD,
1538 visibility_threshold=0.5,
1539 font=("Sans Serif %i" % int(1 + BITRATE_FONT_SIZE * k)),
1540 text=(
1541 "%.2f kbit/s" % (float(drop_bytes * 8) / 1e3 / drop_count / self.sample_period,)
1542 ),
1543 alignment=Pango.Alignment.CENTER,
1544 x=(pos1_x + pos2_x) / 2,
1545 y=(pos1_y + pos2_y) / 2,
1546 )
1547
1548 new_arrows.append((arrow, label))
1549
1550 self._drop_arrows = new_arrows + old_arrows
1551
1552 def _on_simulation_finished(self):
1553 print("Simulation finished.")
1554 if self._update_timeout_id is not None:
1555 GLib.source_remove(self._update_timeout_id)
1556 self._update_timeout_id = None
1557 self.play_button.set_active(False)
1558 self.play_button.set_sensitive(False)
1559
1560 def update_view_timeout(self):
1561 # print "view: update_view_timeout called at real time ", time.time()
1562
1563 # while the simulator is busy, run the gtk event loop
1564 while not self.simulation.lock.acquire(False):
1565 while Gtk.events_pending():
1566 Gtk.main_iteration()
1567 pause_messages = self.simulation.pause_messages
1568 self.simulation.pause_messages = []
1569 try:
1570 self.update_view()
1571 self.simulation.target_time = ns.Simulator.Now().GetSeconds() + self.sample_period
1572 # print "view: target time set to %f" % self.simulation.target_time
1573 finally:
1574 self.simulation.lock.release()
1575
1576 if pause_messages:
1577 # print pause_messages
1578 dialog = Gtk.MessageDialog(
1579 parent=self.window,
1580 flags=0,
1581 type=Gtk.MessageType.WARNING,
1582 buttons=Gtk.ButtonsType.OK,
1583 message_format="\n".join(pause_messages),
1584 )
1585 dialog.connect("response", lambda d, r: d.destroy())
1586 dialog.show()
1587 self.play_button.set_active(False)
1588
1589 # if we're paused, stop the update timer
1590 if not self.play_button.get_active():
1591 self._update_timeout_id = None
1592 return False
1593
1594 # print "view: self.simulation.go.set()"
1595 self.simulation.go.set()
1596 # print "view: done."
1597 return True
1598
1599 def _start_update_timer(self):
1600 if self._update_timeout_id is not None:
1601 GLib.source_remove(self._update_timeout_id)
1602 # print "start_update_timer"
1603 self._update_timeout_id = GLib.timeout_add(
1604 int(SAMPLE_PERIOD / min(self.speed, 1) * 1e3),
1605 self.update_view_timeout,
1606 priority=PRIORITY_UPDATE_VIEW,
1607 )
1608
1609 def _on_play_button_toggled(self, button):
1610 if button.get_active():
1611 self._start_update_timer()
1612 else:
1613 if self._update_timeout_id is not None:
1614 GLib.source_remove(self._update_timeout_id)
1615
1616 def _quit(self, *dummy_args):
1617 if self._update_timeout_id is not None:
1618 GLib.source_remove(self._update_timeout_id)
1619 self._update_timeout_id = None
1620 self.simulation.quit = True
1621 self.simulation.go.set()
1622 self.simulation.join()
1623 Gtk.main_quit()
1624
1625 def _monkey_patch_ipython(self):
1626 # The user may want to access the NS 3 simulation state, but
1627 # NS 3 is not thread safe, so it could cause serious problems.
1628 # To work around this, monkey-patch IPython to automatically
1629 # acquire and release the simulation lock around each code
1630 # that is executed.
1631
1632 original_runcode = self.ipython.runcode
1633
1634 def runcode(ip, *args):
1635 # print "lock"
1636 self.simulation.lock.acquire()
1637 try:
1638 return original_runcode(*args)
1639 finally:
1640 # print "unlock"
1641 self.simulation.lock.release()
1642
1643 import types
1644
1645 self.ipython.runcode = types.MethodType(runcode, self.ipython)
1646
1647 def autoscale_view(self):
1648 if not self.nodes:
1649 return
1650 self._update_node_positions()
1651 positions = [node.get_position() for node in self.nodes.values()]
1652 min_x, min_y = min(x for (x, y) in positions), min(y for (x, y) in positions)
1653 max_x, max_y = max(x for (x, y) in positions), max(y for (x, y) in positions)
1654 min_x_px, min_y_px = self.canvas.convert_to_pixels(min_x, min_y)
1655 max_x_px, max_y_px = self.canvas.convert_to_pixels(max_x, max_y)
1656 dx = max_x - min_x
1657 dy = max_y - min_y
1658 dx_px = max_x_px - min_x_px
1659 dy_px = max_y_px - min_y_px
1660 hadj = self._scrolled_window.get_hadjustment()
1661 vadj = self._scrolled_window.get_vadjustment()
1662 new_dx, new_dy = 1.5 * dx_px, 1.5 * dy_px
1663
1664 if new_dx == 0 or new_dy == 0:
1665 return
1666
1667 self.zoom.set_value(min(hadj.get_page_size() / new_dx, vadj.get_page_size() / new_dy))
1668
1669 x1, y1 = self.canvas.convert_from_pixels(hadj.get_value(), vadj.get_value())
1670 x2, y2 = self.canvas.convert_from_pixels(
1671 (hadj.get_value() + hadj.get_page_size()), (vadj.get_value() + vadj.get_page_size())
1672 )
1673 width = x2 - x1
1674 height = y2 - y1
1675 center_x = (min_x + max_x) / 2
1676 center_y = (min_y + max_y) / 2
1677
1678 self.canvas.scroll_to(center_x - width / 2, center_y - height / 2)
1679
1680 return False
1681
1682 def start(self):
1683 self.scan_topology()
1684 self.window.connect("delete-event", self._quit)
1685 # self._start_update_timer()
1686 GLib.timeout_add(200, self.autoscale_view)
1687 self.simulation.start()
1688
1689 try:
1690 __IPYTHON__
1691 except NameError:
1692 pass
1693 else:
1694 self._monkey_patch_ipython()
1695
1696 Gtk.main()
1697
1698 def on_root_button_press_event(self, view, target, event):
1699 if event.button == 1:
1700 self.select_node(None)
1701 return True
1702
1703 def on_node_button_press_event(self, view, target, event, node):
1704 button = event.button
1705 if button == 1:
1706 self.select_node(node)
1707 return True
1708 elif button == 3:
1709 self.popup_node_menu(node, event)
1710 return True
1711 elif button == 2:
1712 self.begin_node_drag(node, event)
1713 return True
1714 return False
1715
1716 def on_node_button_release_event(self, view, target, event, node):
1717 if event.button == 2:
1718 self.end_node_drag(node)
1719 return True
1720 return False
1721
1722 class NodeDragState(object):
1723 def __init__(self, canvas_x0, canvas_y0, sim_x0, sim_y0):
1724 self.canvas_x0 = canvas_x0
1725 self.canvas_y0 = canvas_y0
1726 self.sim_x0 = sim_x0
1727 self.sim_y0 = sim_y0
1728 self.motion_signal = None
1729
1730 def begin_node_drag(self, node, event):
1731 self.simulation.lock.acquire()
1732 try:
1733 ns3_node = ns.NodeList.GetNode(node.node_index)
1734 mob = ns3_node.GetObject[ns.MobilityModel]()
1735 if not mob:
1736 return
1737 if self.node_drag_state is not None:
1738 return
1739 pos = ns3_node.GetObject[ns.MobilityModel]().__deref__().GetPosition()
1740 finally:
1741 self.simulation.lock.release()
1742 devpos = self.canvas.get_window().get_device_position(event.device)
1743 x0, y0 = self.canvas.convert_from_pixels(devpos.x, devpos.y)
1744 self.node_drag_state = self.NodeDragState(x0, y0, pos.x, pos.y)
1745 self.node_drag_state.motion_signal = node.canvas_item.connect(
1746 "motion-notify-event", self.node_drag_motion, node
1747 )
1748
1749 def node_drag_motion(self, item, targe_item, event, node):
1750 self.simulation.lock.acquire()
1751 try:
1752 ns3_node = ns.NodeList.GetNode(node.node_index)
1753 mob = ns3_node.GetObject[ns.MobilityModel]()
1754 if not mob:
1755 return False
1756 if self.node_drag_state is None:
1757 return False
1758 devpos = self.canvas.get_window().get_device_position(event.device)
1759 canvas_x, canvas_y = self.canvas.convert_from_pixels(devpos.x, devpos.y)
1760 dx = canvas_x - self.node_drag_state.canvas_x0
1761 dy = canvas_y - self.node_drag_state.canvas_y0
1762 pos = mob.GetPosition()
1763 pos.x = self.node_drag_state.sim_x0 + transform_distance_canvas_to_simulation(dx)
1764 pos.y = self.node_drag_state.sim_y0 + transform_distance_canvas_to_simulation(dy)
1765 # print "SetPosition(%G, %G)" % (pos.x, pos.y)
1766 mob.SetPosition(pos)
1767 node.set_position(*transform_point_simulation_to_canvas(pos.x, pos.y))
1768 finally:
1769 self.simulation.lock.release()
1770 return True
1771
1772 def end_node_drag(self, node):
1773 if self.node_drag_state is None:
1774 return
1775 node.canvas_item.disconnect(self.node_drag_state.motion_signal)
1776 self.node_drag_state = None
1777
1778 def popup_node_menu(self, node, event):
1779 menu = Gtk.Menu()
1780 self.emit("populate-node-menu", node, menu)
1781 menu.popup_at_pointer(event)
1782
1783 def _update_ipython_selected_node(self):
1784 # If we are running under ipython -gthread, make this new
1785 # selected node available as a global 'selected_node'
1786 # variable.
1787 try:
1788 __IPYTHON__
1789 except NameError:
1790 pass
1791 else:
1792 if self.selected_node is None:
1793 ns3_node = None
1794 else:
1795 self.simulation.lock.acquire()
1796 try:
1797 ns3_node = ns.NodeList.GetNode(self.selected_node.node_index)
1798 finally:
1799 self.simulation.lock.release()
1800 self.ipython.updateNamespace({"selected_node": ns3_node})
1801
1802 def select_node(self, node):
1803 if isinstance(node, ns.Node):
1804 node = self.nodes[node.GetId()]
1805 elif isinstance(node, int):
1806 node = self.nodes[node]
1807 elif isinstance(node, Node):
1808 pass
1809 elif node is None:
1810 pass
1811 else:
1812 raise TypeError("expected None, int, viz.Node or ns.Node, not %r" % node)
1813
1814 if node is self.selected_node:
1815 return
1816
1817 if self.selected_node is not None:
1818 self.selected_node.selected = False
1819 self.selected_node = node
1820 if self.selected_node is not None:
1821 self.selected_node.selected = True
1822
1823 if self._show_transmissions_mode == ShowTransmissionsMode.SELECTED:
1824 if self.selected_node is None:
1825 self.simulation.set_nodes_of_interest([])
1826 else:
1827 self.simulation.set_nodes_of_interest([self.selected_node.node_index])
1828
1829 self._update_ipython_selected_node()
1830
1831 def add_information_window(self, info_win):
1832 self.information_windows.append(info_win)
1833 self.simulation.lock.acquire()
1834 try:
1835 info_win.update()
1836 finally:
1837 self.simulation.lock.release()
1838
1839 def remove_information_window(self, info_win):
1840 self.information_windows.remove(info_win)
1841
1842 def _canvas_tooltip_cb(self, canvas, x, y, keyboard_mode, tooltip):
1843 # print "tooltip query: ", x, y
1844 hadj = self._scrolled_window.get_hadjustment()
1845 vadj = self._scrolled_window.get_vadjustment()
1846 x, y = self.canvas.convert_from_pixels(hadj.get_value() + x, vadj.get_value() + y)
1847 item = self.canvas.get_item_at(x, y, True)
1848 # print "items at (%f, %f): %r | keyboard_mode=%r" % (x, y, item, keyboard_mode)
1849 if not item:
1850 return False
1851 while item is not None:
1852 obj = getattr(item, "pyviz_object", None)
1853 if obj is not None:
1854 obj.tooltip_query(tooltip)
1855 return True
1856 item = item.props.parent
1857 return False
1858
1859 def _get_export_file_name(self):
1860 sel = Gtk.FileChooserNative.new(
1861 "Save...", self.canvas.get_toplevel(), Gtk.FileChooserAction.SAVE, "_Save", "_Cancel"
1862 )
1863 sel.set_local_only(True)
1864 sel.set_do_overwrite_confirmation(True)
1865 sel.set_current_name("Unnamed.pdf")
1866
1867 filter = Gtk.FileFilter()
1868 filter.set_name("Embedded PostScript")
1869 filter.add_mime_type("image/x-eps")
1870 sel.add_filter(filter)
1871
1872 filter = Gtk.FileFilter()
1873 filter.set_name("Portable Document Graphics")
1874 filter.add_mime_type("application/pdf")
1875 sel.add_filter(filter)
1876
1877 filter = Gtk.FileFilter()
1878 filter.set_name("Scalable Vector Graphics")
1879 filter.add_mime_type("image/svg+xml")
1880 sel.add_filter(filter)
1881
1882 resp = sel.run()
1883 if resp != Gtk.ResponseType.ACCEPT:
1884 sel.destroy()
1885 return None
1886
1887 file_name = sel.get_filename()
1888 sel.destroy()
1889 return file_name
1890
1891 def _take_screenshot(self, dummy_button):
1892 # print "Cheese!"
1893 file_name = self._get_export_file_name()
1894 if file_name is None:
1895 return
1896
1897 # figure out the correct bounding box for what is visible on screen
1898 x1 = self._scrolled_window.get_hadjustment().get_value()
1899 y1 = self._scrolled_window.get_vadjustment().get_value()
1900 x2 = x1 + self._scrolled_window.get_hadjustment().get_page_size()
1901 y2 = y1 + self._scrolled_window.get_vadjustment().get_page_size()
1902 bounds = GooCanvas.CanvasBounds()
1903 bounds.x1, bounds.y1 = self.canvas.convert_from_pixels(x1, y1)
1904 bounds.x2, bounds.y2 = self.canvas.convert_from_pixels(x2, y2)
1905 dest_width = bounds.x2 - bounds.x1
1906 dest_height = bounds.y2 - bounds.y1
1907 # print bounds.x1, bounds.y1, " -> ", bounds.x2, bounds.y2
1908
1909 dummy, extension = os.path.splitext(file_name)
1910 extension = extension.lower()
1911 if extension == ".eps":
1912 surface = cairo.PSSurface(file_name, dest_width, dest_height)
1913 elif extension == ".pdf":
1914 surface = cairo.PDFSurface(file_name, dest_width, dest_height)
1915 elif extension == ".svg":
1916 surface = cairo.SVGSurface(file_name, dest_width, dest_height)
1917 else:
1918 dialog = Gtk.MessageDialog(
1919 parent=self.canvas.get_toplevel(),
1920 flags=Gtk.DialogFlags.DESTROY_WITH_PARENT,
1921 type=Gtk.MessageType.ERROR,
1922 buttons=Gtk.ButtonsType.OK,
1923 message_format="Unknown extension '%s' (valid extensions are '.eps', '.svg', and '.pdf')"
1924 % (extension,),
1925 )
1926 dialog.run()
1927 dialog.destroy()
1928 return
1929
1930 # draw the canvas to a printing context
1931 cr = cairo.Context(surface)
1932 cr.translate(-bounds.x1, -bounds.y1)
1933 self.canvas.render(cr, bounds, self.zoom.get_value())
1934 cr.show_page()
1935 surface.finish()
1936
1937 def set_follow_node(self, node):
1938 if isinstance(node, ns.Node):
1939 node = self.nodes[node.GetId()]
1940 self.follow_node = node
1941
1942 def _start_shell(self, dummy_button):
1943 if self.shell_window is not None:
1944 self.shell_window.present()
1945 return
1946
1947 self.shell_window = Gtk.Window()
1948 self.shell_window.set_size_request(750, 550)
1949 self.shell_window.set_resizable(True)
1950 scrolled_window = Gtk.ScrolledWindow()
1951 scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
1952 self.ipython = ipython_view.IPythonView()
1953 self.ipython.modify_font(Pango.FontDescription(SHELL_FONT))
1954 self.ipython.set_wrap_mode(Gtk.WrapMode.CHAR)
1955 self.ipython.show()
1956 scrolled_window.add(self.ipython)
1957 scrolled_window.show()
1958 self.shell_window.add(scrolled_window)
1959 self.shell_window.show()
1960 self.shell_window.connect("destroy", self._on_shell_window_destroy)
1961
1962 self._update_ipython_selected_node()
1963 self.ipython.updateNamespace({"viz": self})
1964
1965 def _on_shell_window_destroy(self, window):
1966 self.shell_window = None
1967
1968
1969initialization_hooks = []
1970
1971
1973 """
1974 Adds a callback to be called after
1975 the visualizer is initialized, like this::
1976 initialization_hook(visualizer, *args)
1977 """
1978 global initialization_hooks
1979 initialization_hooks.append((hook, args))
1980
1981
1982def set_bounds(x1, y1, x2, y2):
1983 assert x2 > x1
1984 assert y2 > y1
1985
1986 def hook(viz):
1987 cx1, cy1 = transform_point_simulation_to_canvas(x1, y1)
1988 cx2, cy2 = transform_point_simulation_to_canvas(x2, y2)
1989 viz.canvas.set_bounds(cx1, cy1, cx2, cy2)
1990
1992
1993
1994_run_once = False
1995
1996
1997def start():
1998 global _run_once
1999 if _run_once:
2000 return
2001 _run_once = True
2002 assert Visualizer.INSTANCE is None
2003 if _import_error is not None:
2004 import sys
2005
2006 print("No visualization support (%s)." % (str(_import_error),), file=sys.stderr)
2007 ns.Simulator.Run()
2008 return
2009 load_plugins()
2010 viz = Visualizer()
2011 for hook, args in initialization_hooks:
2012 GLib.idle_add(hook, viz, *args)
2013 ns.Packet.EnablePrinting()
2014 viz.start()
PyVizObject class.
Definition base.py:10
list links
list of links
Definition core.py:579
__init__(self, channel)
Initializer function.
Definition core.py:561
get_position(self)
Initializer function.
Definition core.py:596
set_position(self, x, y)
Initializer function.
Definition core.py:581
Node class.
Definition core.py:88
_get_selected(self)
Get selected function.
Definition core.py:346
_update_svg_position(self, x, y)
Update svg position.
Definition core.py:223
svg_align_y
svg align Y
Definition core.py:158
visualizer
visualier object
Definition core.py:143
set_position(self, x, y)
Set position function.
Definition core.py:435
set_color(self, color)
Set color function.
Definition core.py:497
on_leave_notify_event
on_leave_notify_event function
Definition core.py:154
_get_highlighted(self)
Get highlighted function.
Definition core.py:368
_update_appearance(self)
Update the node aspect to reflect the selected/highlighted state.
Definition core.py:390
bool highlighted
highlighted property
Definition core.py:377
_label_canvas_item
label canvas
Definition core.py:160
bool _selected
is selected
Definition core.py:149
remove_link(self, link)
Remove link function.
Definition core.py:527
_set_selected(self, value)
Set selected function.
Definition core.py:335
_set_highlighted(self, value)
Set highlighted function.
Definition core.py:357
on_enter_notify_event
on_enter_notify_event function
Definition core.py:153
bool _highlighted
is highlighted
Definition core.py:150
__init__(self, visualizer, node_index)
Initialize function.
Definition core.py:135
set_svg_icon(self, file_base_name, width=None, height=None, align_x=0.5, align_y=0.5)
Set a background SVG icon for the node.
Definition core.py:164
get_position(self)
Get position function.
Definition core.py:475
svg_item
svg item
Definition core.py:156
_has_mobility
has mobility model
Definition core.py:148
add_link(self, link)
Add link function.
Definition core.py:516
has_mobility(self)
Has mobility function.
Definition core.py:539
int _color
color
Definition core.py:151
list links
links
Definition core.py:147
tooltip_query(self, tooltip)
Query tooltip.
Definition core.py:238
svg_align_x
svg align X
Definition core.py:157
_update_position(self)
Update position function.
Definition core.py:487
set_label(self, label)
Set a label for the node.
Definition core.py:210
canvas_item
canvas item
Definition core.py:145
node_index
node index
Definition core.py:144
set_size(self, size)
Set size function.
Definition core.py:379
ShowTransmissionsMode.
Definition core.py:731
bool quit
quit indicator
Definition core.py:677
run(self)
Initializer function.
Definition core.py:695
sim_helper
helper function
Definition core.py:678
__init__(self, viz)
Initializer function.
Definition core.py:663
list pause_messages
pause messages
Definition core.py:679
viz
Visualizer object.
Definition core.py:672
set_nodes_of_interest(self, nodes)
Set nodes of interest function.
Definition core.py:681
Axes class.
Definition hud.py:9
set_bounds(x1, y1, x2, y2)
Definition core.py:1982
add_initialization_hook(hook, *args)
Definition core.py:1972