Пример #1
0
    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()
Пример #2
0
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)
Пример #3
0
    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()
Пример #4
0
  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()
Пример #5
0
    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()
Пример #6
0
  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()
Пример #7
0
    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()
Пример #8
0
 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()
Пример #9
0
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()
Пример #10
0
  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()
Пример #11
0
  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()
Пример #12
0
 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()
Пример #13
0
  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()
Пример #14
0
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()
Пример #15
0
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      
Пример #16
0
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)
Пример #17
0
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()
Пример #18
0
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()
Пример #19
0
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
Пример #20
0
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
Пример #21
0
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)
Пример #22
0
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)
Пример #23
0
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()
Пример #24
0
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()
Пример #25
0
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:])
Пример #26
0
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:])
Пример #27
0
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)
Пример #28
0
    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()
Пример #29
0
    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()
Пример #30
0
    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()
Пример #31
0
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)
Пример #32
0
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
Пример #33
0
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 )
      
Пример #34
0
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)
Пример #35
0
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()