checking in experimental new version of rxgraph based on Jon Bohren's smach_viewer
This commit is contained in:
parent
12a55635cd
commit
a8e345d03b
|
@ -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})
|
|
@ -0,0 +1 @@
|
|||
include $(shell rospack find mk)/cmake.mk
|
|
@ -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>
|
||||
|
||||
|
|
@ -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 $
|
|
@ -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()
|
||||
|
|
@ -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()
|
|
@ -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()
|
|
@ -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})
|
|
@ -0,0 +1 @@
|
|||
include $(shell rospack find mk)/cmake.mk
|
|
@ -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()
|
|
@ -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.
|
||||
-->
|
||||
|
||||
|
||||
*/
|
|
@ -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>
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
import wxxdot
|
|
@ -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)
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue