checking in experimental new version of rxgraph based on Jon Bohren's smach_viewer

This commit is contained in:
Ken Conley 2010-04-20 22:01:20 +00:00
parent 12a55635cd
commit a8e345d03b
15 changed files with 3526 additions and 0 deletions

View File

@ -0,0 +1,30 @@
cmake_minimum_required(VERSION 2.4.6)
include($ENV{ROS_ROOT}/core/rosbuild/rosbuild.cmake)
# Set the build type. Options are:
# Coverage : w/ debug symbols, w/o optimization, w/ code-coverage
# Debug : w/ debug symbols, w/o optimization
# Release : w/o debug symbols, w/ optimization
# RelWithDebInfo : w/ debug symbols, w/ optimization
# MinSizeRel : w/o debug symbols, w/ optimization, stripped binaries
#set(ROS_BUILD_TYPE RelWithDebInfo)
rosbuild_init()
#set the default path for built executables to the "bin" directory
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
#set the default path for built libraries to the "lib" directory
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
#uncomment if you have defined messages
#rosbuild_genmsg()
#uncomment if you have defined services
#rosbuild_gensrv()
#common commands for building c++ executables and libraries
#rosbuild_add_library(${PROJECT_NAME} src/example.cpp)
#target_link_libraries(${PROJECT_NAME} another_library)
#rosbuild_add_boost_directories()
#rosbuild_link_boost(${PROJECT_NAME} thread)
#rosbuild_add_executable(example examples/example.cpp)
#target_link_libraries(example ${PROJECT_NAME})

1
tools/rxgraph/Makefile Normal file
View File

@ -0,0 +1 @@
include $(shell rospack find mk)/cmake.mk

View File

@ -0,0 +1,18 @@
<package>
<description brief="rxgraph">
rxgraph
</description>
<author>Ken Conley</author>
<license>BSD</license>
<review status="unreviewed" notes=""/>
<url>http://ros.org/wiki/rxgraph</url>
<depend package="rosgraph"/>
<depend package="xdot"/>
<depend package="roslib"/>
</package>

View File

@ -0,0 +1,33 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2010, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id: rxgraph.py 8782 2010-03-22 21:44:43Z kwc $

View File

