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 vbox = Gtk.VBox()
1041 vbox.show()
1042 self.window.add(vbox)
1043
1044 # canvas
1045 self.canvas = GooCanvas.Canvas()
1046 self.canvas.connect_after("button-press-event", self._canvas_button_press)
1047 self.canvas.connect_after("button-release-event", self._canvas_button_release)
1048 self.canvas.connect("scroll-event", self._canvas_scroll_event)
1049 self.canvas.props.has_tooltip = True
1050 self.canvas.connect("query-tooltip", self._canvas_tooltip_cb)
1051 self.canvas.show()
1052 sw = Gtk.ScrolledWindow()
1053 sw.show()
1054 self._scrolled_window = sw
1055 sw.add(self.canvas)
1056 vbox.pack_start(sw, True, True, 4)
1057 self.canvas.set_size_request(600, 450)
1058 self.canvas.set_bounds(-10000, -10000, 10000, 10000)
1059 self.canvas.scroll_to(0, 0)
1060
1061 self.canvas.get_root_item().add_child(self.links_group, -1)
1062 self.links_group.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1063
1064 self.canvas.get_root_item().add_child(self.channels_group, -1)
1065 self.channels_group.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1066 self.channels_group.raise_(self.links_group)
1067
1068 self.canvas.get_root_item().add_child(self.nodes_group, -1)
1069 self.nodes_group.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1070 self.nodes_group.raise_(self.channels_group)
1071
1072 self.hud = hud.Axes(self)
1073
1074 hbox = Gtk.HBox()
1075 hbox.show()
1076 vbox.pack_start(hbox, False, False, 4)
1077
1078 # zoom
1079 zoom_adj = Gtk.Adjustment(
1080 value=1.0,
1081 lower=0.01,
1082 upper=10.0,
1083 step_increment=0.02,
1084 page_increment=1.0,
1085 page_size=1.0,
1086 )
1087 self.zoom = zoom_adj
1088
1089 def _zoom_changed(adj):
1090 self.canvas.set_scale(adj.get_value())
1091
1092 zoom_adj.connect("value-changed", _zoom_changed)
1093 zoom = Gtk.SpinButton.new(zoom_adj, 0.1, 1)
1094 zoom.set_digits(3)
1095 zoom.show()
1096 hbox.pack_start(GObject.new(Gtk.Label, label=" Zoom:", visible=True), False, False, 4)
1097 hbox.pack_start(zoom, False, False, 4)
1098 _zoom_changed(zoom_adj)
1099
1100 # speed
1101 speed_adj = Gtk.Adjustment(
1102 value=1.0, lower=0.01, upper=10.0, step_increment=0.02, page_increment=1.0, page_size=0
1103 )
1104
1105 def _speed_changed(adj):
1106 self.speed = adj.get_value()
1107 self.sample_period = SAMPLE_PERIOD * adj.get_value()
1108 self._start_update_timer()
1109
1110 speed_adj.connect("value-changed", _speed_changed)
1111 speed = Gtk.SpinButton.new(speed_adj, 1, 0)
1112 speed.set_digits(3)
1113 speed.show()
1114 hbox.pack_start(GObject.new(Gtk.Label, label=" Speed:", visible=True), False, False, 4)
1115 hbox.pack_start(speed, False, False, 4)
1116 _speed_changed(speed_adj)
1117
1118 # Current time
1119 self.time_label = GObject.new(Gtk.Label, label=" Speed:", visible=True)
1120 self.time_label.set_width_chars(20)
1121 hbox.pack_start(self.time_label, False, False, 4)
1122
1123 # Screenshot button
1124 screenshot_button = GObject.new(
1125 Gtk.Button,
1126 label="Snapshot",
1127 relief=Gtk.ReliefStyle.NONE,
1128 focus_on_click=False,
1129 visible=True,
1130 )
1131 hbox.pack_start(screenshot_button, False, False, 4)
1132
1133 def load_button_icon(button, icon_name):
1134 if not Gtk.IconTheme.get_default().has_icon(icon_name):
1135 print(f"Could not load icon {icon_name}", file=sys.stderr)
1136 return
1137 image = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.BUTTON)
1138 button.set_image(image)
1139 button.props.always_show_image = True
1140
1141 load_button_icon(screenshot_button, "applets-screenshooter")
1142 screenshot_button.connect("clicked", self._take_screenshot)
1143
1144 # Shell button
1145 if ipython_view is not None:
1146 shell_button = GObject.new(
1147 Gtk.Button,
1148 label="Shell",
1149 relief=Gtk.ReliefStyle.NONE,
1150 focus_on_click=False,
1151 visible=True,
1152 )
1153 hbox.pack_start(shell_button, False, False, 4)
1154 load_button_icon(shell_button, "gnome-terminal")
1155 shell_button.connect("clicked", self._start_shell)
1156
1157 # Play button
1158 self.play_button = GObject.new(
1159 Gtk.ToggleButton,
1160 label="Simulate (F3)",
1161 relief=Gtk.ReliefStyle.NONE,
1162 focus_on_click=False,
1163 visible=True,
1164 )
1165 load_button_icon(self.play_button, "media-playback-start")
1166 accel_group = Gtk.AccelGroup()
1167 self.window.add_accel_group(accel_group)
1168 self.play_button.add_accelerator(
1169 "clicked", accel_group, Gdk.KEY_F3, 0, Gtk.AccelFlags.VISIBLE
1170 )
1171 self.play_button.connect("toggled", self._on_play_button_toggled)
1172 hbox.pack_start(self.play_button, False, False, 4)
1173
1174 self.canvas.get_root_item().connect("button-press-event", self.on_root_button_press_event)
1175
1176 vbox.pack_start(self._create_advanced_controls(), False, False, 4)
1177
1178 display = Gdk.Display.get_default()
1179 try:
1180 monitor = display.get_primary_monitor()
1181 geometry = monitor.get_geometry()
1182 scale_factor = monitor.get_scale_factor()
1183 except AttributeError:
1184 screen = display.get_default_screen()
1185 monitor_id = screen.get_primary_monitor()
1186 geometry = screen.get_monitor_geometry(monitor_id)
1187 scale_factor = screen.get_monitor_scale_factor(monitor_id)
1188 width = scale_factor * geometry.width
1189 height = scale_factor * geometry.height
1190 self.window.set_default_size(width * 2 / 3, height * 2 / 3)
1191 self.window.show()
1192
1193 def scan_topology(self):
1194 print("scanning topology: %i nodes..." % (ns.NodeList.GetNNodes(),))
1195 graph = pygraphviz.AGraph()
1196 seen_nodes = 0
1197 for nodeI in range(ns.NodeList.GetNNodes()):
1198 seen_nodes += 1
1199 if seen_nodes == 100:
1200 print(
1201 "scan topology... %i nodes visited (%.1f%%)"
1202 % (nodeI, 100 * nodeI / ns.NodeList.GetNNodes())
1203 )
1204 seen_nodes = 0
1205 node = ns.NodeList.GetNode(nodeI)
1206 node_name = "Node %i" % nodeI
1207 node_view = self.get_node(nodeI)
1208
1209 mobility = node.GetObject[ns.MobilityModel]()
1210 if mobility:
1211 node_view.set_color("red")
1212 pos = node.GetObject[ns.MobilityModel]().__deref__().GetPosition()
1213 node_view.set_position(*transform_point_simulation_to_canvas(pos.x, pos.y))
1214 # print "node has mobility position -> ", "%f,%f" % (pos.x, pos.y)
1215 else:
1216 graph.add_node(node_name)
1217
1218 for devI in range(node.GetNDevices()):
1219 device = node.GetDevice(devI)
1220 device_traits = lookup_netdevice_traits(type(device.__deref__()))
1221 if device_traits.is_wireless:
1222 continue
1223 if device_traits.is_virtual:
1224 continue
1225 channel = device.GetChannel()
1226 if channel.GetNDevices() > 2:
1227 if REPRESENT_CHANNELS_AS_NODES:
1228 # represent channels as white nodes
1229 if mobility is None:
1230 channel_name = "Channel %s" % id(channel)
1231 graph.add_edge(node_name, channel_name)
1232 self.get_channel(channel)
1233 self.create_link(self.get_node(nodeI), self.get_channel(channel))
1234 else:
1235 # don't represent channels, just add links between nodes in the same channel
1236 for otherDevI in range(channel.GetNDevices()):
1237 otherDev = channel.GetDevice(otherDevI)
1238 otherNode = otherDev.GetNode()
1239 otherNodeView = self.get_node(otherNode.GetId())
1240 if otherNode is not node:
1241 if mobility is None and not otherNodeView.has_mobility:
1242 other_node_name = "Node %i" % otherNode.GetId()
1243 graph.add_edge(node_name, other_node_name)
1244 self.create_link(self.get_node(nodeI), otherNodeView)
1245 else:
1246 for otherDevI in range(channel.GetNDevices()):
1247 otherDev = channel.GetDevice(otherDevI)
1248 otherNode = otherDev.GetNode()
1249 otherNodeView = self.get_node(otherNode.GetId())
1250 if otherNode is not node:
1251 if mobility is None and not otherNodeView.has_mobility:
1252 other_node_name = "Node %i" % otherNode.GetId()
1253 graph.add_edge(node_name, other_node_name)
1254 self.create_link(self.get_node(nodeI), otherNodeView)
1255
1256 print("scanning topology: calling graphviz layout")
1257 graph.layout(LAYOUT_ALGORITHM)
1258 for node in graph.iternodes():
1259 # print node, "=>", node.attr['pos']
1260 node_type, node_id = node.split(" ")
1261 pos_x, pos_y = [float(s) for s in node.attr["pos"].split(",")]
1262 if node_type == "Node":
1263 obj = self.nodes[int(node_id)]
1264 elif node_type == "Channel":
1265 obj = self.channels[int(node_id)]
1266 obj.set_position(pos_x, pos_y)
1267
1268 print("scanning topology: all done.")
1269 self.emit("topology-scanned")
1270
1271 def get_node(self, index):
1272 try:
1273 return self.nodes[index]
1274 except KeyError:
1275 node = Node(self, index)
1276 self.nodes[index] = node
1277 self.nodes_group.add_child(node.canvas_item, -1)
1278 node.canvas_item.connect("button-press-event", self.on_node_button_press_event, node)
1279 node.canvas_item.connect(
1280 "button-release-event", self.on_node_button_release_event, node
1281 )
1282 return node
1283
1284 def get_channel(self, ns3_channel):
1285 try:
1286 return self.channels[id(ns3_channel)]
1287 except KeyError:
1288 channel = Channel(ns3_channel)
1289 self.channels[id(ns3_channel)] = channel
1290 self.channels_group.add_child(channel.canvas_item, -1)
1291 return channel
1292
1293 def create_link(self, node, node_or_channel):
1294 link = WiredLink(node, node_or_channel)
1295 self.links_group.add_child(link.canvas_item, -1)
1296 link.canvas_item.lower(None)
1297
1298 def update_view(self):
1299 # print "update_view"
1300
1301 self.time_label.set_text("Time: %f s" % ns.Simulator.Now().GetSeconds())
1302
1303 self._update_node_positions()
1304
1305 # Update information
1306 for info_win in self.information_windows:
1307 info_win.update()
1308
1309 self._update_transmissions_view()
1310 self._update_drops_view()
1311
1312 self.emit("update-view")
1313
1314 def _update_node_positions(self):
1315 for node in self.nodes.values():
1316 if node.has_mobility:
1317 ns3_node = ns.NodeList.GetNode(node.node_index)
1318 mobility = ns3_node.GetObject[ns.MobilityModel]()
1319 if mobility:
1320 pos = ns3_node.GetObject[ns.MobilityModel]().__deref__().GetPosition()
1321 x, y = transform_point_simulation_to_canvas(pos.x, pos.y)
1322 node.set_position(x, y)
1323 if node is self.follow_node:
1324 hadj = self._scrolled_window.get_hadjustment()
1325 vadj = self._scrolled_window.get_vadjustment()
1326 px, py = self.canvas.convert_to_pixels(x, y)
1327 hadj.set_value(px - hadj.get_page_size() / 2)
1328 vadj.set_value(py - vadj.get_page_size() / 2)
1329
1330 def center_on_node(self, node):
1331 if isinstance(node, ns.Node):
1332 node = self.nodes[node.GetId()]
1333 elif isinstance(node, int):
1334 node = self.nodes[node]
1335 elif isinstance(node, Node):
1336 pass
1337 else:
1338 raise TypeError("expected int, viz.Node or ns.Node, not %r" % node)
1339
1340 x, y = node.get_position()
1341 hadj = self._scrolled_window.get_hadjustment()
1342 vadj = self._scrolled_window.get_vadjustment()
1343 px, py = self.canvas.convert_to_pixels(x, y)
1344 hadj.set_value(px - hadj.get_page_size() / 2)
1345 vadj.set_value(py - vadj.get_page_size() / 2)
1346
1347 def update_model(self):
1348 self.simulation.lock.acquire()
1349 try:
1350 self.emit("simulation-periodic-update")
1351 finally:
1352 self.simulation.lock.release()
1353
1354 def do_simulation_periodic_update(self):
1355 smooth_factor = int(self.transmissions_smoothing_adjustment.get_value() * 10)
1356
1357 transmissions = self.simulation.sim_helper.GetTransmissionSamples()
1358 self._last_transmissions.append(transmissions)
1359 while len(self._last_transmissions) > smooth_factor:
1360 self._last_transmissions.pop(0)
1361
1362 drops = self.simulation.sim_helper.GetPacketDropSamples()
1363 self._last_drops.append(drops)
1364 while len(self._last_drops) > smooth_factor:
1365 self._last_drops.pop(0)
1366
1367 def _get_label_over_line_position(self, pos1_x, pos1_y, pos2_x, pos2_y):
1368 hadj = self._scrolled_window.get_hadjustment()
1369 vadj = self._scrolled_window.get_vadjustment()
1370 bounds_x1, bounds_y1 = self.canvas.convert_from_pixels(hadj.get_value(), vadj.get_value())
1371 bounds_x2, bounds_y2 = self.canvas.convert_from_pixels(
1372 hadj.get_value() + hadj.get_page_size(), vadj.get_value() + vadj.get_page_size()
1373 )
1374 ns.PyViz.LineClipping(
1375 bounds_x1, bounds_y1, bounds_x2, bounds_y2, pos1_x, pos1_y, pos2_x, pos2_y
1376 )
1377 return (pos1_x.value + pos2_x.value) / 2, (pos1_y.value + pos2_y.value) / 2
1378
1379 def _update_transmissions_view(self):
1380 transmissions_average = {}
1381 for transmission_set in self._last_transmissions:
1382 for transmission in transmission_set:
1383 key = (transmission.transmitter.GetId(), transmission.receiver.GetId())
1384 rx_bytes, count = transmissions_average.get(key, (0, 0))
1385 rx_bytes += transmission.bytes
1386 count += 1
1387 transmissions_average[key] = rx_bytes, count
1388
1389 old_arrows = self._transmission_arrows
1390 for arrow, label in old_arrows:
1391 arrow.set_property("visibility", GooCanvas.CanvasItemVisibility.HIDDEN)
1392 label.set_property("visibility", GooCanvas.CanvasItemVisibility.HIDDEN)
1393 new_arrows = []
1394
1395 k = self.node_size_adjustment.get_value() / 5
1396
1397 for (transmitter_id, receiver_id), (rx_bytes, rx_count) in transmissions_average.items():
1398 transmitter = self.get_node(transmitter_id)
1399 receiver = self.get_node(receiver_id)
1400 try:
1401 arrow, label = old_arrows.pop()
1402 except IndexError:
1403 arrow = GooCanvas.CanvasPolyline(
1404 line_width=2.0,
1405 stroke_color_rgba=0x00C000C0,
1406 close_path=False,
1407 end_arrow=True,
1408 pointer_events=GooCanvas.CanvasPointerEvents.NONE,
1409 )
1410 arrow.set_property("parent", self.canvas.get_root_item())
1411 arrow.raise_(None)
1412
1413 label = GooCanvas.CanvasText(
1414 parent=self.canvas.get_root_item(),
1415 pointer_events=GooCanvas.CanvasPointerEvents.NONE,
1416 )
1417 label.raise_(None)
1418
1419 arrow.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1420 line_width = max(0.1, math.log(float(rx_bytes) / rx_count / self.sample_period) * k)
1421 arrow.set_property("line-width", line_width)
1422
1423 pos1_x, pos1_y = transmitter.get_position()
1424 pos2_x, pos2_y = receiver.get_position()
1425 points = GooCanvas.CanvasPoints.new(2)
1426 points.set_point(0, pos1_x, pos1_y)
1427 points.set_point(1, pos2_x, pos2_y)
1428 arrow.set_property("points", points)
1429
1430 kbps = float(rx_bytes * 8) / 1e3 / rx_count / self.sample_period
1431 label.set_properties(
1432 visibility=GooCanvas.CanvasItemVisibility.VISIBLE_ABOVE_THRESHOLD,
1433 visibility_threshold=0.5,
1434 font=("Sans Serif %f" % int(1 + BITRATE_FONT_SIZE * k)),
1435 )
1436
1437 angle = math.atan2((pos2_y - pos1_y), (pos2_x - pos1_x))
1438 if angle > math.pi / 2 or angle < -math.pi / 2:
1439 # Normalize the angle, in essence, adjust the angle to keep
1440 # it from rotating beyond +/- 90 degrees. In this way, the
1441 # direction of the label remain always readable regardless of
1442 # the angle.
1443 angle += math.pi
1444 label.set_properties(
1445 text=("← %.2f kbit/s" % (kbps,)),
1446 alignment=Pango.Alignment.CENTER,
1447 anchor=GooCanvas.CanvasAnchorType.S,
1448 x=0,
1449 y=-line_width / 2,
1450 )
1451 else:
1452 label.set_properties(
1453 text=("%.2f kbit/s →" % (kbps,)),
1454 alignment=Pango.Alignment.CENTER,
1455 anchor=GooCanvas.CanvasAnchorType.N,
1456 x=0,
1457 y=line_width / 2,
1458 )
1459 M = cairo.Matrix()
1460 lx, ly = self._get_label_over_line_position(
1461 c_double(pos1_x), c_double(pos1_y), c_double(pos2_x), c_double(pos2_y)
1462 )
1463 M.translate(lx, ly)
1464 M.rotate(angle)
1465 try:
1466 label.set_transform(M)
1467 except KeyError:
1468 # https://gitlab.gnome.org/GNOME/pygobject/issues/16
1469 warnings.warn(
1470 "PyGobject bug causing label position error; "
1471 "should be fixed in PyGObject >= 3.29.1"
1472 )
1473 label.set_properties(x=(lx + label.props.x), y=(ly + label.props.y))
1474
1475 new_arrows.append((arrow, label))
1476
1477 self._transmission_arrows = new_arrows + old_arrows
1478
1479 def _update_drops_view(self):
1480 drops_average = {}
1481 for drop_set in self._last_drops:
1482 for drop in drop_set:
1483 key = drop.transmitter.GetId()
1484 drop_bytes, count = drops_average.get(key, (0, 0))
1485 drop_bytes += drop.bytes
1486 count += 1
1487 drops_average[key] = drop_bytes, count
1488
1489 old_arrows = self._drop_arrows
1490 for arrow, label in old_arrows:
1491 arrow.set_property("visibility", GooCanvas.CanvasItemVisibility.HIDDEN)
1492 label.set_property("visibility", GooCanvas.CanvasItemVisibility.HIDDEN)
1493 new_arrows = []
1494
1495 # get the coordinates for the edge of screen
1496 vadjustment = self._scrolled_window.get_vadjustment()
1497 bottom_y = vadjustment.get_value() + vadjustment.get_page_size()
1498 dummy, edge_y = self.canvas.convert_from_pixels(0, bottom_y)
1499
1500 k = self.node_size_adjustment.get_value() / 5
1501
1502 for transmitter_id, (drop_bytes, drop_count) in drops_average.items():
1503 transmitter = self.get_node(transmitter_id)
1504 try:
1505 arrow, label = old_arrows.pop()
1506 except IndexError:
1507 arrow = GooCanvas.CanvasPolyline(
1508 line_width=2.0,
1509 stroke_color_rgba=0xC00000C0,
1510 close_path=False,
1511 end_arrow=True,
1512 pointer_events=GooCanvas.CanvasPointerEvents.NONE,
1513 )
1514 arrow.set_property("parent", self.canvas.get_root_item())
1515 arrow.raise_(None)
1516
1517 label = GooCanvas.CanvasText(
1518 pointer_events=GooCanvas.CanvasPointerEvents.NONE
1519 ) # , fill_color_rgba=0x00C000C0)
1520 label.set_property("parent", self.canvas.get_root_item())
1521 label.raise_(None)
1522
1523 arrow.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1524 arrow.set_property(
1525 "line-width",
1526 max(0.1, math.log(float(drop_bytes) / drop_count / self.sample_period) * k),
1527 )
1528 pos1_x, pos1_y = transmitter.get_position()
1529 pos2_x, pos2_y = pos1_x, edge_y
1530 points = GooCanvas.CanvasPoints.new(2)
1531 points.set_point(0, pos1_x, pos1_y)
1532 points.set_point(1, pos2_x, pos2_y)
1533 arrow.set_property("points", points)
1534
1535 label.set_properties(
1536 visibility=GooCanvas.CanvasItemVisibility.VISIBLE_ABOVE_THRESHOLD,
1537 visibility_threshold=0.5,
1538 font=("Sans Serif %i" % int(1 + BITRATE_FONT_SIZE * k)),
1539 text=(
1540 "%.2f kbit/s" % (float(drop_bytes * 8) / 1e3 / drop_count / self.sample_period,)
1541 ),
1542 alignment=Pango.Alignment.CENTER,
1543 x=(pos1_x + pos2_x) / 2,
1544 y=(pos1_y + pos2_y) / 2,
1545 )
1546
1547 new_arrows.append((arrow, label))
1548
1549 self._drop_arrows = new_arrows + old_arrows
1550
1551 def _on_simulation_finished(self):
1552 print("Simulation finished.")
1553 if self._update_timeout_id is not None:
1554 GLib.source_remove(self._update_timeout_id)
1555 self._update_timeout_id = None
1556 self.play_button.set_active(False)
1557 self.play_button.set_sensitive(False)
1558
1559 def update_view_timeout(self):
1560 # print "view: update_view_timeout called at real time ", time.time()
1561
1562 # while the simulator is busy, run the gtk event loop
1563 while not self.simulation.lock.acquire(False):
1564 while Gtk.events_pending():
1565 Gtk.main_iteration()
1566 pause_messages = self.simulation.pause_messages
1567 self.simulation.pause_messages = []
1568 try:
1569 self.update_view()
1570 self.simulation.target_time = ns.Simulator.Now().GetSeconds() + self.sample_period
1571 # print "view: target time set to %f" % self.simulation.target_time
1572 finally:
1573 self.simulation.lock.release()
1574
1575 if pause_messages:
1576 # print pause_messages
1577 dialog = Gtk.MessageDialog(
1578 parent=self.window,
1579 flags=0,
1580 type=Gtk.MessageType.WARNING,
1581 buttons=Gtk.ButtonsType.OK,
1582 message_format="\n".join(pause_messages),
1583 )
1584 dialog.connect("response", lambda d, r: d.destroy())
1585 dialog.show()
1586 self.play_button.set_active(False)
1587
1588 # if we're paused, stop the update timer
1589 if not self.play_button.get_active():
1590 self._update_timeout_id = None
1591 return False
1592
1593 # print "view: self.simulation.go.set()"
1594 self.simulation.go.set()
1595 # print "view: done."
1596 return True
1597
1598 def _start_update_timer(self):
1599 if self._update_timeout_id is not None:
1600 GLib.source_remove(self._update_timeout_id)
1601 # print "start_update_timer"
1602 self._update_timeout_id = GLib.timeout_add(
1603 int(SAMPLE_PERIOD / min(self.speed, 1) * 1e3),
1604 self.update_view_timeout,
1605 priority=PRIORITY_UPDATE_VIEW,
1606 )
1607
1608 def _on_play_button_toggled(self, button):
1609 if button.get_active():
1610 self._start_update_timer()
1611 else:
1612 if self._update_timeout_id is not None:
1613 GLib.source_remove(self._update_timeout_id)
1614
1615 def _quit(self, *dummy_args):
1616 if self._update_timeout_id is not None:
1617 GLib.source_remove(self._update_timeout_id)
1618 self._update_timeout_id = None
1619 self.simulation.quit = True
1620 self.simulation.go.set()
1621 self.simulation.join()
1622 Gtk.main_quit()
1623
1624 def _monkey_patch_ipython(self):
1625 # The user may want to access the NS 3 simulation state, but
1626 # NS 3 is not thread safe, so it could cause serious problems.
1627 # To work around this, monkey-patch IPython to automatically
1628 # acquire and release the simulation lock around each code
1629 # that is executed.
1630
1631 original_runcode = self.ipython.runcode
1632
1633 def runcode(ip, *args):
1634 # print "lock"
1635 self.simulation.lock.acquire()
1636 try:
1637 return original_runcode(*args)
1638 finally:
1639 # print "unlock"
1640 self.simulation.lock.release()
1641
1642 import types
1643
1644 self.ipython.runcode = types.MethodType(runcode, self.ipython)
1645
1646 def autoscale_view(self):
1647 if not self.nodes:
1648 return
1649 self._update_node_positions()
1650 positions = [node.get_position() for node in self.nodes.values()]
1651 min_x, min_y = min(x for (x, y) in positions), min(y for (x, y) in positions)
1652 max_x, max_y = max(x for (x, y) in positions), max(y for (x, y) in positions)
1653 min_x_px, min_y_px = self.canvas.convert_to_pixels(min_x, min_y)
1654 max_x_px, max_y_px = self.canvas.convert_to_pixels(max_x, max_y)
1655 dx = max_x - min_x
1656 dy = max_y - min_y
1657 dx_px = max_x_px - min_x_px
1658 dy_px = max_y_px - min_y_px
1659 hadj = self._scrolled_window.get_hadjustment()
1660 vadj = self._scrolled_window.get_vadjustment()
1661 new_dx, new_dy = 1.5 * dx_px, 1.5 * dy_px
1662
1663 if new_dx == 0 or new_dy == 0:
1664 return
1665
1666 self.zoom.set_value(min(hadj.get_page_size() / new_dx, vadj.get_page_size() / new_dy))
1667
1668 x1, y1 = self.canvas.convert_from_pixels(hadj.get_value(), vadj.get_value())
1669 x2, y2 = self.canvas.convert_from_pixels(
1670 (hadj.get_value() + hadj.get_page_size()), (vadj.get_value() + vadj.get_page_size())
1671 )
1672 width = x2 - x1
1673 height = y2 - y1
1674 center_x = (min_x + max_x) / 2
1675 center_y = (min_y + max_y) / 2
1676
1677 self.canvas.scroll_to(center_x - width / 2, center_y - height / 2)
1678
1679 return False
1680
1681 def start(self):
1682 self.scan_topology()
1683 self.window.connect("delete-event", self._quit)
1684 # self._start_update_timer()
1685 GLib.timeout_add(200, self.autoscale_view)
1686 self.simulation.start()
1687
1688 try:
1689 __IPYTHON__
1690 except NameError:
1691 pass
1692 else:
1693 self._monkey_patch_ipython()
1694
1695 Gtk.main()
1696
1697 def on_root_button_press_event(self, view, target, event):
1698 if event.button == 1:
1699 self.select_node(None)
1700 return True
1701
1702 def on_node_button_press_event(self, view, target, event, node):
1703 button = event.button
1704 if button == 1:
1705 self.select_node(node)
1706 return True
1707 elif button == 3:
1708 self.popup_node_menu(node, event)
1709 return True
1710 elif button == 2:
1711 self.begin_node_drag(node, event)
1712 return True
1713 return False
1714
1715 def on_node_button_release_event(self, view, target, event, node):
1716 if event.button == 2:
1717 self.end_node_drag(node)
1718 return True
1719 return False
1720
1721 class NodeDragState(object):
1722 def __init__(self, canvas_x0, canvas_y0, sim_x0, sim_y0):
1723 self.canvas_x0 = canvas_x0
1724 self.canvas_y0 = canvas_y0
1725 self.sim_x0 = sim_x0
1726 self.sim_y0 = sim_y0
1727 self.motion_signal = None
1728
1729 def begin_node_drag(self, node, event):
1730 self.simulation.lock.acquire()
1731 try:
1732 ns3_node = ns.NodeList.GetNode(node.node_index)
1733 mob = ns3_node.GetObject[ns.MobilityModel]()
1734 if not mob:
1735 return
1736 if self.node_drag_state is not None:
1737 return
1738 pos = ns3_node.GetObject[ns.MobilityModel]().__deref__().GetPosition()
1739 finally:
1740 self.simulation.lock.release()
1741 devpos = self.canvas.get_window().get_device_position(event.device)
1742 x0, y0 = self.canvas.convert_from_pixels(devpos.x, devpos.y)
1743 self.node_drag_state = self.NodeDragState(x0, y0, pos.x, pos.y)
1744 self.node_drag_state.motion_signal = node.canvas_item.connect(
1745 "motion-notify-event", self.node_drag_motion, node
1746 )
1747
1748 def node_drag_motion(self, item, targe_item, event, node):
1749 self.simulation.lock.acquire()
1750 try:
1751 ns3_node = ns.NodeList.GetNode(node.node_index)
1752 mob = ns3_node.GetObject[ns.MobilityModel]()
1753 if not mob:
1754 return False
1755 if self.node_drag_state is None:
1756 return False
1757 devpos = self.canvas.get_window().get_device_position(event.device)
1758 canvas_x, canvas_y = self.canvas.convert_from_pixels(devpos.x, devpos.y)
1759 dx = canvas_x - self.node_drag_state.canvas_x0
1760 dy = canvas_y - self.node_drag_state.canvas_y0
1761 pos = mob.GetPosition()
1762 pos.x = self.node_drag_state.sim_x0 + transform_distance_canvas_to_simulation(dx)
1763 pos.y = self.node_drag_state.sim_y0 + transform_distance_canvas_to_simulation(dy)
1764 # print "SetPosition(%G, %G)" % (pos.x, pos.y)
1765 mob.SetPosition(pos)
1766 node.set_position(*transform_point_simulation_to_canvas(pos.x, pos.y))
1767 finally:
1768 self.simulation.lock.release()
1769 return True
1770
1771 def end_node_drag(self, node):
1772 if self.node_drag_state is None:
1773 return
1774 node.canvas_item.disconnect(self.node_drag_state.motion_signal)
1775 self.node_drag_state = None
1776
1777 def popup_node_menu(self, node, event):
1778 menu = Gtk.Menu()
1779 self.emit("populate-node-menu", node, menu)
1780 menu.popup_at_pointer(event)
1781
1782 def _update_ipython_selected_node(self):
1783 # If we are running under ipython -gthread, make this new
1784 # selected node available as a global 'selected_node'
1785 # variable.
1786 try:
1787 __IPYTHON__
1788 except NameError:
1789 pass
1790 else:
1791 if self.selected_node is None:
1792 ns3_node = None
1793 else:
1794 self.simulation.lock.acquire()
1795 try:
1796 ns3_node = ns.NodeList.GetNode(self.selected_node.node_index)
1797 finally:
1798 self.simulation.lock.release()
1799 self.ipython.updateNamespace({"selected_node": ns3_node})
1800
1801 def select_node(self, node):
1802 if isinstance(node, ns.Node):
1803 node = self.nodes[node.GetId()]
1804 elif isinstance(node, int):
1805 node = self.nodes[node]
1806 elif isinstance(node, Node):
1807 pass
1808 elif node is None:
1809 pass
1810 else:
1811 raise TypeError("expected None, int, viz.Node or ns.Node, not %r" % node)
1812
1813 if node is self.selected_node:
1814 return
1815
1816 if self.selected_node is not None:
1817 self.selected_node.selected = False
1818 self.selected_node = node
1819 if self.selected_node is not None:
1820 self.selected_node.selected = True
1821
1822 if self._show_transmissions_mode == ShowTransmissionsMode.SELECTED:
1823 if self.selected_node is None:
1824 self.simulation.set_nodes_of_interest([])
1825 else:
1826 self.simulation.set_nodes_of_interest([self.selected_node.node_index])
1827
1828 self._update_ipython_selected_node()
1829
1830 def add_information_window(self, info_win):
1831 self.information_windows.append(info_win)
1832 self.simulation.lock.acquire()
1833 try:
1834 info_win.update()
1835 finally:
1836 self.simulation.lock.release()
1837
1838 def remove_information_window(self, info_win):
1839 self.information_windows.remove(info_win)
1840
1841 def _canvas_tooltip_cb(self, canvas, x, y, keyboard_mode, tooltip):
1842 # print "tooltip query: ", x, y
1843 hadj = self._scrolled_window.get_hadjustment()
1844 vadj = self._scrolled_window.get_vadjustment()
1845 x, y = self.canvas.convert_from_pixels(hadj.get_value() + x, vadj.get_value() + y)
1846 item = self.canvas.get_item_at(x, y, True)
1847 # print "items at (%f, %f): %r | keyboard_mode=%r" % (x, y, item, keyboard_mode)
1848 if not item:
1849 return False
1850 while item is not None:
1851 obj = getattr(item, "pyviz_object", None)
1852 if obj is not None:
1853 obj.tooltip_query(tooltip)
1854 return True
1855 item = item.props.parent
1856 return False
1857
1858 def _get_export_file_name(self):
1859 sel = Gtk.FileChooserNative.new(
1860 "Save...", self.canvas.get_toplevel(), Gtk.FileChooserAction.SAVE, "_Save", "_Cancel"
1861 )
1862 sel.set_local_only(True)
1863 sel.set_do_overwrite_confirmation(True)
1864 sel.set_current_name("Unnamed.pdf")
1865
1866 filter = Gtk.FileFilter()
1867 filter.set_name("Embedded PostScript")
1868 filter.add_mime_type("image/x-eps")
1869 sel.add_filter(filter)
1870
1871 filter = Gtk.FileFilter()
1872 filter.set_name("Portable Document Graphics")
1873 filter.add_mime_type("application/pdf")
1874 sel.add_filter(filter)
1875
1876 filter = Gtk.FileFilter()
1877 filter.set_name("Scalable Vector Graphics")
1878 filter.add_mime_type("image/svg+xml")
1879 sel.add_filter(filter)
1880
1881 resp = sel.run()
1882 if resp != Gtk.ResponseType.ACCEPT:
1883 sel.destroy()
1884 return None
1885
1886 file_name = sel.get_filename()
1887 sel.destroy()
1888 return file_name
1889
1890 def _take_screenshot(self, dummy_button):
1891 # print "Cheese!"
1892 file_name = self._get_export_file_name()
1893 if file_name is None:
1894 return
1895
1896 # figure out the correct bounding box for what is visible on screen
1897 x1 = self._scrolled_window.get_hadjustment().get_value()
1898 y1 = self._scrolled_window.get_vadjustment().get_value()
1899 x2 = x1 + self._scrolled_window.get_hadjustment().get_page_size()
1900 y2 = y1 + self._scrolled_window.get_vadjustment().get_page_size()
1901 bounds = GooCanvas.CanvasBounds()
1902 bounds.x1, bounds.y1 = self.canvas.convert_from_pixels(x1, y1)
1903 bounds.x2, bounds.y2 = self.canvas.convert_from_pixels(x2, y2)
1904 dest_width = bounds.x2 - bounds.x1
1905 dest_height = bounds.y2 - bounds.y1
1906 # print bounds.x1, bounds.y1, " -> ", bounds.x2, bounds.y2
1907
1908 dummy, extension = os.path.splitext(file_name)
1909 extension = extension.lower()
1910 if extension == ".eps":
1911 surface = cairo.PSSurface(file_name, dest_width, dest_height)
1912 elif extension == ".pdf":
1913 surface = cairo.PDFSurface(file_name, dest_width, dest_height)
1914 elif extension == ".svg":
1915 surface = cairo.SVGSurface(file_name, dest_width, dest_height)
1916 else:
1917 dialog = Gtk.MessageDialog(
1918 parent=self.canvas.get_toplevel(),
1919 flags=Gtk.DialogFlags.DESTROY_WITH_PARENT,
1920 type=Gtk.MessageType.ERROR,
1921 buttons=Gtk.ButtonsType.OK,
1922 message_format="Unknown extension '%s' (valid extensions are '.eps', '.svg', and '.pdf')"
1923 % (extension,),
1924 )
1925 dialog.run()
1926 dialog.destroy()
1927 return
1928
1929 # draw the canvas to a printing context
1930 cr = cairo.Context(surface)
1931 cr.translate(-bounds.x1, -bounds.y1)
1932 self.canvas.render(cr, bounds, self.zoom.get_value())
1933 cr.show_page()
1934 surface.finish()
1935
1936 def set_follow_node(self, node):
1937 if isinstance(node, ns.Node):
1938 node = self.nodes[node.GetId()]
1939 self.follow_node = node
1940
1941 def _start_shell(self, dummy_button):
1942 if self.shell_window is not None:
1943 self.shell_window.present()
1944 return
1945
1946 self.shell_window = Gtk.Window()
1947 self.shell_window.set_size_request(750, 550)
1948 self.shell_window.set_resizable(True)
1949 scrolled_window = Gtk.ScrolledWindow()
1950 scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
1951 self.ipython = ipython_view.IPythonView()
1952 self.ipython.modify_font(Pango.FontDescription(SHELL_FONT))
1953 self.ipython.set_wrap_mode(Gtk.WrapMode.CHAR)
1954 self.ipython.show()
1955 scrolled_window.add(self.ipython)
1956 scrolled_window.show()
1957 self.shell_window.add(scrolled_window)
1958 self.shell_window.show()
1959 self.shell_window.connect("destroy", self._on_shell_window_destroy)
1960
1961 self._update_ipython_selected_node()
1962 self.ipython.updateNamespace({"viz": self})
1963
1964 def _on_shell_window_destroy(self, window):
1965 self.shell_window = None
1966
1967
1968initialization_hooks = []
1969
1970
1972 """
1973 Adds a callback to be called after
1974 the visualizer is initialized, like this::
1975 initialization_hook(visualizer, *args)
1976 """
1977 global initialization_hooks
1978 initialization_hooks.append((hook, args))
1979
1980
1981def set_bounds(x1, y1, x2, y2):
1982 assert x2 > x1
1983 assert y2 > y1
1984
1985 def hook(viz):
1986 cx1, cy1 = transform_point_simulation_to_canvas(x1, y1)
1987 cx2, cy2 = transform_point_simulation_to_canvas(x2, y2)
1988 viz.canvas.set_bounds(cx1, cy1, cx2, cy2)
1989
1991
1992
1993_run_once = False
1994
1995
1996def start():
1997 global _run_once
1998 if _run_once:
1999 return
2000 _run_once = True
2001 assert Visualizer.INSTANCE is None
2002 if _import_error is not None:
2003 import sys
2004
2005 print("No visualization support (%s)." % (str(_import_error),), file=sys.stderr)
2006 ns.Simulator.Run()
2007 return
2008 load_plugins()
2009 viz = Visualizer()
2010 for hook, args in initialization_hooks:
2011 GLib.idle_add(hook, viz, *args)
2012 ns.Packet.EnablePrinting()
2013 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:1981
add_initialization_hook(hook, *args)
Definition core.py:1971