|
1 # -*- Mode: python; coding: utf-8 -*- |
|
2 from __future__ import division |
|
3 #from __future__ import with_statement |
|
4 |
|
5 LAYOUT_ALGORITHM = 'neato' # ['neato'|'dot'|'twopi'|'circo'|'fdp'|'nop'] |
|
6 REPRESENT_CHANNELS_AS_NODES = 1 |
|
7 DEFAULT_NODE_SIZE = 3.0 # default node size in meters |
|
8 DEFAULT_TRANSMISSIONS_MEMORY = 5 # default number of of past intervals whose transmissions are remembered |
|
9 BITRATE_FONT_SIZE = 10 |
|
10 |
|
11 # internal constants, normally not meant to be changed |
|
12 SAMPLE_PERIOD = 0.1 |
|
13 PRIORITY_UPDATE_MODEL = -100 |
|
14 PRIORITY_UPDATE_VIEW = 200 |
|
15 |
|
16 import platform |
|
17 if platform.system() == "Windows": |
|
18 SHELL_FONT = "Lucida Console 9" |
|
19 else: |
|
20 SHELL_FONT = "Luxi Mono 10" |
|
21 |
|
22 |
|
23 import ns3 |
|
24 import math |
|
25 import os |
|
26 import sys |
|
27 import gobject |
|
28 import time |
|
29 |
|
30 try: |
|
31 import pygraphviz |
|
32 import gtk |
|
33 import pango |
|
34 import goocanvas |
|
35 import cairo |
|
36 import threading |
|
37 import hud |
|
38 #import time |
|
39 import cairo |
|
40 from higcontainer import HIGContainer |
|
41 gobject.threads_init() |
|
42 try: |
|
43 import svgitem |
|
44 except ImportError: |
|
45 svgitem = None |
|
46 except ImportError, _import_error: |
|
47 import dummy_threading as threading |
|
48 else: |
|
49 _import_error = None |
|
50 |
|
51 try: |
|
52 import ipython_view |
|
53 except ImportError: |
|
54 ipython_view = None |
|
55 |
|
56 from base import InformationWindow, PyVizObject, Link, lookup_netdevice_traits, PIXELS_PER_METER |
|
57 from base import transform_distance_simulation_to_canvas, transform_point_simulation_to_canvas |
|
58 from base import transform_distance_canvas_to_simulation, transform_point_canvas_to_simulation |
|
59 from base import load_plugins, register_plugin, plugins |
|
60 |
|
61 PI_OVER_2 = math.pi/2 |
|
62 PI_TIMES_2 = math.pi*2 |
|
63 |
|
64 class Node(PyVizObject): |
|
65 |
|
66 __gsignals__ = { |
|
67 |
|
68 # signal emitted whenever a tooltip is about to be shown for the node |
|
69 # the first signal parameter is a python list of strings, to which information can be appended |
|
70 'query-extra-tooltip-info': (gobject.SIGNAL_RUN_LAST, None, (object,)), |
|
71 |
|
72 } |
|
73 |
|
74 def __init__(self, visualizer, node_index): |
|
75 super(Node, self).__init__() |
|
76 |
|
77 self.visualizer = visualizer |
|
78 self.node_index = node_index |
|
79 self.canvas_item = goocanvas.Ellipse() |
|
80 self.canvas_item.set_data("pyviz-object", self) |
|
81 self.links = [] |
|
82 self._has_mobility = None |
|
83 self._selected = False |
|
84 self._highlighted = False |
|
85 self._color = 0x808080ff |
|
86 self._size = DEFAULT_NODE_SIZE |
|
87 self.canvas_item.connect("enter-notify-event", self.on_enter_notify_event) |
|
88 self.canvas_item.connect("leave-notify-event", self.on_leave_notify_event) |
|
89 self.menu = None |
|
90 self.svg_item = None |
|
91 self.svg_align_x = None |
|
92 self.svg_align_y = None |
|
93 self._label = None |
|
94 self._label_canvas_item = None |
|
95 |
|
96 self._update_appearance() # call this last |
|
97 |
|
98 def set_svg_icon(self, file_base_name, width=None, height=None, align_x=0.5, align_y=0.5): |
|
99 """ |
|
100 Set a background SVG icon for the node. |
|
101 |
|
102 @param file_base_name: base file name, including .svg |
|
103 extension, of the svg file. Place the file in the folder |
|
104 src/contrib/visualizer/resource. |
|
105 |
|
106 @param width: scale to the specified width, in meters |
|
107 @param width: scale to the specified height, in meters |
|
108 |
|
109 @param align_x: horizontal alignment of the icon relative to |
|
110 the node position, from 0 (icon fully to the left of the node) |
|
111 to 1.0 (icon fully to the right of the node) |
|
112 |
|
113 @param align_y: vertical alignment of the icon relative to the |
|
114 node position, from 0 (icon fully to the top of the node) to |
|
115 1.0 (icon fully to the bottom of the node) |
|
116 |
|
117 """ |
|
118 if width is None and height is None: |
|
119 raise ValueError("either width or height must be given") |
|
120 rsvg_handle = svgitem.rsvg_handle_factory(file_base_name) |
|
121 x = self.canvas_item.props.center_x |
|
122 y = self.canvas_item.props.center_y |
|
123 self.svg_item = svgitem.SvgItem(x, y, rsvg_handle) |
|
124 self.svg_item.props.parent = self.visualizer.canvas.get_root_item() |
|
125 self.svg_item.props.pointer_events = 0 |
|
126 self.svg_item.lower(None) |
|
127 self.svg_item.props.visibility = goocanvas.ITEM_VISIBLE_ABOVE_THRESHOLD |
|
128 if width is not None: |
|
129 self.svg_item.props.width = transform_distance_simulation_to_canvas(width) |
|
130 if height is not None: |
|
131 self.svg_item.props.height = transform_distance_simulation_to_canvas(height) |
|
132 |
|
133 #threshold1 = 10.0/self.svg_item.props.height |
|
134 #threshold2 = 10.0/self.svg_item.props.width |
|
135 #self.svg_item.props.visibility_threshold = min(threshold1, threshold2) |
|
136 |
|
137 self.svg_align_x = align_x |
|
138 self.svg_align_y = align_y |
|
139 self._update_svg_position(x, y) |
|
140 self._update_appearance() |
|
141 |
|
142 def set_label(self, label): |
|
143 assert isinstance(label, basestring) |
|
144 self._label = label |
|
145 self._update_appearance() |
|
146 |
|
147 def _update_svg_position(self, x, y): |
|
148 w = self.svg_item.width |
|
149 h = self.svg_item.height |
|
150 self.svg_item.set_properties(x=(x - (1-self.svg_align_x)*w), |
|
151 y=(y - (1-self.svg_align_y)*h)) |
|
152 |
|
153 |
|
154 def tooltip_query(self, tooltip): |
|
155 self.visualizer.simulation.lock.acquire() |
|
156 try: |
|
157 ns3_node = ns3.NodeList.GetNode(self.node_index) |
|
158 ipv4 = ns3_node.GetObject(ns3.Ipv4.GetTypeId()) |
|
159 lines = ['<b><u>Node %i</u></b>' % self.node_index] |
|
160 lines.append('') |
|
161 |
|
162 self.emit("query-extra-tooltip-info", lines) |
|
163 |
|
164 mob = ns3_node.GetObject(ns3.MobilityModel.GetTypeId()) |
|
165 if mob is not None: |
|
166 lines.append(' <b>Mobility Model</b>: %s' % mob.GetInstanceTypeId().GetName()) |
|
167 |
|
168 for devI in range(ns3_node.GetNDevices()): |
|
169 lines.append('') |
|
170 lines.append(' <u>NetDevice %i:</u>' % devI) |
|
171 dev = ns3_node.GetDevice(devI) |
|
172 name = ns3.Names.FindName(dev) |
|
173 if name: |
|
174 lines.append(' <b>Name:</b> %s' % name) |
|
175 devname = dev.GetInstanceTypeId().GetName() |
|
176 lines.append(' <b>Type:</b> %s' % devname) |
|
177 if ipv4 is not None: |
|
178 ipv4_idx = ipv4.GetInterfaceForDevice(dev) |
|
179 if ipv4_idx != -1: |
|
180 addresses = [ |
|
181 '%s/%s' % (ipv4.GetAddress(ipv4_idx, i).GetLocal(), |
|
182 ipv4.GetAddress(ipv4_idx, i).GetMask()) |
|
183 for i in range(ipv4.GetNAddresses(ipv4_idx))] |
|
184 lines.append(' <b>IPv4 Addresses:</b> %s' % '; '.join(addresses)) |
|
185 |
|
186 lines.append(' <b>MAC Address:</b> %s' % (dev.GetAddress(),)) |
|
187 |
|
188 tooltip.set_markup('\n'.join(lines)) |
|
189 finally: |
|
190 self.visualizer.simulation.lock.release() |
|
191 |
|
192 def on_enter_notify_event(self, view, target, event): |
|
193 self.highlighted = True |
|
194 def on_leave_notify_event(self, view, target, event): |
|
195 self.highlighted = False |
|
196 |
|
197 def _set_selected(self, value): |
|
198 self._selected = value |
|
199 self._update_appearance() |
|
200 def _get_selected(self): |
|
201 return self._selected |
|
202 selected = property(_get_selected, _set_selected) |
|
203 |
|
204 def _set_highlighted(self, value): |
|
205 self._highlighted = value |
|
206 self._update_appearance() |
|
207 def _get_highlighted(self): |
|
208 return self._highlighted |
|
209 highlighted = property(_get_highlighted, _set_highlighted) |
|
210 |
|
211 def set_size(self, size): |
|
212 self._size = size |
|
213 self._update_appearance() |
|
214 |
|
215 def _update_appearance(self): |
|
216 """Update the node aspect to reflect the selected/highlighted state""" |
|
217 |
|
218 size = transform_distance_simulation_to_canvas(self._size) |
|
219 if self.svg_item is not None: |
|
220 alpha = 0x80 |
|
221 else: |
|
222 alpha = 0xff |
|
223 fill_color_rgba = (self._color & 0xffffff00) | alpha |
|
224 self.canvas_item.set_properties(radius_x=size, radius_y=size, |
|
225 fill_color_rgba=fill_color_rgba) |
|
226 if self._selected: |
|
227 line_width = size*.3 |
|
228 else: |
|
229 line_width = size*.15 |
|
230 if self.highlighted: |
|
231 stroke_color = 'yellow' |
|
232 else: |
|
233 stroke_color = 'black' |
|
234 self.canvas_item.set_properties(line_width=line_width, stroke_color=stroke_color) |
|
235 |
|
236 if self._label is not None: |
|
237 if self._label_canvas_item is None: |
|
238 self._label_canvas_item = goocanvas.Text(visibility_threshold=0.5, |
|
239 font="Sans Serif 10", |
|
240 fill_color_rgba=0x808080ff, |
|
241 alignment=pango.ALIGN_CENTER, |
|
242 anchor=gtk.ANCHOR_N, |
|
243 parent=self.visualizer.canvas.get_root_item(), |
|
244 pointer_events=0) |
|
245 self._label_canvas_item.lower(None) |
|
246 |
|
247 self._label_canvas_item.set_properties(visibility=goocanvas.ITEM_VISIBLE_ABOVE_THRESHOLD, |
|
248 text=self._label) |
|
249 self._update_position() |
|
250 |
|
251 def set_position(self, x, y): |
|
252 self.canvas_item.set_property("center_x", x) |
|
253 self.canvas_item.set_property("center_y", y) |
|
254 if self.svg_item is not None: |
|
255 self._update_svg_position(x, y) |
|
256 |
|
257 for link in self.links: |
|
258 link.update_points() |
|
259 |
|
260 if self._label_canvas_item is not None: |
|
261 self._label_canvas_item.set_properties(x=x, y=(y+self._size*3)) |
|
262 |
|
263 def get_position(self): |
|
264 return (self.canvas_item.get_property("center_x"), self.canvas_item.get_property("center_y")) |
|
265 |
|
266 def _update_position(self): |
|
267 x, y = self.get_position() |
|
268 self.set_position(x, y) |
|
269 |
|
270 def set_color(self, color): |
|
271 if isinstance(color, str): |
|
272 color = gtk.gdk.color_parse(color) |
|
273 color = ((color.red>>8) << 24) | ((color.green>>8) << 16) | ((color.blue>>8) << 8) | 0xff |
|
274 self._color = color |
|
275 self._update_appearance() |
|
276 |
|
277 def add_link(self, link): |
|
278 assert isinstance(link, Link) |
|
279 self.links.append(link) |
|
280 |
|
281 def remove_link(self, link): |
|
282 assert isinstance(link, Link) |
|
283 self.links.remove(link) |
|
284 |
|
285 @property |
|
286 def has_mobility(self): |
|
287 if self._has_mobility is None: |
|
288 node = ns3.NodeList.GetNode(self.node_index) |
|
289 mobility = node.GetObject(ns3.MobilityModel.GetTypeId()) |
|
290 self._has_mobility = (mobility is not None) |
|
291 return self._has_mobility |
|
292 |
|
293 |
|
294 class Channel(PyVizObject): |
|
295 def __init__(self, channel): |
|
296 self.channel = channel |
|
297 self.canvas_item = goocanvas.Ellipse(radius_x=30, radius_y=30, |
|
298 fill_color="white", |
|
299 stroke_color="grey", line_width=2.0, |
|
300 line_dash=goocanvas.LineDash([10.0, 10.0 ]), |
|
301 visibility=goocanvas.ITEM_VISIBLE) |
|
302 self.canvas_item.set_data("pyviz-object", self) |
|
303 self.links = [] |
|
304 |
|
305 def set_position(self, x, y): |
|
306 self.canvas_item.set_property("center_x", x) |
|
307 self.canvas_item.set_property("center_y", y) |
|
308 |
|
309 for link in self.links: |
|
310 link.update_points() |
|
311 |
|
312 def get_position(self): |
|
313 return (self.canvas_item.get_property("center_x"), self.canvas_item.get_property("center_y")) |
|
314 |
|
315 |
|
316 class WiredLink(Link): |
|
317 def __init__(self, node1, node2): |
|
318 assert isinstance(node1, Node) |
|
319 assert isinstance(node2, (Node, Channel)) |
|
320 self.node1 = node1 |
|
321 self.node2 = node2 |
|
322 self.canvas_item = goocanvas.Path(line_width=1.0, stroke_color="black") |
|
323 self.canvas_item.set_data("pyviz-object", self) |
|
324 self.node1.links.append(self) |
|
325 self.node2.links.append(self) |
|
326 |
|
327 def update_points(self): |
|
328 pos1_x, pos1_y = self.node1.get_position() |
|
329 pos2_x, pos2_y = self.node2.get_position() |
|
330 self.canvas_item.set_property("data", "M %r %r L %r %r" % (pos1_x, pos1_y, pos2_x, pos2_y)) |
|
331 |
|
332 |
|
333 |
|
334 class SimulationThread(threading.Thread): |
|
335 def __init__(self, viz): |
|
336 super(SimulationThread, self).__init__() |
|
337 assert isinstance(viz, Visualizer) |
|
338 self.viz = viz # Visualizer object |
|
339 self.lock = threading.Lock() |
|
340 self.go = threading.Event() |
|
341 self.go.clear() |
|
342 self.target_time = 0 # in seconds |
|
343 self.quit = False |
|
344 self.sim_helper = ns3.PyViz() |
|
345 self.pause_messages = [] |
|
346 |
|
347 def set_nodes_of_interest(self, nodes): |
|
348 self.lock.acquire() |
|
349 try: |
|
350 self.sim_helper.SetNodesOfInterest(nodes) |
|
351 finally: |
|
352 self.lock.release() |
|
353 |
|
354 def run(self): |
|
355 while not self.quit: |
|
356 #print "sim: Wait for go" |
|
357 self.go.wait() # wait until the main (view) thread gives us the go signal |
|
358 self.go.clear() |
|
359 if self.quit: |
|
360 break |
|
361 #self.go.clear() |
|
362 #print "sim: Acquire lock" |
|
363 self.lock.acquire() |
|
364 try: |
|
365 if 0: |
|
366 if ns3.Simulator.IsFinished(): |
|
367 self.viz.play_button.set_sensitive(False) |
|
368 break |
|
369 #print "sim: Current time is %f; Run until: %f" % (ns3.Simulator.Now ().GetSeconds (), self.target_time) |
|
370 #if ns3.Simulator.Now ().GetSeconds () > self.target_time: |
|
371 # print "skipping, model is ahead of view!" |
|
372 self.sim_helper.SimulatorRunUntil(ns3.Seconds(self.target_time)) |
|
373 #print "sim: Run until ended at current time: ", ns3.Simulator.Now ().GetSeconds () |
|
374 self.pause_messages.extend(self.sim_helper.GetPauseMessages()) |
|
375 gobject.idle_add(self.viz.update_model, priority=PRIORITY_UPDATE_MODEL) |
|
376 #print "sim: Run until: ", self.target_time, ": finished." |
|
377 finally: |
|
378 self.lock.release() |
|
379 #print "sim: Release lock, loop." |
|
380 |
|
381 # enumeration |
|
382 class ShowTransmissionsMode(object): |
|
383 __slots__ = [] |
|
384 ShowTransmissionsMode.ALL = ShowTransmissionsMode() |
|
385 ShowTransmissionsMode.NONE = ShowTransmissionsMode() |
|
386 ShowTransmissionsMode.SELECTED = ShowTransmissionsMode() |
|
387 |
|
388 class Visualizer(gobject.GObject): |
|
389 INSTANCE = None |
|
390 |
|
391 if _import_error is None: |
|
392 __gsignals__ = { |
|
393 |
|
394 # signal emitted whenever a right-click-on-node popup menu is being constructed |
|
395 'populate-node-menu': (gobject.SIGNAL_RUN_LAST, None, (object, gtk.Menu,)), |
|
396 |
|
397 # signal emitted after every simulation period (SAMPLE_PERIOD seconds of simulated time) |
|
398 # the simulation lock is acquired while the signal is emitted |
|
399 'simulation-periodic-update': (gobject.SIGNAL_RUN_LAST, None, ()), |
|
400 |
|
401 # signal emitted right after the topology is scanned |
|
402 'topology-scanned': (gobject.SIGNAL_RUN_LAST, None, ()), |
|
403 |
|
404 # signal emitted when it's time to update the view objects |
|
405 'update-view': (gobject.SIGNAL_RUN_LAST, None, ()), |
|
406 |
|
407 } |
|
408 |
|
409 def __init__(self): |
|
410 assert Visualizer.INSTANCE is None |
|
411 Visualizer.INSTANCE = self |
|
412 super(Visualizer, self).__init__() |
|
413 self.nodes = {} # node index -> Node |
|
414 self.channels = {} # id(ns3.Channel) -> Channel |
|
415 self.window = None # toplevel window |
|
416 self.canvas = None # goocanvas.Canvas |
|
417 self.time_label = None # gtk.Label |
|
418 self.play_button = None # gtk.ToggleButton |
|
419 self.zoom = None # gtk.Adjustment |
|
420 self._scrolled_window = None # gtk.ScrolledWindow |
|
421 |
|
422 self.links_group = goocanvas.Group() |
|
423 self.channels_group = goocanvas.Group() |
|
424 self.nodes_group = goocanvas.Group() |
|
425 |
|
426 self._update_timeout_id = None |
|
427 self.simulation = SimulationThread(self) |
|
428 self.selected_node = None # node currently selected |
|
429 self.speed = 1.0 |
|
430 self.information_windows = [] |
|
431 self._transmission_arrows = [] |
|
432 self._last_transmissions = [] |
|
433 self._drop_arrows = [] |
|
434 self._last_drops = [] |
|
435 self._show_transmissions_mode = None |
|
436 self.set_show_transmissions_mode(ShowTransmissionsMode.ALL) |
|
437 self._panning_state = None |
|
438 self.node_size_adjustment = None |
|
439 self.transmissions_smoothing_adjustment = None |
|
440 self.sample_period = SAMPLE_PERIOD |
|
441 self.node_drag_state = None |
|
442 self.follow_node = None |
|
443 self.shell_window = None |
|
444 |
|
445 self.create_gui() |
|
446 |
|
447 for plugin in plugins: |
|
448 plugin(self) |
|
449 |
|
450 def set_show_transmissions_mode(self, mode): |
|
451 assert isinstance(mode, ShowTransmissionsMode) |
|
452 self._show_transmissions_mode = mode |
|
453 if self._show_transmissions_mode == ShowTransmissionsMode.ALL: |
|
454 self.simulation.set_nodes_of_interest(range(ns3.NodeList.GetNNodes())) |
|
455 elif self._show_transmissions_mode == ShowTransmissionsMode.NONE: |
|
456 self.simulation.set_nodes_of_interest([]) |
|
457 elif self._show_transmissions_mode == ShowTransmissionsMode.SELECTED: |
|
458 if self.selected_node is None: |
|
459 self.simulation.set_nodes_of_interest([]) |
|
460 else: |
|
461 self.simulation.set_nodes_of_interest([self.selected_node.node_index]) |
|
462 |
|
463 def _create_advanced_controls(self): |
|
464 expander = gtk.Expander("Advanced") |
|
465 expander.show() |
|
466 |
|
467 main_vbox = gobject.new(gtk.VBox, border_width=8, visible=True) |
|
468 expander.add(main_vbox) |
|
469 |
|
470 main_hbox1 = gobject.new(gtk.HBox, border_width=8, visible=True) |
|
471 main_vbox.pack_start(main_hbox1) |
|
472 |
|
473 show_transmissions_group = HIGContainer("Show transmissions") |
|
474 show_transmissions_group.show() |
|
475 main_hbox1.pack_start(show_transmissions_group, False, False, 8) |
|
476 |
|
477 vbox = gtk.VBox(True, 4) |
|
478 vbox.show() |
|
479 show_transmissions_group.add(vbox) |
|
480 |
|
481 all_nodes = gtk.RadioButton(None) |
|
482 all_nodes.set_label("All nodes") |
|
483 all_nodes.set_active(True) |
|
484 all_nodes.show() |
|
485 vbox.add(all_nodes) |
|
486 |
|
487 selected_node = gtk.RadioButton(all_nodes) |
|
488 selected_node.show() |
|
489 selected_node.set_label("Selected node") |
|
490 selected_node.set_active(False) |
|
491 vbox.add(selected_node) |
|
492 |
|
493 no_node = gtk.RadioButton(all_nodes) |
|
494 no_node.show() |
|
495 no_node.set_label("Disabled") |
|
496 no_node.set_active(False) |
|
497 vbox.add(no_node) |
|
498 |
|
499 def toggled(radio): |
|
500 if radio.get_active(): |
|
501 self.set_show_transmissions_mode(ShowTransmissionsMode.ALL) |
|
502 all_nodes.connect("toggled", toggled) |
|
503 |
|
504 def toggled(radio): |
|
505 if radio.get_active(): |
|
506 self.set_show_transmissions_mode(ShowTransmissionsMode.NONE) |
|
507 no_node.connect("toggled", toggled) |
|
508 |
|
509 def toggled(radio): |
|
510 if radio.get_active(): |
|
511 self.set_show_transmissions_mode(ShowTransmissionsMode.SELECTED) |
|
512 selected_node.connect("toggled", toggled) |
|
513 |
|
514 |
|
515 # -- misc settings |
|
516 misc_settings_group = HIGContainer("Misc Settings") |
|
517 misc_settings_group.show() |
|
518 main_hbox1.pack_start(misc_settings_group, False, False, 8) |
|
519 settings_hbox = gobject.new(gtk.HBox, border_width=8, visible=True) |
|
520 misc_settings_group.add(settings_hbox) |
|
521 |
|
522 # --> node size |
|
523 vbox = gobject.new(gtk.VBox, border_width=0, visible=True) |
|
524 scale = gobject.new(gtk.HScale, visible=True, digits=2) |
|
525 vbox.pack_start(scale, True, True, 0) |
|
526 vbox.pack_start(gobject.new(gtk.Label, label="Node Size", visible=True), True, True, 0) |
|
527 settings_hbox.pack_start(vbox, False, False, 6) |
|
528 self.node_size_adjustment = scale.get_adjustment() |
|
529 def node_size_changed(adj): |
|
530 for node in self.nodes.itervalues(): |
|
531 node.set_size(adj.value) |
|
532 self.node_size_adjustment.connect("value-changed", node_size_changed) |
|
533 self.node_size_adjustment.set_all(DEFAULT_NODE_SIZE, 0.01, 20, 0.1) |
|
534 |
|
535 # --> transmissions smooth factor |
|
536 vbox = gobject.new(gtk.VBox, border_width=0, visible=True) |
|
537 scale = gobject.new(gtk.HScale, visible=True, digits=1) |
|
538 vbox.pack_start(scale, True, True, 0) |
|
539 vbox.pack_start(gobject.new(gtk.Label, label="Tx. Smooth Factor (s)", visible=True), True, True, 0) |
|
540 settings_hbox.pack_start(vbox, False, False, 6) |
|
541 self.transmissions_smoothing_adjustment = scale.get_adjustment() |
|
542 self.transmissions_smoothing_adjustment.set_all(DEFAULT_TRANSMISSIONS_MEMORY*0.1, 0.1, 10, 0.1) |
|
543 |
|
544 return expander |
|
545 |
|
546 class _PanningState(object): |
|
547 __slots__ = ['initial_mouse_pos', 'initial_canvas_pos', 'motion_signal'] |
|
548 |
|
549 def _begin_panning(self, widget, event): |
|
550 self.canvas.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR)) |
|
551 self._panning_state = self._PanningState() |
|
552 x, y, dummy = widget.window.get_pointer() |
|
553 self._panning_state.initial_mouse_pos = (x, y) |
|
554 x = self._scrolled_window.get_hadjustment().value |
|
555 y = self._scrolled_window.get_vadjustment().value |
|
556 self._panning_state.initial_canvas_pos = (x, y) |
|
557 self._panning_state.motion_signal = self.canvas.connect("motion-notify-event", self._panning_motion) |
|
558 |
|
559 def _end_panning(self, event): |
|
560 if self._panning_state is None: |
|
561 return |
|
562 self.canvas.window.set_cursor(None) |
|
563 self.canvas.disconnect(self._panning_state.motion_signal) |
|
564 self._panning_state = None |
|
565 |
|
566 def _panning_motion(self, widget, event): |
|
567 assert self._panning_state is not None |
|
568 if event.is_hint: |
|
569 x, y, dummy = widget.window.get_pointer() |
|
570 else: |
|
571 x, y = event.x, event.y |
|
572 |
|
573 hadj = self._scrolled_window.get_hadjustment() |
|
574 vadj = self._scrolled_window.get_vadjustment() |
|
575 mx0, my0 = self._panning_state.initial_mouse_pos |
|
576 cx0, cy0 = self._panning_state.initial_canvas_pos |
|
577 |
|
578 dx = x - mx0 |
|
579 dy = y - my0 |
|
580 hadj.value = cx0 - dx |
|
581 vadj.value = cy0 - dy |
|
582 return True |
|
583 |
|
584 def _canvas_button_press(self, widget, event): |
|
585 if event.button == 2: |
|
586 self._begin_panning(widget, event) |
|
587 return True |
|
588 return False |
|
589 |
|
590 def _canvas_button_release(self, dummy_widget, event): |
|
591 if event.button == 2: |
|
592 self._end_panning(event) |
|
593 return True |
|
594 return False |
|
595 |
|
596 def _canvas_scroll_event(self, dummy_widget, event): |
|
597 if event.direction == gtk.gdk.SCROLL_UP: |
|
598 self.zoom.value *= 1.25 |
|
599 return True |
|
600 elif event.direction == gtk.gdk.SCROLL_DOWN: |
|
601 self.zoom.value /= 1.25 |
|
602 return True |
|
603 return False |
|
604 |
|
605 def get_hadjustment(self): |
|
606 return self._scrolled_window.get_hadjustment() |
|
607 def get_vadjustment(self): |
|
608 return self._scrolled_window.get_vadjustment() |
|
609 |
|
610 def create_gui(self): |
|
611 self.window = gtk.Window() |
|
612 vbox = gtk.VBox(); vbox.show() |
|
613 self.window.add(vbox) |
|
614 |
|
615 # canvas |
|
616 self.canvas = goocanvas.Canvas() |
|
617 self.canvas.connect_after("button-press-event", self._canvas_button_press) |
|
618 self.canvas.connect_after("button-release-event", self._canvas_button_release) |
|
619 self.canvas.connect("scroll-event", self._canvas_scroll_event) |
|
620 self.canvas.props.has_tooltip = True |
|
621 self.canvas.connect("query-tooltip", self._canvas_tooltip_cb) |
|
622 self.canvas.show() |
|
623 sw = gtk.ScrolledWindow(); sw.show() |
|
624 self._scrolled_window = sw |
|
625 sw.add(self.canvas) |
|
626 vbox.pack_start(sw, True, True, 4) |
|
627 self.canvas.set_size_request(600, 450) |
|
628 self.canvas.set_bounds(-10000, -10000, 10000, 10000) |
|
629 self.canvas.scroll_to(0, 0) |
|
630 |
|
631 |
|
632 self.canvas.get_root_item().add_child(self.links_group) |
|
633 self.links_group.set_property("visibility", goocanvas.ITEM_VISIBLE) |
|
634 |
|
635 self.canvas.get_root_item().add_child(self.channels_group) |
|
636 self.channels_group.set_property("visibility", goocanvas.ITEM_VISIBLE) |
|
637 self.channels_group.raise_(self.links_group) |
|
638 |
|
639 self.canvas.get_root_item().add_child(self.nodes_group) |
|
640 self.nodes_group.set_property("visibility", goocanvas.ITEM_VISIBLE) |
|
641 self.nodes_group.raise_(self.channels_group) |
|
642 |
|
643 self.hud = hud.Axes(self) |
|
644 |
|
645 hbox = gtk.HBox(); hbox.show() |
|
646 vbox.pack_start(hbox, False, False, 4) |
|
647 |
|
648 # zoom |
|
649 zoom_adj = gtk.Adjustment(1.0, 0.01, 10.0, 0.02, 1.0, 0) |
|
650 self.zoom = zoom_adj |
|
651 def _zoom_changed(adj): |
|
652 self.canvas.set_scale(adj.value) |
|
653 zoom_adj.connect("value-changed", _zoom_changed) |
|
654 zoom = gtk.SpinButton(zoom_adj) |
|
655 zoom.set_digits(3) |
|
656 zoom.show() |
|
657 hbox.pack_start(gobject.new(gtk.Label, label=" Zoom:", visible=True), False, False, 4) |
|
658 hbox.pack_start(zoom, False, False, 4) |
|
659 _zoom_changed(zoom_adj) |
|
660 |
|
661 # speed |
|
662 speed_adj = gtk.Adjustment(1.0, 0.01, 10.0, 0.02, 1.0, 0) |
|
663 def _speed_changed(adj): |
|
664 self.speed = adj.value |
|
665 self.sample_period = SAMPLE_PERIOD*adj.value |
|
666 self._start_update_timer() |
|
667 speed_adj.connect("value-changed", _speed_changed) |
|
668 speed = gtk.SpinButton(speed_adj) |
|
669 speed.set_digits(3) |
|
670 speed.show() |
|
671 hbox.pack_start(gobject.new(gtk.Label, label=" Speed:", visible=True), False, False, 4) |
|
672 hbox.pack_start(speed, False, False, 4) |
|
673 _speed_changed(speed_adj) |
|
674 |
|
675 # Current time |
|
676 self.time_label = gobject.new(gtk.Label, label=" Speed:", visible=True) |
|
677 self.time_label.set_width_chars(20) |
|
678 hbox.pack_start(self.time_label, False, False, 4) |
|
679 |
|
680 # Screenshot button |
|
681 screenshot_button = gobject.new(gtk.Button, |
|
682 label="Snapshot", |
|
683 relief=gtk.RELIEF_NONE, focus_on_click=False, |
|
684 visible=True) |
|
685 hbox.pack_start(screenshot_button, False, False, 4) |
|
686 |
|
687 def load_button_icon(button, icon_name): |
|
688 try: |
|
689 import gnomedesktop |
|
690 except ImportError: |
|
691 sys.stderr.write("Could not load icon %s due to missing gnomedesktop Python module\n" % icon_name) |
|
692 else: |
|
693 icon = gnomedesktop.find_icon(gtk.icon_theme_get_default(), icon_name, 16, 0) |
|
694 if icon is not None: |
|
695 button.props.image = gobject.new(gtk.Image, file=icon, visible=True) |
|
696 |
|
697 load_button_icon(screenshot_button, "applets-screenshooter") |
|
698 screenshot_button.connect("clicked", self._take_screenshot) |
|
699 |
|
700 # Shell button |
|
701 if ipython_view is not None: |
|
702 shell_button = gobject.new(gtk.Button, |
|
703 label="Shell", |
|
704 relief=gtk.RELIEF_NONE, focus_on_click=False, |
|
705 visible=True) |
|
706 hbox.pack_start(shell_button, False, False, 4) |
|
707 load_button_icon(shell_button, "gnome-terminal") |
|
708 shell_button.connect("clicked", self._start_shell) |
|
709 |
|
710 # Play button |
|
711 self.play_button = gobject.new(gtk.ToggleButton, |
|
712 image=gobject.new(gtk.Image, stock=gtk.STOCK_MEDIA_PLAY, visible=True), |
|
713 label="Simulate (F3)", |
|
714 relief=gtk.RELIEF_NONE, focus_on_click=False, |
|
715 use_stock=True, visible=True) |
|
716 accel_group = gtk.AccelGroup() |
|
717 self.window.add_accel_group(accel_group) |
|
718 self.play_button.add_accelerator("clicked", accel_group, |
|
719 gtk.keysyms.F3, 0, gtk.ACCEL_VISIBLE) |
|
720 self.play_button.connect("toggled", self._on_play_button_toggled) |
|
721 hbox.pack_start(self.play_button, False, False, 4) |
|
722 |
|
723 self.canvas.get_root_item().connect("button-press-event", self.on_root_button_press_event) |
|
724 |
|
725 vbox.pack_start(self._create_advanced_controls(), False, False, 4) |
|
726 |
|
727 self.window.show() |
|
728 |
|
729 def scan_topology(self): |
|
730 print "scanning topology: %i nodes..." % (ns3.NodeList.GetNNodes(),) |
|
731 graph = pygraphviz.AGraph() |
|
732 seen_nodes = 0 |
|
733 for nodeI in range(ns3.NodeList.GetNNodes()): |
|
734 seen_nodes += 1 |
|
735 if seen_nodes == 100: |
|
736 print "scan topology... %i nodes visited (%.1f%%)" % (nodeI, 100*nodeI/ns3.NodeList.GetNNodes()) |
|
737 seen_nodes = 0 |
|
738 node = ns3.NodeList.GetNode(nodeI) |
|
739 node_name = "Node %i" % nodeI |
|
740 node_view = self.get_node(nodeI) |
|
741 |
|
742 mobility = node.GetObject(ns3.MobilityModel.GetTypeId()) |
|
743 if mobility is not None: |
|
744 node_view.set_color("red") |
|
745 pos = mobility.GetPosition() |
|
746 node_view.set_position(*transform_point_simulation_to_canvas(pos.x, pos.y)) |
|
747 #print "node has mobility position -> ", "%f,%f" % (pos.x, pos.y) |
|
748 else: |
|
749 graph.add_node(node_name) |
|
750 |
|
751 for devI in range(node.GetNDevices()): |
|
752 device = node.GetDevice(devI) |
|
753 device_traits = lookup_netdevice_traits(type(device)) |
|
754 if device_traits.is_wireless: |
|
755 continue |
|
756 if device_traits.is_virtual: |
|
757 continue |
|
758 channel = device.GetChannel() |
|
759 if channel.GetNDevices() > 2: |
|
760 if REPRESENT_CHANNELS_AS_NODES: |
|
761 # represent channels as white nodes |
|
762 if mobility is None: |
|
763 channel_name = "Channel %s" % id(channel) |
|
764 graph.add_edge(node_name, channel_name) |
|
765 self.get_channel(channel) |
|
766 self.create_link(self.get_node(nodeI), self.get_channel(channel)) |
|
767 else: |
|
768 # don't represent channels, just add links between nodes in the same channel |
|
769 for otherDevI in range(channel.GetNDevices()): |
|
770 otherDev = channel.GetDevice(otherDevI) |
|
771 otherNode = otherDev.GetNode() |
|
772 otherNodeView = self.get_node(otherNode.GetId()) |
|
773 if otherNode is not node: |
|
774 if mobility is None and not otherNodeView.has_mobility: |
|
775 other_node_name = "Node %i" % otherNode.GetId() |
|
776 graph.add_edge(node_name, other_node_name) |
|
777 self.create_link(self.get_node(nodeI), otherNodeView) |
|
778 else: |
|
779 for otherDevI in range(channel.GetNDevices()): |
|
780 otherDev = channel.GetDevice(otherDevI) |
|
781 otherNode = otherDev.GetNode() |
|
782 otherNodeView = self.get_node(otherNode.GetId()) |
|
783 if otherNode is not node: |
|
784 if mobility is None and not otherNodeView.has_mobility: |
|
785 other_node_name = "Node %i" % otherNode.GetId() |
|
786 graph.add_edge(node_name, other_node_name) |
|
787 self.create_link(self.get_node(nodeI), otherNodeView) |
|
788 |
|
789 print "scanning topology: calling graphviz layout" |
|
790 graph.layout(LAYOUT_ALGORITHM) |
|
791 for node in graph.iternodes(): |
|
792 #print node, "=>", node.attr['pos'] |
|
793 node_type, node_id = node.split(' ') |
|
794 pos_x, pos_y = [float(s) for s in node.attr['pos'].split(',')] |
|
795 if node_type == 'Node': |
|
796 obj = self.nodes[int(node_id)] |
|
797 elif node_type == 'Channel': |
|
798 obj = self.channels[int(node_id)] |
|
799 obj.set_position(pos_x, pos_y) |
|
800 |
|
801 print "scanning topology: all done." |
|
802 self.emit("topology-scanned") |
|
803 |
|
804 def get_node(self, index): |
|
805 try: |
|
806 return self.nodes[index] |
|
807 except KeyError: |
|
808 node = Node(self, index) |
|
809 self.nodes[index] = node |
|
810 self.nodes_group.add_child(node.canvas_item) |
|
811 node.canvas_item.connect("button-press-event", self.on_node_button_press_event, node) |
|
812 node.canvas_item.connect("button-release-event", self.on_node_button_release_event, node) |
|
813 return node |
|
814 |
|
815 def get_channel(self, ns3_channel): |
|
816 try: |
|
817 return self.channels[id(ns3_channel)] |
|
818 except KeyError: |
|
819 channel = Channel(ns3_channel) |
|
820 self.channels[id(ns3_channel)] = channel |
|
821 self.channels_group.add_child(channel.canvas_item) |
|
822 return channel |
|
823 |
|
824 def create_link(self, node, node_or_channel): |
|
825 link = WiredLink(node, node_or_channel) |
|
826 self.links_group.add_child(link.canvas_item) |
|
827 link.canvas_item.lower(None) |
|
828 |
|
829 def update_view(self): |
|
830 #print "update_view" |
|
831 |
|
832 self.time_label.set_text("Time: %f s" % ns3.Simulator.Now().GetSeconds()) |
|
833 |
|
834 self._update_node_positions() |
|
835 |
|
836 # Update information |
|
837 for info_win in self.information_windows: |
|
838 info_win.update() |
|
839 |
|
840 self._update_transmissions_view() |
|
841 self._update_drops_view() |
|
842 |
|
843 self.emit("update-view") |
|
844 |
|
845 def _update_node_positions(self): |
|
846 for node in self.nodes.itervalues(): |
|
847 if node.has_mobility: |
|
848 ns3_node = ns3.NodeList.GetNode(node.node_index) |
|
849 mobility = ns3_node.GetObject(ns3.MobilityModel.GetTypeId()) |
|
850 if mobility is not None: |
|
851 pos = mobility.GetPosition() |
|
852 x, y = transform_point_simulation_to_canvas(pos.x, pos.y) |
|
853 node.set_position(x, y) |
|
854 if node is self.follow_node: |
|
855 hadj = self._scrolled_window.get_hadjustment() |
|
856 vadj = self._scrolled_window.get_vadjustment() |
|
857 px, py = self.canvas.convert_to_pixels(x, y) |
|
858 hadj.value = px - hadj.page_size/2 |
|
859 vadj.value = py - vadj.page_size/2 |
|
860 |
|
861 def center_on_node(self, node): |
|
862 if isinstance(node, ns3.Node): |
|
863 node = self.nodes[node.GetId()] |
|
864 elif isinstance(node, (int, long)): |
|
865 node = self.nodes[node] |
|
866 elif isinstance(node, Node): |
|
867 pass |
|
868 else: |
|
869 raise TypeError("expected int, viz.Node or ns3.Node, not %r" % node) |
|
870 |
|
871 x, y = node.get_position() |
|
872 hadj = self._scrolled_window.get_hadjustment() |
|
873 vadj = self._scrolled_window.get_vadjustment() |
|
874 px, py = self.canvas.convert_to_pixels(x, y) |
|
875 hadj.value = px - hadj.page_size/2 |
|
876 vadj.value = py - vadj.page_size/2 |
|
877 |
|
878 |
|
879 def update_model(self): |
|
880 self.simulation.lock.acquire() |
|
881 try: |
|
882 self.emit("simulation-periodic-update") |
|
883 finally: |
|
884 self.simulation.lock.release() |
|
885 |
|
886 def do_simulation_periodic_update(self): |
|
887 smooth_factor = int(self.transmissions_smoothing_adjustment.value*10) |
|
888 |
|
889 transmissions = self.simulation.sim_helper.GetTransmissionSamples() |
|
890 self._last_transmissions.append(transmissions) |
|
891 while len(self._last_transmissions) > smooth_factor: |
|
892 self._last_transmissions.pop(0) |
|
893 |
|
894 drops = self.simulation.sim_helper.GetPacketDropSamples() |
|
895 self._last_drops.append(drops) |
|
896 while len(self._last_drops) > smooth_factor: |
|
897 self._last_drops.pop(0) |
|
898 |
|
899 def _get_label_over_line_position(self, pos1_x, pos1_y, pos2_x, pos2_y): |
|
900 hadj = self._scrolled_window.get_hadjustment() |
|
901 vadj = self._scrolled_window.get_vadjustment() |
|
902 bounds_x1, bounds_y1 = self.canvas.convert_from_pixels(hadj.value, vadj.value) |
|
903 bounds_x2, bounds_y2 = self.canvas.convert_from_pixels(hadj.value + hadj.page_size, |
|
904 vadj.value + vadj.page_size) |
|
905 pos1_x, pos1_y, pos2_x, pos2_y = ns3.PyViz.LineClipping(bounds_x1, bounds_y1, |
|
906 bounds_x2, bounds_y2, |
|
907 pos1_x, pos1_y, |
|
908 pos2_x, pos2_y) |
|
909 return (pos1_x + pos2_x)/2, (pos1_y + pos2_y)/2 |
|
910 |
|
911 def _update_transmissions_view(self): |
|
912 transmissions_average = {} |
|
913 for transmission_set in self._last_transmissions: |
|
914 for transmission in transmission_set: |
|
915 key = (transmission.transmitter.GetId(), transmission.receiver.GetId()) |
|
916 rx_bytes, count = transmissions_average.get(key, (0, 0)) |
|
917 rx_bytes += transmission.bytes |
|
918 count += 1 |
|
919 transmissions_average[key] = rx_bytes, count |
|
920 |
|
921 old_arrows = self._transmission_arrows |
|
922 for arrow, label in old_arrows: |
|
923 arrow.set_property("visibility", goocanvas.ITEM_HIDDEN) |
|
924 label.set_property("visibility", goocanvas.ITEM_HIDDEN) |
|
925 new_arrows = [] |
|
926 |
|
927 k = self.node_size_adjustment.value/5 |
|
928 |
|
929 for (transmitter_id, receiver_id), (rx_bytes, rx_count) in transmissions_average.iteritems(): |
|
930 transmitter = self.get_node(transmitter_id) |
|
931 receiver = self.get_node(receiver_id) |
|
932 try: |
|
933 arrow, label = old_arrows.pop() |
|
934 except IndexError: |
|
935 arrow = goocanvas.Polyline(line_width=2.0, stroke_color_rgba=0x00C000C0, close_path=False, end_arrow=True) |
|
936 arrow.set_property("parent", self.canvas.get_root_item()) |
|
937 arrow.props.pointer_events = 0 |
|
938 arrow.raise_(None) |
|
939 |
|
940 label = goocanvas.Text(parent=self.canvas.get_root_item(), pointer_events=0) |
|
941 label.raise_(None) |
|
942 |
|
943 arrow.set_property("visibility", goocanvas.ITEM_VISIBLE) |
|
944 line_width = max(0.1, math.log(float(rx_bytes)/rx_count/self.sample_period)*k) |
|
945 arrow.set_property("line-width", line_width) |
|
946 |
|
947 pos1_x, pos1_y = transmitter.get_position() |
|
948 pos2_x, pos2_y = receiver.get_position() |
|
949 points = goocanvas.Points([(pos1_x, pos1_y), (pos2_x, pos2_y)]) |
|
950 arrow.set_property("points", points) |
|
951 |
|
952 kbps = float(rx_bytes*8)/1e3/rx_count/self.sample_period |
|
953 label.set_properties(visibility=goocanvas.ITEM_VISIBLE_ABOVE_THRESHOLD, |
|
954 visibility_threshold=0.5, |
|
955 font=("Sans Serif %f" % int(1+BITRATE_FONT_SIZE*k))) |
|
956 angle = math.atan2((pos2_y - pos1_y), (pos2_x - pos1_x)) |
|
957 if -PI_OVER_2 <= angle <= PI_OVER_2: |
|
958 label.set_properties(text=("%.2f kbit/s →" % (kbps,)), |
|
959 alignment=pango.ALIGN_CENTER, |
|
960 anchor=gtk.ANCHOR_S, |
|
961 x=0, y=-line_width/2) |
|
962 M = cairo.Matrix() |
|
963 M.translate(*self._get_label_over_line_position(pos1_x, pos1_y, pos2_x, pos2_y)) |
|
964 M.rotate(angle) |
|
965 label.set_transform(M) |
|
966 else: |
|
967 label.set_properties(text=("← %.2f kbit/s" % (kbps,)), |
|
968 alignment=pango.ALIGN_CENTER, |
|
969 anchor=gtk.ANCHOR_N, |
|
970 x=0, y=line_width/2) |
|
971 M = cairo.Matrix() |
|
972 M.translate(*self._get_label_over_line_position(pos1_x, pos1_y, pos2_x, pos2_y)) |
|
973 M.rotate(angle) |
|
974 M.scale(-1, -1) |
|
975 label.set_transform(M) |
|
976 |
|
977 new_arrows.append((arrow, label)) |
|
978 |
|
979 self._transmission_arrows = new_arrows + old_arrows |
|
980 |
|
981 |
|
982 def _update_drops_view(self): |
|
983 drops_average = {} |
|
984 for drop_set in self._last_drops: |
|
985 for drop in drop_set: |
|
986 key = drop.transmitter.GetId() |
|
987 drop_bytes, count = drops_average.get(key, (0, 0)) |
|
988 drop_bytes += drop.bytes |
|
989 count += 1 |
|
990 drops_average[key] = drop_bytes, count |
|
991 |
|
992 old_arrows = self._drop_arrows |
|
993 for arrow, label in old_arrows: |
|
994 arrow.set_property("visibility", goocanvas.ITEM_HIDDEN) |
|
995 label.set_property("visibility", goocanvas.ITEM_HIDDEN) |
|
996 new_arrows = [] |
|
997 |
|
998 # get the coordinates for the edge of screen |
|
999 vadjustment = self._scrolled_window.get_vadjustment() |
|
1000 bottom_y = vadjustment.value + vadjustment.page_size |
|
1001 dummy, edge_y = self.canvas.convert_from_pixels(0, bottom_y) |
|
1002 |
|
1003 k = self.node_size_adjustment.value/5 |
|
1004 |
|
1005 for transmitter_id, (drop_bytes, drop_count) in drops_average.iteritems(): |
|
1006 transmitter = self.get_node(transmitter_id) |
|
1007 try: |
|
1008 arrow, label = old_arrows.pop() |
|
1009 except IndexError: |
|
1010 arrow = goocanvas.Polyline(line_width=2.0, stroke_color_rgba=0xC00000C0, close_path=False, end_arrow=True) |
|
1011 arrow.props.pointer_events = 0 |
|
1012 arrow.set_property("parent", self.canvas.get_root_item()) |
|
1013 arrow.raise_(None) |
|
1014 |
|
1015 label = goocanvas.Text()#, fill_color_rgba=0x00C000C0) |
|
1016 label.props.pointer_events = 0 |
|
1017 label.set_property("parent", self.canvas.get_root_item()) |
|
1018 label.raise_(None) |
|
1019 |
|
1020 arrow.set_property("visibility", goocanvas.ITEM_VISIBLE) |
|
1021 arrow.set_property("line-width", max(0.1, math.log(float(drop_bytes)/drop_count/self.sample_period)*k)) |
|
1022 pos1_x, pos1_y = transmitter.get_position() |
|
1023 pos2_x, pos2_y = pos1_x, edge_y |
|
1024 points = goocanvas.Points([(pos1_x, pos1_y), (pos2_x, pos2_y)]) |
|
1025 arrow.set_property("points", points) |
|
1026 |
|
1027 label.set_properties(visibility=goocanvas.ITEM_VISIBLE_ABOVE_THRESHOLD, |
|
1028 visibility_threshold=0.5, |
|
1029 font=("Sans Serif %i" % int(1+BITRATE_FONT_SIZE*k)), |
|
1030 text=("%.2f kbit/s" % (float(drop_bytes*8)/1e3/drop_count/self.sample_period,)), |
|
1031 alignment=pango.ALIGN_CENTER, |
|
1032 x=(pos1_x + pos2_x)/2, |
|
1033 y=(pos1_y + pos2_y)/2) |
|
1034 |
|
1035 new_arrows.append((arrow, label)) |
|
1036 |
|
1037 self._drop_arrows = new_arrows + old_arrows |
|
1038 |
|
1039 |
|
1040 def update_view_timeout(self): |
|
1041 #print "view: update_view_timeout called at real time ", time.time() |
|
1042 |
|
1043 # while the simulator is busy, run the gtk event loop |
|
1044 while not self.simulation.lock.acquire(False): |
|
1045 while gtk.events_pending(): |
|
1046 gtk.main_iteration() |
|
1047 pause_messages = self.simulation.pause_messages |
|
1048 self.simulation.pause_messages = [] |
|
1049 try: |
|
1050 self.update_view() |
|
1051 self.simulation.target_time = ns3.Simulator.Now ().GetSeconds () + self.sample_period |
|
1052 #print "view: target time set to %f" % self.simulation.target_time |
|
1053 finally: |
|
1054 self.simulation.lock.release() |
|
1055 |
|
1056 if pause_messages: |
|
1057 #print pause_messages |
|
1058 dialog = gtk.MessageDialog(parent=self.window, flags=0, type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_OK, |
|
1059 message_format='\n'.join(pause_messages)) |
|
1060 dialog.connect("response", lambda d, r: d.destroy()) |
|
1061 dialog.show() |
|
1062 self.play_button.set_active(False) |
|
1063 |
|
1064 # if we're paused, stop the update timer |
|
1065 if not self.play_button.get_active(): |
|
1066 self._update_timeout_id = None |
|
1067 return False |
|
1068 |
|
1069 #print "view: self.simulation.go.set()" |
|
1070 self.simulation.go.set() |
|
1071 #print "view: done." |
|
1072 return True |
|
1073 |
|
1074 def _start_update_timer(self): |
|
1075 if self._update_timeout_id is not None: |
|
1076 gobject.source_remove(self._update_timeout_id) |
|
1077 #print "start_update_timer" |
|
1078 self._update_timeout_id = gobject.timeout_add(int(SAMPLE_PERIOD/min(self.speed, 1)*1e3), |
|
1079 self.update_view_timeout, |
|
1080 priority=PRIORITY_UPDATE_VIEW) |
|
1081 |
|
1082 def _on_play_button_toggled(self, button): |
|
1083 if button.get_active(): |
|
1084 self._start_update_timer() |
|
1085 else: |
|
1086 if self._update_timeout_id is not None: |
|
1087 gobject.source_remove(self._update_timeout_id) |
|
1088 |
|
1089 def _quit(self, *dummy_args): |
|
1090 if self._update_timeout_id is not None: |
|
1091 gobject.source_remove(self._update_timeout_id) |
|
1092 self._update_timeout_id = None |
|
1093 self.simulation.quit = True |
|
1094 self.simulation.go.set() |
|
1095 self.simulation.join() |
|
1096 gtk.main_quit() |
|
1097 |
|
1098 def _monkey_patch_ipython(self): |
|
1099 # The user may want to access the NS 3 simulation state, but |
|
1100 # NS 3 is not thread safe, so it could cause serious problems. |
|
1101 # To work around this, monkey-patch IPython to automatically |
|
1102 # acquire and release the simulation lock around each code |
|
1103 # that is executed. |
|
1104 |
|
1105 original_runcode = __IPYTHON__.runcode |
|
1106 def runcode(ip, *args): |
|
1107 #print "lock" |
|
1108 self.simulation.lock.acquire() |
|
1109 try: |
|
1110 return original_runcode(*args) |
|
1111 finally: |
|
1112 #print "unlock" |
|
1113 self.simulation.lock.release() |
|
1114 import types |
|
1115 __IPYTHON__.runcode = types.MethodType(runcode, __IPYTHON__) |
|
1116 |
|
1117 def autoscale_view(self): |
|
1118 self._update_node_positions() |
|
1119 positions = [node.get_position() for node in self.nodes.itervalues()] |
|
1120 min_x, min_y = min(x for (x,y) in positions), min(y for (x,y) in positions) |
|
1121 max_x, max_y = max(x for (x,y) in positions), max(y for (x,y) in positions) |
|
1122 min_x_px, min_y_px = self.canvas.convert_to_pixels(min_x, min_y) |
|
1123 max_x_px, max_y_px = self.canvas.convert_to_pixels(max_x, max_y) |
|
1124 dx = max_x - min_x |
|
1125 dy = max_y - min_y |
|
1126 dx_px = max_x_px - min_x_px |
|
1127 dy_px = max_y_px - min_y_px |
|
1128 hadj = self._scrolled_window.get_hadjustment() |
|
1129 vadj = self._scrolled_window.get_vadjustment() |
|
1130 new_dx, new_dy = 1.5*dx_px, 1.5*dy_px |
|
1131 |
|
1132 if new_dx == 0 or new_dy == 0: |
|
1133 return |
|
1134 |
|
1135 self.zoom.value = min(hadj.page_size/new_dx, vadj.page_size/new_dy) |
|
1136 |
|
1137 x1, y1 = self.canvas.convert_from_pixels(hadj.value, vadj.value) |
|
1138 x2, y2 = self.canvas.convert_from_pixels(hadj.value+hadj.page_size, vadj.value+vadj.page_size) |
|
1139 width = x2 - x1 |
|
1140 height = y2 - y1 |
|
1141 center_x = (min_x + max_x) / 2 |
|
1142 center_y = (min_y + max_y) / 2 |
|
1143 |
|
1144 self.canvas.scroll_to(center_x - width/2, center_y - height/2) |
|
1145 |
|
1146 return False |
|
1147 |
|
1148 def start(self): |
|
1149 self.scan_topology() |
|
1150 self.window.connect("delete-event", self._quit) |
|
1151 #self._start_update_timer() |
|
1152 gobject.timeout_add(200, self.autoscale_view) |
|
1153 self.simulation.start() |
|
1154 |
|
1155 try: |
|
1156 __IPYTHON__ |
|
1157 except NameError: |
|
1158 pass |
|
1159 else: |
|
1160 self._monkey_patch_ipython() |
|
1161 |
|
1162 gtk.main() |
|
1163 |
|
1164 |
|
1165 def on_root_button_press_event(self, view, target, event): |
|
1166 if event.button == 1: |
|
1167 self.select_node(None) |
|
1168 return True |
|
1169 |
|
1170 def on_node_button_press_event(self, view, target, event, node): |
|
1171 if event.button == 1: |
|
1172 self.select_node(node) |
|
1173 return True |
|
1174 elif event.button == 3: |
|
1175 self.popup_node_menu(node, event) |
|
1176 return True |
|
1177 elif event.button == 2: |
|
1178 self.begin_node_drag(node) |
|
1179 return True |
|
1180 return False |
|
1181 |
|
1182 def on_node_button_release_event(self, view, target, event, node): |
|
1183 if event.button == 2: |
|
1184 self.end_node_drag(node) |
|
1185 return True |
|
1186 return False |
|
1187 |
|
1188 class NodeDragState(object): |
|
1189 def __init__(self, canvas_x0, canvas_y0, sim_x0, sim_y0): |
|
1190 self.canvas_x0 = canvas_x0 |
|
1191 self.canvas_y0 = canvas_y0 |
|
1192 self.sim_x0 = sim_x0 |
|
1193 self.sim_y0 = sim_y0 |
|
1194 self.motion_signal = None |
|
1195 |
|
1196 def begin_node_drag(self, node): |
|
1197 self.simulation.lock.acquire() |
|
1198 try: |
|
1199 ns3_node = ns3.NodeList.GetNode(node.node_index) |
|
1200 mob = ns3_node.GetObject(ns3.MobilityModel.GetTypeId()) |
|
1201 if mob is None: |
|
1202 return |
|
1203 if self.node_drag_state is not None: |
|
1204 return |
|
1205 pos = mob.GetPosition() |
|
1206 finally: |
|
1207 self.simulation.lock.release() |
|
1208 x, y, dummy = self.canvas.window.get_pointer() |
|
1209 x0, y0 = self.canvas.convert_from_pixels(x, y) |
|
1210 self.node_drag_state = self.NodeDragState(x0, y0, pos.x, pos.y) |
|
1211 self.node_drag_state.motion_signal = node.canvas_item.connect("motion-notify-event", self.node_drag_motion, node) |
|
1212 |
|
1213 def node_drag_motion(self, item, targe_item, event, node): |
|
1214 self.simulation.lock.acquire() |
|
1215 try: |
|
1216 ns3_node = ns3.NodeList.GetNode(node.node_index) |
|
1217 mob = ns3_node.GetObject(ns3.MobilityModel.GetTypeId()) |
|
1218 if mob is None: |
|
1219 return False |
|
1220 if self.node_drag_state is None: |
|
1221 return False |
|
1222 x, y, dummy = self.canvas.window.get_pointer() |
|
1223 canvas_x, canvas_y = self.canvas.convert_from_pixels(x, y) |
|
1224 dx = (canvas_x - self.node_drag_state.canvas_x0) |
|
1225 dy = (canvas_y - self.node_drag_state.canvas_y0) |
|
1226 pos = mob.GetPosition() |
|
1227 pos.x = self.node_drag_state.sim_x0 + transform_distance_canvas_to_simulation(dx) |
|
1228 pos.y = self.node_drag_state.sim_y0 + transform_distance_canvas_to_simulation(dy) |
|
1229 #print "SetPosition(%G, %G)" % (pos.x, pos.y) |
|
1230 mob.SetPosition(pos) |
|
1231 node.set_position(*transform_point_simulation_to_canvas(pos.x, pos.y)) |
|
1232 finally: |
|
1233 self.simulation.lock.release() |
|
1234 return True |
|
1235 |
|
1236 def end_node_drag(self, node): |
|
1237 if self.node_drag_state is None: |
|
1238 return |
|
1239 node.canvas_item.disconnect(self.node_drag_state.motion_signal) |
|
1240 self.node_drag_state = None |
|
1241 |
|
1242 def popup_node_menu(self, node, event): |
|
1243 menu = gtk.Menu() |
|
1244 self.emit("populate-node-menu", node, menu) |
|
1245 menu.popup(None, None, None, event.button, event.time) |
|
1246 |
|
1247 def _update_ipython_selected_node(self): |
|
1248 # If we are running under ipython -gthread, make this new |
|
1249 # selected node available as a global 'selected_node' |
|
1250 # variable. |
|
1251 try: |
|
1252 __IPYTHON__ |
|
1253 except NameError: |
|
1254 pass |
|
1255 else: |
|
1256 if self.selected_node is None: |
|
1257 ns3_node = None |
|
1258 else: |
|
1259 self.simulation.lock.acquire() |
|
1260 try: |
|
1261 ns3_node = ns3.NodeList.GetNode(self.selected_node.node_index) |
|
1262 finally: |
|
1263 self.simulation.lock.release() |
|
1264 __IPYTHON__.user_ns['selected_node'] = ns3_node |
|
1265 |
|
1266 |
|
1267 def select_node(self, node): |
|
1268 if isinstance(node, ns3.Node): |
|
1269 node = self.nodes[node.GetId()] |
|
1270 elif isinstance(node, (int, long)): |
|
1271 node = self.nodes[node] |
|
1272 elif isinstance(node, Node): |
|
1273 pass |
|
1274 elif node is None: |
|
1275 pass |
|
1276 else: |
|
1277 raise TypeError("expected None, int, viz.Node or ns3.Node, not %r" % node) |
|
1278 |
|
1279 if node is self.selected_node: |
|
1280 return |
|
1281 |
|
1282 if self.selected_node is not None: |
|
1283 self.selected_node.selected = False |
|
1284 self.selected_node = node |
|
1285 if self.selected_node is not None: |
|
1286 self.selected_node.selected = True |
|
1287 |
|
1288 if self._show_transmissions_mode == ShowTransmissionsMode.SELECTED: |
|
1289 if self.selected_node is None: |
|
1290 self.simulation.set_nodes_of_interest([]) |
|
1291 else: |
|
1292 self.simulation.set_nodes_of_interest([self.selected_node.node_index]) |
|
1293 |
|
1294 self._update_ipython_selected_node() |
|
1295 |
|
1296 |
|
1297 def add_information_window(self, info_win): |
|
1298 self.information_windows.append(info_win) |
|
1299 self.simulation.lock.acquire() |
|
1300 try: |
|
1301 info_win.update() |
|
1302 finally: |
|
1303 self.simulation.lock.release() |
|
1304 |
|
1305 def remove_information_window(self, info_win): |
|
1306 self.information_windows.remove(info_win) |
|
1307 |
|
1308 def _canvas_tooltip_cb(self, canvas, x, y, keyboard_mode, tooltip): |
|
1309 #print "tooltip query: ", x, y |
|
1310 hadj = self._scrolled_window.get_hadjustment() |
|
1311 vadj = self._scrolled_window.get_vadjustment() |
|
1312 x, y = self.canvas.convert_from_pixels(hadj.value + x, vadj.value + y) |
|
1313 item = self.canvas.get_item_at(x, y, True) |
|
1314 #print "items at (%f, %f): %r | keyboard_mode=%r" % (x, y, item, keyboard_mode) |
|
1315 if not item: |
|
1316 return False |
|
1317 while item is not None: |
|
1318 obj = item.get_data("pyviz-object") |
|
1319 if obj is not None: |
|
1320 obj.tooltip_query(tooltip) |
|
1321 return True |
|
1322 item = item.props.parent |
|
1323 return False |
|
1324 |
|
1325 def _get_export_file_name(self): |
|
1326 sel = gtk.FileChooserDialog("Save...", self.canvas.get_toplevel(), |
|
1327 gtk.FILE_CHOOSER_ACTION_SAVE, |
|
1328 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, |
|
1329 gtk.STOCK_SAVE, gtk.RESPONSE_OK)) |
|
1330 sel.set_default_response(gtk.RESPONSE_OK) |
|
1331 sel.set_local_only(True) |
|
1332 sel.set_do_overwrite_confirmation(True) |
|
1333 sel.set_current_name("Unnamed.pdf") |
|
1334 |
|
1335 filter = gtk.FileFilter() |
|
1336 filter.set_name("Embedded PostScript") |
|
1337 filter.add_mime_type("image/x-eps") |
|
1338 sel.add_filter(filter) |
|
1339 |
|
1340 filter = gtk.FileFilter() |
|
1341 filter.set_name("Portable Document Graphics") |
|
1342 filter.add_mime_type("application/pdf") |
|
1343 sel.add_filter(filter) |
|
1344 |
|
1345 filter = gtk.FileFilter() |
|
1346 filter.set_name("Scalable Vector Graphics") |
|
1347 filter.add_mime_type("image/svg+xml") |
|
1348 sel.add_filter(filter) |
|
1349 |
|
1350 resp = sel.run() |
|
1351 if resp != gtk.RESPONSE_OK: |
|
1352 sel.destroy() |
|
1353 return None |
|
1354 |
|
1355 file_name = sel.get_filename() |
|
1356 sel.destroy() |
|
1357 return file_name |
|
1358 |
|
1359 def _take_screenshot(self, dummy_button): |
|
1360 #print "Cheese!" |
|
1361 file_name = self._get_export_file_name() |
|
1362 if file_name is None: |
|
1363 return |
|
1364 |
|
1365 # figure out the correct bounding box for what is visible on screen |
|
1366 x1 = self._scrolled_window.get_hadjustment().value |
|
1367 y1 = self._scrolled_window.get_vadjustment().value |
|
1368 x2 = x1 + self._scrolled_window.get_hadjustment().page_size |
|
1369 y2 = y1 + self._scrolled_window.get_vadjustment().page_size |
|
1370 bounds = goocanvas.Bounds() |
|
1371 bounds.x1, bounds.y1 = self.canvas.convert_from_pixels(x1, y1) |
|
1372 bounds.x2, bounds.y2 = self.canvas.convert_from_pixels(x2, y2) |
|
1373 dest_width = bounds.x2 - bounds.x1 |
|
1374 dest_height = bounds.y2 - bounds.y1 |
|
1375 #print bounds.x1, bounds.y1, " -> ", bounds.x2, bounds.y2 |
|
1376 |
|
1377 dummy, extension = os.path.splitext(file_name) |
|
1378 extension = extension.lower() |
|
1379 if extension == '.eps': |
|
1380 surface = cairo.PSSurface(file_name, dest_width, dest_height) |
|
1381 elif extension == '.pdf': |
|
1382 surface = cairo.PDFSurface(file_name, dest_width, dest_height) |
|
1383 elif extension == '.svg': |
|
1384 surface = cairo.SVGSurface(file_name, dest_width, dest_height) |
|
1385 else: |
|
1386 dialog = gtk.MessageDialog(parent = self.canvas.get_toplevel(), |
|
1387 flags = gtk.DIALOG_DESTROY_WITH_PARENT, |
|
1388 type = gtk.MESSAGE_ERROR, |
|
1389 buttons = gtk.BUTTONS_OK, |
|
1390 message_format = "Unknown extension '%s' (valid extensions are '.eps', '.svg', and '.pdf')" |
|
1391 % (extension,)) |
|
1392 dialog.run() |
|
1393 dialog.destroy() |
|
1394 return |
|
1395 |
|
1396 # draw the canvas to a printing context |
|
1397 cr = cairo.Context(surface) |
|
1398 cr.translate(-bounds.x1, -bounds.y1) |
|
1399 self.canvas.render(cr, bounds, self.zoom.value) |
|
1400 cr.show_page() |
|
1401 surface.finish() |
|
1402 |
|
1403 def set_follow_node(self, node): |
|
1404 if isinstance(node, ns3.Node): |
|
1405 node = self.nodes[node.GetId()] |
|
1406 self.follow_node = node |
|
1407 |
|
1408 def _start_shell(self, dummy_button): |
|
1409 if self.shell_window is not None: |
|
1410 self.shell_window.present() |
|
1411 return |
|
1412 |
|
1413 self.shell_window = gtk.Window() |
|
1414 self.shell_window.set_size_request(750,550) |
|
1415 self.shell_window.set_resizable(True) |
|
1416 scrolled_window = gtk.ScrolledWindow() |
|
1417 scrolled_window.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) |
|
1418 ipython = ipython_view.IPythonView() |
|
1419 ipython.modify_font(pango.FontDescription(SHELL_FONT)) |
|
1420 ipython.set_wrap_mode(gtk.WRAP_CHAR) |
|
1421 ipython.show() |
|
1422 scrolled_window.add(ipython) |
|
1423 scrolled_window.show() |
|
1424 self.shell_window.add(scrolled_window) |
|
1425 self.shell_window.show() |
|
1426 self.shell_window.connect('destroy', self._on_shell_window_destroy) |
|
1427 |
|
1428 self._update_ipython_selected_node() |
|
1429 __IPYTHON__.user_ns['viz'] = self |
|
1430 |
|
1431 |
|
1432 def _on_shell_window_destroy(self, window): |
|
1433 self.shell_window = None |
|
1434 |
|
1435 |
|
1436 initialization_hooks = [] |
|
1437 |
|
1438 def add_initialization_hook(hook, *args): |
|
1439 """ |
|
1440 Adds a callback to be called after |
|
1441 the visualizer is initialized, like this:: |
|
1442 initialization_hook(visualizer, *args) |
|
1443 """ |
|
1444 global initialization_hooks |
|
1445 initialization_hooks.append((hook, args)) |
|
1446 |
|
1447 |
|
1448 def set_bounds(x1, y1, x2, y2): |
|
1449 assert x2>x1 |
|
1450 assert y2>y1 |
|
1451 def hook(viz): |
|
1452 cx1, cy1 = transform_point_simulation_to_canvas(x1, y1) |
|
1453 cx2, cy2 = transform_point_simulation_to_canvas(x2, y2) |
|
1454 viz.canvas.set_bounds(cx1, cy1, cx2, cy2) |
|
1455 add_initialization_hook(hook) |
|
1456 |
|
1457 |
|
1458 def start(): |
|
1459 assert Visualizer.INSTANCE is None |
|
1460 if _import_error is not None: |
|
1461 import sys |
|
1462 print >> sys.stderr, "No visualization support (%s)." % (str(_import_error),) |
|
1463 ns3.Simulator.Run() |
|
1464 return |
|
1465 load_plugins() |
|
1466 viz = Visualizer() |
|
1467 for hook, args in initialization_hooks: |
|
1468 gobject.idle_add(hook, viz, *args) |
|
1469 ns3.Packet.EnablePrinting() |
|
1470 viz.start() |