def __init__(self, atom3i): self.cb = atom3i.cb # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_ArrowOptimizer.py', 'Arrow Optimizer Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp(self.USE_SPLINE_OPTIMIZATION, True, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points") newOp( self.ARROW_CURVATURE, 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, set to 0 for a straight arrow." ) # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase()
class ArrowOptimizer: instance = None # Option keys USE_SPLINE_OPTIMIZATION = 'Spline optimization' ARROW_CURVATURE = 'Arrow curvature' def __init__(self, atom3i): self.cb = atom3i.cb # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_ArrowOptimizer.py', 'Arrow Optimizer Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp(self.USE_SPLINE_OPTIMIZATION, True, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points") newOp( self.ARROW_CURVATURE, 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, set to 0 for a straight arrow." ) # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase() def updateATOM3instance(self, atom3i): """ Possible to have multiple instances of atom3 """ self.cb = atom3i.cb def settings(self, selection): """ Dialog to interactively change the spring's behavior Automatically applies layout if not canceled """ if (self.__optionsDatabase.showOptionsDatabase()): self.main(selection) def main(self, selection): setSmooth = self.__optionsDatabase.get(self.USE_SPLINE_OPTIMIZATION) setCurvature = self.__optionsDatabase.get(self.ARROW_CURVATURE) if (selection): optimizeLinks(self.cb, setSmooth, setCurvature, selectedLinks=selection) else: optimizeLinks(self.cb, setSmooth, setCurvature)
def __init__(self, atom3i, dc): self.atom3i = atom3i self.dc = dc # <-- Canvas widget self.__mask = [] self.__box = None self.__boxOutline = None self.__activeSide = None self.__lastPos = None self.__abort = False self.__maskColor = "red" self.__transparentMask = True self.__restoreSnapGrid = False # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(atom3i.parent, 'Options_Postscript.py', 'Postscript Settings', autoSave=True) # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption EN = OptionDialog.ENUM_ENTRY L = OptionDialog.LABEL BE = OptionDialog.BOOLEAN_ENTRY CE = OptionDialog.COLOR_ENTRY newOp(self.COLOR_MODE, "color", [EN, 'color', 'grey', 'mono'], "Export color mode") newOp(self.ROTATION, "portrait", [EN, 'portrait', 'landscape'], "Export rotation") newOp(self.MASK_COLOR_KEY, "red", [CE, 'Choose color'], "Boundary mask color") newOp(self.TRANSPARENT_MASK, True, BE, "Transparent boundary mask") newOp('L0', None, [L, 'times 12', 'blue', 'left'], "") newOp( 'L1', None, [L, 'times 12', 'blue', 'left'], "After pressing OK, you must select the canvas area to export as postscript" ) newOp( 'L2', None, [L, 'times 12', 'blue', 'left'], "You can modify boundaries by left-clicking and moving the mouse around" ) newOp('L3', None, [L, 'times 12', 'blue', 'left'], "Right-clicking will set the new boundary position") newOp('L4', None, [L, 'times 12', 'blue', 'left'], "Right-clicking again will do the actual postscript export") newOp("seperator1", '', OptionDialog.SEPERATOR, '', '') newOp(self.SVG_EXPORT_MODE, True, BE, "Export to SVG instead of postscript") newOp( 'L5', None, [L, 'times 12', 'blue', 'left'], "NOTE: SVG exports selected items only (if no selection then entire canvas)" ) # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions()
def __init__(self,atom3i ): self.atom3i = atom3i # AToM3 instance self.dc = self.atom3i.UMLmodel # Canvas # Last known zoom/stretch applied self.__lastZoom = 100 self.__lastStretchX = 100 self.__lastStretchY = 100 # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(self.atom3i.parent, 'Options_ZoomFocus.py', 'Zoom & Focus',autoSave=False) # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY L = OptionDialog.LABEL # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp( 'WARNING', None, [L,"Times 12","red", "center" ], "WARNING: Zoom can cause graphical glitching (use rescue if stuck)", "" ) text = """ If zoom causes your layout to disintegrate (particulary when save/loading) try: 1) Control-A to select everything on the canvas 2) R to initiate a resize 2a) Right-click to set the default size 2b) Left-click to accept re-size 3) Spacebar to go to label (text) re-size mode 4) Repeat step 2 (it will re-size text now) 5) Save, restart AToM3, reload your model. """ newOp( self.ZOOM, 100, IE, "Zoom", text ) text = """ Attempts to restore your model to normalacy after a bad experience with zoom """ newOp( self.ZOOM_RESCUE, False, BE, "Zoom Rescue", text ) newOp( self.STRETCH_X, 100, IE, "Stretch X", "Model entities will have their positions scaled by the given amount" ) newOp( self.STRETCH_Y, 100, IE, "Stretch Y", "Model entities will have their positions scaled by the given amount" ) newOp( self.CANVAS_X, atom3i.CANVAS_SIZE_TUPLE[2], IE, "Canvas max X", "Set the maximum scrollable canvas region" ) newOp( self.CANVAS_Y, atom3i.CANVAS_SIZE_TUPLE[3], IE, "Canvas max Y", "Set the maximum scrollable canvas region" ) # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions()
def __init__(self, atom3i): self.cb = atom3i.cb self.atom3i = atom3i # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_TreeLikeLayout.py', 'TreeLikeLayout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [OptionDialog.LABEL, "Times 12", "blue", "left"] newOp('label0001', None, optionList, 'Node spacing', '') newOp( 'xOffset', 20, IE, "Minimum X Distance", "Minimum horizontal distance between any 2 tree nodes (Default 20)" ) newOp( 'yOffset', 70, IE, "Minimum Y Distance", "Minimum vertical distance between any 2 tree nodes (Default 70)") newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0002', None, optionList, 'Miscellaneous options', '') newOp('Origin', False, BE, "Start tree at origin?", "If false, the current position of the selected nodes is used") newOp('Manual Cycles', False, BE, "Manual Cycle Breaking", "Forces the user to break cycles by manually clicking on nodes") newOp('sep0001', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp('Spline optimization', True, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points") newOp( 'Arrow curvature', 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " + "set to 0 for a straight arrow.") # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase()
def __init__(self, atom3i ): self.cb = atom3i.cb self.atom3i = atom3i # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_HiearchicalLayout.py', 'Hieararchical Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [OptionDialog.LABEL, "Times 12", "blue", "left" ] newOp( 'label0001', None, optionList, 'Node spacing', '' ) newOp( 'xOffset', 30, IE, "Minimum X Distance", "Minimum horizontal distance between any 2 tree nodes (negative" + " values work too) (Default 30)" ) newOp( 'yOffset', 30, IE, "Minimum Y Distance", "Minimum vertical distance between any 2 tree nodes (Default 30)" ) newOp( 'addEdgeObjHeight', True, BE, "Add edge object height", "Increment spacing between node layers with edge object drawing of"\ + " maximum height between 2 given layers" ) newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp( 'label0002', None, optionList, 'Miscellaneous options', '' ) newOp( 'Origin', False, BE, "Start tree at origin?", "If false, the current position of the selected nodes is used" ) # newOp( 'Manual Cycles', False, BE, "Manual Cycle Breaking", # "Forces the user to break cycles by manually clicking on nodes" ) newOp( 'uncrossPhase1', 5, IE, "Maximum uncrossing iterations", "Maximum number of passes to try to reduce edge crossings" \ + "\nNote: these only count when no progress is being made" ) newOp( 'uncrossPhase2', 15, IE, "Maximum uncrossing random restarts", "These can significantly improve quality, but they restart the " \ + "uncrossing phase to the beginning..." \ + "\nNote: these only count when no progress is being made" ) newOp( 'baryPlaceMax', 10, IE, "Maximum gridpositioning iterations", "Number of times a barycenter placement heuristic is run to " \ + "ensure everything is centered" ) newOp('sep0001', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp( 'Spline optimization' , False, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points" ) newOp( 'Arrow curvature', 0, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " +"set to 0 for a straight arrow." ) # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase()
def __init__(self, atom3i): # Keep track of item handlers so that the lines can be removed (if needed) self.__gridItemHandlers = [] self.atom3i = atom3i # AToM3 instance self.dc = self.atom3i.getCanvas() # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(self.atom3i.parent, 'Options_SnapGrid.py', 'Snap Grid Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY CE = OptionDialog.COLOR_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp(self.GRID_ENABLED, True, BE, "Enable Snap Grid") newOp(self.GRID_ARROWNODE, False, BE, "Snap arrow node") newOp(self.GRID_CONTROLPOINTS, False, BE, "Snap arrow control points") newOp(self.GRID_PIXELS, 20, IE, "Grid square size in pixels", "Snapping will occur at every X pixels square") newOp(self.GRID_DOT_MODE, True, BE, "Grid dots", "Dot mode is much slower than using lines") newOp(self.GRID_WIDTH, 1, IE, "Grid square width in pixels") newOp(self.GRID_COLOR, '#c8c8c8', [CE, "Choose Color"], "Grid square color") newOp(self.GRID_SUDIVISIONS, 5, IE, "Grid square subdivisions", "Every X number of divisions, a subdivsion will be placed") newOp(self.GRID_SUBDIVISION_SHOW, True, BE, "Show subdivision lines", "Makes it easier to visually measure distances") newOp(self.GRID_SUDIVISIONS_WIDTH, 1, IE, "Grid square sudivision width") newOp(self.GRID_SUBDIVISION_COLOR, '#e8e8e8', [CE, "Choose Color"], "Grid square subdivision color") # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions()
def __init__(self, atom3i ): self.atom3i = atom3i self.dc = atom3i.UMLmodel # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(self.atom3i.parent, 'Options_SpringLayout.py', 'Spring Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY FE = OptionDialog.FLOAT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY L = OptionDialog.LABEL # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp( self.INFO0, None, [L,"Times 12","black", "center" ], "This spring-electrical algorithm:", "" ) newOp( self.INFO1, None, [L,"Times 12","black", "left" ], "Has O(n^2) complexity", "" ) newOp( self.INFO3, None, [L,"Times 12","black", "left" ], "Does not work with hyper-edges", "" ) newOp( self.INFO2, None, [L,"Times 12","black", "left" ], "Is applied only on selected nodes & edges", "" ) newOp( self.INFO4, None, [L,"Times 12","black", "left" ],"", "" ) newOp( self.MAXIMUM_ITERATIONS, 100, IE, "Maximum Iterations", "Duration of the spring simulation, longer generally gives better results." ) newOp( self.ANIMATION_UPDATES, 5, IE, "Animation updates", "Force update of the canvas every X simulation frames." ) newOp( self.SPRING_CONSTANT, 0.1, FE, "Spring Constant", "The restoring force of the spring, larger values make the spring \"stiffer\"") newOp( self.SPRING_LENGTH, 100, IE, "Spring rest length", "This is the minimum distance between the 2 nodes") newOp( self.CHARGE_STRENGTH, 1000.00, FE, "Charge strength", "A multiplier on the repulsive force between each and every node." ) newOp( self.FRICTION, 0.01, FE, "Friction", "Limits the ability of the repulsive force to affect another node." ) newOp( self.RANDOM_AMOUNT, 0.0, FE, "Initial randomization", "Randomizes the initial position of linked nodes as a percentage of spring length." ) newOp( self.ARROW_CURVATURE, 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, set to 0 for a straight arrow." ) newOp( self.SPLINE_ARROWS, True, BE, "Spline arrows", "Arrows are set to smooth/spline mode and given additional control points." ) newOp( self.STICKY_BOUNDARY, True, BE, "Sticky boundary", "Prevents nodes from escaping the canvas boundaries." ) # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions()
def __loadOptions(atom3i): """ Use: Sets default option values for Hierarchical layout, unless a save option file is found, in which case the value in the file is used. Parameter: atom3i is an instance of ATOM3 """ # Instantiate the Option Database module AToM3CircleOptions.OptionDatabase = OptionDatabase( atom3i.parent, 'Options_CircleLayout.py', 'Circle Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = AToM3CircleOptions.OptionDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY EE = OptionDialog.ENUM_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [OptionDialog.LABEL, "Times 12", "blue", "center"] newOp('label0000', None, optionList, 'Complexity: O(n)', '') newOp('sep0003', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") optionList = [OptionDialog.LABEL, "Times 12", "blue", "left"] newOp('label0001', None, optionList, 'Node positioning', '') newOp(FORCE_TOPLEFT_TO_ORIGIN, False, BE, "Start circle at origin?", "If false, the current position of the selected nodes is used") newOp( MIN_NODE_SPACING, 0, IE, "Minimum node spacing", "Minimum pixel distance between any 2 tree nodes." \ + "\n\nDefault: 0" + "\n\nNOTE: negative values are useful for maximizing compactness.") enumOptions = [EE, 'Never', 'Smart', 'Always'] newOp(PROMOTE_EDGE_TO_NODE, 'Never', enumOptions, "Promote edge centers to nodes?", "For directed edges with large center drawings, promoting the center to "\ + "a node can lead to a much superiour layout\n\n" + "Example: One directed edge becomes one node and 2 directed edges\n\n" + "The 'smart' option will promote only if a center drawing is present" ) newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp(USE_SPLINES, True, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points") newOp( ARROW_CURVATURE, 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " + "set to 0 for a straight arrow.") # Load the options from the file, on failure the defaults above are used. AToM3CircleOptions.OptionDatabase.loadOptionsDatabase()
def __init__(self, atom3i ): self.atom3i = atom3i # AToM3 instance self.dc = self.atom3i.UMLmodel # Canvas # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(self.atom3i.parent, 'Options_ForceTransfer.py', 'Force Transfer Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY FE = OptionDialog.FLOAT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY LA = OptionDialog.LABEL # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [ LA, "Times 12", "blue", "left" ] newOp( 'label0', False, optionList, "\nThis algorithm is applied only to selected "+ "nodes.\nHowever, if no nodes are selected it is applied globally.\n") newOp( self.AUTO_APPLY, False, BE, "Always active", "Runs force transfer whenever a node is added/dragged in the model" ) newOp( self.USE_STATUSBAR, False, BE, "Enable statusbar info", "Shows number of iterations used to find stable configuration in the statusbar" ) newOp( self.MIN_NODE_DISTANCE, 20, IE, "Minimum node seperation", "Node entities will be seperated by a minimum of this many pixels") newOp( self.MIN_LINK_DISTANCE, 20, IE, "Minimum link node seperation", "Distance in pixels that link nodes should be seperated from other nodes") newOp( self.MIN_CONTROL_DISTANCE, 20, IE, "Minimum link control point seperation", "Distance that link control points should be seperated from other nodes") newOp( self.SEPERATION_FORCE, 0.2, FE, "Seperation force", "Magnitude of the force that will seperate overlapping nodes") newOp( self.ANIMATION_TIME, 0.01, FE, "Animation time", "Seconds between animation frame updates, set 0 to disable animations" ) newOp( self.MAX_ANIM_ITERATIONS, 15, IE, "Max animation iterations", "Stop updating animation to screen after max iterations to speed process up") newOp( self.MAX_TOTAL_ITERATIONS, 50, IE, "Max total iterations", "Stop iterating, even if stable configuration not reached, to prevent unreasonably long periods of non-interactivity") newOp( self.BORDER_DISTANCE, 30, IE, "Border distance", "If an entity is pushed off the canvas, the canvas will be re-centered plus this pixel offset to the top left") # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions()
def __init__(self, atom3i ): self.cb = atom3i.cb self.atom3i = atom3i # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_TreeLikeLayout.py', 'TreeLikeLayout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [OptionDialog.LABEL, "Times 12", "blue", "left" ] newOp( 'label0001', None, optionList, 'Node spacing', '' ) newOp( 'xOffset', 20, IE, "Minimum X Distance", "Minimum horizontal distance between any 2 tree nodes (Default 20)" ) newOp( 'yOffset', 70, IE, "Minimum Y Distance", "Minimum vertical distance between any 2 tree nodes (Default 70)" ) newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp( 'label0002', None, optionList, 'Miscellaneous options', '' ) newOp( 'Origin', False, BE, "Start tree at origin?", "If false, the current position of the selected nodes is used" ) newOp( 'Manual Cycles', False, BE, "Manual Cycle Breaking", "Forces the user to break cycles by manually clicking on nodes" ) newOp('sep0001', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp( 'Spline optimization' , True, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points" ) newOp( 'Arrow curvature', 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " +"set to 0 for a straight arrow." ) # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase()
def __init__(self, atom3i,dc ): self.atom3i = atom3i self.dc = dc # <-- Canvas widget self.__mask = [] self.__box = None self.__boxOutline = None self.__activeSide = None self.__lastPos = None self.__abort = False self.__maskColor = "red" self.__transparentMask = True self.__restoreSnapGrid = False # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_Postscript.py', 'Postscript Settings',autoSave=True) # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption EN = OptionDialog.ENUM_ENTRY L = OptionDialog.LABEL BE = OptionDialog.BOOLEAN_ENTRY CE = OptionDialog.COLOR_ENTRY newOp( self.COLOR_MODE, "color", [EN, 'color', 'grey', 'mono'], "Export color mode" ) newOp( self.ROTATION, "portrait", [EN, 'portrait', 'landscape'], "Export rotation" ) newOp( self.MASK_COLOR_KEY, "red", [CE, 'Choose color'], "Boundary mask color" ) newOp( self.TRANSPARENT_MASK, True, BE, "Transparent boundary mask" ) newOp( 'L0', None, [L, 'times 12','blue','left'], "" ) newOp( 'L1', None, [L, 'times 12','blue','left'], "After pressing OK, you must select the canvas area to export as postscript" ) newOp( 'L2', None, [L, 'times 12','blue','left'], "You can modify boundaries by left-clicking and moving the mouse around" ) newOp( 'L3', None, [L, 'times 12','blue','left'], "Right-clicking will set the new boundary position" ) newOp( 'L4', None, [L, 'times 12','blue','left'], "Right-clicking again will do the actual postscript export" ) newOp( "seperator1", '', OptionDialog.SEPERATOR, '', '') newOp( self.SVG_EXPORT_MODE, True, BE, "Export to SVG instead of postscript") newOp( 'L5', None, [L, 'times 12','blue','left'], "NOTE: SVG exports selected items only (if no selection then entire canvas)" ) # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions()
def __init__(self, atom3i): # Keep track of item handlers so that the lines can be removed (if needed) self.__gridItemHandlers = [] self.atom3i = atom3i # AToM3 instance self.dc = self.atom3i.getCanvas() # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(self.atom3i.parent, 'Options_SnapGrid.py', 'Snap Grid Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY CE = OptionDialog.COLOR_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp( self.GRID_ENABLED, True, BE, "Enable Snap Grid" ) newOp( self.GRID_ARROWNODE, False, BE, "Snap arrow node" ) newOp( self.GRID_CONTROLPOINTS, False, BE, "Snap arrow control points" ) newOp( self.GRID_PIXELS, 20, IE, "Grid square size in pixels", "Snapping will occur at every X pixels square" ) newOp( self.GRID_DOT_MODE, True, BE, "Grid dots", "Dot mode is much slower than using lines" ) newOp( self.GRID_WIDTH, 1, IE, "Grid square width in pixels" ) newOp( self.GRID_COLOR, '#c8c8c8', [CE,"Choose Color"], "Grid square color" ) newOp( self.GRID_SUDIVISIONS, 5, IE, "Grid square subdivisions", "Every X number of divisions, a subdivsion will be placed" ) newOp( self.GRID_SUBDIVISION_SHOW, True, BE, "Show subdivision lines","Makes it easier to visually measure distances" ) newOp( self.GRID_SUDIVISIONS_WIDTH, 1, IE, "Grid square sudivision width" ) newOp( self.GRID_SUBDIVISION_COLOR, '#e8e8e8', [CE,"Choose Color"], "Grid square subdivision color" ) # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions()
def __loadOptions(atom3i): """ Use: Sets default option values for Hierarchical layout, unless a save option file is found, in which case the value in the file is used. Parameter: atom3i is an instance of ATOM3 """ # Instantiate the Option Database module AToM3HierarchicalOptions.OptionDatabase = OptionDatabase( atom3i.parent, 'Options_HiearchicalLayout2.py', 'Hieararchical Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = AToM3HierarchicalOptions.OptionDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY EE = OptionDialog.ENUM_ENTRY EEH = OptionDialog.ENUM_ENTRY_HORIZ # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp('label0005', None, [OptionDialog.LABEL, "Times 12", "blue", "center"], "Complexity: O(iterations*n^2)", "") newOp('sep0005', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") optionList = [OptionDialog.LABEL, "Times 12", "blue", "left"] enumOptions = [EEH, 'Never', 'Smart', 'Always'] newOp(PROMOTE_EDGE_TO_NODE, 'Never', enumOptions, "Promote edge centers to nodes?", "For directed edges with large center drawings, promoting the center to "\ + "a node can lead to a much superiour layout\n\n" + "Example: One directed edge becomes one node and 2 directed edges\n\n" + "The 'smart' option will promote only if a center drawing is present" ) enumOptions = [EEH, 'Never', 'Smart', 'Always'] enumOptions = [EEH, 'BFS', 'Longest-path', 'Minimum-width'] newOp(LAYERING_ALGORITHM, 'BFS', enumOptions, 'Layering algorithm', 'The algorithm will assign each node to a row') newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0001', None, optionList, 'Crossing minimization', '') newOp( MAX_TOTAL_ROUNDS, 30, IE, "Maximum total rounds", "The MAXIMUM number of outer loop crossing reduction attempts." \ + "\n\nFeel free to use large numbers, they are unlikely to be reached!" \ + "\nIn fact, expect the progress checker to break the loop after a" \ + " small number of iterations." \ + "\n\nWARNING: if random restarts are enabled, the max may be reached." \ + "\n\nDefault: 30" ) newOp( MAX_NO_PROGRESS_ROUNDS, 6, IE, "Max rounds without progress", "The maximum number of consecutive rounds without reducing crossings." \ + "\nIf this number is rounds is reached the algorithm terminates early."\ + "\n\nThis parameter can significantly reduce running time without" \ + " affecting the quality of the resulting layout!" \ + "\n\nDefault: 10" ) # newOp(USE_RANDOM_RESTARTS, True, BE, "Use random restarts?", # "If true, whenever crossing reduction hits a snag, the position of each" \ # + " vertex is randomized. Applied with the Barycenter heuristic\n\n" \ # + "This option can easily double total running time, but almost always" \ # + " reduces crossings.") enumOptions = [EEH, 'None', 'Barycenter', 'Transpose', 'Both'] newOp( CROSS_ALG_CHOICE, 'Barycenter', enumOptions, 'Use heuristic:', 'Choose the crossing reduction strategy to use.\n' + '\nNone: Does no crossing minimization... very fast... bad quality' + '\nBarycenter: O(n log n) fast heuristic' + '\nTranspose: O(n^2) slow heuristic.' + '\nBoth: Barycenter first then Transpose' + '\n\nNote: Transpose = Greedy Switch = Adjacent Exchange' + '\n\nDefault: Barycenter') enumOptions = [EEH, 'None', 'Barycenter', 'Transpose', 'Both'] newOp( USE_RANDOM_RESTARTS, 'None', enumOptions, 'Use random restarts with:', 'Random restarts enable crossing minimization to make progress when it' + ' would otherwise get stuck.' + '\nNOTE: This can significantly increase running time, but almost' + ' always reduces crossings a bit more.' + '\n\nNone: never use randomization' + '\nBarycenter: use randomization just with barycenter' + '\nTranspose: use randomization just with transpose' + '\nBoth: use randomization with both algorithms' + '\n\nDefault: None') newOp('sep0001', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0002', None, optionList, 'Final node positioning', '') newOp(FORCE_TOPLEFT_TO_ORIGIN, True, BE, "Start drawing at origin?", "If false, the current position of the selected nodes is used") newOp( MIN_HORIZONTAL_DISTANCE, 30, IE, "Minimum X Distance", "Minimum horizontal distance between any 2 tree nodes (negative" + " values work too) (Default 30)") newOp(MIN_VERTICAL_DISTANCE, 30, IE, "Minimum Y Distance", "Minimum vertical distance between any 2 tree nodes (Default 30)") # newOp( ADD_EDGEDRAWING_HEIGHT, True, BE, "Add edge object height", # "Increment spacing between node layers with edge object drawing of"\ # + " maximum height between 2 given layers" ) newOp( MAX_BARYCENTER_ITER, 100, IE, "Max horizontal positioning rounds", "Maximum horizontal position rounds. \nUses barycenter. " \ + "\nConvergence testing will usually cut-off at <5 rounds." \ + "\n\nDefault: 100" ) enumOptions = [EEH, 'North', 'East', 'South', 'West'] newOp( LAYOUT_DIRECTION, 'South', enumOptions, 'Layout direction', 'The drawing will point in the given direction.\n' + '\nNorth: all arrows point north' + '\nEast: all arrows point east' + '\nSouth: all arrows point south' + '\nWest: all arrows point west') newOp('sep0002', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp(USE_SPLINES, True, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points") newOp( ARROW_CURVATURE, 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " + "set to 0 for a straight arrow.") # Load the options from the file, on failure the defaults above are used. AToM3HierarchicalOptions.OptionDatabase.loadOptionsDatabase()
class ForceTransfer: instance = None MIN_NODE_DISTANCE = 'Minimum node distance' MIN_LINK_DISTANCE = 'Minimum link distance' MIN_CONTROL_DISTANCE = 'Minimum control point distance' SEPERATION_FORCE = 'Seperation Force' ANIMATION_TIME = 'Animation Time Updates' MAX_ANIM_ITERATIONS = 'Max Animation Iterations' MAX_TOTAL_ITERATIONS = 'Max Total Iterations' USE_STATUSBAR = 'Use Statusbar' AUTO_APPLY = 'Auto apply' BORDER_DISTANCE = 'Border Distance' def __init__(self, atom3i ): self.atom3i = atom3i # AToM3 instance self.dc = self.atom3i.UMLmodel # Canvas # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(self.atom3i.parent, 'Options_ForceTransfer.py', 'Force Transfer Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY FE = OptionDialog.FLOAT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY LA = OptionDialog.LABEL # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [ LA, "Times 12", "blue", "left" ] newOp( 'label0', False, optionList, "\nThis algorithm is applied only to selected "+ "nodes.\nHowever, if no nodes are selected it is applied globally.\n") newOp( self.AUTO_APPLY, False, BE, "Always active", "Runs force transfer whenever a node is added/dragged in the model" ) newOp( self.USE_STATUSBAR, False, BE, "Enable statusbar info", "Shows number of iterations used to find stable configuration in the statusbar" ) newOp( self.MIN_NODE_DISTANCE, 20, IE, "Minimum node seperation", "Node entities will be seperated by a minimum of this many pixels") newOp( self.MIN_LINK_DISTANCE, 20, IE, "Minimum link node seperation", "Distance in pixels that link nodes should be seperated from other nodes") newOp( self.MIN_CONTROL_DISTANCE, 20, IE, "Minimum link control point seperation", "Distance that link control points should be seperated from other nodes") newOp( self.SEPERATION_FORCE, 0.2, FE, "Seperation force", "Magnitude of the force that will seperate overlapping nodes") newOp( self.ANIMATION_TIME, 0.01, FE, "Animation time", "Seconds between animation frame updates, set 0 to disable animations" ) newOp( self.MAX_ANIM_ITERATIONS, 15, IE, "Max animation iterations", "Stop updating animation to screen after max iterations to speed process up") newOp( self.MAX_TOTAL_ITERATIONS, 50, IE, "Max total iterations", "Stop iterating, even if stable configuration not reached, to prevent unreasonably long periods of non-interactivity") newOp( self.BORDER_DISTANCE, 30, IE, "Border distance", "If an entity is pushed off the canvas, the canvas will be re-centered plus this pixel offset to the top left") # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions() def __processLoadedOptions(self): """ After loading the database, have to get & store each option value """ self.__autoApply = self.__optionsDatabase.get(self.AUTO_APPLY) self.__useStatusBar = self.__optionsDatabase.get(self.USE_STATUSBAR) self.__minNodeDist = self.__optionsDatabase.get(self.MIN_NODE_DISTANCE) self.__minLinkDist = self.__optionsDatabase.get(self.MIN_LINK_DISTANCE) self.__minControlDist = self.__optionsDatabase.get(self.MIN_CONTROL_DISTANCE) self.__seperationForce = self.__optionsDatabase.get(self.SEPERATION_FORCE) self.__animationTime = self.__optionsDatabase.get(self.ANIMATION_TIME) self.__maxAnimIterations = self.__optionsDatabase.get(self.MAX_ANIM_ITERATIONS) self.__maxIterations = self.__optionsDatabase.get(self.MAX_TOTAL_ITERATIONS) self.__borderDistance = self.__optionsDatabase.get(self.BORDER_DISTANCE) # Inform AToM3 that it should call this algorithm whenever a node is # added or dragged if( self.__autoApply ): self.atom3i.isAutoForceTransferEnabled = True else: self.atom3i.isAutoForceTransferEnabled = False def updateATOM3instance( self, atom3i ): """ Possible to have multiple instances of atom3 """ self.atom3i = atom3i # AToM3 instance self.dc = self.atom3i.UMLmodel # Canvas def settings(self, selection): """ Show the dialog, load the options, transfer some force! """ if( self.__optionsDatabase.showOptionsDatabase() ): self.__processLoadedOptions() def main(self, selection): Object.objList = [] atom3i = self.atom3i dc = self.dc # Specific objects have been chosen on the canvas if( selection ): for obj in selection: self.__grabInfoFromGraphicalObject( obj ) # Nothing on canvas selected, do all! else: # Grab all the nodes in the diagram, except those with 0 size (arrows) # Store them in the "nodeObject" and reference them by objList for nodetype in atom3i.ASGroot.nodeTypes: for node in atom3i.ASGroot.listNodes[nodetype]: obj = node.graphObject_ #print obj self.__grabInfoFromGraphicalObject( obj ) self.__totalNodes = len( Object.objList ) #self.__sortNodes() # Trivial non-overlap case if( self.__totalNodes <= 1 ): return self.__isLayoutStable = False # Keep at it till the layout is stable i = 0 while( not self.__isLayoutStable ): self.__isLayoutStable = True # Optimism is good... self.__calculationLoop() # Disgusting: I have to actually sleep, otherwise I'll be done so fast # you won't have even seen it move :p if( self.__animationTime and i < self.__maxAnimIterations): self.dc.update_idletasks() time.sleep(self.__animationTime) if( i > self.__maxIterations ): break i += 1 # Hijack the status bar to show what the FTA is doing... if( self.__useStatusBar ): if( i >= self.__maxIterations ): atom3i.statusbar.set(1,"FTA halted at max iterations, layout unstable",None) else: atom3i.statusbar.set(1,"FTA needed "+str(i)+" iterations to find stable layout",None) # Keep the whole thing in the viewable area of the canvas minY = minX = 10000 for node in Object.objList: if( isinstance( node, NodeObject ) ): x,y = node.getTopLeftPos() else: x,y = node.getCoords() if( x < minX ): minX = x if( y < minY ): minY = y if( minX < self.__borderDistance ): minX = abs(minX) + self.__borderDistance else: minX = 0 if( minY < self.__borderDistance ): minY = abs(minY) + self.__borderDistance else: minY = 0 # Push on it! for node in Object.objList: node.recenteringPush(minX, minY ) # All that moving stuff around can mess up the connections... if( selection ): optimizeConnectionPorts(atom3i, entityList=selection ) else: optimizeConnectionPorts(atom3i, doAllLinks=True ) def __grabInfoFromGraphicalObject( self, obj ): """ Takes a graphical object and stores relevent info in a data structure """ # This be a node/entity object if( not isConnectionLink( obj ) ): try: x0,y0,x1,y1 = obj.getbbox() width = abs( (x0 - x1) ) height = abs( (y0 - y1) ) center = [ x0 + width/2, y0 + height/2 ] except: print "ERROR caught and handled in ForceTransfer.py in __grabInfoFromGraphicalObject" width = 4 height = 4 center = [obj.x, obj.y] x0 = obj.x - 2 y0 = obj.y - 2 NodeObject( obj, center, [width,height], self.__minNodeDist, topLeftPos = [x0,y0] ) # This be a link/edge object elif( self.__minLinkDist > 0 ) : # Treat the link center as a repulsive object EdgeObject( obj, obj.getCenterCoord(), self.__minLinkDist ) # Treat each control point as a repulsive object if( self.__minControlDist > 0 ): if( not self.dc ): return for connTuple in obj.connections: itemHandler = connTuple[0] c = self.dc.coords( itemHandler ) for i in range(2,len(c)-2,2): ControlPoint( c[i:i+2], self.__minControlDist, itemHandler, i, self.dc ) def __sortNodes(self): """ Sorts the nodes according to their distance from the origin (0,0) This can have a large impact on performance, especially as the number of objects in contact with one another goes up. """ sortedList = [] for node in Object.objList: sortedList.append( (node.getDistanceFromOrigin(),node) ) sortedList.sort() Object.objList = [] for x,node in sortedList: Object.objList.append( node ) def __calculationLoop(self): """ Loop through all the nodes """ # Go through all the nodes, and find the overlap forces i = 0 j = 1 while( i < self.__totalNodes ): while( j < self.__totalNodes ): if( i != j ): self.__forceCalculation( Object.objList[i], \ Object.objList[j] ) j+=1 i += 1 j = i # Go through all the nodes and apply the forces to the positions for node in Object.objList: node.commitForceApplication() def __forceCalculation(self, n1,n2 ): """ Evaluates distances betweens nodes (ie: do they overlap) and calculates a force sufficient to pry them apart. """ # Absolute distance along X and Y vectors between the nodes pointA = n1.getCoords() pointB = n2.getCoords() dx = abs( pointB[0] - pointA[0] ) dy = abs( pointB[1] - pointA[1] ) # Zero division error prevention measures if (dx == 0.0 ): dx = 0.1 if( dy == 0.0 ): dy = 0.1 # Node-Node Distances dist = math.sqrt(dx*dx+dy*dy) # Normalized-Vector norm = [ dx / dist , dy / dist ] # Overlap due to size of nodes sizeA = n1.getSize() sizeB = n2.getSize() sizeOverlap = [ ( sizeA[0] + sizeB[0] ) / 2 , ( sizeA[1] + sizeB[1] ) / 2 ] # Desired distance with resulting force minSeperationDist = min( n1.getSeperationDistance(),n2.getSeperationDistance() ) d1 = (1.0 / norm[0]) * (sizeOverlap[0] + minSeperationDist) d2 = (1.0 / norm[1]) * (sizeOverlap[1] + minSeperationDist) forceMagnitude = self.__seperationForce * ( dist - min(d1,d2) ) # The force should be less than -1 (or it won't be having much of an effect) if (forceMagnitude < -1): #print forceMagnitude, dist, d1,d2 , (sizeOverlap[0] + minSeperationDist),(sizeOverlap[1] + minSeperationDist),(1.0 / norm[0]),(1.0 / norm[1]) force = [ forceMagnitude * norm[0], forceMagnitude * norm[1] ] # Maximize compactness by only pushing nodes along a single axis if( force[0] > force[1] ): force[0] = 0 else: force[1] = 0 # Determine the direction of the force direction = [ 1, 1 ] if( pointA[0] > pointB[0] ): direction[0] = -1 if( pointA[1] > pointB[1] ): direction[1] = -1 # Add up the forces to the two interacting objects n1.forceIncrement( [ direction[0] * force[0], direction[1] * force[1] ] ) n2.forceIncrement( [ -direction[0] * force[0], -direction[1] * force[1] ] ) # If a force was applied this iteration, definately not stable yet self.__isLayoutStable = False
class ZoomFocus: """ Allows existing graphs to be scaled along both axis indepently, as well as fit everything on the canvas via an offset value. """ instance = None ZOOM = 'zoom' ZOOM_RESCUE = 'zoomRescue' STRETCH_X = 'strech x' STRETCH_Y = 'strech y' CANVAS_X = 'canvas x' CANVAS_Y = 'canvas y' def __init__(self,atom3i ): self.atom3i = atom3i # AToM3 instance self.dc = self.atom3i.UMLmodel # Canvas # Last known zoom/stretch applied self.__lastZoom = 100 self.__lastStretchX = 100 self.__lastStretchY = 100 # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(self.atom3i.parent, 'Options_ZoomFocus.py', 'Zoom & Focus',autoSave=False) # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY L = OptionDialog.LABEL # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp( 'WARNING', None, [L,"Times 12","red", "center" ], "WARNING: Zoom can cause graphical glitching (use rescue if stuck)", "" ) text = """ If zoom causes your layout to disintegrate (particulary when save/loading) try: 1) Control-A to select everything on the canvas 2) R to initiate a resize 2a) Right-click to set the default size 2b) Left-click to accept re-size 3) Spacebar to go to label (text) re-size mode 4) Repeat step 2 (it will re-size text now) 5) Save, restart AToM3, reload your model. """ newOp( self.ZOOM, 100, IE, "Zoom", text ) text = """ Attempts to restore your model to normalacy after a bad experience with zoom """ newOp( self.ZOOM_RESCUE, False, BE, "Zoom Rescue", text ) newOp( self.STRETCH_X, 100, IE, "Stretch X", "Model entities will have their positions scaled by the given amount" ) newOp( self.STRETCH_Y, 100, IE, "Stretch Y", "Model entities will have their positions scaled by the given amount" ) newOp( self.CANVAS_X, atom3i.CANVAS_SIZE_TUPLE[2], IE, "Canvas max X", "Set the maximum scrollable canvas region" ) newOp( self.CANVAS_Y, atom3i.CANVAS_SIZE_TUPLE[3], IE, "Canvas max Y", "Set the maximum scrollable canvas region" ) # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions() def __processLoadedOptions(self): """ After loading the database, have to get & store each option value """ self.__zoom = self.__optionsDatabase.get(self.ZOOM) self.__zoomRescue = self.__optionsDatabase.get(self.ZOOM_RESCUE) self.__stretchX = self.__optionsDatabase.get(self.STRETCH_X) self.__stretchY = self.__optionsDatabase.get(self.STRETCH_Y) self.__canvasX = self.__optionsDatabase.get(self.CANVAS_X) self.__canvasY = self.__optionsDatabase.get(self.CANVAS_Y) def getZoom( self ): """ Return the zoom factor """ return self.__zoom / 100.0 def main(self, atom3i ): # Configure it up if(not self.__optionsDatabase.showOptionsDatabase()): return # User cancelled the dialog self.__processLoadedOptions() # Rescue mode if(self.__zoomRescue == True): self.__optionsDatabase.set(self.ZOOM_RESCUE, False) for nodetype in atom3i.ASGroot.nodeTypes: for node in atom3i.ASGroot.listNodes[nodetype]: obj = node.graphObject_ if(obj.__dict__.has_key('centerObject')): obj = obj.centerObject # Just kill all layout info if(obj.__dict__.has_key('layConstraints')): for key in obj.layConstraints.keys(): value = obj.layConstraints[key] if(type(value) == type(1) or type(value == type(0.1))): obj.layConstraints[key] = 0 if(type(value) == type([1,2]) or type(value) == type((1,2))): obj.layConstraints[key] = [] for item in value: obj.layConstraints[key].append(0) return # Check if the canvas size has changed: if so apply the change x,y = [self.__canvasX,self.__canvasY] if( x != atom3i.CANVAS_SIZE_TUPLE[2] or y != atom3i.CANVAS_SIZE_TUPLE[3] ): if( x > 600 and y > 600 ): atom3i.CANVAS_SIZE_TUPLE = (0,0,x,y) atom3i.UMLmodel.configure( scrollregion=atom3i.CANVAS_SIZE_TUPLE ) # No ASG? Halt! if(not atom3i.ASGroot): return # Build a list of nodes Object.nodeList = [] entityList = [] for nodetype in atom3i.ASGroot.nodeTypes: if( atom3i.ASGroot.listNodes.has_key( nodetype ) ): for node in atom3i.ASGroot.listNodes[nodetype]: if( node.isSubclass(node.graphObject_, "graphLink")): edgeObject( node, node.graphObject_.getCenterCoord() ) else: nodeObject( node, node.graphObject_.getCenterCoord() ) entityList.append( node.graphObject_ ) # Apply zoom factor (effect depends on previously applied zoom) realZoom = float(self.__zoom ) / float(self.__lastZoom) self.__lastZoom = self.__zoom for node in Object.nodeList: node.zoomify( realZoom ) # Apply the stretch factor lsx, lsy = [ self.__lastStretchX ,self.__lastStretchY ] sx,sy = self.__lastStretchX, self.__lastStretchY = [ self.__stretchX, self.__stretchY ] realStretch = [ float(sx ) / float(lsx), float(sy) / float(lsy) ] for node in Object.nodeList: node.scalePosition( realStretch ) # Commit zooming & stretching for node in Object.nodeList: node.commitMove() # All that scaling & moving can mess up connections... optimizeConnectionPorts(atom3i, entityList)
def __loadOptions(atom3i): """ Use: Sets default option values for Hierarchical layout, unless a save option file is found, in which case the value in the file is used. Parameter: atom3i is an instance of ATOM3 """ # Instantiate the Option Database module AToM3FTAOptions.OptionDatabase = OptionDatabase( atom3i.parent, 'Options_ForceTransfer2.py', 'Force Transfer Configuration') # Local methods/variables with short names to make things more readable :D newOp = AToM3FTAOptions.OptionDatabase.createNewOption IE = OptionDialog.INT_ENTRY FE = OptionDialog.FLOAT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY EE = OptionDialog.ENUM_ENTRY LA = OptionDialog.LABEL # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp('label0005', None, [LA, "Times 12", "blue", "center"], "Complexity: O(iterations*n^2)", "") newOp('sep0005', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") optionList = [LA, "Times 12", "blue", "left"] enumOptions = [EE, 'Never', 'Smart', 'Always'] newOp(PROMOTE_EDGE_TO_NODE, 'Never', enumOptions, "Promote edge centers to nodes?", "For directed edges with large center drawings, promoting the center to "\ + "a node can lead to a much superiour layout\n\n" + "Example: One directed edge becomes one node and 2 directed edges\n\n" + "The 'smart' option will promote only if a center drawing is present" ) newOp(MIN_NODE_DISTANCE, 20, IE, "Minimum node seperation", "Node entities will be seperated by a minimum of this many pixels") newOp( MIN_LINK_DISTANCE, 20, IE, "Minimum link node seperation", "Distance in pixels that link nodes should be seperated from other nodes" ) newOp(SEPERATION_FORCE, 0.2, FE, "Seperation force", "Magnitude of the force that will seperate overlapping nodes") newOp( MAX_TOTAL_ITERATIONS, 50, IE, "Max total iterations", "Stop iterating, even if stable configuration not reached, to prevent unreasonably long periods of non-interactivity" ) newOp( BORDER_DISTANCE, 0, IE, "Border distance", "If an entity is pushed off the canvas, the canvas will be re-centered plus this pixel offset to the top left" ) newOp('sep0001', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp(USE_SPLINES, True, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points") newOp( ARROW_CURVATURE, 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " + "set to 0 for a straight arrow.") # Load the options from the file, on failure the defaults above are used. AToM3FTAOptions.OptionDatabase.loadOptionsDatabase()
def __loadOptions(atom3i): """ Use: Sets default option values for Hierarchical layout, unless a save option file is found, in which case the value in the file is used. Parameter: atom3i is an instance of ATOM3 """ # Instantiate the Option Database module AToM3OrthogonalOptions.OptionDatabase = OptionDatabase( atom3i.parent, 'Options_Orthogonal.py', 'Orthogonal Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = AToM3OrthogonalOptions.OptionDatabase.createNewOption # IE = OptionDialog.INT_ENTRY # FE = OptionDialog.FLOAT_ENTRY # BE = OptionDialog.BOOLEAN_ENTRY LA = OptionDialog.LABEL EE = OptionDialog.ENUM_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp('label0001', None, [LA, "Times 12", "blue", "center"], "Complexity: O(n)", "") newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") # newOp(RANDOM_AMOUNT, 0.0, FE, "Initial randomization", # "Randomizes the initial position of linked nodes as a percentage of " + # "spring length.") # # newOp(MAXIMUM_ITERATIONS, 100, IE, "Maximum Iterations", # 'Duration of the spring simulation, tradeof between layout running-time '+ # 'and the quality of the layout') enumOptions = [EE, 'Never', 'Smart', 'Always'] newOp(PROMOTE_EDGE_TO_NODE, 'Never', enumOptions, "Promote edge centers to nodes?", "For directed edges with large center drawings, promoting the center to "\ + "a node can lead to a much superiour layout\n\n" + "Example: One directed edge becomes one node and 2 directed edges\n\n" + "The 'smart' option will promote only if a center drawing is present" ) # newOp('sep0002', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") # newOp('label0005', None, [OptionDialog.LABEL, "Times 12", "blue", "center" ], # 'Physical simulation parameters', '') # # newOp(SPRING_CONSTANT, 0.1, FE, "Spring Constant (Do not change)", # 'The restoring force of the spring, larger values make the spring ' + # '"stiffer"\nIn other words: a larger value makes it attracts/repulses ' + # 'connected nodes much more violently '+ # '\nWARNING: Do not change, 0.1 works quite well.') # newOp(SPRING_LENGTH, 100, IE, "Spring rest length", # 'The ideal distance between any two connected nodes' + # '\nBecause of other forces, this distance may never be achieved' + # '\nDefault: 100' + # '\nNote: negative spring lengths are possible, and have the effect of ' + # 'pulling connected nodes closer together.' + # '\n The physics of this are rather dubious however.') # # newOp(CHARGE_STRENGTH, 10.00, FE, "Electric charge strength", # 'A multiplier on the repulsive force between each and every node.' + # '\nIf set to 0.0, no repulsive forces are calculated' + # '\nDefault: 10.0') # newOp(CHARGE_THRESHOLD, 300, IE, "Charge treshold distance", # 'The effect of electric charges diminishes with the square of the ' + # '\ndistance until this threshold distance is reached... at which point' + # '\nelectric charge has no effect at all.' + # '\nWhy a charge distance threshold?' + # '\n 1) Slight improvement to running time' + # '\n 2) Allows you to set higher charge strength without pushing ' + # '\n distant objects obscenely far away...' + # '\n 3) Improves convergence?' + # '\nDefault: 300') # newOp(GRAVITY_STRENGTH, 10, IE, "Gravitional force strength", # 'A coarse simulation of gravity, all nodes are drawn to the center of ' + # 'the graph.' + # '\nThe gravity strength integer is multiplied with the unit vector each '+ # 'node makes with the graph center.' # '\nDefault value: 10') # newOp('sep0001', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") # newOp('label0004', None, [OptionDialog.LABEL, "Times 12", "blue", "center" ], # 'Post-processing options', '') # newOp(FORCE_TOPLEFT_TO_ORIGIN, True, BE, "Force topleft to origin", # "If False, some nodes may move outside the viewable canvas area" ) # Load the options from the file, on failure the defaults above are used. AToM3OrthogonalOptions.OptionDatabase.loadOptionsDatabase()
class HierarchicalLayout: instance = None def __init__(self, atom3i ): self.cb = atom3i.cb self.atom3i = atom3i # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_HiearchicalLayout.py', 'Hieararchical Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [OptionDialog.LABEL, "Times 12", "blue", "left" ] newOp( 'label0001', None, optionList, 'Node spacing', '' ) newOp( 'xOffset', 30, IE, "Minimum X Distance", "Minimum horizontal distance between any 2 tree nodes (negative" + " values work too) (Default 30)" ) newOp( 'yOffset', 30, IE, "Minimum Y Distance", "Minimum vertical distance between any 2 tree nodes (Default 30)" ) newOp( 'addEdgeObjHeight', True, BE, "Add edge object height", "Increment spacing between node layers with edge object drawing of"\ + " maximum height between 2 given layers" ) newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp( 'label0002', None, optionList, 'Miscellaneous options', '' ) newOp( 'Origin', False, BE, "Start tree at origin?", "If false, the current position of the selected nodes is used" ) # newOp( 'Manual Cycles', False, BE, "Manual Cycle Breaking", # "Forces the user to break cycles by manually clicking on nodes" ) newOp( 'uncrossPhase1', 5, IE, "Maximum uncrossing iterations", "Maximum number of passes to try to reduce edge crossings" \ + "\nNote: these only count when no progress is being made" ) newOp( 'uncrossPhase2', 15, IE, "Maximum uncrossing random restarts", "These can significantly improve quality, but they restart the " \ + "uncrossing phase to the beginning..." \ + "\nNote: these only count when no progress is being made" ) newOp( 'baryPlaceMax', 10, IE, "Maximum gridpositioning iterations", "Number of times a barycenter placement heuristic is run to " \ + "ensure everything is centered" ) newOp('sep0001', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp( 'Spline optimization' , False, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points" ) newOp( 'Arrow curvature', 0, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " +"set to 0 for a straight arrow." ) # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase() def updateATOM3instance( self, atom3i ): """ Possible to have multiple instances of atom3 """ self.cb = atom3i.cb self.atom3i = atom3i def settings( self, selection ): """ Dialog to interactively change the spring's behavior Automatically applies layout if not canceled """ if( self.__optionsDatabase.showOptionsDatabase() ): self.main( selection ) def main( self, selection ): """ Main algorithm, does all the high-level steps, delegates details to other methods. """ t = time.time() # Step 1: Get all entity nodes (semantic objects) and wrap them entityNodeList, linkNodeDict = self.__getEntityLinkTuple(selection) if(len(entityNodeList) == 0): return # Initilize the node wrapper class attributes initilizeNodeWrapper() # Wrap the AToM3 semantic nodes to make applying the algorithms easier wrappedNodeList = [] for node in entityNodeList: wrappedNodeList.append(NodeWrapper(node, NodeWrapper.REGULAR_NODE)) # Build a connection map (rapid access maps to children and parents) for wrappedNode in wrappedNodeList: wrappedNode.buildConnectivityMaps(linkNodeDict) # Step 2: Build a proper layered hieararchy wrappedNodeList = greedyCycleRemover(wrappedNodeList) layerTime = time.time() if(1): levelDictionary = longestPathLayeringTopDown(wrappedNodeList) levelDictionary = addDummyNodes(levelDictionary, isGoingDown=True) elif(0): levelDictionary = longestPathLayeringBottomUp(wrappedNodeList) levelDictionary = addDummyNodes(levelDictionary, isGoingDown=False) else: mwl = MinimumWidthLayering(wrappedNodeList) # UBW = 1..4, c = 1..2 levelDictionary = mwl(2, 2) levelDictionary = addDummyNodes(levelDictionary, isGoingDown=False) print ' Layering algorithm required', time.time() - layerTime, \ 'seconds to assign each node a layer' #return print '\n Added dummy nodes, dumping layers:' debugLevelDict(levelDictionary) # Step 3: Minimize crossings levelDictionary = barycentricOrdering(levelDictionary, self.__optionsDatabase.get('uncrossPhase1'), self.__optionsDatabase.get('uncrossPhase2')) # Step 4: Horizontal grid positioner priorityBarycenterPositioner(levelDictionary, self.__optionsDatabase.get('baryPlaceMax') ) # Step 5: Draw nodes and edges on the canvas if(len(selection) != 0): topLeft = self.__getMaxUpperLeftCoordinate(entityNodeList) else: topLeft = [0, 0] self.__drawNodes(levelDictionary, linkNodeDict, topLeft) debugLevelDict(levelDictionary) print '\nHierarchical layout took', time.time() - t, 'seconds to compute' def __getMaxUpperLeftCoordinate(self, entityNodeList): """ Returns the maximum upper left coordinate of all the nodes the layout is being applied to This corresponds to the minumum x and y coords of all the nodes """ minX = sys.maxint minY = sys.maxint for node in entityNodeList: if(node.graphObject_.y < minY): minY = node.graphObject_.y if(node.graphObject_.x < minX): minX = node.graphObject_.x return (minX, minY) def __getEntityLinkTuple(self, selection): """ If selection is empty, get all nodes & links on the canvas Else returns the entities and links in the selection Returns a tuple containing: entityList = List of entity ASG nodes linkNodeDict = Mapping of link ASG nodes to VisualObj graph objects """ entityNodeList = [] # Non-edge entities linkNodeDict = dict() # Regular and self-looping edges # Selection may contain a mixed bag of nodes and links if(selection): for node in selection: semObj = node.semanticObject if(isConnectionLink(node)): #linkNodeList.append(semObj) linkNodeDict[semObj] = node else: entityNodeList.append(semObj) # No selection? Grab all nodes in diagram else: if(not self.atom3i.ASGroot): return ([], []) for nodetype in self.atom3i.ASGroot.nodeTypes: for node in self.atom3i.ASGroot.listNodes[nodetype]: if(isConnectionLink(node.graphObject_)): #linkNodeList.append(node) linkNodeDict[node] = node.graphObject_ else: entityNodeList.append(node) if(selection): return (entityNodeList, linkNodeDict) return (entityNodeList, linkNodeDict) def __drawNodes(self, levelDictionary, linkNodeDict, topLeft): """ Takes size of nodes into account to translate grid positions into actual canvas coordinates """ setSmooth = self.__optionsDatabase.get('Spline optimization') setCurvature = self.__optionsDatabase.get('Arrow curvature') minOffsetY = self.__optionsDatabase.get('yOffset') minOffsetX = self.__optionsDatabase.get('xOffset') giveExtraSpaceForLinks = self.__optionsDatabase.get('addEdgeObjHeight') # Caclulate x, y offsets offsetX = 0 levelInt2offsetY = dict() for levelInt in levelDictionary.keys(): currentLevel = levelDictionary[levelInt] levelInt2offsetY[levelInt] = 0 # Calculate maximum node size on a per level basis (X is for all levels) # Then add minimum seperation distance between nodes for node in currentLevel: # getSize returns node width, and height of the node & child link icon x, y = node.getSize(giveExtraSpaceForLinks) offsetX = max(offsetX, x) levelInt2offsetY[levelInt] = max(levelInt2offsetY[levelInt], y) maxOffsetX = offsetX + minOffsetX halfOffsetX = offsetX / 2 # Send nodes to their final destination, assign final pos to dummy edges x, y = topLeft for levelInt in levelDictionary.keys(): currentLevel = levelDictionary[levelInt] longEdgeOffset = [halfOffsetX, levelInt2offsetY[levelInt] / 3] # Move each node in the level (Dummy edges save the pos but don't move) for node in currentLevel: node.moveTo(x + node.getGridPosition() * maxOffsetX, y, longEdgeOffset) # Increment y for the next iteration y += levelInt2offsetY[levelInt] + minOffsetY # Self-looping edges (Must move these manually into position) for selfLoopedEdge in NodeWrapper.SelfLoopList: x, y = selfLoopedEdge.getEdgePosition() obj = selfLoopedEdge.getASGNode().graphObject_ obj.moveTo(x, y) # Re-doing links can take a while, lets show something in meanwhile... self.atom3i.parent.update() # Re-wire the links to take into account the new node positions selectedLinks = [] for obj in linkNodeDict.values(): selectedLinks.append(obj) optimizeLinks(self.cb, setSmooth, setCurvature, selectedLinks=selectedLinks) # Re-doing links can take a while, lets show something in meanwhile... self.atom3i.parent.update() # Route multi-layer edges self.__edgeRouter() def __edgeRouter(self): """ Previously, edges traversing multiple layers were represented as a chain of dummy nodes. Now these nodes are used as points on a continuous spline. """ def getEndpoint(nodeTuple, pointList, direction, isReversedEdge): """ Gets the nearest arrow endpoint. Handles edge reversal """ if((direction == 'start' and not isReversedEdge) or (direction == 'end' and isReversedEdge)): endNode = nodeTuple[0] if(isReversedEdge): ix = -2 iy = -1 else: ix = 0 iy = 1 else: endNode = nodeTuple[1] if(isReversedEdge): ix = 0 iy = 1 else: ix = -2 iy = -1 # Is it connected to a named port!?! if(endNode.isConnectedByNamedPort(edgeObject)): handler = endNode.getConnectedByNamedPortHandler(nodeTuple[2]) return dc.coords(handler)[:2] # Not a named port... return list(endNode.getClosestConnector2Point( endNode, pointList[ix], pointList[iy])) #todo: improve method for spline arrows + add comments + optimize? print '----------------Dummy Edge Routing-----------------' for dummyEdge in NodeWrapper.ID2LayerEdgeDict.keys(): dummyList = NodeWrapper.ID2LayerEdgeDict[dummyEdge] dummyNode = dummyList[0] dummyChild = dummyNode.children.keys()[0] linkFlagList = dummyNode.children[dummyChild] # Real nodes at start/end of the edge edgeSourceNode = dummyNode.parents.keys()[0] edgeSourceNode = edgeSourceNode.getASGNode().graphObject_ dummyNode = dummyList[-1] edgeTargetNode = dummyNode.children.keys()[0] #print 'Dummy edge number', dummyEdge, #print dummyList[0].parents.keys()[0].getName(), edgeTargetNode.getName() edgeTargetNode = edgeTargetNode.getASGNode().graphObject_ nodeTuple = [edgeSourceNode, edgeTargetNode, None] # Some edges are internally reversed to break cycles, when drawing # this must be taken into account isReversedEdge = False edgesToRoute = [] for linkNode, isReversed in linkFlagList: edgesToRoute.append(linkNode) if(isReversed): isReversedEdge = True # Get all the points the edge must pass through (sorted by layer order) dummyList.sort(lambda a, b: cmp(a.getLayer(), b.getLayer())) if(isReversedEdge): dummyList.reverse() sortedDummyRouteList = [] for node in dummyList: sortedDummyRouteList += node.getEdgePosition() # Set the coordinates of the edge directly # This is complicated by the fact that AToM3 treats edges as two # segments that join poorly (for spline arrows) for edgeObject in edgesToRoute: dc = edgeObject.graphObject_.dc linkObj = edgeObject.graphObject_ tag = linkObj.tag if(isReversedEdge): inPoint = dc.coords( tag + "2ndSeg0" )[:2] outPoint = dc.coords( tag + "1stSeg0" )[:2] else: inPoint = dc.coords( tag + "1stSeg0" )[:2] outPoint = dc.coords( tag + "2ndSeg0" )[:2] #print 'Dummy route', sortedDummyRouteList numPoints = len(sortedDummyRouteList) / 2 # Add 2 extra control points for odd case (to make splines nice) if(numPoints % 2 == 1): if(numPoints == 1): center = sortedDummyRouteList else: start = sortedDummyRouteList[:numPoints - 1] end = sortedDummyRouteList[numPoints + 1:] center = sortedDummyRouteList[numPoints - 1:numPoints + 1] if(not isReversedEdge): newMid1 = [center[0], center[1] - 20] newMid2 = [center[0], center[1] + 20] else: newMid2 = [center[0], center[1] - 20] newMid1 = [center[0], center[1] + 20] if(numPoints == 1): sortedDummyRouteList = newMid1 + center + newMid2 else: sortedDummyRouteList = start + newMid1 + center + newMid2 + end centerIndex = numPoints - 1 + 2 # Add 1 extra control point for even case (to make splines nice) else: start = sortedDummyRouteList[:numPoints] end = sortedDummyRouteList[numPoints:] center = [start[-2] + (end[0] - start[-2]) / 2, start[-1] + (end[1] - start[-1]) / 2] sortedDummyRouteList = start + center + end centerIndex = numPoints # Now I know where the center is... so lets move the center object # Is the edge object a hyperlink? if(len(edgeObject.in_connections_ + edgeObject.out_connections_) > 2): fromObjs = [] for semObj in edgeObject.in_connections_: fromObjs.append(semObj.graphObject_) toObjs = [] for semObj in edgeObject.out_connections_: toObjs.append(semObj.graphObject_) optimizerHyperLink(dc, linkObj, fromObjs, toObjs, 0, 0, 0, center ) continue else: linkObj.moveTo(* center) # Go through the 2 segments in the link nodeTuple[2] = edgeObject for connTuple in linkObj.connections: itemHandler = connTuple[0] direction = connTuple[1] if( direction ): inPoint = getEndpoint(nodeTuple, sortedDummyRouteList, 'start', isReversedEdge) segCoords = inPoint + sortedDummyRouteList[:centerIndex+2] else: outPoint = getEndpoint(nodeTuple, sortedDummyRouteList, 'end', isReversedEdge) segCoords = sortedDummyRouteList[centerIndex:] + outPoint segCoords = self.__reverseCoordList(segCoords) # Applies the changed coords to the canvas dc.coords( * [itemHandler] + segCoords ) # This may change the associated link drawings: # move them to the new point if( direction ): linkObj.updateDrawingsTo(inPoint[0], inPoint[1], itemHandler, segmentNumber=1) else: linkObj.updateDrawingsTo(outPoint[0], outPoint[1], itemHandler, segmentNumber=2) def __reverseCoordList(self, segCoords): """ Input: list of coordinates [x0, y0, x1, y1, ..., xn, yn] Output: list of coordinates reversed [xn, yn, ..., x1, y1, x0, y0] """ reversedCoords = [] for i in range(len(segCoords) - 1, 0, -2): reversedCoords += [segCoords[i - 1], segCoords[i]] return reversedCoords
class HierarchicalLayout: instance = None def __init__(self, atom3i): self.cb = atom3i.cb self.atom3i = atom3i # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_HiearchicalLayout.py', 'Hieararchical Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [OptionDialog.LABEL, "Times 12", "blue", "left"] newOp('label0001', None, optionList, 'Node spacing', '') newOp( 'xOffset', 30, IE, "Minimum X Distance", "Minimum horizontal distance between any 2 tree nodes (negative" + " values work too) (Default 30)") newOp( 'yOffset', 30, IE, "Minimum Y Distance", "Minimum vertical distance between any 2 tree nodes (Default 30)") newOp( 'addEdgeObjHeight', True, BE, "Add edge object height", "Increment spacing between node layers with edge object drawing of"\ + " maximum height between 2 given layers" ) newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0002', None, optionList, 'Miscellaneous options', '') newOp('Origin', False, BE, "Start tree at origin?", "If false, the current position of the selected nodes is used") # newOp( 'Manual Cycles', False, BE, "Manual Cycle Breaking", # "Forces the user to break cycles by manually clicking on nodes" ) newOp( 'uncrossPhase1', 5, IE, "Maximum uncrossing iterations", "Maximum number of passes to try to reduce edge crossings" \ + "\nNote: these only count when no progress is being made" ) newOp( 'uncrossPhase2', 15, IE, "Maximum uncrossing random restarts", "These can significantly improve quality, but they restart the " \ + "uncrossing phase to the beginning..." \ + "\nNote: these only count when no progress is being made" ) newOp( 'baryPlaceMax', 10, IE, "Maximum gridpositioning iterations", "Number of times a barycenter placement heuristic is run to " \ + "ensure everything is centered" ) newOp('sep0001', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp('Spline optimization', False, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points") newOp( 'Arrow curvature', 0, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " + "set to 0 for a straight arrow.") # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase() def updateATOM3instance(self, atom3i): """ Possible to have multiple instances of atom3 """ self.cb = atom3i.cb self.atom3i = atom3i def settings(self, selection): """ Dialog to interactively change the spring's behavior Automatically applies layout if not canceled """ if (self.__optionsDatabase.showOptionsDatabase()): self.main(selection) def main(self, selection): """ Main algorithm, does all the high-level steps, delegates details to other methods. """ t = time.time() # Step 1: Get all entity nodes (semantic objects) and wrap them entityNodeList, linkNodeDict = self.__getEntityLinkTuple(selection) if (len(entityNodeList) == 0): return # Initilize the node wrapper class attributes initilizeNodeWrapper() # Wrap the AToM3 semantic nodes to make applying the algorithms easier wrappedNodeList = [] for node in entityNodeList: wrappedNodeList.append(NodeWrapper(node, NodeWrapper.REGULAR_NODE)) # Build a connection map (rapid access maps to children and parents) for wrappedNode in wrappedNodeList: wrappedNode.buildConnectivityMaps(linkNodeDict) # Step 2: Build a proper layered hieararchy wrappedNodeList = greedyCycleRemover(wrappedNodeList) layerTime = time.time() if (1): levelDictionary = longestPathLayeringTopDown(wrappedNodeList) levelDictionary = addDummyNodes(levelDictionary, isGoingDown=True) elif (0): levelDictionary = longestPathLayeringBottomUp(wrappedNodeList) levelDictionary = addDummyNodes(levelDictionary, isGoingDown=False) else: mwl = MinimumWidthLayering(wrappedNodeList) # UBW = 1..4, c = 1..2 levelDictionary = mwl(2, 2) levelDictionary = addDummyNodes(levelDictionary, isGoingDown=False) print ' Layering algorithm required', time.time() - layerTime, \ 'seconds to assign each node a layer' #return print '\n Added dummy nodes, dumping layers:' debugLevelDict(levelDictionary) # Step 3: Minimize crossings levelDictionary = barycentricOrdering( levelDictionary, self.__optionsDatabase.get('uncrossPhase1'), self.__optionsDatabase.get('uncrossPhase2')) # Step 4: Horizontal grid positioner priorityBarycenterPositioner( levelDictionary, self.__optionsDatabase.get('baryPlaceMax')) # Step 5: Draw nodes and edges on the canvas if (len(selection) != 0): topLeft = self.__getMaxUpperLeftCoordinate(entityNodeList) else: topLeft = [0, 0] self.__drawNodes(levelDictionary, linkNodeDict, topLeft) debugLevelDict(levelDictionary) print '\nHierarchical layout took', time.time( ) - t, 'seconds to compute' def __getMaxUpperLeftCoordinate(self, entityNodeList): """ Returns the maximum upper left coordinate of all the nodes the layout is being applied to This corresponds to the minumum x and y coords of all the nodes """ minX = sys.maxint minY = sys.maxint for node in entityNodeList: if (node.graphObject_.y < minY): minY = node.graphObject_.y if (node.graphObject_.x < minX): minX = node.graphObject_.x return (minX, minY) def __getEntityLinkTuple(self, selection): """ If selection is empty, get all nodes & links on the canvas Else returns the entities and links in the selection Returns a tuple containing: entityList = List of entity ASG nodes linkNodeDict = Mapping of link ASG nodes to VisualObj graph objects """ entityNodeList = [] # Non-edge entities linkNodeDict = dict() # Regular and self-looping edges # Selection may contain a mixed bag of nodes and links if (selection): for node in selection: semObj = node.semanticObject if (isConnectionLink(node)): #linkNodeList.append(semObj) linkNodeDict[semObj] = node else: entityNodeList.append(semObj) # No selection? Grab all nodes in diagram else: if (not self.atom3i.ASGroot): return ([], []) for nodetype in self.atom3i.ASGroot.nodeTypes: for node in self.atom3i.ASGroot.listNodes[nodetype]: if (isConnectionLink(node.graphObject_)): #linkNodeList.append(node) linkNodeDict[node] = node.graphObject_ else: entityNodeList.append(node) if (selection): return (entityNodeList, linkNodeDict) return (entityNodeList, linkNodeDict) def __drawNodes(self, levelDictionary, linkNodeDict, topLeft): """ Takes size of nodes into account to translate grid positions into actual canvas coordinates """ setSmooth = self.__optionsDatabase.get('Spline optimization') setCurvature = self.__optionsDatabase.get('Arrow curvature') minOffsetY = self.__optionsDatabase.get('yOffset') minOffsetX = self.__optionsDatabase.get('xOffset') giveExtraSpaceForLinks = self.__optionsDatabase.get('addEdgeObjHeight') # Caclulate x, y offsets offsetX = 0 levelInt2offsetY = dict() for levelInt in levelDictionary.keys(): currentLevel = levelDictionary[levelInt] levelInt2offsetY[levelInt] = 0 # Calculate maximum node size on a per level basis (X is for all levels) # Then add minimum seperation distance between nodes for node in currentLevel: # getSize returns node width, and height of the node & child link icon x, y = node.getSize(giveExtraSpaceForLinks) offsetX = max(offsetX, x) levelInt2offsetY[levelInt] = max(levelInt2offsetY[levelInt], y) maxOffsetX = offsetX + minOffsetX halfOffsetX = offsetX / 2 # Send nodes to their final destination, assign final pos to dummy edges x, y = topLeft for levelInt in levelDictionary.keys(): currentLevel = levelDictionary[levelInt] longEdgeOffset = [halfOffsetX, levelInt2offsetY[levelInt] / 3] # Move each node in the level (Dummy edges save the pos but don't move) for node in currentLevel: node.moveTo(x + node.getGridPosition() * maxOffsetX, y, longEdgeOffset) # Increment y for the next iteration y += levelInt2offsetY[levelInt] + minOffsetY # Self-looping edges (Must move these manually into position) for selfLoopedEdge in NodeWrapper.SelfLoopList: x, y = selfLoopedEdge.getEdgePosition() obj = selfLoopedEdge.getASGNode().graphObject_ obj.moveTo(x, y) # Re-doing links can take a while, lets show something in meanwhile... self.atom3i.parent.update() # Re-wire the links to take into account the new node positions selectedLinks = [] for obj in linkNodeDict.values(): selectedLinks.append(obj) optimizeLinks(self.cb, setSmooth, setCurvature, selectedLinks=selectedLinks) # Re-doing links can take a while, lets show something in meanwhile... self.atom3i.parent.update() # Route multi-layer edges self.__edgeRouter() def __edgeRouter(self): """ Previously, edges traversing multiple layers were represented as a chain of dummy nodes. Now these nodes are used as points on a continuous spline. """ def getEndpoint(nodeTuple, pointList, direction, isReversedEdge): """ Gets the nearest arrow endpoint. Handles edge reversal """ if ((direction == 'start' and not isReversedEdge) or (direction == 'end' and isReversedEdge)): endNode = nodeTuple[0] if (isReversedEdge): ix = -2 iy = -1 else: ix = 0 iy = 1 else: endNode = nodeTuple[1] if (isReversedEdge): ix = 0 iy = 1 else: ix = -2 iy = -1 # Is it connected to a named port!?! if (endNode.isConnectedByNamedPort(edgeObject)): handler = endNode.getConnectedByNamedPortHandler(nodeTuple[2]) return dc.coords(handler)[:2] # Not a named port... return list( endNode.getClosestConnector2Point(endNode, pointList[ix], pointList[iy])) #todo: improve method for spline arrows + add comments + optimize? print '----------------Dummy Edge Routing-----------------' for dummyEdge in NodeWrapper.ID2LayerEdgeDict.keys(): dummyList = NodeWrapper.ID2LayerEdgeDict[dummyEdge] dummyNode = dummyList[0] dummyChild = dummyNode.children.keys()[0] linkFlagList = dummyNode.children[dummyChild] # Real nodes at start/end of the edge edgeSourceNode = dummyNode.parents.keys()[0] edgeSourceNode = edgeSourceNode.getASGNode().graphObject_ dummyNode = dummyList[-1] edgeTargetNode = dummyNode.children.keys()[0] #print 'Dummy edge number', dummyEdge, #print dummyList[0].parents.keys()[0].getName(), edgeTargetNode.getName() edgeTargetNode = edgeTargetNode.getASGNode().graphObject_ nodeTuple = [edgeSourceNode, edgeTargetNode, None] # Some edges are internally reversed to break cycles, when drawing # this must be taken into account isReversedEdge = False edgesToRoute = [] for linkNode, isReversed in linkFlagList: edgesToRoute.append(linkNode) if (isReversed): isReversedEdge = True # Get all the points the edge must pass through (sorted by layer order) dummyList.sort(lambda a, b: cmp(a.getLayer(), b.getLayer())) if (isReversedEdge): dummyList.reverse() sortedDummyRouteList = [] for node in dummyList: sortedDummyRouteList += node.getEdgePosition() # Set the coordinates of the edge directly # This is complicated by the fact that AToM3 treats edges as two # segments that join poorly (for spline arrows) for edgeObject in edgesToRoute: dc = edgeObject.graphObject_.dc linkObj = edgeObject.graphObject_ tag = linkObj.tag if (isReversedEdge): inPoint = dc.coords(tag + "2ndSeg0")[:2] outPoint = dc.coords(tag + "1stSeg0")[:2] else: inPoint = dc.coords(tag + "1stSeg0")[:2] outPoint = dc.coords(tag + "2ndSeg0")[:2] #print 'Dummy route', sortedDummyRouteList numPoints = len(sortedDummyRouteList) / 2 # Add 2 extra control points for odd case (to make splines nice) if (numPoints % 2 == 1): if (numPoints == 1): center = sortedDummyRouteList else: start = sortedDummyRouteList[:numPoints - 1] end = sortedDummyRouteList[numPoints + 1:] center = sortedDummyRouteList[numPoints - 1:numPoints + 1] if (not isReversedEdge): newMid1 = [center[0], center[1] - 20] newMid2 = [center[0], center[1] + 20] else: newMid2 = [center[0], center[1] - 20] newMid1 = [center[0], center[1] + 20] if (numPoints == 1): sortedDummyRouteList = newMid1 + center + newMid2 else: sortedDummyRouteList = start + newMid1 + center + newMid2 + end centerIndex = numPoints - 1 + 2 # Add 1 extra control point for even case (to make splines nice) else: start = sortedDummyRouteList[:numPoints] end = sortedDummyRouteList[numPoints:] center = [ start[-2] + (end[0] - start[-2]) / 2, start[-1] + (end[1] - start[-1]) / 2 ] sortedDummyRouteList = start + center + end centerIndex = numPoints # Now I know where the center is... so lets move the center object # Is the edge object a hyperlink? if (len(edgeObject.in_connections_ + edgeObject.out_connections_) > 2): fromObjs = [] for semObj in edgeObject.in_connections_: fromObjs.append(semObj.graphObject_) toObjs = [] for semObj in edgeObject.out_connections_: toObjs.append(semObj.graphObject_) optimizerHyperLink(dc, linkObj, fromObjs, toObjs, 0, 0, 0, center) continue else: linkObj.moveTo(*center) # Go through the 2 segments in the link nodeTuple[2] = edgeObject for connTuple in linkObj.connections: itemHandler = connTuple[0] direction = connTuple[1] if (direction): inPoint = getEndpoint(nodeTuple, sortedDummyRouteList, 'start', isReversedEdge) segCoords = inPoint + sortedDummyRouteList[:centerIndex + 2] else: outPoint = getEndpoint(nodeTuple, sortedDummyRouteList, 'end', isReversedEdge) segCoords = sortedDummyRouteList[ centerIndex:] + outPoint segCoords = self.__reverseCoordList(segCoords) # Applies the changed coords to the canvas dc.coords(*[itemHandler] + segCoords) # This may change the associated link drawings: # move them to the new point if (direction): linkObj.updateDrawingsTo(inPoint[0], inPoint[1], itemHandler, segmentNumber=1) else: linkObj.updateDrawingsTo(outPoint[0], outPoint[1], itemHandler, segmentNumber=2) def __reverseCoordList(self, segCoords): """ Input: list of coordinates [x0, y0, x1, y1, ..., xn, yn] Output: list of coordinates reversed [xn, yn, ..., x1, y1, x0, y0] """ reversedCoords = [] for i in range(len(segCoords) - 1, 0, -2): reversedCoords += [segCoords[i - 1], segCoords[i]] return reversedCoords
class CircleLayout: instance = None def __init__(self, atom3i ): self.cb = atom3i.cb self.atom3i = atom3i # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_CicleLayout.py', 'Circle Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [OptionDialog.LABEL, "Times 12", "blue", "left" ] newOp( 'label0001', None, optionList, 'Node positioning', '' ) newOp( 'Origin', False, BE, "Start circle at origin?", "If false, the current position of the selected nodes is used" ) newOp( 'Offset', 20, IE, "Minimum node spacing", "Minimum distance between any 2 tree nodes (Default 20)" ) newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp( 'Spline optimization' , True, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points" ) newOp( 'Arrow curvature', 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " +"set to 0 for a straight arrow." ) # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase() def updateATOM3instance( self, atom3i ): """ Possible to have multiple instances of atom3 """ self.cb = atom3i.cb self.atom3i = atom3i def settings( self, selection ): """ Dialog to interactively change the spring's behavior Automatically applies layout if not canceled """ if( self.__optionsDatabase.showOptionsDatabase() ): self.main( selection ) def main( self, selection ): setSmooth = self.__optionsDatabase.get('Spline optimization') setCurvature = self.__optionsDatabase.get('Arrow curvature') entityNodeList = self.__getEntityList(selection) if(len(entityNodeList) == 0): return self.__positionNodes(entityNodeList) optimizeLinks( self.cb, setSmooth, setCurvature, selectedLinks=self.__getLinkList(entityNodeList) ) def __getLinkList(self, entityNodeList): """ Find all links disturbed by the circle layout algorithm """ linkList = [] for obj in entityNodeList: semObject = obj.semanticObject linkNodes = semObject.in_connections_ + semObject.out_connections_ for semObj in linkNodes: if(semObj.graphObject_ not in linkList): linkList.append(semObj.graphObject_) return linkList def __getEntityList(self, selection): """ If selection is empty, get all nodes on the canvas Else filter out links """ entityNodeList = [] # Selection may contain a mixed bag of nodes and links if(selection): for node in selection: if(not isConnectionLink(node)): entityNodeList.append(node) # No selection? Grab all nodes in diagram else: if(not self.atom3i.ASGroot): return [] for nodetype in self.atom3i.ASGroot.nodeTypes: for node in self.atom3i.ASGroot.listNodes[nodetype]: if(not isConnectionLink(node.graphObject_)): entityNodeList.append(node.graphObject_) return entityNodeList def __computeCircleRadius(self, entityNodeList): """ Takes a list of entity nodes, computes the perimeter they will occupy and resulting radius of circle required """ # Compute radius automatically # Line up all the nodes diagonally (or max of H and W), count length # Use eqution: perimeter = 2*pi*r to get radius offset = self.__optionsDatabase.get('Offset') perimeter = 0 for node in entityNodeList: perimeter += max(node.getSize()) + offset return (perimeter, perimeter / (2 * math.pi)) def __positionNodes(self, entityNodeList): """ Position the nodes """ useOrigin = self.__optionsDatabase.get('Origin') if(useOrigin): baseX = 0 baseY = 0 else: (baseX, baseY) = self.__getMaxUpperLeftCoordinate(entityNodeList) # Compute circle positions # Angle per step = 2*pi / # of nodes # For each node: # positionX[i] = r + r * sin(i * anglePerStep) # positionY[i] = r + r * cos(i * anglePerStep) (perimeter, radius) = self.__computeCircleRadius(entityNodeList) anglePerStep = (2.0 * math.pi) / float(len(entityNodeList)) for i in range(0, len(entityNodeList)): x = baseX + radius + radius * math.sin(i * anglePerStep) y = baseY + radius + radius * math.cos(i * anglePerStep) entityNodeList[i].moveTo(x, y) def __getMaxUpperLeftCoordinate(self, entityNodeList): """ Returns the maximum upper left coordinate of all the nodes the layout is being applied to This corresponds to the minumum x and y coords of all the nodes """ minX = sys.maxint minY = sys.maxint for node in entityNodeList: if(node.y < minY): minY = node.y if(node.x < minX): minX = node.x return (minX, minY)
class SnapGrid: instance = None highestItemHandler = None """ highestItemHandler variable is used to place items just above the grid lines Example pattern: dc.tag_lower(myItemHandler) # Under everything if(SnapGrid.highestItemHandler): dc.tag_raise(myItemHandler, SnapGrid.highestItemHandler) # Above grid """ GRID_ENABLED = 'snapgrid enabled' GRID_ARROWNODE = 'snap arrow node' GRID_CONTROLPOINTS = 'snap control points' GRID_PIXELS = 'gridsquare pixels' GRID_WIDTH = 'gridsquare width' GRID_COLOR = 'gridsquare color' GRID_DOT_MODE = 'use gridsquare dots' GRID_SUDIVISIONS = 'gridsquare subdivisions' GRID_SUDIVISIONS_WIDTH = 'gridsquare sudvision width' GRID_SUBDIVISION_COLOR = 'gridsquare subdivision color' GRID_SUBDIVISION_SHOW = 'enable gridsquare subdivisions' def __init__(self, atom3i): # Keep track of item handlers so that the lines can be removed (if needed) self.__gridItemHandlers = [] self.atom3i = atom3i # AToM3 instance self.dc = self.atom3i.getCanvas() # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(self.atom3i.parent, 'Options_SnapGrid.py', 'Snap Grid Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY CE = OptionDialog.COLOR_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp(self.GRID_ENABLED, True, BE, "Enable Snap Grid") newOp(self.GRID_ARROWNODE, False, BE, "Snap arrow node") newOp(self.GRID_CONTROLPOINTS, False, BE, "Snap arrow control points") newOp(self.GRID_PIXELS, 20, IE, "Grid square size in pixels", "Snapping will occur at every X pixels square") newOp(self.GRID_DOT_MODE, True, BE, "Grid dots", "Dot mode is much slower than using lines") newOp(self.GRID_WIDTH, 1, IE, "Grid square width in pixels") newOp(self.GRID_COLOR, '#c8c8c8', [CE, "Choose Color"], "Grid square color") newOp(self.GRID_SUDIVISIONS, 5, IE, "Grid square subdivisions", "Every X number of divisions, a subdivsion will be placed") newOp(self.GRID_SUBDIVISION_SHOW, True, BE, "Show subdivision lines", "Makes it easier to visually measure distances") newOp(self.GRID_SUDIVISIONS_WIDTH, 1, IE, "Grid square sudivision width") newOp(self.GRID_SUBDIVISION_COLOR, '#e8e8e8', [CE, "Choose Color"], "Grid square subdivision color") # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions() def __processLoadedOptions(self): """ After loading the database, have to get & store each option value """ # Enabled? self.__gridEnabled = self.__optionsDatabase.get(self.GRID_ENABLED) self.__gridArrowNode = self.__optionsDatabase.get(self.GRID_ARROWNODE) self.__gridControlPoints = self.__optionsDatabase.get( self.GRID_CONTROLPOINTS) # Primary Grid self.__gridLineSeperation = self.__optionsDatabase.get( self.GRID_PIXELS) self.__gridLineColor = self.__optionsDatabase.get(self.GRID_COLOR) self.__gridDotMode = self.__optionsDatabase.get(self.GRID_DOT_MODE) self.__gridWidth = self.__optionsDatabase.get( self.GRID_SUDIVISIONS_WIDTH) # Grid Subdivisions self.__gridSubdivisions = self.__optionsDatabase.get( self.GRID_SUDIVISIONS) self.__gridLineSubdivisionColor = self.__optionsDatabase.get( self.GRID_SUBDIVISION_COLOR) self.__gridSubdivisionShow = self.__optionsDatabase.get( self.GRID_SUBDIVISION_SHOW) self.__gridSubdivisionWidth = self.__optionsDatabase.get( self.GRID_SUDIVISIONS_WIDTH) def updateATOM3instance(self, atom3i): self.atom3i = atom3i # Atom3 instance self.dc = self.atom3i.getCanvas() # Canvas def settings(self): """ Show the dialog, load the options, snap it on! """ self.__optionsDatabase.showOptionsDatabase() self.__processLoadedOptions() self.drawGrid() def drawGrid(self): """ Draws the grid """ # Do we really want to draw the grid? :D if (not self.__gridEnabled): return self.destroy() # Is the grid already drawn? Wipe it clean, then go at it again! elif (self.__gridItemHandlers): self.destroy() # Starting the Grid up for the first time, let AToM3 know about it... else: self.__updateMainApp() canvasBox = self.atom3i.CANVAS_SIZE_TUPLE # Create the "subdivision grid", this is really just a visual aid if (self.__gridSubdivisionShow): subdivisionSeperation = self.__gridLineSeperation * self.__gridSubdivisions for x in range(canvasBox[0], canvasBox[2], subdivisionSeperation): line = self.dc.create_line( x, 0, x, canvasBox[3], width=self.__gridSubdivisionWidth, fill=self.__gridLineSubdivisionColor) self.__gridItemHandlers.append(line) for y in range(canvasBox[1], canvasBox[3], subdivisionSeperation): line = self.dc.create_line( 0, y, canvasBox[2], y, width=self.__gridSubdivisionWidth, fill=self.__gridLineSubdivisionColor) self.__gridItemHandlers.append(line) # Create the 'real' grid, this is where snapping occurs # Use Dots: less visual clutter but slow since it is O(n^2) if (self.__gridDotMode): for x in range(canvasBox[0], canvasBox[2], self.__gridLineSeperation): for y in range(canvasBox[1], canvasBox[3], self.__gridLineSeperation): oval = self.dc.create_oval(x - self.__gridWidth, y - self.__gridWidth, x + self.__gridWidth, y + self.__gridWidth, width=0, fill=self.__gridLineColor) self.__gridItemHandlers.append(oval) # Use lines: much faster since it is O(n) else: for x in range(canvasBox[0], canvasBox[2], self.__gridLineSeperation): line = self.dc.create_line(x, 0, x, canvasBox[2], width=self.__gridWidth, fill=self.__gridLineColor) self.__gridItemHandlers.append(line) for y in range(canvasBox[1], canvasBox[3], self.__gridLineSeperation): line = self.dc.create_line(0, y, canvasBox[3], y, width=self.__gridWidth, fill=self.__gridLineColor) self.__gridItemHandlers.append(line) # Push all this stuff behind what's already on the canvas for itemHandler in self.__gridItemHandlers: self.dc.tag_lower(itemHandler) SnapGrid.highestItemHandler = self.__gridItemHandlers[0] def __updateMainApp(self, disableForPrinting=False): """ Updates the main application with information it needs to snap """ if (self.__gridEnabled and not disableForPrinting): self.atom3i.snapGridInfoTuple = (self.__gridLineSeperation, self.__gridArrowNode, self.__gridControlPoints) else: self.atom3i.snapGridInfoTuple = None def destroy(self, disableForPrinting=False): """ Grid is displayed? Kill it """ SnapGrid.highestItemHandler = None if (self.__gridItemHandlers): for itemHandler in self.__gridItemHandlers: self.dc.delete(itemHandler) self.__gridItemHandlers = [] self.__updateMainApp(disableForPrinting)
class Postscript: MASK_STIPPLE = "gray12" TOP = 0 BOTTOM = 1 LEFT = 2 RIGHT = 3 # How close you must click to a mask boundary in order to select it MIN_SIDE_DIST = 100 # Option Keys COLOR_MODE = 'Color mode' ROTATION = 'Rotation' MASK_COLOR_KEY = 'Mask color' TRANSPARENT_MASK = 'Mask transparency' SVG_EXPORT_MODE = 'SVG export mode' def __init__(self, atom3i,dc ): self.atom3i = atom3i self.dc = dc # <-- Canvas widget self.__mask = [] self.__box = None self.__boxOutline = None self.__activeSide = None self.__lastPos = None self.__abort = False self.__maskColor = "red" self.__transparentMask = True self.__restoreSnapGrid = False # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_Postscript.py', 'Postscript Settings',autoSave=True) # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption EN = OptionDialog.ENUM_ENTRY L = OptionDialog.LABEL BE = OptionDialog.BOOLEAN_ENTRY CE = OptionDialog.COLOR_ENTRY newOp( self.COLOR_MODE, "color", [EN, 'color', 'grey', 'mono'], "Export color mode" ) newOp( self.ROTATION, "portrait", [EN, 'portrait', 'landscape'], "Export rotation" ) newOp( self.MASK_COLOR_KEY, "red", [CE, 'Choose color'], "Boundary mask color" ) newOp( self.TRANSPARENT_MASK, True, BE, "Transparent boundary mask" ) newOp( 'L0', None, [L, 'times 12','blue','left'], "" ) newOp( 'L1', None, [L, 'times 12','blue','left'], "After pressing OK, you must select the canvas area to export as postscript" ) newOp( 'L2', None, [L, 'times 12','blue','left'], "You can modify boundaries by left-clicking and moving the mouse around" ) newOp( 'L3', None, [L, 'times 12','blue','left'], "Right-clicking will set the new boundary position" ) newOp( 'L4', None, [L, 'times 12','blue','left'], "Right-clicking again will do the actual postscript export" ) newOp( "seperator1", '', OptionDialog.SEPERATOR, '', '') newOp( self.SVG_EXPORT_MODE, True, BE, "Export to SVG instead of postscript") newOp( 'L5', None, [L, 'times 12','blue','left'], "NOTE: SVG exports selected items only (if no selection then entire canvas)" ) # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions() def __processLoadedOptions(self): """ After loading the database, have to get & store each option value """ self.__colormode = self.__optionsDatabase.get(self.COLOR_MODE) self.__rotation = self.__optionsDatabase.get(self.ROTATION) self.__maskColor = self.__optionsDatabase.get(self.MASK_COLOR_KEY) self.__transparentMask = self.__optionsDatabase.get(self.TRANSPARENT_MASK) self.__svgExportMode = self.__optionsDatabase.get(self.SVG_EXPORT_MODE) def enteringPostscript( self ): if( self.__abort ): self.atom3i.UI_Statechart.event( "Done" ) def createMask( self, pos ): """ Creates 4 transparent rectangles that mask out what won't be included in the postscript generation. """ # Pos could be an event or a [x,y] list if( type( pos ) != type( list() ) ): pos = [pos.x, pos.y] minX,minY, maxX,maxY = self.atom3i.CANVAS_SIZE_TUPLE # Uh oh snap grid is on! This will mess up the boundary calculation! if( self.atom3i.snapGridInfoTuple ): self.atom3i.disableSnapGridForPrinting(True) self.__box = self.dc.bbox('all') # Do we have an initial boundary box? Did the options dialog get OK'd? if( self.__box and self.__optionsDatabase.showOptionsDatabase( pos ) ): x0,y0, x1,y1 = self.__box self.__processLoadedOptions() if(self.__svgExportMode): self.exportSVG() self.__abort = True return else: self.__abort = False # Error! Cancel! Abort! else: self.__abort = True return # The boundary box outline self.__boxOutline = self.dc.create_rectangle(x0,y0,x1,y1, outline = 'black',fill='', width=1) # Use transparent boundary mask? It's somewhat slower... if( self.__transparentMask ): stipple = self.MASK_STIPPLE else: stipple = '' # The masks on the 4 sides of the boundary box topBox = self.dc.create_rectangle( minX,minY, maxX, y0, outline = '', fill=self.__maskColor,stipple=stipple, width=1) botBox = self.dc.create_rectangle( minX,y1, maxX, maxY, outline = '', fill=self.__maskColor,stipple=stipple, width=1) leftBox = self.dc.create_rectangle( minX,minY, x0, maxY, outline = '', fill=self.__maskColor,stipple=stipple, width=1) rightBox = self.dc.create_rectangle( x1,minY, maxX, maxY, outline = '', fill=self.__maskColor,stipple=stipple, width=1) self.__mask = [topBox,botBox,leftBox,rightBox ] def destroy( self ): """ Reset everything back to defaults & remove stuff from canvas """ for item in self.__mask: self.dc.delete( item ) self.__mask = [] self.__box = None self.__activeSide = None self.dc.delete( self.__boxOutline ) self.__boxOutline = None def setActiveSide( self, pos ): """ Sets the nearest side of the bounding box to active modification Side must be within a certain distance of the given position, or the side will not be selected, and False will be returned. """ x,y = self.__lastPos = pos x0,y0,x1,y1 = self.__box xDist = abs( x1-x0 ) yDist = abs( y1-y0 ) closestHitDist = self.MIN_SIDE_DIST closestHitIndex = None # Quick but not so great method if( 0 ): # Use top box line if( y < y0 ): self.__activeSide = self.TOP # Use right box line elif( x > x1 ): self.__activeSide = self.RIGHT # Use bottom box line elif( y > y1 ): self.__activeSide = self.BOTTOM # Use left box line else: self.__activeSide = self.LEFT return True # Slower but more interactive method else: # Distance to the left-most bounding box segment dist = point2SegmentDistance(x,y, x0,y0,x0,y0+yDist) if( dist < closestHitDist ): closestHitDist = dist closestHitIndex = self.LEFT # Distance to the right-most bounding box segment dist = point2SegmentDistance(x,y, x1,y0,x1,y0+yDist) if( dist < closestHitDist ): closestHitDist = dist closestHitIndex = self.RIGHT # Distance to the top-most bounding box segment dist = point2SegmentDistance(x,y, x0,y0,x0+xDist,y0) if( dist < closestHitDist ): closestHitDist = dist closestHitIndex = self.TOP # Distance to the bottom-most bounding box segment dist = point2SegmentDistance(x,y, x0,y1,x0+xDist,y1) if( dist < closestHitDist ): closestHitDist = dist closestHitIndex = self.BOTTOM if( closestHitIndex != None ): self.__activeSide = closestHitIndex return True else: self.__activeSide = None return False def inMotion( self, pos ): """ Moves the active side of the selection box with the mouse motion """ if( self.__activeSide == None ): return oldX, oldY = self.__lastPos newX, newY = self.__lastPos = pos dx,dy = ( newX - oldX, newY - oldY ) x0,y0,x1,y1 = self.__box # Apply motion delta to the active side if( self.__activeSide == self.LEFT ): x0 += dx elif(self.__activeSide == self.RIGHT ): x1 += dx elif(self.__activeSide == self.TOP): y0 += dy elif(self.__activeSide == self.BOTTOM ): y1 += dy minX,minY, maxX,maxY = self.atom3i.CANVAS_SIZE_TUPLE topBox,botBox,leftBox,rightBox = self.__mask # Move the mask around self.dc.coords( topBox, minX, minY, maxX, y0 ) self.dc.coords( botBox, minX, y1, maxX, maxY ) self.dc.coords( leftBox, minX, minY, x0, maxY ) self.dc.coords( rightBox, x1, minY, maxX, maxY ) # Update the box self.__box = [ x0,y0,x1,y1 ] self.dc.coords( self.__boxOutline, x0,y0,x1,y1 ) def generatePostscript(self, autoSaveToFileName = None): """ Generate the printable postscript file using the bounding box """ if( self.__rotation == "landscape" ): rotation = True else: rotation = False if( autoSaveToFileName ): # Uh oh snap grid is on! This will mess up the boundary calculation! if( self.atom3i.snapGridInfoTuple ): self.atom3i.disableSnapGridForPrinting(True) # Bounding box b = self.dc.bbox('all') if( b == None ): print 'Bounding box is empty', b, 'for', autoSaveToFileName # b = [0,0, 1,1] # Empty canvas return None # Abort fileName = autoSaveToFileName if(fileName[-4:] != '.eps' and fileName[-3:] != '.ps'): fileName += '.eps' else: # Make the box go bye bye b = self.__box self.destroy() # No box? No postscript :p if( not b or self.__abort ): return # Save Dialog fileName = tkFileDialog.asksaveasfilename(initialfile='x.eps', filetypes=[ ("Encapsulated Postscript", "*.eps"), ("Postscript", "*.ps")]) # Canceled! if( fileName == '' ): return # This is for lazy people (like me) who don't add the extension :D if( fileName[-4:] != '.eps' and fileName[-3:] != '.ps' ): fileName += '.ps' self.dc.postscript( file = fileName, x = b[0], y = b[1], width = b[2] - b[0], height = b[3] - b[1], colormode = self.__colormode, rotate = rotation ) return b # return the bounding box def exportSVG(self): """ Sends selected objects or the entire canvas (if no selection) to the SVG exporter and writes the results to a file. """ # Save Dialog fileName = tkFileDialog.asksaveasfilename(initialfile='x.svg', filetypes=[ ("SVG", "*.svg"), ("All files", "*.*")]) # Canceled! if( fileName == '' ): return if(fileName[-4:].lower() == '.svg'): from AToM3Selection2SVG import AToM3Selection2SVG selectionList = self.atom3i.cb.buildSelectionObjectSet() if(not selectionList): selectionList = [] for nodeList in self.atom3i.ASGroot.listNodes.values(): for node in nodeList: selectionList.append(node.graphObject_) SVGtext = AToM3Selection2SVG(selectionList) #print SVGtext f = open(fileName, 'w') f.write(SVGtext) f.close()
def __loadOptions(atom3i): """ Use: Sets default option values for Hierarchical layout, unless a save option file is found, in which case the value in the file is used. Parameter: atom3i is an instance of ATOM3 """ # Instantiate the Option Database module AToM3TreeLikeOptions.OptionDatabase = OptionDatabase( atom3i.parent, 'Options_TreeLikeLayout.py', 'TreeLikeLayout Configuration') # Local methods/variables with short names to make things more readable :D newOp = AToM3TreeLikeOptions.OptionDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY EE = OptionDialog.ENUM_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [OptionDialog.LABEL, "Times 12", "blue", "center"] newOp('label0000', None, optionList, 'Complexity: O(n)', '') newOp('sep0003', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") optionList = [OptionDialog.LABEL, "Times 12", "blue", "left"] newOp('label0001', None, optionList, 'Node spacing', '') newOp(MIN_HORIZONTAL_DISTANCE, 20, IE, "Minimum X Distance", "Minimum horizontal distance between any 2 tree nodes (Default 20)") newOp(MIN_VERTICAL_DISTANCE, 70, IE, "Minimum Y Distance", "Minimum vertical distance between any 2 tree nodes (Default 70)") newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0002', None, optionList, 'Miscellaneous options', '') newOp( TIP_OVER_STYLE, True, BE, "Tip over style", "If true, using a more space efficient tip over style drawing technique." + "If false, uses a traditional top to bottom drawing technique.") newOp(FORCE_TOPLEFT_TO_ORIGIN, False, BE, "Start tree at origin?", "If false, the current position of the selected nodes is used") newOp(MANUAL_CYCLE_BREAKING, False, BE, "Manual Cycle Breaking", "Forces the user to break cycles by manually clicking on nodes") enumOptions = [EE, 'Never', 'Smart', 'Always'] newOp(PROMOTE_EDGE_TO_NODE, 'Never', enumOptions, "Promote edge centers to nodes?", "For directed edges with large center drawings, promoting the center to "\ + "a node can lead to a much superiour layout\n\n" + "Example: One directed edge becomes one node and 2 directed edges\n\n" + "The 'smart' option will promote only if a center drawing is present" ) newOp('sep0001', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp(USE_SPLINES, True, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points") newOp( ARROW_CURVATURE, 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " + "set to 0 for a straight arrow.") # Load the options from the file, on failure the defaults above are used. AToM3TreeLikeOptions.OptionDatabase.loadOptionsDatabase()
class TreeLikeLayout: instance = None def __init__(self, atom3i): self.cb = atom3i.cb self.atom3i = atom3i # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_TreeLikeLayout.py', 'TreeLikeLayout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [OptionDialog.LABEL, "Times 12", "blue", "left"] newOp('label0001', None, optionList, 'Node spacing', '') newOp( 'xOffset', 20, IE, "Minimum X Distance", "Minimum horizontal distance between any 2 tree nodes (Default 20)" ) newOp( 'yOffset', 70, IE, "Minimum Y Distance", "Minimum vertical distance between any 2 tree nodes (Default 70)") newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0002', None, optionList, 'Miscellaneous options', '') newOp('Origin', False, BE, "Start tree at origin?", "If false, the current position of the selected nodes is used") newOp('Manual Cycles', False, BE, "Manual Cycle Breaking", "Forces the user to break cycles by manually clicking on nodes") newOp('sep0001', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp('Spline optimization', True, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points") newOp( 'Arrow curvature', 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " + "set to 0 for a straight arrow.") # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase() def updateATOM3instance(self, atom3i): """ Possible to have multiple instances of atom3 """ self.cb = atom3i.cb self.atom3i = atom3i def settings(self, selection): """ Dialog to interactively change the spring's behavior Automatically applies layout if not canceled """ if (self.__optionsDatabase.showOptionsDatabase()): self.main(selection) def main(self, selection): setSmooth = self.__optionsDatabase.get('Spline optimization') setCurvature = self.__optionsDatabase.get('Arrow curvature') doManualCycles = self.__optionsDatabase.get('Manual Cycles') doStartAtOrigin = self.__optionsDatabase.get('Origin') # Get all entity nodes (semantic objects) entityNodeList = self.__getEntityList(selection) if (len(entityNodeList) == 0): return # Get all root nodes (cycle in children possible), pure cycles will remain rootNodes = [] for node in entityNodeList: if (node._treeVisit == False and len(node.in_connections_) == 0): node._treeVisit = True rootNodes.append(node) self.__markChildrenNodesBFS(node, []) # Gather all the cycle nodes cycleNodes = [] for node in entityNodeList: if (node._treeVisit == False): cycleNodes.append(node) # Node cycle breakers --> choice of nodes as root to break the cycle if (doManualCycles): rootNodes = self.__getCycleRootsManually(cycleNodes, rootNodes) else: rootNodes = self.__getCycleRootsAuto(cycleNodes, rootNodes) #self.debugTree(rootNodes) # DFS printer # This does the actual moving around of nodes if (doStartAtOrigin): self.__layoutRoots(rootNodes, (0, 0)) else: self.__layoutRoots( rootNodes, self.__getMaxUpperLeftCoordinate(entityNodeList)) # Clean up for node in entityNodeList: del node._treeVisit del node._treeChildren # Re-wire the links to take into account the new node positions optimizeLinks( self.cb, setSmooth, setCurvature, selectedLinks=self.__getLinkListfromEntityList(entityNodeList)) def __getMaxUpperLeftCoordinate(self, entityNodeList): """ Returns the maximum upper left coordinate of all the nodes the layout is being applied to This corresponds to the minumum x and y coords of all the nodes """ minX = sys.maxint minY = sys.maxint for node in entityNodeList: if (node.graphObject_.y < minY): minY = node.graphObject_.y if (node.graphObject_.x < minX): minX = node.graphObject_.x return (minX, minY) def __getEntityList(self, selection): """ If selection is empty, get all nodes on the canvas Else filter out links Returns semantic objects, subclasses of ASGNode rather than VisualObj """ entityNodeList = [] # Selection may contain a mixed bag of nodes and links if (selection): for node in selection: if (not isConnectionLink(node)): semObj = node.semanticObject semObj._treeVisit = False semObj._treeChildren = [] entityNodeList.append(semObj) # No selection? Grab all nodes in diagram else: if (not self.atom3i.ASGroot): return [] for nodetype in self.atom3i.ASGroot.nodeTypes: for node in self.atom3i.ASGroot.listNodes[nodetype]: if (not isConnectionLink(node.graphObject_)): node._treeVisit = False node._treeChildren = [] entityNodeList.append(node) return entityNodeList def __getLinkListfromEntityList(self, entityNodeList): """ Find all links attached to the list of nodes """ linkList = [] for semObject in entityNodeList: linkNodes = semObject.in_connections_ + semObject.out_connections_ for semObj in linkNodes: if (semObj.graphObject_ not in linkList): linkList.append(semObj.graphObject_) return linkList def __getCycleRootsAuto(self, cycleNodes, rootNodes): """ Breaks cycles by automatically choosing root nodes Nodes with the highest out degree are the preferred choice Returns root nodes including the ones that break the cycles """ OutDegree2cycleNodeList = dict() # Associate each out degree with a list of nodes for node in cycleNodes: outDegree = len(node.out_connections_) if (OutDegree2cycleNodeList.has_key(outDegree)): OutDegree2cycleNodeList[outDegree].append(node) else: OutDegree2cycleNodeList[outDegree] = [node] # Get the list of out degrees, and sort them from big to small degreeList = OutDegree2cycleNodeList.keys() degreeList.sort() degreeList.reverse() # Now go through each node in the order of big to small for outDegree in degreeList: for node in OutDegree2cycleNodeList[outDegree]: if (node._treeVisit == False): node._treeVisit = True rootNodes.append(node) self.__markChildrenNodesBFS(node, []) return rootNodes def __getCycleRootsManually(self, cycleNodes, rootNodes): """ Allows the user to break cycles by clicking on nodes to choose tree roots Returns the final rootNodes (ie: including those that break cycles) """ if (len(cycleNodes) > 0): showinfo( 'TreeLikeLayout: Cycle/s Detected', 'Manual cycle breaking mode in effect\n\n' + 'Cyclic nodes will be highlighted\n\n' + 'Please break the cycle/s by clicking on the node/s' + ' you want as tree root/s') while (len(cycleNodes) > 0): self.cb.clearSelectionDict() index = self.__chooseRoot(cycleNodes) if (index != None): chosenRootNode = cycleNodes[index] chosenRootNode._treeVisit = True rootNodes.append(chosenRootNode) self.__markChildrenNodesBFS(chosenRootNode, []) # Cleanup: leave only nodes that still form a cycle temp = cycleNodes[:] for node in temp: if (node._treeVisit == True): cycleNodes.remove(node) node.graphObject_.HighLight(0) return rootNodes def __chooseRoot(self, cycleNodes): """ Allows the user to manually choose a node in a cycle to be root """ # Makes a special list of the cycle nodes, highlight them on the canvas matchList = [] for node in cycleNodes: node.graphObject_.HighLight(1) matchList.append([0, [node]]) # Initilize the choosing system. Special behaviour mode in statechart # This means that the user MUST choose a node, nothing else will work no_value_yet = -1 self.cb.initMatchChoice(no_value_yet, matchList) self.atom3i.UI_Statechart.event("GG Select", None) # Now we wait for the user to click somewhere, polling style while (self.cb.getMatchChoice() == no_value_yet): time.sleep(0.1) # Time in seconds self.atom3i.parent.update() return self.cb.getMatchChoice() def __layoutRoots(self, rootNodes, originPointXY): """ General graph may have more than a single root, so pretend the roots are all tied to some imaginary root high in the sky... """ (xPos, yPos) = originPointXY xOffset = self.__optionsDatabase.get('xOffset') yOffset = self.__optionsDatabase.get('yOffset') # Find the max height of all the root level nodes maxHeight = 0 for rootNode in rootNodes: maxHeight = max(maxHeight, rootNode.graphObject_.getSize()[1]) for rootNode in rootNodes: w = 0 # Layout the children of the root, then we'll know exactly where the # root itself goes... for childNode in rootNode._treeChildren: w0 = self.__layoutNode(childNode, xPos + w, yPos + yOffset + maxHeight, xOffset, yOffset) w += w0 + xOffset # Okay, now we place the root half-way between its children # but move it back a bit to account for its width (center it) widthOffset = rootNode.graphObject_.getSize()[0] / 2 rootNode.graphObject_.moveTo(xPos + w / 2 - widthOffset, yPos) xPos += w def __layoutNode(self, node, xPos, yPos, xOffset, yOffset): """ Do the layout for an ordinary node """ # No children? Then this is very easy to position... if (len(node._treeChildren) == 0): (w, h) = node.graphObject_.getSize() widthOffset = node.graphObject_.getSize( )[0] / 2 # Node center offset node.graphObject_.moveTo(xPos + (w + xOffset) / 2 - widthOffset, yPos) return w # Has children, doh! Layout children first, then position parent else: w = 0 h = node.graphObject_.getSize()[1] + yOffset for childNode in node._treeChildren: w0 = self.__layoutNode(childNode, xPos + w, yPos + h, xOffset, yOffset) w += w0 + xOffset # Position the parent of the children, knowning width children occupy widthOffset = node.graphObject_.getSize( )[0] / 2 # Node center offset node.graphObject_.moveTo(xPos + w / 2 - widthOffset, yPos) return w - xOffset def debugTree(self, rootNodes): for node in rootNodes: print 'Root nodes found', node.name.toString( ) #node.__class__.__name__, self.debugTreeChildrenDFS(node) def debugTreeChildrenDFS(self, node): for childNode in node._treeChildren: print 'Child node', childNode.name.toString() self.debugTreeChildrenDFS(childNode) def __markChildrenNodesDFS(self, node): """ Depth first search algorithm Descends a tree, marking all children as visited In case of cycle, the child node is ignored """ for link in node.out_connections_: for childNode in link.out_connections_: # childNode may not be in the selection, in that case it will not have # the _treeVisit attribute if (childNode.__dict__.has_key('_treeVisit') and childNode._treeVisit == False): node._treeChildren.append(childNode) childNode._treeVisit = True self.__markChildrenNodesDFS(childNode) def __markChildrenNodesBFS(self, node, queuedNodeList): """ Breadth first search algorithm Descends a tree, marking all children as visited In case of cycle, the child node is ignored """ for link in node.out_connections_: for childNode in link.out_connections_: # childNode may not be in the selection, in that case it will not have # the _treeVisit attribute if (childNode.__dict__.has_key('_treeVisit') and childNode._treeVisit == False): node._treeChildren.append(childNode) childNode._treeVisit = True queuedNodeList.append(childNode) if (len(queuedNodeList) == 1): self.__markChildrenNodesBFS(queuedNodeList[0], []) elif (len(queuedNodeList) > 1): self.__markChildrenNodesBFS(queuedNodeList[0], queuedNodeList[1:])
class TreeLikeLayout: instance = None def __init__(self, atom3i ): self.cb = atom3i.cb self.atom3i = atom3i # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_TreeLikeLayout.py', 'TreeLikeLayout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [OptionDialog.LABEL, "Times 12", "blue", "left" ] newOp( 'label0001', None, optionList, 'Node spacing', '' ) newOp( 'xOffset', 20, IE, "Minimum X Distance", "Minimum horizontal distance between any 2 tree nodes (Default 20)" ) newOp( 'yOffset', 70, IE, "Minimum Y Distance", "Minimum vertical distance between any 2 tree nodes (Default 70)" ) newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp( 'label0002', None, optionList, 'Miscellaneous options', '' ) newOp( 'Origin', False, BE, "Start tree at origin?", "If false, the current position of the selected nodes is used" ) newOp( 'Manual Cycles', False, BE, "Manual Cycle Breaking", "Forces the user to break cycles by manually clicking on nodes" ) newOp('sep0001', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp( 'Spline optimization' , True, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points" ) newOp( 'Arrow curvature', 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " +"set to 0 for a straight arrow." ) # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase() def updateATOM3instance( self, atom3i ): """ Possible to have multiple instances of atom3 """ self.cb = atom3i.cb self.atom3i = atom3i def settings( self, selection ): """ Dialog to interactively change the spring's behavior Automatically applies layout if not canceled """ if( self.__optionsDatabase.showOptionsDatabase() ): self.main( selection ) def main( self, selection ): setSmooth = self.__optionsDatabase.get('Spline optimization') setCurvature = self.__optionsDatabase.get('Arrow curvature') doManualCycles = self.__optionsDatabase.get('Manual Cycles') doStartAtOrigin = self.__optionsDatabase.get('Origin') # Get all entity nodes (semantic objects) entityNodeList = self.__getEntityList(selection) if(len(entityNodeList) == 0): return # Get all root nodes (cycle in children possible), pure cycles will remain rootNodes = [] for node in entityNodeList: if(node._treeVisit == False and len(node.in_connections_) == 0): node._treeVisit = True rootNodes.append(node) self.__markChildrenNodesBFS(node, []) # Gather all the cycle nodes cycleNodes = [] for node in entityNodeList: if(node._treeVisit == False): cycleNodes.append(node) # Node cycle breakers --> choice of nodes as root to break the cycle if(doManualCycles): rootNodes = self.__getCycleRootsManually(cycleNodes, rootNodes) else: rootNodes = self.__getCycleRootsAuto(cycleNodes, rootNodes) #self.debugTree(rootNodes) # DFS printer # This does the actual moving around of nodes if(doStartAtOrigin): self.__layoutRoots(rootNodes, (0, 0)) else: self.__layoutRoots(rootNodes, self.__getMaxUpperLeftCoordinate(entityNodeList)) # Clean up for node in entityNodeList: del node._treeVisit del node._treeChildren # Re-wire the links to take into account the new node positions optimizeLinks(self.cb, setSmooth, setCurvature, selectedLinks=self.__getLinkListfromEntityList(entityNodeList)) def __getMaxUpperLeftCoordinate(self, entityNodeList): """ Returns the maximum upper left coordinate of all the nodes the layout is being applied to This corresponds to the minumum x and y coords of all the nodes """ minX = sys.maxint minY = sys.maxint for node in entityNodeList: if(node.graphObject_.y < minY): minY = node.graphObject_.y if(node.graphObject_.x < minX): minX = node.graphObject_.x return (minX, minY) def __getEntityList(self, selection): """ If selection is empty, get all nodes on the canvas Else filter out links Returns semantic objects, subclasses of ASGNode rather than VisualObj """ entityNodeList = [] # Selection may contain a mixed bag of nodes and links if(selection): for node in selection: if(not isConnectionLink(node)): semObj = node.semanticObject semObj._treeVisit = False semObj._treeChildren = [] entityNodeList.append(semObj) # No selection? Grab all nodes in diagram else: if(not self.atom3i.ASGroot): return [] for nodetype in self.atom3i.ASGroot.nodeTypes: for node in self.atom3i.ASGroot.listNodes[nodetype]: if(not isConnectionLink(node.graphObject_)): node._treeVisit = False node._treeChildren = [] entityNodeList.append(node) return entityNodeList def __getLinkListfromEntityList(self, entityNodeList): """ Find all links attached to the list of nodes """ linkList = [] for semObject in entityNodeList: linkNodes = semObject.in_connections_ + semObject.out_connections_ for semObj in linkNodes: if(semObj.graphObject_ not in linkList): linkList.append(semObj.graphObject_) return linkList def __getCycleRootsAuto(self, cycleNodes, rootNodes): """ Breaks cycles by automatically choosing root nodes Nodes with the highest out degree are the preferred choice Returns root nodes including the ones that break the cycles """ OutDegree2cycleNodeList = dict() # Associate each out degree with a list of nodes for node in cycleNodes: outDegree = len(node.out_connections_) if(OutDegree2cycleNodeList.has_key(outDegree)): OutDegree2cycleNodeList[outDegree].append(node) else: OutDegree2cycleNodeList[outDegree] = [node] # Get the list of out degrees, and sort them from big to small degreeList = OutDegree2cycleNodeList.keys() degreeList.sort() degreeList.reverse() # Now go through each node in the order of big to small for outDegree in degreeList: for node in OutDegree2cycleNodeList[outDegree]: if(node._treeVisit == False): node._treeVisit = True rootNodes.append(node) self.__markChildrenNodesBFS(node, []) return rootNodes def __getCycleRootsManually(self, cycleNodes, rootNodes): """ Allows the user to break cycles by clicking on nodes to choose tree roots Returns the final rootNodes (ie: including those that break cycles) """ if(len(cycleNodes) > 0): showinfo('TreeLikeLayout: Cycle/s Detected', 'Manual cycle breaking mode in effect\n\n' + 'Cyclic nodes will be highlighted\n\n' + 'Please break the cycle/s by clicking on the node/s' + ' you want as tree root/s') while(len(cycleNodes) > 0): self.cb.clearSelectionDict() index = self.__chooseRoot(cycleNodes) if(index != None): chosenRootNode = cycleNodes[index] chosenRootNode._treeVisit = True rootNodes.append(chosenRootNode) self.__markChildrenNodesBFS(chosenRootNode, []) # Cleanup: leave only nodes that still form a cycle temp = cycleNodes[:] for node in temp: if(node._treeVisit == True): cycleNodes.remove(node) node.graphObject_.HighLight(0) return rootNodes def __chooseRoot(self, cycleNodes): """ Allows the user to manually choose a node in a cycle to be root """ # Makes a special list of the cycle nodes, highlight them on the canvas matchList = [] for node in cycleNodes: node.graphObject_.HighLight(1) matchList.append([0, [node]]) # Initilize the choosing system. Special behaviour mode in statechart # This means that the user MUST choose a node, nothing else will work no_value_yet = -1 self.cb.initMatchChoice( no_value_yet, matchList ) self.atom3i.UI_Statechart.event("GG Select", None) # Now we wait for the user to click somewhere, polling style while(self.cb.getMatchChoice() == no_value_yet ): time.sleep( 0.1 ) # Time in seconds self.atom3i.parent.update() return self.cb.getMatchChoice() def __layoutRoots(self, rootNodes, originPointXY): """ General graph may have more than a single root, so pretend the roots are all tied to some imaginary root high in the sky... """ (xPos, yPos) = originPointXY xOffset = self.__optionsDatabase.get('xOffset') yOffset = self.__optionsDatabase.get('yOffset') # Find the max height of all the root level nodes maxHeight = 0 for rootNode in rootNodes: maxHeight = max(maxHeight, rootNode.graphObject_.getSize()[1]) for rootNode in rootNodes: w = 0 # Layout the children of the root, then we'll know exactly where the # root itself goes... for childNode in rootNode._treeChildren: w0 = self.__layoutNode(childNode, xPos + w, yPos + yOffset + maxHeight, xOffset, yOffset) w += w0 + xOffset # Okay, now we place the root half-way between its children # but move it back a bit to account for its width (center it) widthOffset = rootNode.graphObject_.getSize()[0] / 2 rootNode.graphObject_.moveTo(xPos + w / 2 - widthOffset, yPos) xPos += w def __layoutNode(self, node, xPos, yPos, xOffset, yOffset): """ Do the layout for an ordinary node """ # No children? Then this is very easy to position... if(len(node._treeChildren) == 0): (w, h) = node.graphObject_.getSize() widthOffset = node.graphObject_.getSize()[0] / 2 # Node center offset node.graphObject_.moveTo(xPos + (w + xOffset) / 2 - widthOffset, yPos) return w # Has children, doh! Layout children first, then position parent else: w = 0 h = node.graphObject_.getSize()[1] + yOffset for childNode in node._treeChildren: w0 = self.__layoutNode(childNode, xPos + w, yPos + h, xOffset, yOffset) w += w0 + xOffset # Position the parent of the children, knowning width children occupy widthOffset = node.graphObject_.getSize()[0] / 2 # Node center offset node.graphObject_.moveTo(xPos + w / 2 - widthOffset, yPos) return w - xOffset def debugTree(self, rootNodes): for node in rootNodes: print 'Root nodes found', node.name.toString() #node.__class__.__name__, self.debugTreeChildrenDFS(node) def debugTreeChildrenDFS(self, node): for childNode in node._treeChildren: print 'Child node', childNode.name.toString() self.debugTreeChildrenDFS(childNode) def __markChildrenNodesDFS(self, node): """ Depth first search algorithm Descends a tree, marking all children as visited In case of cycle, the child node is ignored """ for link in node.out_connections_: for childNode in link.out_connections_: # childNode may not be in the selection, in that case it will not have # the _treeVisit attribute if(childNode.__dict__.has_key('_treeVisit') and childNode._treeVisit == False): node._treeChildren.append(childNode) childNode._treeVisit = True self.__markChildrenNodesDFS(childNode) def __markChildrenNodesBFS(self, node, queuedNodeList): """ Breadth first search algorithm Descends a tree, marking all children as visited In case of cycle, the child node is ignored """ for link in node.out_connections_: for childNode in link.out_connections_: # childNode may not be in the selection, in that case it will not have # the _treeVisit attribute if(childNode.__dict__.has_key('_treeVisit') and childNode._treeVisit == False): node._treeChildren.append(childNode) childNode._treeVisit = True queuedNodeList.append(childNode) if(len(queuedNodeList) == 1): self.__markChildrenNodesBFS(queuedNodeList[0], []) elif(len(queuedNodeList) > 1): self.__markChildrenNodesBFS(queuedNodeList[0], queuedNodeList[1:])
class SpringLayout: instance = None # Option keys MAXIMUM_ITERATIONS = 'Maximum iterations' ANIMATION_UPDATES = 'Animation updates' SPRING_CONSTANT = 'Spring constant' SPRING_LENGTH = 'Spring rest length' CHARGE_STRENGTH = 'Charge strength' FRICTION = 'Friction' RANDOM_AMOUNT = 'Random amount' ARROW_CURVATURE = 'Arrow curvature' SPLINE_ARROWS = 'Spline arrows' STICKY_BOUNDARY = 'Sticky boundary' INFO0 = 'Info0' INFO1 = 'Info1' INFO2 = 'Info2' INFO3 = 'Info3' INFO4 = 'Info4' def __init__(self, atom3i): self.atom3i = atom3i self.dc = atom3i.UMLmodel # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(self.atom3i.parent, 'Options_SpringLayout.py', 'Spring Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY FE = OptionDialog.FLOAT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY L = OptionDialog.LABEL # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp(self.INFO0, None, [L, "Times 12", "black", "center"], "This spring-electrical algorithm:", "") newOp(self.INFO1, None, [L, "Times 12", "black", "left"], "Has O(n^2) complexity", "") newOp(self.INFO3, None, [L, "Times 12", "black", "left"], "Does not work with hyper-edges", "") newOp(self.INFO2, None, [L, "Times 12", "black", "left"], "Is applied only on selected nodes & edges", "") newOp(self.INFO4, None, [L, "Times 12", "black", "left"], "", "") newOp( self.MAXIMUM_ITERATIONS, 100, IE, "Maximum Iterations", "Duration of the spring simulation, longer generally gives better results." ) newOp(self.ANIMATION_UPDATES, 5, IE, "Animation updates", "Force update of the canvas every X simulation frames.") newOp( self.SPRING_CONSTANT, 0.1, FE, "Spring Constant", "The restoring force of the spring, larger values make the spring \"stiffer\"" ) newOp(self.SPRING_LENGTH, 100, IE, "Spring rest length", "This is the minimum distance between the 2 nodes") newOp( self.CHARGE_STRENGTH, 1000.00, FE, "Charge strength", "A multiplier on the repulsive force between each and every node.") newOp( self.FRICTION, 0.01, FE, "Friction", "Limits the ability of the repulsive force to affect another node." ) newOp( self.RANDOM_AMOUNT, 0.0, FE, "Initial randomization", "Randomizes the initial position of linked nodes as a percentage of spring length." ) newOp( self.ARROW_CURVATURE, 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, set to 0 for a straight arrow." ) newOp( self.SPLINE_ARROWS, True, BE, "Spline arrows", "Arrows are set to smooth/spline mode and given additional control points." ) newOp(self.STICKY_BOUNDARY, True, BE, "Sticky boundary", "Prevents nodes from escaping the canvas boundaries.") # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions() def __processLoadedOptions(self): """ After loading the database, have to get & store each option value """ self.__maxIterations = self.__optionsDatabase.get( self.MAXIMUM_ITERATIONS) self.__animationUpdates = self.__optionsDatabase.get( self.ANIMATION_UPDATES) self.__springConstant = self.__optionsDatabase.get( self.SPRING_CONSTANT) self.__springLength = self.__optionsDatabase.get(self.SPRING_LENGTH) self.__chargeStrength = self.__optionsDatabase.get( self.CHARGE_STRENGTH) self.__friction = self.__optionsDatabase.get(self.FRICTION) self.__stickyBoundary = self.__optionsDatabase.get( self.STICKY_BOUNDARY) self.__splineArrows = self.__optionsDatabase.get(self.SPLINE_ARROWS) self.__arrowCurvature = self.__optionsDatabase.get( self.ARROW_CURVATURE) self.__randomness = self.__optionsDatabase.get(self.RANDOM_AMOUNT) def updateATOM3instance(self, atom3i): self.atom3i = atom3i def settings(self, selection): """ Dialog to interactively change the spring's behavior Automatically applies spring layout if not canceled """ if (self.__optionsDatabase.showOptionsDatabase()): self.__processLoadedOptions() self.main(selection) def main(self, selection): if (not selection): return atom3i = self.atom3i nodeObject.nodeList = [] edgeObject.edgeList = [] edgeObject.dc = self.dc #------------------------- INFORMATION GATHERING ------------------------- # Generate a datastructure for the Nodes and Edges in the diagram, containing # only the information needed by this algorithm. edgeList = [] nodeDict = dict() self.sourceTargetDict = dict() for obj in selection: if (isConnectionLink(obj)): # Edge! edgeList.append(obj.getSemanticObject()) else: # Node pos = obj.getCenterCoord() boundBox = obj.getbbox() if (self.__stickyBoundary): boundary = self.atom3i.CANVAS_SIZE_TUPLE else: boundary = None n = nodeObject(obj, pos, boundBox, self.__chargeStrength, boundary) nodeDict.update({obj: n}) # Now lets go through the "node" edges... for node in edgeList: # Source object key = node.in_connections_[0].graphObject_ if (not nodeDict.has_key(key)): continue source = nodeDict[key] # Target object key = node.out_connections_[0].graphObject_ if (not nodeDict.has_key(key)): continue target = nodeDict[key] # Make the edge object with the info... edgeObject(node, source, target) self.sourceTargetDict[source] = target # These nodes have edges... source.setHasEdgeTrue() target.setHasEdgeTrue() # Count the beans... self.__totalNodes = len(nodeObject.nodeList) if (self.__totalNodes <= 1): return #-------------------------- MAIN SIMULATION LOOP ------------------------- # Initial card shuffling :D if (self.__randomness): self.__shakeThingsUp(self.__randomness) i = 0 while (i < self.__maxIterations): # Calculate the powers that be self.__calculateRepulsiveForces() self.__calculateAttractiveForces() # Move move move! for node in nodeObject.nodeList: node.commitMove() # Force a screen update every x calculation if (i % self.__animationUpdates == 0): self.dc.update_idletasks() i += 1 #--------------------------- FINAL OPTIMIZATIONS ------------------------- # Optimize the arrows to use the nearest connectors optimizeLinks(self.atom3i.cb, self.__splineArrows, self.__arrowCurvature) # Make sure the canvas is updated self.dc.update_idletasks() def __shakeThingsUp(self, randomness): """ Randomizes positions of the nodes forming a link """ amount = int(randomness * self.__springLength) for edgeObj in edgeObject.edgeList: source = edgeObj.getSource() target = edgeObj.getTarget() dx = randint(-amount, amount) dy = randint(-amount, amount) source.incrementDisplacement([dx, dy]) source.commitMove() dx = randint(-amount, amount) dy = randint(-amount, amount) target.incrementDisplacement([dx, dy]) target.commitMove() def __calculateRepulsiveForces(self): """ Every node exerts a force on every other node, prevent overlap """ # If two nodes overlap, set the distance to this value to seperate them # Hint: the closer two nodes are, the more powerful the repulsion overlapDistance = 1 i = 0 while (i < self.__totalNodes): j = 0 ax = ay = 0 source = nodeObject.nodeList[i] iPos = source.getCoords() sourceCharge = source.getCharge() sourceHasEdge = source.hasEdge() while (j < self.__totalNodes): if (i == j): j += 1 continue target = nodeObject.nodeList[j] j += 1 # This prevents an unattached node from affecting attached nodes if (sourceHasEdge and not target.hasEdge()): continue dx, dy, distance = self.__getDistancesTuple( source, target, overlapDistance) # Reduce repulsion of nodes that are tied together by springs if (self.sourceTargetDict.has_key(source) and self.sourceTargetDict[source] == target): distance *= 2 elif (self.sourceTargetDict.has_key(target) and self.sourceTargetDict[target] == source): distance *= 2 charge = max(sourceCharge, target.getCharge()) electricForce = charge / (distance * distance) # The cutoff prevents unnecessary movement if (electricForce < self.__friction): continue # Accumlate displacement factor ax += dx * electricForce ay += dy * electricForce # Store the accumlated displacement source.incrementDisplacement([ax, ay]) #print vDisp, "<-- Repulsion displacement" i += 1 def __calculateAttractiveForces(self): """ Every edge exerts forces to draw its nodes close """ # If two nodes overlap, set distance to this value to seperate them # Hint: the smaller the distance is relative to the spring rest length, the # greater the serperating force will be. overlapDistance = self.__springLength * 0.75 for edge in edgeObject.edgeList: source = edge.getSource() target = edge.getTarget() # No fake edges permitted! if (source == target or source == None or target == None): continue dx, dy, distance = self.__getDistancesTuple( source, target, overlapDistance) ''' Basic Spring Equation: F = - k * x Replacing x with (d-l)/d, you get no force at the resting spring length, and lots of force the farther away from the spring length you are. The neat thing here: the spring will contract when the distance exceeds the length and expand when the distance is less than the length. Spring Equation: F = k * ( distance - length ) / distance ''' attractForce = self.__springConstant * (distance - self.__springLength) if (abs(distance) > 0): attractForce /= distance disp = [attractForce * dx, attractForce * dy] #print attractForce, disp, "<---- attract, disp" # Accumulate the displacement factor at the source & target nodes target.incrementDisplacement(disp) source.incrementDisplacement([-disp[0], -disp[1]]) def __vectorLength2D(self, v): """ Calculates the length of the 2D vector v """ return math.sqrt(v[0] * v[0] + v[1] * v[1]) def __getDistancesTuple(self, sourceNode, targetNode, overlapDistance): """ Finds the distance between two nodes and handles overlapping. Returns normalized dx,dy components as well as the magnitude of the distance in a tuple. """ def normalizeAndFixDistance(dx, dy, distance, overlapDistance, overlap=False): """ Normalizes the dx & dy variables If distance is too small, then it modifies dx & dy arbitrarily and sets the distance so that the spring will expand violently or the electrical charge will blast away... """ if (distance < 1 or overlap): if (abs(dx) < 1): if (dx < 0): dx = -1 else: dx = 1 if (abs(dy) < 1): if (dy < 0): dy = -1 else: dy = 1 return (dx, dy, overlapDistance) else: return (dx / distance, dy / distance, distance) sx, sy, sw, sh = sourceNode.getCoordsAndSize() tx, ty, tw, th = targetNode.getCoordsAndSize() # Position Delta dx = sx - tx dy = sy - ty # Overlap area ox = (sw + tw) / 2.0 oy = (sh + th) / 2.0 if (dx < 0): ox = -ox if (dy < 0): oy = -oy # Total Node overlap if (abs(dx) < abs(ox) and abs(dy) < abs(oy)): distance = self.__vectorLength2D([dx, dy]) return normalizeAndFixDistance(dx, dy, distance, overlapDistance, overlap=True) # No Node Overlap (but maybe overlap along an axis) else: # If the distance exceeds the size of the nodes, subtract the size # otherwsie, set the distance to zero. if (abs(dx) > abs(ox)): dx -= ox else: dx = 0 if (abs(dy) > abs(oy)): dy -= oy else: dy = 0 distance = self.__vectorLength2D([dx, dy]) return normalizeAndFixDistance(dx, dy, distance, overlapDistance)
def __init__(self, atom3i): self.atom3i = atom3i # AToM3 instance self.dc = self.atom3i.UMLmodel # Canvas # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( self.atom3i.parent, 'Options_ForceTransfer.py', 'Force Transfer Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY FE = OptionDialog.FLOAT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY LA = OptionDialog.LABEL # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [LA, "Times 12", "blue", "left"] newOp( 'label0', False, optionList, "\nThis algorithm is applied only to selected " + "nodes.\nHowever, if no nodes are selected it is applied globally.\n" ) newOp( self.AUTO_APPLY, False, BE, "Always active", "Runs force transfer whenever a node is added/dragged in the model" ) newOp( self.USE_STATUSBAR, False, BE, "Enable statusbar info", "Shows number of iterations used to find stable configuration in the statusbar" ) newOp( self.MIN_NODE_DISTANCE, 20, IE, "Minimum node seperation", "Node entities will be seperated by a minimum of this many pixels") newOp( self.MIN_LINK_DISTANCE, 20, IE, "Minimum link node seperation", "Distance in pixels that link nodes should be seperated from other nodes" ) newOp( self.MIN_CONTROL_DISTANCE, 20, IE, "Minimum link control point seperation", "Distance that link control points should be seperated from other nodes" ) newOp(self.SEPERATION_FORCE, 0.2, FE, "Seperation force", "Magnitude of the force that will seperate overlapping nodes") newOp( self.ANIMATION_TIME, 0.01, FE, "Animation time", "Seconds between animation frame updates, set 0 to disable animations" ) newOp( self.MAX_ANIM_ITERATIONS, 15, IE, "Max animation iterations", "Stop updating animation to screen after max iterations to speed process up" ) newOp( self.MAX_TOTAL_ITERATIONS, 50, IE, "Max total iterations", "Stop iterating, even if stable configuration not reached, to prevent unreasonably long periods of non-interactivity" ) newOp( self.BORDER_DISTANCE, 30, IE, "Border distance", "If an entity is pushed off the canvas, the canvas will be re-centered plus this pixel offset to the top left" ) # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions()
def __init__(self, atom3i): self.atom3i = atom3i self.dc = atom3i.UMLmodel # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(self.atom3i.parent, 'Options_SpringLayout.py', 'Spring Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY FE = OptionDialog.FLOAT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY L = OptionDialog.LABEL # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp(self.INFO0, None, [L, "Times 12", "black", "center"], "This spring-electrical algorithm:", "") newOp(self.INFO1, None, [L, "Times 12", "black", "left"], "Has O(n^2) complexity", "") newOp(self.INFO3, None, [L, "Times 12", "black", "left"], "Does not work with hyper-edges", "") newOp(self.INFO2, None, [L, "Times 12", "black", "left"], "Is applied only on selected nodes & edges", "") newOp(self.INFO4, None, [L, "Times 12", "black", "left"], "", "") newOp( self.MAXIMUM_ITERATIONS, 100, IE, "Maximum Iterations", "Duration of the spring simulation, longer generally gives better results." ) newOp(self.ANIMATION_UPDATES, 5, IE, "Animation updates", "Force update of the canvas every X simulation frames.") newOp( self.SPRING_CONSTANT, 0.1, FE, "Spring Constant", "The restoring force of the spring, larger values make the spring \"stiffer\"" ) newOp(self.SPRING_LENGTH, 100, IE, "Spring rest length", "This is the minimum distance between the 2 nodes") newOp( self.CHARGE_STRENGTH, 1000.00, FE, "Charge strength", "A multiplier on the repulsive force between each and every node.") newOp( self.FRICTION, 0.01, FE, "Friction", "Limits the ability of the repulsive force to affect another node." ) newOp( self.RANDOM_AMOUNT, 0.0, FE, "Initial randomization", "Randomizes the initial position of linked nodes as a percentage of spring length." ) newOp( self.ARROW_CURVATURE, 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, set to 0 for a straight arrow." ) newOp( self.SPLINE_ARROWS, True, BE, "Spline arrows", "Arrows are set to smooth/spline mode and given additional control points." ) newOp(self.STICKY_BOUNDARY, True, BE, "Sticky boundary", "Prevents nodes from escaping the canvas boundaries.") # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions()
def __init__(self, atom3i): self.cb = atom3i.cb self.atom3i = atom3i # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( atom3i.parent, 'Options_HiearchicalLayout.py', 'Hieararchical Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [OptionDialog.LABEL, "Times 12", "blue", "left"] newOp('label0001', None, optionList, 'Node spacing', '') newOp( 'xOffset', 30, IE, "Minimum X Distance", "Minimum horizontal distance between any 2 tree nodes (negative" + " values work too) (Default 30)") newOp( 'yOffset', 30, IE, "Minimum Y Distance", "Minimum vertical distance between any 2 tree nodes (Default 30)") newOp( 'addEdgeObjHeight', True, BE, "Add edge object height", "Increment spacing between node layers with edge object drawing of"\ + " maximum height between 2 given layers" ) newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0002', None, optionList, 'Miscellaneous options', '') newOp('Origin', False, BE, "Start tree at origin?", "If false, the current position of the selected nodes is used") # newOp( 'Manual Cycles', False, BE, "Manual Cycle Breaking", # "Forces the user to break cycles by manually clicking on nodes" ) newOp( 'uncrossPhase1', 5, IE, "Maximum uncrossing iterations", "Maximum number of passes to try to reduce edge crossings" \ + "\nNote: these only count when no progress is being made" ) newOp( 'uncrossPhase2', 15, IE, "Maximum uncrossing random restarts", "These can significantly improve quality, but they restart the " \ + "uncrossing phase to the beginning..." \ + "\nNote: these only count when no progress is being made" ) newOp( 'baryPlaceMax', 10, IE, "Maximum gridpositioning iterations", "Number of times a barycenter placement heuristic is run to " \ + "ensure everything is centered" ) newOp('sep0001', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp('Spline optimization', False, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points") newOp( 'Arrow curvature', 0, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " + "set to 0 for a straight arrow.") # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase()
class SpringLayout: instance = None # Option keys MAXIMUM_ITERATIONS = 'Maximum iterations' ANIMATION_UPDATES = 'Animation updates' SPRING_CONSTANT = 'Spring constant' SPRING_LENGTH = 'Spring rest length' CHARGE_STRENGTH = 'Charge strength' FRICTION = 'Friction' RANDOM_AMOUNT = 'Random amount' ARROW_CURVATURE = 'Arrow curvature' SPLINE_ARROWS = 'Spline arrows' STICKY_BOUNDARY = 'Sticky boundary' INFO0 = 'Info0' INFO1 = 'Info1' INFO2 = 'Info2' INFO3 = 'Info3' INFO4 = 'Info4' def __init__(self, atom3i ): self.atom3i = atom3i self.dc = atom3i.UMLmodel # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(self.atom3i.parent, 'Options_SpringLayout.py', 'Spring Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY FE = OptionDialog.FLOAT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY L = OptionDialog.LABEL # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp( self.INFO0, None, [L,"Times 12","black", "center" ], "This spring-electrical algorithm:", "" ) newOp( self.INFO1, None, [L,"Times 12","black", "left" ], "Has O(n^2) complexity", "" ) newOp( self.INFO3, None, [L,"Times 12","black", "left" ], "Does not work with hyper-edges", "" ) newOp( self.INFO2, None, [L,"Times 12","black", "left" ], "Is applied only on selected nodes & edges", "" ) newOp( self.INFO4, None, [L,"Times 12","black", "left" ],"", "" ) newOp( self.MAXIMUM_ITERATIONS, 100, IE, "Maximum Iterations", "Duration of the spring simulation, longer generally gives better results." ) newOp( self.ANIMATION_UPDATES, 5, IE, "Animation updates", "Force update of the canvas every X simulation frames." ) newOp( self.SPRING_CONSTANT, 0.1, FE, "Spring Constant", "The restoring force of the spring, larger values make the spring \"stiffer\"") newOp( self.SPRING_LENGTH, 100, IE, "Spring rest length", "This is the minimum distance between the 2 nodes") newOp( self.CHARGE_STRENGTH, 1000.00, FE, "Charge strength", "A multiplier on the repulsive force between each and every node." ) newOp( self.FRICTION, 0.01, FE, "Friction", "Limits the ability of the repulsive force to affect another node." ) newOp( self.RANDOM_AMOUNT, 0.0, FE, "Initial randomization", "Randomizes the initial position of linked nodes as a percentage of spring length." ) newOp( self.ARROW_CURVATURE, 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, set to 0 for a straight arrow." ) newOp( self.SPLINE_ARROWS, True, BE, "Spline arrows", "Arrows are set to smooth/spline mode and given additional control points." ) newOp( self.STICKY_BOUNDARY, True, BE, "Sticky boundary", "Prevents nodes from escaping the canvas boundaries." ) # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions() def __processLoadedOptions(self): """ After loading the database, have to get & store each option value """ self.__maxIterations = self.__optionsDatabase.get(self.MAXIMUM_ITERATIONS) self.__animationUpdates = self.__optionsDatabase.get(self.ANIMATION_UPDATES) self.__springConstant = self.__optionsDatabase.get(self.SPRING_CONSTANT) self.__springLength = self.__optionsDatabase.get(self.SPRING_LENGTH) self.__chargeStrength = self.__optionsDatabase.get(self.CHARGE_STRENGTH) self.__friction = self.__optionsDatabase.get(self.FRICTION) self.__stickyBoundary = self.__optionsDatabase.get(self.STICKY_BOUNDARY) self.__splineArrows = self.__optionsDatabase.get(self.SPLINE_ARROWS) self.__arrowCurvature = self.__optionsDatabase.get(self.ARROW_CURVATURE) self.__randomness = self.__optionsDatabase.get(self.RANDOM_AMOUNT) def updateATOM3instance( self, atom3i ): self.atom3i = atom3i def settings(self, selection): """ Dialog to interactively change the spring's behavior Automatically applies spring layout if not canceled """ if( self.__optionsDatabase.showOptionsDatabase() ): self.__processLoadedOptions() self.main(selection) def main(self, selection): if( not selection ): return atom3i = self.atom3i nodeObject.nodeList = [] edgeObject.edgeList = [] edgeObject.dc = self.dc #------------------------- INFORMATION GATHERING ------------------------- # Generate a datastructure for the Nodes and Edges in the diagram, containing # only the information needed by this algorithm. edgeList = [] nodeDict = dict() self.sourceTargetDict = dict() for obj in selection: if( isConnectionLink( obj ) ): # Edge! edgeList.append( obj.getSemanticObject() ) else: # Node pos = obj.getCenterCoord() boundBox = obj.getbbox() if( self.__stickyBoundary ): boundary = self.atom3i.CANVAS_SIZE_TUPLE else: boundary = None n = nodeObject( obj, pos, boundBox, self.__chargeStrength, boundary ) nodeDict.update( { obj : n } ) # Now lets go through the "node" edges... for node in edgeList: # Source object key = node.in_connections_[0].graphObject_ if( not nodeDict.has_key( key ) ): continue source = nodeDict[ key ] # Target object key = node.out_connections_[0].graphObject_ if( not nodeDict.has_key( key ) ): continue target = nodeDict[ key ] # Make the edge object with the info... edgeObject(node, source, target) self.sourceTargetDict[ source ] = target # These nodes have edges... source.setHasEdgeTrue() target.setHasEdgeTrue() # Count the beans... self.__totalNodes = len( nodeObject.nodeList ) if( self.__totalNodes <= 1 ): return #-------------------------- MAIN SIMULATION LOOP ------------------------- # Initial card shuffling :D if( self.__randomness ): self.__shakeThingsUp( self.__randomness ) i = 0 while( i < self.__maxIterations ): # Calculate the powers that be self.__calculateRepulsiveForces() self.__calculateAttractiveForces() # Move move move! for node in nodeObject.nodeList: node.commitMove() # Force a screen update every x calculation if( i % self.__animationUpdates == 0 ): self.dc.update_idletasks() i+=1 #--------------------------- FINAL OPTIMIZATIONS ------------------------- # Optimize the arrows to use the nearest connectors optimizeLinks( self.atom3i.cb, self.__splineArrows, self.__arrowCurvature ) # Make sure the canvas is updated self.dc.update_idletasks() def __shakeThingsUp( self, randomness ): """ Randomizes positions of the nodes forming a link """ amount = int( randomness * self.__springLength ) for edgeObj in edgeObject.edgeList: source = edgeObj.getSource() target = edgeObj.getTarget() dx = randint( -amount, amount ) dy = randint( -amount, amount ) source.incrementDisplacement( [dx,dy] ) source.commitMove() dx = randint( -amount, amount ) dy = randint( -amount, amount ) target.incrementDisplacement( [dx,dy] ) target.commitMove() def __calculateRepulsiveForces(self): """ Every node exerts a force on every other node, prevent overlap """ # If two nodes overlap, set the distance to this value to seperate them # Hint: the closer two nodes are, the more powerful the repulsion overlapDistance = 1 i = 0 while( i < self.__totalNodes ): j = 0 ax = ay = 0 source = nodeObject.nodeList[i] iPos = source.getCoords() sourceCharge = source.getCharge() sourceHasEdge = source.hasEdge() while( j < self.__totalNodes ): if( i == j ): j += 1 continue target = nodeObject.nodeList[j] j += 1 # This prevents an unattached node from affecting attached nodes if( sourceHasEdge and not target.hasEdge() ): continue dx, dy, distance = self.__getDistancesTuple( source, target, overlapDistance ) # Reduce repulsion of nodes that are tied together by springs if( self.sourceTargetDict.has_key( source ) and self.sourceTargetDict[ source ] == target ): distance *= 2 elif( self.sourceTargetDict.has_key( target ) and self.sourceTargetDict[ target ] == source ): distance *= 2 charge = max( sourceCharge, target.getCharge() ) electricForce = charge / ( distance * distance ) # The cutoff prevents unnecessary movement if( electricForce < self.__friction ): continue # Accumlate displacement factor ax += dx * electricForce ay += dy * electricForce # Store the accumlated displacement source.incrementDisplacement( [ax,ay] ) #print vDisp, "<-- Repulsion displacement" i+=1 def __calculateAttractiveForces(self): """ Every edge exerts forces to draw its nodes close """ # If two nodes overlap, set distance to this value to seperate them # Hint: the smaller the distance is relative to the spring rest length, the # greater the serperating force will be. overlapDistance = self.__springLength * 0.75 for edge in edgeObject.edgeList: source = edge.getSource() target = edge.getTarget() # No fake edges permitted! if( source == target or source == None or target == None): continue dx, dy, distance = self.__getDistancesTuple( source, target, overlapDistance ) ''' Basic Spring Equation: F = - k * x Replacing x with (d-l)/d, you get no force at the resting spring length, and lots of force the farther away from the spring length you are. The neat thing here: the spring will contract when the distance exceeds the length and expand when the distance is less than the length. Spring Equation: F = k * ( distance - length ) / distance ''' attractForce = self.__springConstant * (distance - self.__springLength ) if( abs(distance) > 0 ): attractForce /= distance disp = [ attractForce * dx, attractForce * dy ] #print attractForce, disp, "<---- attract, disp" # Accumulate the displacement factor at the source & target nodes target.incrementDisplacement( disp ) source.incrementDisplacement( [-disp[0], -disp[1]] ) def __vectorLength2D(self, v ): """ Calculates the length of the 2D vector v """ return math.sqrt( v[0] * v[0] + v[1] * v[1] ) def __getDistancesTuple( self, sourceNode, targetNode, overlapDistance ): """ Finds the distance between two nodes and handles overlapping. Returns normalized dx,dy components as well as the magnitude of the distance in a tuple. """ def normalizeAndFixDistance( dx, dy, distance, overlapDistance, overlap=False ): """ Normalizes the dx & dy variables If distance is too small, then it modifies dx & dy arbitrarily and sets the distance so that the spring will expand violently or the electrical charge will blast away... """ if( distance < 1 or overlap ): if( abs( dx ) < 1 ): if( dx < 0 ): dx = -1 else: dx = 1 if( abs( dy ) < 1 ): if( dy < 0 ): dy = -1 else: dy = 1 return (dx,dy, overlapDistance ) else: return ( dx / distance, dy / distance, distance ) sx, sy, sw, sh = sourceNode.getCoordsAndSize() tx, ty, tw, th = targetNode.getCoordsAndSize() # Position Delta dx = sx - tx dy = sy - ty # Overlap area ox = ( sw + tw ) / 2.0 oy = ( sh + th ) / 2.0 if( dx < 0 ): ox = -ox if( dy < 0 ): oy = -oy # Total Node overlap if( abs( dx ) < abs( ox ) and abs( dy ) < abs( oy ) ): distance = self.__vectorLength2D( [dx,dy] ) return normalizeAndFixDistance( dx, dy, distance, overlapDistance, overlap=True) # No Node Overlap (but maybe overlap along an axis) else: # If the distance exceeds the size of the nodes, subtract the size # otherwsie, set the distance to zero. if( abs( dx ) > abs( ox ) ): dx -= ox else: dx = 0 if( abs( dy ) > abs( oy ) ): dy -= oy else: dy = 0 distance = self.__vectorLength2D( [dx,dy] ) return normalizeAndFixDistance( dx, dy, distance, overlapDistance)
class ForceTransfer: instance = None MIN_NODE_DISTANCE = 'Minimum node distance' MIN_LINK_DISTANCE = 'Minimum link distance' MIN_CONTROL_DISTANCE = 'Minimum control point distance' SEPERATION_FORCE = 'Seperation Force' ANIMATION_TIME = 'Animation Time Updates' MAX_ANIM_ITERATIONS = 'Max Animation Iterations' MAX_TOTAL_ITERATIONS = 'Max Total Iterations' USE_STATUSBAR = 'Use Statusbar' AUTO_APPLY = 'Auto apply' BORDER_DISTANCE = 'Border Distance' def __init__(self, atom3i): self.atom3i = atom3i # AToM3 instance self.dc = self.atom3i.UMLmodel # Canvas # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase( self.atom3i.parent, 'Options_ForceTransfer.py', 'Force Transfer Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY FE = OptionDialog.FLOAT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY LA = OptionDialog.LABEL # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [LA, "Times 12", "blue", "left"] newOp( 'label0', False, optionList, "\nThis algorithm is applied only to selected " + "nodes.\nHowever, if no nodes are selected it is applied globally.\n" ) newOp( self.AUTO_APPLY, False, BE, "Always active", "Runs force transfer whenever a node is added/dragged in the model" ) newOp( self.USE_STATUSBAR, False, BE, "Enable statusbar info", "Shows number of iterations used to find stable configuration in the statusbar" ) newOp( self.MIN_NODE_DISTANCE, 20, IE, "Minimum node seperation", "Node entities will be seperated by a minimum of this many pixels") newOp( self.MIN_LINK_DISTANCE, 20, IE, "Minimum link node seperation", "Distance in pixels that link nodes should be seperated from other nodes" ) newOp( self.MIN_CONTROL_DISTANCE, 20, IE, "Minimum link control point seperation", "Distance that link control points should be seperated from other nodes" ) newOp(self.SEPERATION_FORCE, 0.2, FE, "Seperation force", "Magnitude of the force that will seperate overlapping nodes") newOp( self.ANIMATION_TIME, 0.01, FE, "Animation time", "Seconds between animation frame updates, set 0 to disable animations" ) newOp( self.MAX_ANIM_ITERATIONS, 15, IE, "Max animation iterations", "Stop updating animation to screen after max iterations to speed process up" ) newOp( self.MAX_TOTAL_ITERATIONS, 50, IE, "Max total iterations", "Stop iterating, even if stable configuration not reached, to prevent unreasonably long periods of non-interactivity" ) newOp( self.BORDER_DISTANCE, 30, IE, "Border distance", "If an entity is pushed off the canvas, the canvas will be re-centered plus this pixel offset to the top left" ) # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions() def __processLoadedOptions(self): """ After loading the database, have to get & store each option value """ self.__autoApply = self.__optionsDatabase.get(self.AUTO_APPLY) self.__useStatusBar = self.__optionsDatabase.get(self.USE_STATUSBAR) self.__minNodeDist = self.__optionsDatabase.get(self.MIN_NODE_DISTANCE) self.__minLinkDist = self.__optionsDatabase.get(self.MIN_LINK_DISTANCE) self.__minControlDist = self.__optionsDatabase.get( self.MIN_CONTROL_DISTANCE) self.__seperationForce = self.__optionsDatabase.get( self.SEPERATION_FORCE) self.__animationTime = self.__optionsDatabase.get(self.ANIMATION_TIME) self.__maxAnimIterations = self.__optionsDatabase.get( self.MAX_ANIM_ITERATIONS) self.__maxIterations = self.__optionsDatabase.get( self.MAX_TOTAL_ITERATIONS) self.__borderDistance = self.__optionsDatabase.get( self.BORDER_DISTANCE) # Inform AToM3 that it should call this algorithm whenever a node is # added or dragged if (self.__autoApply): self.atom3i.isAutoForceTransferEnabled = True else: self.atom3i.isAutoForceTransferEnabled = False def updateATOM3instance(self, atom3i): """ Possible to have multiple instances of atom3 """ self.atom3i = atom3i # AToM3 instance self.dc = self.atom3i.UMLmodel # Canvas def settings(self, selection): """ Show the dialog, load the options, transfer some force! """ if (self.__optionsDatabase.showOptionsDatabase()): self.__processLoadedOptions() def main(self, selection): Object.objList = [] atom3i = self.atom3i dc = self.dc # Specific objects have been chosen on the canvas if (selection): for obj in selection: self.__grabInfoFromGraphicalObject(obj) # Nothing on canvas selected, do all! else: # Grab all the nodes in the diagram, except those with 0 size (arrows) # Store them in the "nodeObject" and reference them by objList for nodetype in atom3i.ASGroot.nodeTypes: for node in atom3i.ASGroot.listNodes[nodetype]: obj = node.graphObject_ #print obj self.__grabInfoFromGraphicalObject(obj) self.__totalNodes = len(Object.objList) #self.__sortNodes() # Trivial non-overlap case if (self.__totalNodes <= 1): return self.__isLayoutStable = False # Keep at it till the layout is stable i = 0 while (not self.__isLayoutStable): self.__isLayoutStable = True # Optimism is good... self.__calculationLoop() # Disgusting: I have to actually sleep, otherwise I'll be done so fast # you won't have even seen it move :p if (self.__animationTime and i < self.__maxAnimIterations): self.dc.update_idletasks() time.sleep(self.__animationTime) if (i > self.__maxIterations): break i += 1 # Hijack the status bar to show what the FTA is doing... if (self.__useStatusBar): if (i >= self.__maxIterations): atom3i.statusbar.set( 1, "FTA halted at max iterations, layout unstable", None) else: atom3i.statusbar.set( 1, "FTA needed " + str(i) + " iterations to find stable layout", None) # Keep the whole thing in the viewable area of the canvas minY = minX = 10000 for node in Object.objList: if (isinstance(node, NodeObject)): x, y = node.getTopLeftPos() else: x, y = node.getCoords() if (x < minX): minX = x if (y < minY): minY = y if (minX < self.__borderDistance): minX = abs(minX) + self.__borderDistance else: minX = 0 if (minY < self.__borderDistance): minY = abs(minY) + self.__borderDistance else: minY = 0 # Push on it! for node in Object.objList: node.recenteringPush(minX, minY) # All that moving stuff around can mess up the connections... if (selection): optimizeConnectionPorts(atom3i, entityList=selection) else: optimizeConnectionPorts(atom3i, doAllLinks=True) def __grabInfoFromGraphicalObject(self, obj): """ Takes a graphical object and stores relevent info in a data structure """ # This be a node/entity object if (not isConnectionLink(obj)): try: x0, y0, x1, y1 = obj.getbbox() width = abs((x0 - x1)) height = abs((y0 - y1)) center = [x0 + width / 2, y0 + height / 2] except: print "ERROR caught and handled in ForceTransfer.py in __grabInfoFromGraphicalObject" width = 4 height = 4 center = [obj.x, obj.y] x0 = obj.x - 2 y0 = obj.y - 2 NodeObject(obj, center, [width, height], self.__minNodeDist, topLeftPos=[x0, y0]) # This be a link/edge object elif (self.__minLinkDist > 0): # Treat the link center as a repulsive object EdgeObject(obj, obj.getCenterCoord(), self.__minLinkDist) # Treat each control point as a repulsive object if (self.__minControlDist > 0): if (not self.dc): return for connTuple in obj.connections: itemHandler = connTuple[0] c = self.dc.coords(itemHandler) for i in range(2, len(c) - 2, 2): ControlPoint(c[i:i + 2], self.__minControlDist, itemHandler, i, self.dc) def __sortNodes(self): """ Sorts the nodes according to their distance from the origin (0,0) This can have a large impact on performance, especially as the number of objects in contact with one another goes up. """ sortedList = [] for node in Object.objList: sortedList.append((node.getDistanceFromOrigin(), node)) sortedList.sort() Object.objList = [] for x, node in sortedList: Object.objList.append(node) def __calculationLoop(self): """ Loop through all the nodes """ # Go through all the nodes, and find the overlap forces i = 0 j = 1 while (i < self.__totalNodes): while (j < self.__totalNodes): if (i != j): self.__forceCalculation( Object.objList[i], \ Object.objList[j] ) j += 1 i += 1 j = i # Go through all the nodes and apply the forces to the positions for node in Object.objList: node.commitForceApplication() def __forceCalculation(self, n1, n2): """ Evaluates distances betweens nodes (ie: do they overlap) and calculates a force sufficient to pry them apart. """ # Absolute distance along X and Y vectors between the nodes pointA = n1.getCoords() pointB = n2.getCoords() dx = abs(pointB[0] - pointA[0]) dy = abs(pointB[1] - pointA[1]) # Zero division error prevention measures if (dx == 0.0): dx = 0.1 if (dy == 0.0): dy = 0.1 # Node-Node Distances dist = math.sqrt(dx * dx + dy * dy) # Normalized-Vector norm = [dx / dist, dy / dist] # Overlap due to size of nodes sizeA = n1.getSize() sizeB = n2.getSize() sizeOverlap = [(sizeA[0] + sizeB[0]) / 2, (sizeA[1] + sizeB[1]) / 2] # Desired distance with resulting force minSeperationDist = min(n1.getSeperationDistance(), n2.getSeperationDistance()) d1 = (1.0 / norm[0]) * (sizeOverlap[0] + minSeperationDist) d2 = (1.0 / norm[1]) * (sizeOverlap[1] + minSeperationDist) forceMagnitude = self.__seperationForce * (dist - min(d1, d2)) # The force should be less than -1 (or it won't be having much of an effect) if (forceMagnitude < -1): #print forceMagnitude, dist, d1,d2 , (sizeOverlap[0] + minSeperationDist),(sizeOverlap[1] + minSeperationDist),(1.0 / norm[0]),(1.0 / norm[1]) force = [forceMagnitude * norm[0], forceMagnitude * norm[1]] # Maximize compactness by only pushing nodes along a single axis if (force[0] > force[1]): force[0] = 0 else: force[1] = 0 # Determine the direction of the force direction = [1, 1] if (pointA[0] > pointB[0]): direction[0] = -1 if (pointA[1] > pointB[1]): direction[1] = -1 # Add up the forces to the two interacting objects n1.forceIncrement( [direction[0] * force[0], direction[1] * force[1]]) n2.forceIncrement( [-direction[0] * force[0], -direction[1] * force[1]]) # If a force was applied this iteration, definately not stable yet self.__isLayoutStable = False
class SnapGrid: instance = None highestItemHandler = None """ highestItemHandler variable is used to place items just above the grid lines Example pattern: dc.tag_lower(myItemHandler) # Under everything if(SnapGrid.highestItemHandler): dc.tag_raise(myItemHandler, SnapGrid.highestItemHandler) # Above grid """ GRID_ENABLED = 'snapgrid enabled' GRID_ARROWNODE = 'snap arrow node' GRID_CONTROLPOINTS = 'snap control points' GRID_PIXELS = 'gridsquare pixels' GRID_WIDTH = 'gridsquare width' GRID_COLOR = 'gridsquare color' GRID_DOT_MODE = 'use gridsquare dots' GRID_SUDIVISIONS = 'gridsquare subdivisions' GRID_SUDIVISIONS_WIDTH = 'gridsquare sudvision width' GRID_SUBDIVISION_COLOR = 'gridsquare subdivision color' GRID_SUBDIVISION_SHOW = 'enable gridsquare subdivisions' def __init__(self, atom3i): # Keep track of item handlers so that the lines can be removed (if needed) self.__gridItemHandlers = [] self.atom3i = atom3i # AToM3 instance self.dc = self.atom3i.getCanvas() # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(self.atom3i.parent, 'Options_SnapGrid.py', 'Snap Grid Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY CE = OptionDialog.COLOR_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString newOp( self.GRID_ENABLED, True, BE, "Enable Snap Grid" ) newOp( self.GRID_ARROWNODE, False, BE, "Snap arrow node" ) newOp( self.GRID_CONTROLPOINTS, False, BE, "Snap arrow control points" ) newOp( self.GRID_PIXELS, 20, IE, "Grid square size in pixels", "Snapping will occur at every X pixels square" ) newOp( self.GRID_DOT_MODE, True, BE, "Grid dots", "Dot mode is much slower than using lines" ) newOp( self.GRID_WIDTH, 1, IE, "Grid square width in pixels" ) newOp( self.GRID_COLOR, '#c8c8c8', [CE,"Choose Color"], "Grid square color" ) newOp( self.GRID_SUDIVISIONS, 5, IE, "Grid square subdivisions", "Every X number of divisions, a subdivsion will be placed" ) newOp( self.GRID_SUBDIVISION_SHOW, True, BE, "Show subdivision lines","Makes it easier to visually measure distances" ) newOp( self.GRID_SUDIVISIONS_WIDTH, 1, IE, "Grid square sudivision width" ) newOp( self.GRID_SUBDIVISION_COLOR, '#e8e8e8', [CE,"Choose Color"], "Grid square subdivision color" ) # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions() def __processLoadedOptions(self): """ After loading the database, have to get & store each option value """ # Enabled? self.__gridEnabled = self.__optionsDatabase.get(self.GRID_ENABLED) self.__gridArrowNode = self.__optionsDatabase.get(self.GRID_ARROWNODE) self.__gridControlPoints = self.__optionsDatabase.get(self.GRID_CONTROLPOINTS) # Primary Grid self.__gridLineSeperation = self.__optionsDatabase.get(self.GRID_PIXELS) self.__gridLineColor = self.__optionsDatabase.get(self.GRID_COLOR) self.__gridDotMode = self.__optionsDatabase.get(self.GRID_DOT_MODE) self.__gridWidth = self.__optionsDatabase.get(self.GRID_SUDIVISIONS_WIDTH) # Grid Subdivisions self.__gridSubdivisions = self.__optionsDatabase.get(self.GRID_SUDIVISIONS) self.__gridLineSubdivisionColor = self.__optionsDatabase.get(self.GRID_SUBDIVISION_COLOR) self.__gridSubdivisionShow = self.__optionsDatabase.get(self.GRID_SUBDIVISION_SHOW) self.__gridSubdivisionWidth = self.__optionsDatabase.get(self.GRID_SUDIVISIONS_WIDTH) def updateATOM3instance( self, atom3i ): self.atom3i = atom3i # Atom3 instance self.dc = self.atom3i.getCanvas() # Canvas def settings(self ): """ Show the dialog, load the options, snap it on! """ self.__optionsDatabase.showOptionsDatabase() self.__processLoadedOptions() self.drawGrid() def drawGrid(self ): """ Draws the grid """ # Do we really want to draw the grid? :D if( not self.__gridEnabled ): return self.destroy() # Is the grid already drawn? Wipe it clean, then go at it again! elif( self.__gridItemHandlers ): self.destroy() # Starting the Grid up for the first time, let AToM3 know about it... else: self.__updateMainApp() canvasBox = self.atom3i.CANVAS_SIZE_TUPLE # Create the "subdivision grid", this is really just a visual aid if( self.__gridSubdivisionShow ): subdivisionSeperation = self.__gridLineSeperation * self.__gridSubdivisions for x in range( canvasBox[0], canvasBox[2], subdivisionSeperation ): line = self.dc.create_line(x,0,x,canvasBox[3], width = self.__gridSubdivisionWidth, fill=self.__gridLineSubdivisionColor ) self.__gridItemHandlers.append( line ) for y in range( canvasBox[1], canvasBox[3], subdivisionSeperation ): line = self.dc.create_line(0,y,canvasBox[2],y, width = self.__gridSubdivisionWidth, fill=self.__gridLineSubdivisionColor ) self.__gridItemHandlers.append( line ) # Create the 'real' grid, this is where snapping occurs # Use Dots: less visual clutter but slow since it is O(n^2) if( self.__gridDotMode ): for x in range( canvasBox[0], canvasBox[2], self.__gridLineSeperation ): for y in range( canvasBox[1], canvasBox[3], self.__gridLineSeperation ): oval = self.dc.create_oval( x-self.__gridWidth,y-self.__gridWidth, x+self.__gridWidth,y+self.__gridWidth, width = 0,fill=self.__gridLineColor ) self.__gridItemHandlers.append( oval ) # Use lines: much faster since it is O(n) else: for x in range( canvasBox[0], canvasBox[2], self.__gridLineSeperation ): line = self.dc.create_line(x,0,x,canvasBox[2], width = self.__gridWidth,fill=self.__gridLineColor ) self.__gridItemHandlers.append( line ) for y in range( canvasBox[1], canvasBox[3], self.__gridLineSeperation ): line = self.dc.create_line(0,y,canvasBox[3],y, width = self.__gridWidth, fill=self.__gridLineColor ) self.__gridItemHandlers.append( line ) # Push all this stuff behind what's already on the canvas for itemHandler in self.__gridItemHandlers: self.dc.tag_lower( itemHandler ) SnapGrid.highestItemHandler = self.__gridItemHandlers[0] def __updateMainApp(self, disableForPrinting = False ): """ Updates the main application with information it needs to snap """ if( self.__gridEnabled and not disableForPrinting ): self.atom3i.snapGridInfoTuple = ( self.__gridLineSeperation, self.__gridArrowNode, self.__gridControlPoints ) else: self.atom3i.snapGridInfoTuple = None def destroy(self, disableForPrinting = False ): """ Grid is displayed? Kill it """ SnapGrid.highestItemHandler = None if( self.__gridItemHandlers ): for itemHandler in self.__gridItemHandlers: self.dc.delete( itemHandler ) self.__gridItemHandlers = [] self.__updateMainApp( disableForPrinting )
class CircleLayout: instance = None def __init__(self, atom3i): self.cb = atom3i.cb self.atom3i = atom3i # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(atom3i.parent, 'Options_CicleLayout.py', 'Circle Layout Configuration') # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption IE = OptionDialog.INT_ENTRY BE = OptionDialog.BOOLEAN_ENTRY # Create New Options # Format: OptionKey, defaultValue, optionTuple, promptString, helpString optionList = [OptionDialog.LABEL, "Times 12", "blue", "left"] newOp('label0001', None, optionList, 'Node positioning', '') newOp('Origin', False, BE, "Start circle at origin?", "If false, the current position of the selected nodes is used") newOp('Offset', 20, IE, "Minimum node spacing", "Minimum distance between any 2 tree nodes (Default 20)") newOp('sep0000', 'Ignored', OptionDialog.SEPERATOR, "Ignored?", "Ignored!") newOp('label0003', None, optionList, 'Arrow post-processing options', '') newOp('Spline optimization', True, BE, "Spline optimization", "Sets the arrow to smooth mode and adds 2 extra control points") newOp( 'Arrow curvature', 10, IE, "Arrow curvature", "Adds a curve of magnitude X to the arrows, " + "set to 0 for a straight arrow.") # Load the options from the file, on failure the defaults above are used. self.__optionsDatabase.loadOptionsDatabase() def updateATOM3instance(self, atom3i): """ Possible to have multiple instances of atom3 """ self.cb = atom3i.cb self.atom3i = atom3i def settings(self, selection): """ Dialog to interactively change the spring's behavior Automatically applies layout if not canceled """ if (self.__optionsDatabase.showOptionsDatabase()): self.main(selection) def main(self, selection): setSmooth = self.__optionsDatabase.get('Spline optimization') setCurvature = self.__optionsDatabase.get('Arrow curvature') entityNodeList = self.__getEntityList(selection) if (len(entityNodeList) == 0): return self.__positionNodes(entityNodeList) optimizeLinks(self.cb, setSmooth, setCurvature, selectedLinks=self.__getLinkList(entityNodeList)) def __getLinkList(self, entityNodeList): """ Find all links disturbed by the circle layout algorithm """ linkList = [] for obj in entityNodeList: semObject = obj.semanticObject linkNodes = semObject.in_connections_ + semObject.out_connections_ for semObj in linkNodes: if (semObj.graphObject_ not in linkList): linkList.append(semObj.graphObject_) return linkList def __getEntityList(self, selection): """ If selection is empty, get all nodes on the canvas Else filter out links """ entityNodeList = [] # Selection may contain a mixed bag of nodes and links if (selection): for node in selection: if (not isConnectionLink(node)): entityNodeList.append(node) # No selection? Grab all nodes in diagram else: if (not self.atom3i.ASGroot): return [] for nodetype in self.atom3i.ASGroot.nodeTypes: for node in self.atom3i.ASGroot.listNodes[nodetype]: if (not isConnectionLink(node.graphObject_)): entityNodeList.append(node.graphObject_) return entityNodeList def __computeCircleRadius(self, entityNodeList): """ Takes a list of entity nodes, computes the perimeter they will occupy and resulting radius of circle required """ # Compute radius automatically # Line up all the nodes diagonally (or max of H and W), count length # Use eqution: perimeter = 2*pi*r to get radius offset = self.__optionsDatabase.get('Offset') perimeter = 0 for node in entityNodeList: perimeter += max(node.getSize()) + offset return (perimeter, perimeter / (2 * math.pi)) def __positionNodes(self, entityNodeList): """ Position the nodes """ useOrigin = self.__optionsDatabase.get('Origin') if (useOrigin): baseX = 0 baseY = 0 else: (baseX, baseY) = self.__getMaxUpperLeftCoordinate(entityNodeList) # Compute circle positions # Angle per step = 2*pi / # of nodes # For each node: # positionX[i] = r + r * sin(i * anglePerStep) # positionY[i] = r + r * cos(i * anglePerStep) (perimeter, radius) = self.__computeCircleRadius(entityNodeList) anglePerStep = (2.0 * math.pi) / float(len(entityNodeList)) for i in range(0, len(entityNodeList)): x = baseX + radius + radius * math.sin(i * anglePerStep) y = baseY + radius + radius * math.cos(i * anglePerStep) entityNodeList[i].moveTo(x, y) def __getMaxUpperLeftCoordinate(self, entityNodeList): """ Returns the maximum upper left coordinate of all the nodes the layout is being applied to This corresponds to the minumum x and y coords of all the nodes """ minX = sys.maxint minY = sys.maxint for node in entityNodeList: if (node.y < minY): minY = node.y if (node.x < minX): minX = node.x return (minX, minY)
class Postscript: MASK_STIPPLE = "gray12" TOP = 0 BOTTOM = 1 LEFT = 2 RIGHT = 3 # How close you must click to a mask boundary in order to select it MIN_SIDE_DIST = 100 # Option Keys COLOR_MODE = 'Color mode' ROTATION = 'Rotation' MASK_COLOR_KEY = 'Mask color' TRANSPARENT_MASK = 'Mask transparency' SVG_EXPORT_MODE = 'SVG export mode' def __init__(self, atom3i, dc): self.atom3i = atom3i self.dc = dc # <-- Canvas widget self.__mask = [] self.__box = None self.__boxOutline = None self.__activeSide = None self.__lastPos = None self.__abort = False self.__maskColor = "red" self.__transparentMask = True self.__restoreSnapGrid = False # Instantiate the Option Database module self.__optionsDatabase = OptionDatabase(atom3i.parent, 'Options_Postscript.py', 'Postscript Settings', autoSave=True) # Local methods/variables with short names to make things more readable :D newOp = self.__optionsDatabase.createNewOption EN = OptionDialog.ENUM_ENTRY L = OptionDialog.LABEL BE = OptionDialog.BOOLEAN_ENTRY CE = OptionDialog.COLOR_ENTRY newOp(self.COLOR_MODE, "color", [EN, 'color', 'grey', 'mono'], "Export color mode") newOp(self.ROTATION, "portrait", [EN, 'portrait', 'landscape'], "Export rotation") newOp(self.MASK_COLOR_KEY, "red", [CE, 'Choose color'], "Boundary mask color") newOp(self.TRANSPARENT_MASK, True, BE, "Transparent boundary mask") newOp('L0', None, [L, 'times 12', 'blue', 'left'], "") newOp( 'L1', None, [L, 'times 12', 'blue', 'left'], "After pressing OK, you must select the canvas area to export as postscript" ) newOp( 'L2', None, [L, 'times 12', 'blue', 'left'], "You can modify boundaries by left-clicking and moving the mouse around" ) newOp('L3', None, [L, 'times 12', 'blue', 'left'], "Right-clicking will set the new boundary position") newOp('L4', None, [L, 'times 12', 'blue', 'left'], "Right-clicking again will do the actual postscript export") newOp("seperator1", '', OptionDialog.SEPERATOR, '', '') newOp(self.SVG_EXPORT_MODE, True, BE, "Export to SVG instead of postscript") newOp( 'L5', None, [L, 'times 12', 'blue', 'left'], "NOTE: SVG exports selected items only (if no selection then entire canvas)" ) # Load the options from the file, on failure the defaults will be returned. self.__optionsDatabase.loadOptionsDatabase() self.__processLoadedOptions() def __processLoadedOptions(self): """ After loading the database, have to get & store each option value """ self.__colormode = self.__optionsDatabase.get(self.COLOR_MODE) self.__rotation = self.__optionsDatabase.get(self.ROTATION) self.__maskColor = self.__optionsDatabase.get(self.MASK_COLOR_KEY) self.__transparentMask = self.__optionsDatabase.get( self.TRANSPARENT_MASK) self.__svgExportMode = self.__optionsDatabase.get(self.SVG_EXPORT_MODE) def enteringPostscript(self): if (self.__abort): self.atom3i.UI_Statechart.event("Done") def createMask(self, pos): """ Creates 4 transparent rectangles that mask out what won't be included in the postscript generation. """ # Pos could be an event or a [x,y] list if (type(pos) != type(list())): pos = [pos.x, pos.y] minX, minY, maxX, maxY = self.atom3i.CANVAS_SIZE_TUPLE # Uh oh snap grid is on! This will mess up the boundary calculation! if (self.atom3i.snapGridInfoTuple): self.atom3i.disableSnapGridForPrinting(True) self.__box = self.dc.bbox('all') # Do we have an initial boundary box? Did the options dialog get OK'd? if (self.__box and self.__optionsDatabase.showOptionsDatabase(pos)): x0, y0, x1, y1 = self.__box self.__processLoadedOptions() if (self.__svgExportMode): self.exportSVG() self.__abort = True return else: self.__abort = False # Error! Cancel! Abort! else: self.__abort = True return # The boundary box outline self.__boxOutline = self.dc.create_rectangle(x0, y0, x1, y1, outline='black', fill='', width=1) # Use transparent boundary mask? It's somewhat slower... if (self.__transparentMask): stipple = self.MASK_STIPPLE else: stipple = '' # The masks on the 4 sides of the boundary box topBox = self.dc.create_rectangle(minX, minY, maxX, y0, outline='', fill=self.__maskColor, stipple=stipple, width=1) botBox = self.dc.create_rectangle(minX, y1, maxX, maxY, outline='', fill=self.__maskColor, stipple=stipple, width=1) leftBox = self.dc.create_rectangle(minX, minY, x0, maxY, outline='', fill=self.__maskColor, stipple=stipple, width=1) rightBox = self.dc.create_rectangle(x1, minY, maxX, maxY, outline='', fill=self.__maskColor, stipple=stipple, width=1) self.__mask = [topBox, botBox, leftBox, rightBox] def destroy(self): """ Reset everything back to defaults & remove stuff from canvas """ for item in self.__mask: self.dc.delete(item) self.__mask = [] self.__box = None self.__activeSide = None self.dc.delete(self.__boxOutline) self.__boxOutline = None def setActiveSide(self, pos): """ Sets the nearest side of the bounding box to active modification Side must be within a certain distance of the given position, or the side will not be selected, and False will be returned. """ x, y = self.__lastPos = pos x0, y0, x1, y1 = self.__box xDist = abs(x1 - x0) yDist = abs(y1 - y0) closestHitDist = self.MIN_SIDE_DIST closestHitIndex = None # Quick but not so great method if (0): # Use top box line if (y < y0): self.__activeSide = self.TOP # Use right box line elif (x > x1): self.__activeSide = self.RIGHT # Use bottom box line elif (y > y1): self.__activeSide = self.BOTTOM # Use left box line else: self.__activeSide = self.LEFT return True # Slower but more interactive method else: # Distance to the left-most bounding box segment dist = point2SegmentDistance(x, y, x0, y0, x0, y0 + yDist) if (dist < closestHitDist): closestHitDist = dist closestHitIndex = self.LEFT # Distance to the right-most bounding box segment dist = point2SegmentDistance(x, y, x1, y0, x1, y0 + yDist) if (dist < closestHitDist): closestHitDist = dist closestHitIndex = self.RIGHT # Distance to the top-most bounding box segment dist = point2SegmentDistance(x, y, x0, y0, x0 + xDist, y0) if (dist < closestHitDist): closestHitDist = dist closestHitIndex = self.TOP # Distance to the bottom-most bounding box segment dist = point2SegmentDistance(x, y, x0, y1, x0 + xDist, y1) if (dist < closestHitDist): closestHitDist = dist closestHitIndex = self.BOTTOM if (closestHitIndex != None): self.__activeSide = closestHitIndex return True else: self.__activeSide = None return False def inMotion(self, pos): """ Moves the active side of the selection box with the mouse motion """ if (self.__activeSide == None): return oldX, oldY = self.__lastPos newX, newY = self.__lastPos = pos dx, dy = (newX - oldX, newY - oldY) x0, y0, x1, y1 = self.__box # Apply motion delta to the active side if (self.__activeSide == self.LEFT): x0 += dx elif (self.__activeSide == self.RIGHT): x1 += dx elif (self.__activeSide == self.TOP): y0 += dy elif (self.__activeSide == self.BOTTOM): y1 += dy minX, minY, maxX, maxY = self.atom3i.CANVAS_SIZE_TUPLE topBox, botBox, leftBox, rightBox = self.__mask # Move the mask around self.dc.coords(topBox, minX, minY, maxX, y0) self.dc.coords(botBox, minX, y1, maxX, maxY) self.dc.coords(leftBox, minX, minY, x0, maxY) self.dc.coords(rightBox, x1, minY, maxX, maxY) # Update the box self.__box = [x0, y0, x1, y1] self.dc.coords(self.__boxOutline, x0, y0, x1, y1) def generatePostscript(self, autoSaveToFileName=None): """ Generate the printable postscript file using the bounding box """ if (self.__rotation == "landscape"): rotation = True else: rotation = False if (autoSaveToFileName): # Uh oh snap grid is on! This will mess up the boundary calculation! if (self.atom3i.snapGridInfoTuple): self.atom3i.disableSnapGridForPrinting(True) # Bounding box b = self.dc.bbox('all') if (b == None): print 'Bounding box is empty', b, 'for', autoSaveToFileName # b = [0,0, 1,1] # Empty canvas return None # Abort fileName = autoSaveToFileName if (fileName[-4:] != '.eps' and fileName[-3:] != '.ps'): fileName += '.eps' else: # Make the box go bye bye b = self.__box self.destroy() # No box? No postscript :p if (not b or self.__abort): return # Save Dialog fileName = tkFileDialog.asksaveasfilename( initialfile='x.eps', filetypes=[("Encapsulated Postscript", "*.eps"), ("Postscript", "*.ps")]) # Canceled! if (fileName == ''): return # This is for lazy people (like me) who don't add the extension :D if (fileName[-4:] != '.eps' and fileName[-3:] != '.ps'): fileName += '.ps' self.dc.postscript(file=fileName, x=b[0], y=b[1], width=b[2] - b[0], height=b[3] - b[1], colormode=self.__colormode, rotate=rotation) return b # return the bounding box def exportSVG(self): """ Sends selected objects or the entire canvas (if no selection) to the SVG exporter and writes the results to a file. """ # Save Dialog fileName = tkFileDialog.asksaveasfilename(initialfile='x.svg', filetypes=[("SVG", "*.svg"), ("All files", "*.*")]) # Canceled! if (fileName == ''): return if (fileName[-4:].lower() == '.svg'): from AToM3Selection2SVG import AToM3Selection2SVG selectionList = self.atom3i.cb.buildSelectionObjectSet() if (not selectionList): selectionList = [] for nodeList in self.atom3i.ASGroot.listNodes.values(): for node in nodeList: selectionList.append(node.graphObject_) SVGtext = AToM3Selection2SVG(selectionList) #print SVGtext f = open(fileName, 'w') f.write(SVGtext) f.close()