36 from sensor_msgs.msg
import Image
45 from distutils.version
import LooseVersion
49 if wxversion.checkInstalled(
"2.8"):
50 wxversion.select(
"2.8")
52 print(
"wxversion 2.8 is not installed, installed versions are {}".format(wxversion.getInstalled()))
61 custom_name = custom_name
or name
63 path = filter(
lambda x: x != os.path.dirname(os.path.abspath(__file__)), sys.path)
64 f, pathname, desc = imp.find_module(name, path)
66 module = imp.load_module(custom_name, f, pathname, desc)
73 from smach_viewer
import xdot
78 this_dir = os.path.dirname(__file__)
82 this_dir_cwd = os.getcwd()
85 sys.path = [a
for a
in sys.path
if a
not in [this_dir, this_dir_cwd]]
87 sys.path = [a
for a
in sys.path
if not a.endswith(
'smach_viewer/lib/smach_viewer')]
104 def set_styles(self, selected_paths, depth, max_depth, items, subgraph_shapes, containers):
105 """Update the styles for a list of containers without regenerating the dotcode.
107 This function is called recursively to update an entire tree.
109 @param selected_paths: A list of paths to nodes that are currently selected.
110 @param depth: The depth to start traversing the tree
111 @param max_depth: The depth to traverse into the tree
112 @param items: A dict of all the graph items, keyed by url
113 @param subgraph_shapes: A dictionary of shapes from the rendering engine
114 @param containers: A dict of all the containers
120 container_shapes = subgraph_shapes['cluster_'+self._path]
121 container_color = (0,0,0,0)
122 container_fillcolor = (0,0,0,0)
124 for shape in container_shapes:
125 shape.pen.color = container_color
126 shape.pen.fillcolor = container_fillcolor
132 if max_depth == -1
or depth <= max_depth:
135 child_path =
'/'.join([self.
_path,child_label])
137 child_color = [0.5,0.5,0.5,1]
138 child_fillcolor = [1,1,1,1]
141 active_color =
hex2t(
'#5C7600FF')
142 active_fillcolor =
hex2t(
'#C0F700FF')
144 initial_color =
hex2t(
'#000000FF')
148 child_color = active_color
149 child_fillcolor = active_fillcolor
154 child_color = initial_color
158 if child_path
in selected_paths:
159 child_color =
hex2t(
'#FB000DFF')
162 if child_path
in containers:
163 subgraph_id =
'cluster_'+child_path
164 if subgraph_id
in subgraph_shapes:
166 child_fillcolor[3] = 0.25
168 child_fillcolor[3] = 0.25
171 v = 1.0-0.25*((depth+1)/float(max_depth))
174 child_fillcolor = [v,v,v,1.0]
176 for shape
in subgraph_shapes[
'cluster_'+child_path]:
178 if len(pen.color) > 3:
179 pen_color_opacity = pen.color[3]
180 if pen_color_opacity < 0.01:
181 pen_color_opacity = 0
183 pen_color_opacity = 0.5
184 shape.pen.color = child_color[0:3]+[pen_color_opacity]
185 shape.pen.fillcolor = [child_fillcolor[i]
for i
in range(min(3,len(pen.fillcolor)))]
186 shape.pen.linewidth = child_linewidth
196 if child_path
in items:
197 for shape
in items[child_path].shapes:
198 if not isinstance(shape, TextShape):
199 shape.pen.color = child_color
200 shape.pen.fillcolor = child_fillcolor
201 shape.pen.linewidth = child_linewidth
209 This class provides a GUI application for viewing SMACH plans.
212 _container_class = WxContainerNode
215 wx.Frame.__init__(self,
None, -1,
"Smach Viewer", size=(720,480))
216 SmachViewerBase.__init__(self)
217 vbox = wx.BoxSizer(wx.VERTICAL)
228 nb = wx.Notebook(viewer,-1,style=wx.NB_TOP | wx.WANTS_CHARS)
229 viewer_box = wx.BoxSizer()
230 viewer_box.Add(nb,1,wx.EXPAND | wx.ALL, 4)
231 viewer.SetSizer(viewer_box)
234 graph_view = wx.Panel(nb,-1)
235 gv_vbox = wx.BoxSizer(wx.VERTICAL)
236 graph_view.SetSizer(gv_vbox)
239 toolbar = wx.ToolBar(graph_view, -1)
241 toolbar.AddControl(wx.StaticText(toolbar,-1,
"Path: "))
244 self.
path_combo = wx.ComboBox(toolbar, -1, style=wx.CB_DROPDOWN)
257 toolbar.AddControl(wx.StaticText(toolbar,-1,
" Depth: "))
267 toolbar.AddControl(wx.StaticText(toolbar,-1,
" Label Width: "))
271 toggle_all = wx.ToggleButton(toolbar,-1,
'Show Implicit')
274 toolbar.AddControl(wx.StaticText(toolbar,-1,
" "))
275 toolbar.AddControl(toggle_all)
277 toggle_auto_focus = wx.ToggleButton(toolbar, -1,
'Auto Focus')
281 toolbar.AddControl(wx.StaticText(toolbar, -1,
" "))
282 toolbar.AddControl(toggle_auto_focus)
284 toolbar.AddControl(wx.StaticText(toolbar,-1,
" "))
285 if LooseVersion(wx.__version__) >= LooseVersion(
'4.0'):
286 toolbar.AddTool(wx.ID_HELP,
'Help',
287 wx.ArtProvider.GetBitmap(wx.ART_HELP,wx.ART_OTHER,(16,16)) )
288 toolbar.AddTool(wx.ID_SAVE,
'Save',
289 wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE,wx.ART_OTHER,(16,16)) )
291 toolbar.AddLabelTool(wx.ID_HELP,
'Help',
292 wx.ArtProvider.GetBitmap(wx.ART_HELP,wx.ART_OTHER,(16,16)) )
293 toolbar.AddLabelTool(wx.ID_SAVE,
'Save',
294 wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE,wx.ART_OTHER,(16,16)) )
298 self.Bind(wx.EVT_TOOL, self.
SaveDotGraph, id=wx.ID_SAVE)
301 self.
widget = wxxdot.WxDotWindow(graph_view, -1)
303 gv_vbox.Add(toolbar, 0, wx.EXPAND)
304 gv_vbox.Add(self.
widget, 1, wx.EXPAND)
307 self.
tree = wx.TreeCtrl(nb,-1,style=wx.TR_HAS_BUTTONS)
308 nb.AddPage(graph_view,
"Graph View")
309 nb.AddPage(self.
tree,
"Tree View")
313 borders = wx.LEFT | wx.RIGHT | wx.TOP
316 self.
ud_gs = wx.BoxSizer(wx.VERTICAL)
318 self.
ud_gs.Add(wx.StaticText(self.
ud_win,-1,
"Path:"),0, borders, border)
325 self.
ud_gs.Add(wx.StaticText(self.
ud_win,-1,
"Userdata:"),0, borders, border)
327 self.
ud_txt = wx.TextCtrl(self.
ud_win,-1,style=wx.TE_MULTILINE | wx.TE_READONLY)
328 self.
ud_gs.Add(self.
ud_txt,1,wx.EXPAND | borders, border)
334 self.
ud_gs.Add(self.
is_button,0,wx.EXPAND | wx.BOTTOM | borders, border)
352 self.
_pub = rospy.Publisher(
'~image', Image, queue_size=1)
354 self.Bind(wx.EVT_IDLE,self.
OnIdle)
355 self.Bind(wx.EVT_CLOSE,self.
OnQuit)
366 self.Bind(wx.EVT_TIMER, self.
OnTimer)
367 self.
timer.Start(200)
371 """Quit Event: kill threads and wait for join."""
377 """Notify all that the graph needs to be updated."""
382 """Event: Change the initial state of the server."""
387 server_name = self.
_containers[parent_path]._server_name
388 self.
_client.set_initial_state(server_name,parent_path,[state],timeout = rospy.Duration(60.0))
391 """Event: Change the viewable path and update the graph."""
403 """Event: Change the maximum depth and update the graph."""
415 """Event: Change the label wrapper width and update the graph."""
421 """Event: Change whether automatic transitions are hidden and update the graph."""
427 """Event: Enable/Disable automatically focusing"""
437 """Event: Click to select a graph node to display user data and update the graph."""
441 if not type(item.url)
is str:
443 except AttributeError:
448 if event.ButtonUp(wx.MOUSE_BTN_LEFT):
455 wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED,self.
path_input.GetId()))
459 """Event: Selection dropdown changed."""
463 if len(path_input_str) > 0:
465 path = path_input_str.split(
':')[0]
482 pos = self.
ud_txt.HitTestPos(wx.Point(0,0))
483 sel = self.
ud_txt.GetSelection()
487 if not isinstance(container._local_data._data, dict):
488 rospy.logwarn(
"userdata is not dictionary({}), please fix sender proram".format(container._local_data._data))
489 container._local_data._data = {}
490 for (k,v)
in container._local_data._data.items():
491 ud_str += str(k)+
": "
494 if vstr.find(
'\n') != -1:
499 self.
ud_txt.SetValue(ud_str)
502 self.
ud_txt.ShowPosition(pos[1])
504 self.
ud_txt.SetSelection(sel[0],sel[1])
510 """Update the structure of the SMACH plan (re-generate the dotcode)."""
516 SmachViewerBase._structure_msg_update(self, msg, server_name)
523 """Process status messages."""
533 SmachViewerBase._status_msg_update(self, msg)
545 wx.wxEVT_COMMAND_COMBOBOX_SELECTED,
549 containers_to_update = SmachViewerBase._update_graph_step(self)
556 except UnicodeDecodeError
as e:
559 rospy.logerr(
'label width {} causes error'.format(label_width))
560 rospy.logerr(
'maybe multibyte word is in your label.')
562 rospy.logerr(
'changing width label width to {}'.format(
567 if hasattr(self.
widget,
'subgraph_shapes'):
569 for path, tc
in containers_to_update.items():
574 self.
widget.subgraph_shapes,
581 """Set the xdot view's dotcode and refresh the display."""
584 self.SetTitle(
'Smach Viewer')
591 wx.PostEvent(self.GetEventHandler(), wx.IdleEvent())
594 """Update the tree view."""
598 self.
tree.DeleteAllItems()
604 """Add a path to the tree view."""
612 child_path =
'/'.join([path,label])
616 self.
tree.AppendItem(container,label)
619 """Append an item to the tree view."""
621 node = self.
tree.AddRoot(container._label)
622 for child_label
in container._children:
623 self.
tree.AppendItem(node,child_label)
626 """Event: On Idle, refresh the display if necessary, then un-set the flag."""
633 if self.
_pub.get_num_connections() < 1:
634 rospy.logwarn_once(
"Publishing {} requires at least one subscriber".format(self.
_pub.name))
637 context = wx.ClientDC(self)
638 memory = wx.MemoryDC()
639 x, y = self.ClientSize
640 if LooseVersion(wx.__version__) >= LooseVersion(
'4.0'):
641 bitmap = wx.Bitmap(x, y, -1)
643 bitmap = wx.EmptyBitmap(x, y, -1)
644 memory.SelectObject(bitmap)
645 memory.Blit(0, 0, x, y, context, 0, 0)
646 memory.SelectObject(wx.NullBitmap)
647 if LooseVersion(wx.__version__) >= LooseVersion(
'4.0'):
648 buf = bitmap.ConvertToImage().GetDataBuffer()
650 buf = wx.ImageFromBitmap(bitmap).GetDataBuffer()
651 img = np.frombuffer(buf, dtype=np.uint8)
652 bridge = cv_bridge.CvBridge()
653 img_msg = bridge.cv2_to_imgmsg(img.reshape((y, x, 3)), encoding=
'rgb8')
654 img_msg.header.stamp = rospy.Time.now()
655 self.
_pub.publish(img_msg)
658 dial = wx.MessageDialog(
None,
659 "Pan: Arrow Keys\nZoom: PageUp / PageDown\nZoom To Fit: F\nRefresh: R",
660 'Keyboard Controls', wx.OK)
664 timestr = time.strftime(
"%Y%m%d-%H%M%S")
665 directory = rospkg.get_ros_home()+
'/dotfiles/'
666 if not os.path.exists(directory):
667 os.makedirs(directory)
668 filename = directory+timestr+
'.dot'
669 print(
'Writing to file: %s' % filename)
670 with open(filename,
'w')
as f:
680 from argparse
import ArgumentParser
682 p.add_argument(
'-f',
'--auto-focus',
684 help=
"Enable 'AutoFocus to subgraph' as default",
685 dest=
'enable_auto_focus')
686 args = p.parse_args()
690 frame.set_filter(
'dot')
694 if args.enable_auto_focus:
695 frame.toggle_auto_focus(
None)
699 if __name__ ==
'__main__':
700 rospy.init_node(
'smach_viewer',anonymous=
False, disable_signals=
True,log_level=rospy.INFO)
701 sys.argv = rospy.myargv()