@ -0,0 +1,175 @@
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id: rxgraph.py 8782 2010-03-22 21:44:43Z kwc $
import roslib.names
import rosgraph.graph
ORIENTATIONS = ['LR', 'TB', 'RL', 'BT']
INIT_DOTCODE = """
digraph G { initializing [label="initializing..."]; }
"""
# node/node connectivity
NODE_NODE_GRAPH = "node_node"
# node/topic connections where an actual network connection exists
NODE_TOPIC_GRAPH = "node_topic"
# all node/topic connections, even if no actual network connection
NODE_TOPIC_ALL_GRAPH = "node_topic_all"
def safe_dotcode_name(name):
"""
encode the name for dotcode symbol-safe syntax
"""
# not terribly efficient or sophisticated
ret = name.replace('/', '_')
ret = ret.replace(' ', '_')
ret = ret.replace('-', '_')
return ret.replace('.', '_')
def _edge_to_dot(e):
if e.label:
return ' %s->%s [label="%s"]'%(safe_dotcode_name(e.start), safe_dotcode_name(e.end), e.label)
else:
return ' %s->%s'%(safe_dotcode_name(e.start), safe_dotcode_name(e.end))
def _generate_node_dotcode(node, g, quiet):
if node in g.bad_nodes:
bn = g.bad_nodes[node]
if bn.type == rosgraph.graph.BadNode.DEAD:
return ' %s [color="red", shape="doublecircle", label="%s", URL="node:%s"];'%(
safe_dotcode_name(node), node, node)
else:
return ' %s [color="orange", shape="doublecircle", label="%s", URL="node:%s"];'%(
safe_dotcode_name(node), node, node, node)
else:
return ' %s [label="%s", URL="node:%s"];'%(safe_dotcode_name(node), node, node)
QUIET_NAMES = ['/diag_agg', '/runtime_logger', '/pr2_dashboard', '/rviz', '/rosout', '/cpu_monitor', '/monitor','/hd_monitor', '/rxloggerlevel', '/clock']
def _quiet_filter(name):
# ignore viewers
for n in QUIET_NAMES:
if n in name:
return False
return True
def _quiet_filter_edge(edge):
for quiet_label in ['/time', '/clock', '/rosout']:
if quiet_label == edge.label:
return False
return _quiet_filter(edge.start) and _quiet_filter(edge.end)
def generate_namespaces(g, graph_mode, quiet=False):
"""
Determine the namespaces of the nodes being displayed
"""
namespaces = []
if graph_mode == NODE_NODE_GRAPH:
nodes = g.nn_nodes
if quiet:
nodes = [n for n in nodes if not n in QUIET_NAMES]
namespaces = list(set([roslib.names.namespace(n) for n in nodes]))
elif graph_mode == NODE_TOPIC_GRAPH or \
graph_mode == NODE_TOPIC_ALL_GRAPH:
nn_nodes = g.nn_nodes
nt_nodes = g.nt_nodes
if quiet:
nn_nodes = [n for n in nn_nodes if not n in QUIET_NAMES]
nt_nodes = [n for n in nt_nodes if not n in QUIET_NAMES]
if nn_nodes or nt_nodes:
namespaces = [roslib.names.namespace(n) for n in nn_nodes]
namespaces.extend([roslib.names.namespace(n) for n in nt_nodes])
return list(set(namespaces))
def _filter_edges(edges, nodes):
return [e for e in edges if e.start in nodes or e.end in nodes]
def generate_dotcode(g, ns_filter, graph_mode, orientation, quiet=False):
"""
@param g: Graph instance
@param ns_filter: namespace filter (must be canonicalized with trailing '/')
@type ns_filter: string
@param graph_mode str: NODE_NODE_GRAPH | NODE_TOPIC_GRAPH | NODE_TOPIC_ALL_GRAPH
@type graph_mode: str
@param orientation: rankdir value (see ORIENTATIONS dict)
@return: dotcode generated from graph singleton
@rtype: str
"""
#print "generate_dotcode", graph_mode
if ns_filter:
name_filter = ns_filter[:-1]
# create the node definitions
if graph_mode == NODE_NODE_GRAPH:
nodes = g.nn_nodes
if quiet:
nodes = [n for n in nodes if not n in QUIET_NAMES]
if ns_filter and ns_filter != '/':
nodes = [n for n in nodes if n.startswith(ns_filter) or n == name_filter]
if nodes:
nodes_str = '\n'.join([_generate_node_dotcode(n, g, quiet) for n in nodes])
else:
nodes_str = ' empty;'
elif graph_mode == NODE_TOPIC_GRAPH or \
graph_mode == NODE_TOPIC_ALL_GRAPH:
nn_nodes = g.nn_nodes
nt_nodes = g.nt_nodes
if quiet:
nn_nodes = [n for n in nn_nodes if not n in QUIET_NAMES]
nt_nodes = [n for n in nt_nodes if not n in QUIET_NAMES]
if nn_nodes or nt_nodes:
nodes_str = '\n'.join([_generate_node_dotcode(n, g, quiet) for n in nn_nodes])
nodes_str += '\n'.join([' %s [shape=box,label="%s",URL="topic:%s"];'%(
safe_dotcode_name(n), rosgraph.graph.node_topic(n), rosgraph.graph.node_topic(n)) for n in nt_nodes])
else:
nodes_str = ' empty;'
nodes = list(nn_nodes) + list(nt_nodes)
# create the edge definitions
if graph_mode == NODE_NODE_GRAPH:
edges = g.nn_edges
elif graph_mode == NODE_TOPIC_GRAPH:
edges = g.nt_edges
else:
edges = g.nt_all_edges
if quiet:
edges = filter(_quiet_filter_edge, edges)
edges = _filter_edges(edges, nodes)
edges_str = '\n'.join([_edge_to_dot(e) for e in edges])
return "digraph G {\n rankdir=%(orientation)s;\n%(nodes_str)s\n%(edges_str)s}\n"%vars()

View File

@ -0,0 +1,187 @@
#!/usr/bin/env python
# Software License Agreement (BSD License)
#
# Copyright (c) 2008, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Revision $Id: rxgraph.py 8782 2010-03-22 21:44:43Z kwc $
from __future__ import with_statement
import roslib; roslib.load_manifest('rxgraph')
import sys
import time
import threading
import traceback
import logging
from optparse import OptionParser
import rosgraph.graph
import rxgraph.dotcode
from rxgraph.dotcode import generate_dotcode, generate_namespaces, NODE_NODE_GRAPH, NODE_TOPIC_GRAPH
from rxgraph.viewer import RxGraphViewerFrame
import roslib.roslogging
# have to import later than others due to xdot calling wxversion
import wx
class DotUpdate(threading.Thread):
"""Thread to control update of dot file"""
def __init__(self, graph, viewer, output_file=None):
threading.Thread.__init__(self, name="DotUpdate")
self.viewer = viewer
self.graph = graph
self.output_file = None
def run(self):
viewer = self.viewer
current_ns_filter = viewer.ns_filter
g = self.graph
output_file = self.output_file
last_graph_mode = NODE_NODE_GRAPH
quiet = False
orientation = rxgraph.dotcode.ORIENTATIONS[0]
g.set_master_stale(5.0)
g.set_node_stale(5.0)
GUPDATE_INTERVAL = 0.5
last_gupdate = time.time() - GUPDATE_INTERVAL
try:
while not is_shutdown():
# throttle calls to g.update(). we want fast refresh
# on changes to the viewer's ns_filter, less so on the
# graph polling.
now = time.time()
if now - last_gupdate >= GUPDATE_INTERVAL:
changed = g.update()
last_gupdate = now
graph_mode = NODE_TOPIC_GRAPH if viewer.topic_boxes else NODE_NODE_GRAPH
changed |= viewer.ns_filter != current_ns_filter
changed |= quiet != viewer.quiet
changed |= graph_mode != last_graph_mode
quiet = viewer.quiet
last_graph_mode = graph_mode
if changed:
current_ns_filter = viewer.ns_filter
dotcode = generate_dotcode(g, current_ns_filter, graph_mode, orientation, quiet)
#compute path-combo box
namespaces = generate_namespaces(g, graph_mode, quiet)
viewer.update_namespaces(namespaces)
viewer.set_dotcode(dotcode)
# store dotcode if requested
if output_file:
with file(output_file, 'w') as f:
f.write(dotcode)
#information still changing, shorter yield
time.sleep(0.1)
else:
# no new information, let bad nodes update
g.bad_update()
time.sleep(0.1)
changed = False
except wx.PyDeadObjectError:
# shutdown
pass
except:
traceback.print_exc()
_is_shutdown = False
def is_shutdown():
return _is_shutdown
def set_shutdown(is_shutdown):
global _is_shutdown
_is_shutdown = is_shutdown
def rxgraph_main():
parser = OptionParser(usage="usage: rxgraph [options]")
parser.add_option("-o", "--dot",
dest="output_file", default=None,
help="ouput graph as graphviz dot file", metavar="DOTFILE")
parser.add_option("--nodens",
dest="node_ns", default=None,
help="only show nodes in specified namespace")
parser.add_option("--topicns",
dest="topic_ns", default=None,
help="only show topics in specified namespace")
options, args = parser.parse_args()
if args:
parser.error("invalid arguments")
import subprocess
try:
subprocess.check_call(['dot', '-V'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except:
print >> sys.stderr, "Graphviz does not appear to be installed on your system. Please run:\n\n\trosdep install rosgraph\n\nto install the necessary dependencies on your system"
sys.exit(1)
roslib.roslogging.configure_logging('rxgraph', logging.DEBUG, additional=['rospy', 'roslib', 'rosgraph'])
try:
# make gtk play nice with Python threads
#gtk.gdk.threads_init()
graph = rosgraph.graph.Graph(options.node_ns, options.topic_ns)
app = wx.App()
frame = RxGraphViewerFrame()
frame.set_dotcode(rxgraph.dotcode.INIT_DOTCODE)
DotUpdate(graph, frame, output_file=options.output_file).start()
frame.Show()
app.MainLoop()
except KeyboardInterrupt:
pass
finally:
set_shutdown(True)
if __name__ == '__main__':
rxgraph_main()

View File

@ -0,0 +1,191 @@
#!/usr/bin/env python
# Copyright (c) 2010, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Author: Jonathan Bohren
import roslib; roslib.load_manifest('rxgraph')
import rospy
import sys
import xdot
import wx
DOTCODE= """digraph G {
rankdir=LR;
_included_talker [label="/included/talker"];
_included_wg2_talker [label="/included/wg2/talker"];
_included2_talker [label="/included2/talker"];
_included2_listener [label="/included2/listener"];
_included_wg2_listener [label="/included/wg2/listener"];
_rosout [label="/rosout"];
_included_listener [label="/included/listener"];
_wg_listener [label="/wg/listener"];
_wg_talker2 [label="/wg/talker2"];
_wg_talker1 [label="/wg/talker1"];
_included2_wg2_talker [label="/included2/wg2/talker"];
_included2_wg2_listener [label="/included2/wg2/listener"];
_wg_talker1->_rosout [label="/rosout"]
_included2_wg2_listener->_rosout [label="/rosout"]
_wg_talker2->_wg_listener [label="/wg/hello"]
_wg_talker2->_rosout [label="/rosout"]
_included2_wg2_talker->_included2_wg2_listener [label="/included2/wg2/chatter"]
_included2_listener->_rosout [label="/rosout"]
_included_wg2_listener->_rosout [label="/rosout"]
_wg_talker1->_wg_listener [label="/wg/hello"]
_included2_talker->_included2_listener [label="/included2/chatter"]
_wg_listener->_rosout [label="/rosout"]
_included_wg2_talker->_included_wg2_listener [label="/included/wg2/chatter"]
_included_talker->_included_listener [label="/included/chatter"]
_included_wg2_talker->_rosout [label="/rosout"]
_included2_talker->_rosout [label="/rosout"]
_included2_wg2_talker->_rosout [label="/rosout"]
_included_talker->_rosout [label="/rosout"]
_included_listener->_rosout [label="/rosout"]}
"""
class RxGraphViewerFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, "rxgraph", size=(720,480))
# TODO: convert to read-only property
self.ns_filter = None
self.quiet = False
self.topic_boxes = False
self._needs_refresh = False
vbox = wx.BoxSizer(wx.VERTICAL)
graph_view = wx.Panel(self, -1)
gv_vbox = wx.BoxSizer(wx.VERTICAL)
graph_view.SetSizer(gv_vbox)
# Construct toolbar
toolbar = wx.ToolBar(graph_view, -1)
toolbar.AddControl(wx.StaticText(toolbar,-1,"Path: "))
self._ns_combo = wx.ComboBox(toolbar, -1, style=wx.CB_DROPDOWN)
self._ns_combo .Bind(wx.EVT_COMBOBOX, self.set_path)
self._ns_combo.Append('/')
self._ns_combo.SetValue('/')
self._namespaces = ['/']
# display options
quiet_check = wx.CheckBox(toolbar, -1, label="Quiet")
quiet_check.Bind(wx.EVT_CHECKBOX, self.set_quiet_check)
topic_check = wx.CheckBox(toolbar, -1, label="All topics")
topic_check.Bind(wx.EVT_CHECKBOX, self.set_topic_boxes)
toolbar.AddControl(self._ns_combo)
toolbar.AddControl(wx.StaticText(toolbar,-1," "))
toolbar.AddControl(quiet_check)
toolbar.AddControl(wx.StaticText(toolbar,-1," "))
toolbar.AddControl(topic_check)
toolbar.AddControl(wx.StaticText(toolbar,-1," "))
toolbar.AddLabelTool(wx.ID_HELP, 'Help',
wx.ArtProvider.GetBitmap(wx.ART_HELP,wx.ART_OTHER,(16,16)) )
toolbar.Realize()
self.Bind(wx.EVT_TOOL, self.ShowControlsDialog, id=wx.ID_HELP)
# Create dot graph widget
self._widget = xdot.wxxdot.WxDotWindow(graph_view, -1)
self._widget.set_dotcode(DOTCODE)
self._widget.zoom_to_fit()
vbox.Add(graph_view, 1, wx.EXPAND | wx.ALL)
gv_vbox.Add(toolbar, 0, wx.EXPAND)
gv_vbox.Add(self._widget, 1, wx.EXPAND)
self.SetSizer(vbox)
self.Center()
self.Bind(wx.EVT_IDLE,self.OnIdle)
# user callback for select
self._widget.register_select_callback(self.select_cb)
def OnIdle(self, event):
if self._needs_refresh:
self.Refresh()
self._needs_refresh = False
def select_cb(self, *args):
print args[0].item.url
def set_quiet_check(self, event):
self.quiet = event.Checked()
def set_topic_boxes(self, event):
self.topic_boxes = event.Checked()
def set_path(self, event):
self.ns_filter = self._ns_combo.GetValue()
self._needs_zoom = True
def update_namespaces(self, namespaces):
# unfortunately this routine will not alphanumerically sort
curr = self._namespaces
new_filters = [n for n in namespaces if n not in curr]
for f in new_filters:
self._ns_combo.Append(f)
curr.extend(new_filters)
def ShowControlsDialog(self,event):
dial = wx.MessageDialog(None,
"Pan: Arrow Keys\nZoom: PageUp / PageDown\nZoom To Fit: F\n",
'Keyboard Controls', wx.OK)
dial.ShowModal()
def set_dotcode(self, dotcode, zoom=True):
if self._widget.set_dotcode(dotcode, None):
self.SetTitle('rxgraph')
if zoom or self._needs_zoom:
self._widget.zoom_to_fit()
self._needs_zoom = False
self._needs_refresh = True
wx.PostEvent(self.GetEventHandler(), wx.IdleEvent())
def main():
app = wx.App()
frame = RxGraphViewerFrame()
frame.Show()
# for testing only
frame.set_dotcode(DOTCODE)
app.MainLoop()
if __name__ == '__main__':
main()

30
tools/xdot/CMakeLists.txt Normal file
View File

@ -0,0 +1,30 @@
cmake_minimum_required(VERSION 2.4.6)
include($ENV{ROS_ROOT}/core/rosbuild/rosbuild.cmake)
# Set the build type. Options are:
# Coverage : w/ debug symbols, w/o optimization, w/ code-coverage
# Debug : w/ debug symbols, w/o optimization
# Release : w/o debug symbols, w/ optimization
# RelWithDebInfo : w/ debug symbols, w/ optimization
# MinSizeRel : w/o debug symbols, w/ optimization, stripped binaries
#set(ROS_BUILD_TYPE RelWithDebInfo)
rosbuild_init()
#set the default path for built executables to the "bin" directory
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
#set the default path for built libraries to the "lib" directory
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
#uncomment if you have defined messages
#rosbuild_genmsg()
#uncomment if you have defined services
#rosbuild_gensrv()
#common commands for building c++ executables and libraries
#rosbuild_add_library(${PROJECT_NAME} src/example.cpp)
#target_link_libraries(${PROJECT_NAME} another_library)
#rosbuild_add_boost_directories()
#rosbuild_link_boost(${PROJECT_NAME} thread)
#rosbuild_add_executable(example examples/example.cpp)
#target_link_libraries(example ${PROJECT_NAME})

1
tools/xdot/Makefile Normal file
View File

@ -0,0 +1 @@
include $(shell rospack find mk)/cmake.mk

74
tools/xdot/dot_viewer.py Executable file
View File

@ -0,0 +1,74 @@
#!/usr/bin/env python
# Copyright (c) 2010, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Willow Garage, Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Author: Jonathan Bohren
import roslib; roslib.load_manifest('xdot')
import rospy
import sys
import xdot
import wx
def main():
import optparse
parser = optparse.OptionParser(
usage='\n\t%prog [file]')
parser.add_option(
'-f', '--filter',
type='choice', choices=('dot', 'neato', 'twopi', 'circo', 'fdp'),
dest='filter', default='dot',
help='graphviz filter: dot, neato, twopi, circo, or fdp [default: %default]')
(options, args) = parser.parse_args(sys.argv[1:])
if len(args) > 1:
parser.error('incorrect number of arguments')
app = wx.App()
frame = xdot.wxxdot.WxDotFrame()
frame.set_filter(options.filter)
"""Sample mouse event."""
#def print_cb(item,event):
# print "MOUSE_EVENT: "+str(item)
#frame.widget.register_select_callback(print_cb)
frame.Show()
if len(args) >= 1:
if args[0] == '-':
frame.set_dotcode(sys.stdin.read())
else:
frame.open_file(args[0])
app.MainLoop()
if __name__ == '__main__':
main()

26
tools/xdot/mainpage.dox Normal file
View File

@ -0,0 +1,26 @@
/**
\mainpage
\htmlinclude manifest.html
\b xdot is ...
<!--
Provide an overview of your package.
-->
\section codeapi Code API
<!--
Provide links to specific auto-generated API documentation within your
package that is of particular interest to a reader. Doxygen will
document pretty much every part of your code, so do your best here to
point the reader to the actual API.
If your codebase is fairly large or has different sets of APIs, you
should use the doxygen 'group' tag to keep these APIs together. For
example, the roscpp documentation has 'libros' group.
-->
*/

20
tools/xdot/manifest.xml Normal file
View File

@ -0,0 +1,20 @@
<package>
<description brief="xdot">
XDot is an interactive viewer for graphs written in Graphviz's dot
language.
This package adds front-end capabilities to XDot including WX Widget
support and a mechanism for receiving callbacks when nodes are clicked.
</description>
<author>Jose Fonseca, ROS package and WX frontend by Jonathan Bohren</author>
<license>LGPL/BSD</license>
<review status="unreviewed" notes=""/>
<url>http://code.google.com/p/jrfonseca/wiki/XDot</url>
<depend package="rospy"/>
<rosdep name="wxpython"/>
<rosdep name="graphviz"/>
</package>

View File

@ -0,0 +1,2 @@
import wxxdot

View File

@ -0,0 +1,575 @@
#!/usr/bin/env python
#
# wxpython widgets for using Jose Fonseca's cairo graphviz visualizer
# Copyright (c) 2010, Willow Garage, Inc.
#
# Source modified from Jose Fonseca's XDot pgtk widgets. That code is
# Copyright 2008 Jose Fonseca
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from xdot import *
__all__ = ['WxDotWindow', 'WxDotFrame']
# We need to get the wx version with built-in cairo support
import wxversion
wxversion.select("2.8")
import wx
import wx.lib.wxcairo as wxcairo
# This is a crazy hack to get this to work on 64-bit systems
if 'wxMac' in wx.PlatformInfo:
pass # Implement if necessary
elif 'wxMSW' in wx.PlatformInfo:
pass # Implement if necessary
elif 'wxGTK' in wx.PlatformInfo:
import ctypes
gdkLib = wx.lib.wxcairo._findGDKLib()
gdkLib.gdk_cairo_create.restype = ctypes.c_void_p
class WxDragAction(object):
def __init__(self, dot_widget):
self.dot_widget = dot_widget
def on_button_press(self, event):
x,y = event.GetPositionTuple()
self.startmousex = self.prevmousex = x
self.startmousey = self.prevmousey = y
self.start()
def on_motion_notify(self, event):
x,y = event.GetPositionTuple()
deltax = self.prevmousex - x
deltay = self.prevmousey - y
self.drag(deltax, deltay)
self.prevmousex = x
self.prevmousey = y
def on_button_release(self, event):
x,y = event.GetPositionTuple()
self.stopmousex = x
self.stopmousey = y
self.stop()
def draw(self, cr):
pass
def start(self):
pass
def drag(self, deltax, deltay):
pass
def stop(self):
pass
def abort(self):
pass
class WxNullAction(WxDragAction):
def on_motion_notify(self, event):
pass
class WxPanAction(WxDragAction):
def start(self):
self.dot_widget.set_cursor(wx.CURSOR_SIZING)
def drag(self, deltax, deltay):
self.dot_widget.x += deltax / self.dot_widget.zoom_ratio
self.dot_widget.y += deltay / self.dot_widget.zoom_ratio
self.dot_widget.Refresh()
def stop(self):
self.dot_widget.set_cursor(wx.CURSOR_ARROW)
abort = stop
class WxZoomAction(WxDragAction):
def drag(self, deltax, deltay):
self.dot_widget.zoom_ratio *= 1.005 ** (deltax + deltay)
self.dot_widget.zoom_to_fit_on_resize = False
self.dot_widget.Refresh()
def stop(self):
self.dot_widget.Refresh()
class WxZoomAreaAction(WxDragAction):
def drag(self, deltax, deltay):
self.dot_widget.Refresh()
def draw(self, cr):
cr.save()
cr.set_source_rgba(.5, .5, 1.0, 0.25)
cr.rectangle(self.startmousex, self.startmousey,
self.prevmousex - self.startmousex,
self.prevmousey - self.startmousey)
cr.fill()
cr.set_source_rgba(.5, .5, 1.0, 1.0)
cr.set_line_width(1)
cr.rectangle(self.startmousex - .5, self.startmousey - .5,
self.prevmousex - self.startmousex + 1,
self.prevmousey - self.startmousey + 1)
cr.stroke()
cr.restore()
def stop(self):
x1, y1 = self.dot_widget.window2graph(self.startmousex,
self.startmousey)
x2, y2 = self.dot_widget.window2graph(self.stopmousex,
self.stopmousey)
self.dot_widget.zoom_to_area(x1, y1, x2, y2)
def abort(self):
self.dot_widget.Refresh()
class WxDotWindow(wx.Panel):
"""wxpython Frame that draws dot graphs."""
filter = 'dot'
def __init__(self, parent, id):
"""constructor"""
wx.Panel.__init__(self, parent, id)
self.graph = Graph()
self.openfilename = None
self.x, self.y = 0.0, 0.0
self.zoom_ratio = 1.0
self.zoom_to_fit_on_resize = False
self.animation = NoAnimation(self)
self.drag_action = WxNullAction(self)
self.presstime = None
self.highlight = None
# Bind events
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_SIZE, self.OnResize)
self.Bind(wx.EVT_MOUSEWHEEL, self.OnScroll)
self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
# Callback register
self.select_cbs = []
self.dc = None
self.ctx = None
### User callbacks
def register_select_callback(self, cb):
self.select_cbs.append(cb)
### Event handlers
def OnResize(self, event):
self.Refresh()
def OnPaint(self, event):
"""Redraw the graph."""
dc = wx.PaintDC(self)
#print dc
ctx = wxcairo.ContextFromDC(dc)
ctx = pangocairo.CairoContext(ctx)
# Get widget size
width, height = self.GetSize()
#width,height = self.dc.GetSizeTuple()
ctx.rectangle(0,0,width,height)
ctx.clip()
ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0)
ctx.paint()
ctx.save()
ctx.translate(0.5*width, 0.5*height)
ctx.scale(self.zoom_ratio, self.zoom_ratio)
ctx.translate(-self.x, -self.y)
self.graph.draw(ctx, highlight_items=self.highlight)
ctx.restore()
self.drag_action.draw(ctx)
def OnScroll(self, event):
"""Zoom the view."""
if event.GetWheelRotation() > 0:
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT,
pos=(event.GetX(), event.GetY()))
else:
self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT,
pos=(event.GetX(), event.GetY()))
def OnKeyDown(self, event):
"""Process key down event."""
key = event.GetKeyCode()
if key == wx.WXK_LEFT:
self.x -= self.POS_INCREMENT/self.zoom_ratio
self.Refresh()
if key == wx.WXK_RIGHT:
self.x += self.POS_INCREMENT/self.zoom_ratio
self.Refresh()
if key == wx.WXK_UP:
self.y -= self.POS_INCREMENT/self.zoom_ratio
self.Refresh()
if key == wx.WXK_DOWN:
self.y += self.POS_INCREMENT/self.zoom_ratio
self.Refresh()
if key == wx.WXK_PAGEUP:
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
self.Refresh()
if key == wx.WXK_PAGEDOWN:
self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
self.Refresh()
if key == wx.WXK_ESCAPE:
self.drag_action.abort()
self.drag_action = WxNullAction(self)
if key == ord('F'):
self.zoom_to_fit()
if key == ord('R'):
self.reload()
if key == ord('Q'):
self.reload()
exit(0)
event.Skip()
### Helper functions
def get_current_pos(self):
"""Get the current graph position."""
return self.x, self.y
def set_current_pos(self, x, y):
"""Set the current graph position."""
self.x = x
self.y = y
self.Refresh()
def set_highlight(self, items):
"""Set a number of items to be hilighted."""
if self.highlight != items:
self.highlight = items
self.Refresh()
### Cursor manipulation
def set_cursor(self, cursor_type):
self.cursor = wx.StockCursor(cursor_type)
self.SetCursor(self.cursor)
### Zooming methods
def zoom_image(self, zoom_ratio, center=False, pos=None):
"""Zoom the graph."""
if center:
self.x = self.graph.width/2
self.y = self.graph.height/2
elif pos is not None:
width, height = self.GetSize()
x, y = pos
x -= 0.5*width
y -= 0.5*height
self.x += x / self.zoom_ratio - x / zoom_ratio
self.y += y / self.zoom_ratio - y / zoom_ratio
self.zoom_ratio = zoom_ratio
self.zoom_to_fit_on_resize = False
self.Refresh()
def zoom_to_area(self, x1, y1, x2, y2):
"""Zoom to an area of the graph."""
width, height = self.GetSize()
area_width = abs(x1 - x2)
area_height = abs(y1 - y2)
self.zoom_ratio = min(
float(width)/float(area_width),
float(height)/float(area_height)
)
self.zoom_to_fit_on_resize = False
self.x = (x1 + x2) / 2
self.y = (y1 + y2) / 2
self.Refresh()
def zoom_to_fit(self):
"""Zoom to fit the size of the graph."""
width,height = self.GetSize()
x = self.ZOOM_TO_FIT_MARGIN
y = self.ZOOM_TO_FIT_MARGIN
width -= 2 * self.ZOOM_TO_FIT_MARGIN
height -= 2 * self.ZOOM_TO_FIT_MARGIN
if float(self.graph.width) > 0 and float(self.graph.height) > 0 and width > 0 and height > 0:
zoom_ratio = min(
float(width)/float(self.graph.width),
float(height)/float(self.graph.height)
)
self.zoom_image(zoom_ratio, center=True)
self.zoom_to_fit_on_resize = True
ZOOM_INCREMENT = 1.25
ZOOM_TO_FIT_MARGIN = 12
def on_zoom_in(self, action):
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
def on_zoom_out(self, action):
self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
def on_zoom_fit(self, action):
self.zoom_to_fit()
def on_zoom_100(self, action):
self.zoom_image(1.0)
POS_INCREMENT = 100
def get_drag_action(self, event):
"""Get a drag action for this click."""
# Grab the button
button = event.GetButton()
# Grab modifier keys
control_down = event.ControlDown()
alt_down = event.AltDown()
shift_down = event.ShiftDown()
drag = event.Dragging()
motion = event.Moving()
# Get the correct drag action for this click
if button in (wx.MOUSE_BTN_LEFT, wx.MOUSE_BTN_MIDDLE): # left or middle button
if control_down:
if shift_down:
return WxZoomAreaAction(self)
else:
return WxZoomAction(self)
else:
return WxPanAction(self)
return WxNullAction(self)
def OnMouse(self, event):
x,y = event.GetPositionTuple()
item = None
# Get the item
if not event.Dragging():
item = self.get_url(x, y)
if item is None:
item = self.get_jump(x, y)
if item is not None:
self.set_cursor(wx.CURSOR_HAND)
self.set_highlight(item.highlight)
for cb in self.select_cbs:
cb(item,event)
else:
self.set_cursor(wx.CURSOR_ARROW)
self.set_highlight(None)
if item is None:
if event.ButtonDown():
self.animation.stop()
self.drag_action.abort()
# Get the drag action
self.drag_action = self.get_drag_action(event)
self.drag_action.on_button_press(event)
self.pressx = x
self.pressy = y
if event.Dragging() or event.Moving():
self.drag_action.on_motion_notify(event)
if event.ButtonUp():
self.drag_action.on_button_release(event)
self.drag_action = WxNullAction(self)
event.Skip()
def on_area_size_allocate(self, area, allocation):
if self.zoom_to_fit_on_resize:
self.zoom_to_fit()
def animate_to(self, x, y):
self.animation = ZoomToAnimation(self, x, y)
self.animation.start()
def window2graph(self, x, y):
"Get the x,y coordinates in the graph from the x,y coordinates in the window."""
width, height = self.GetSize()
x -= 0.5*width
y -= 0.5*height
x /= self.zoom_ratio
y /= self.zoom_ratio
x += self.x
y += self.y
return x, y
def get_url(self, x, y):
x, y = self.window2graph(x, y)
return self.graph.get_url(x, y)
def get_jump(self, x, y):
x, y = self.window2graph(x, y)
return self.graph.get_jump(x, y)
def set_filter(self, filter):
self.filter = filter
def set_dotcode(self, dotcode, filename='<stdin>'):
if isinstance(dotcode, unicode):
dotcode = dotcode.encode('utf8')
p = subprocess.Popen(
[self.filter, '-Txdot'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False,
universal_newlines=True
)
xdotcode, error = p.communicate(dotcode)
if p.returncode != 0:
print "ERROR PARSING DOT CODE", error
dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
message_format=error,
buttons=gtk.BUTTONS_OK)
dialog.set_title('Dot Viewer')
dialog.run()
dialog.destroy()
return False
try:
self.set_xdotcode(xdotcode)
except ParseError, ex:
print "ERROR PARSING XDOT CODE"
dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
message_format=str(ex),
buttons=gtk.BUTTONS_OK)
dialog.set_title('Dot Viewer')
dialog.run()
dialog.destroy()
return False
else:
self.openfilename = filename
return True
def set_xdotcode(self, xdotcode):
"""Set xdot code."""
#print xdotcode
parser = XDotParser(xdotcode)
self.graph = parser.parse()
self.highlight = None
#self.zoom_image(self.zoom_ratio, center=True)
def reload(self):
if self.openfilename is not None:
try:
fp = file(self.openfilename, 'rt')
self.set_dotcode(fp.read(), self.openfilename)
fp.close()
except IOError:
pass
class WxDotFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, "Dot Viewer", size=(512,512))
vbox = wx.BoxSizer(wx.VERTICAL)
# Construct toolbar
toolbar = wx.ToolBar(self, -1)
toolbar.AddLabelTool(wx.ID_OPEN, 'Open File',
wx.ArtProvider.GetBitmap(wx.ART_FOLDER_OPEN,wx.ART_OTHER,(16,16)))
toolbar.AddLabelTool(wx.ID_HELP, 'Help',
wx.ArtProvider.GetBitmap(wx.ART_HELP,wx.ART_OTHER,(16,16)) )
toolbar.Realize()
self.Bind(wx.EVT_TOOL, self.DoOpenFile, id=wx.ID_OPEN)
self.Bind(wx.EVT_TOOL, self.ShowControlsDialog, id=wx.ID_HELP)
# Create dot widge
self.widget = WxDotWindow(self, -1)
# Add elements to sizer
vbox.Add(toolbar, 0, wx.EXPAND)
vbox.Add(self.widget, 100, wx.EXPAND | wx.ALL)
self.SetSizer(vbox)
self.Center()
def ShowControlsDialog(self,event):
dial = wx.MessageDialog(None,
"\
Pan: Arrow Keys\n\
Zoom: PageUp / PageDown\n\
Zoom To Fit: F\n\
Refresh: R",
'Keyboard Controls', wx.OK)
dial.ShowModal()
def DoOpenFile(self,event):
wcd = 'All files (*)|*|GraphViz Dot Files(*.dot)|*.dot|'
dir = os.getcwd()
open_dlg = wx.FileDialog(self, message='Choose a file', defaultDir=dir, defaultFile='',
wildcard=wcd, style=wx.OPEN|wx.CHANGE_DIR)
if open_dlg.ShowModal() == wx.ID_OK:
path = open_dlg.GetPath()
try:
self.open_file(path)
except IOError, error:
dlg = wx.MessageDialog(self, 'Error opening file\n' + str(error))
dlg.ShowModal()
except UnicodeDecodeError, error:
dlg = wx.MessageDialog(self, 'Error opening file\n' + str(error))
dlg.ShowModal()
open_dlg.Destroy()
def OnExit(self, event):
pass
def set_dotcode(self, dotcode, filename='<stdin>'):
if self.widget.set_dotcode(dotcode, filename):
self.SetTitle(os.path.basename(filename) + ' - Dot Viewer')
self.widget.zoom_to_fit()
def set_xdotcode(self, xdotcode, filename='<stdin>'):
if self.widget.set_xdotcode(xdotcode):
self.SetTitle(os.path.basename(filename) + ' - Dot Viewer')
self.widget.zoom_to_fit()
def open_file(self, filename):
try:
fp = file(filename, 'rt')
self.set_dotcode(fp.read(), filename)
fp.close()
except IOError, ex:
"""
dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
message_format=str(ex),
buttons=gtk.BUTTONS_OK)
dlg.set_title('Dot Viewer')
dlg.run()
dlg.destroy()
"""
def set_filter(self, filter):
self.widget.set_filter(filter)

2163
tools/xdot/src/xdot/xdot.py Executable file

File diff suppressed because it is too large Load Diff