예제 #1
0
    def __init__(self,
                 screensize=(800, 600),
                 fullscreen=False,
                 caption="3D Topology Viewer",
                 particleTypes=None,
                 initialTopology=None,
                 laws=None,
                 simCyclesPerRedraw=1,
                 border=0):
        """x.__init__(...) initializes x; see x.__class__.__doc__ for signature"""

        super(TopologyViewer3D, self).__init__()

        glutInit(sys.argv)

        tracker = _cat.coordinatingassistanttracker.getcat()
        try:
            self.display = tracker.retrieveService("ogl_display")[0]
        except KeyError:
            self.display = OpenGLDisplay(width=screensize[0],
                                         height=screensize[1],
                                         fullscreen=fullscreen,
                                         title=caption)
            self.display.activate()
            OpenGLDisplay.setDisplayService(self.display, tracker)
        self.display = OpenGLDisplay.getDisplayService()[0]
        self.link((self, "display_signal"), (self.display, "notify"))
        self.link((self.display, "signal"), (self, "control"))

        self.border = border

        if particleTypes == None:
            self.particleTypes = {
                "-": CuboidParticle3D,
                "cuboid": CuboidParticle3D,
                "sphere": SphereParticle3D,
                "teapot": TeapotParticle3D
            }
        else:
            self.particleTypes = particleTypes

        if initialTopology == None:
            initialTopology = ([], [])
        self.initialNodes = list(initialTopology[0])
        self.initialBonds = list(initialTopology[1])

        self.hitParticles = []
        self.multiSelectMode = False
        self.selectedParticles = []
        self.grabbed = False
        self.rotationMode = False

        if laws == None:
            self.laws = Kamaelia.Support.Particles.SimpleLaws(bondLength=2)
        else:
            self.laws = laws

        self.physics = ParticleSystem(self.laws, [], 0)
        self.biggestRadius = 0

        # Do interaction
        self.simCyclesPerRedraw = simCyclesPerRedraw
        self.lastIdleTime = time.time()

        # Tell if new node is added; if true, new id needs adding to OpenGLDisplay list
        self.isNewNode = False

        # For hierarchy structure
        self.maxLevel = 0
        self.currentLevel = 0
        self.previousParentParticleID = self.currentParentParticleID = ''
        self.viewerOldPos = Vector()
        self.levelViewerPos = {}
        # The Physics particle system of current display level for display
        self.currentDisplayedPhysics = ParticleSystem(self.laws, [], 0)

        # For double click
        self.lastClickPos = (0, 0)
        self.lastClickTime = time.time()
        self.dClickRes = 0.3
예제 #2
0
 def __init__(self, screensize         = (800,600),
                    fullscreen         = False, 
                    caption            = "3D Topology Viewer", 
                    particleTypes      = None,
                    initialTopology    = None,
                    laws               = None,
                    simCyclesPerRedraw = 1,
                    border             = 0):
     """x.__init__(...) initializes x; see x.__class__.__doc__ for signature"""
     
     super(TopologyViewer3D, self).__init__()
     
     glutInit(sys.argv)
     
     tracker = _cat.coordinatingassistanttracker.getcat()
     try:
         self.display = tracker.retrieveService("ogl_display")[0]
     except KeyError:
         self.display = OpenGLDisplay(width=screensize[0], height=screensize[1],fullscreen=fullscreen,
                                 title=caption)
         self.display.activate()
         OpenGLDisplay.setDisplayService(self.display, tracker)
     self.display = OpenGLDisplay.getDisplayService()[0]                
     self.link((self,"display_signal"), (self.display,"notify"))
     self.link((self.display,"signal"), (self,"control"))
     
     self.border = border
     
     if particleTypes == None:
         self.particleTypes = {"-":CuboidParticle3D, "cuboid":CuboidParticle3D, "sphere":SphereParticle3D,
                               "teapot":TeapotParticle3D}
     else:
         self.particleTypes = particleTypes
         
     if initialTopology == None:
         initialTopology = ([],[])
     self.initialNodes   = list(initialTopology[0])
     self.initialBonds   = list(initialTopology[1])
     
     self.hitParticles = []
     self.multiSelectMode = False
     self.selectedParticles = []
     self.grabbed = False
     self.rotationMode = False  
     
     if laws==None:
         self.laws = Kamaelia.Support.Particles.SimpleLaws(bondLength=2)
     else:
         self.laws = laws
         
     self.physics = ParticleSystem(self.laws, [], 0)
     self.biggestRadius = 0
     
     # Do interaction
     self.simCyclesPerRedraw = simCyclesPerRedraw
     self.lastIdleTime = time.time()
     
     # Tell if new node is added; if true, new id needs adding to OpenGLDisplay list
     self.isNewNode = False
     
     # For hierarchy structure
     self.maxLevel = 0
     self.currentLevel = 0
     self.previousParentParticleID = self.currentParentParticleID = ''
     self.viewerOldPos = Vector()
     self.levelViewerPos = {}
     # The Physics particle system of current display level for display
     self.currentDisplayedPhysics = ParticleSystem(self.laws, [], 0)
     
     # For double click
     self.lastClickPos = (0,0)
     self.lastClickTime = time.time()
     self.dClickRes = 0.3
예제 #3
0
class TopologyViewer3D(Axon.Component.component):
    """\
    TopologyViewer3D(...) -> new TopologyViewer3D component.
    
    A component that takes incoming topology (change) data and displays it live
    using pygame OpenGL. A simple physics model assists with visual layout. Particle
    types, appearance and physics interactions can be customised.
    
    Keyword arguments (in order):
    
    - screensize          -- (width,height) of the display area (default = (800,600))
    - fullscreen          -- True to start up in fullscreen mode (default = False)
    - caption             -- Caption for the pygame window (default = "3D Topology Viewer")
    - particleTypes       -- dict("type" -> klass) mapping types of particle to classes used to render them (default = {"-":CuboidParticle3D})
    - initialTopology     -- (nodes,bonds) where bonds=list((src,dst)) starting state for the topology  (default=([],[]))
    - laws                -- Physics laws to apply between particles (default = SimpleLaws(bondlength=2))
    - simCyclesPerRedraw  -- number of physics sim cycles to run between each redraw (default=1)
    - border              -- Minimum distance from edge of display area that new particles appear (default=0)
    """

    Inboxes = {
        "inbox": "Topology (change) data describing an Axon system",
        "control": "Shutdown signalling",
        "callback": "for the response after a displayrequest",
        "events": "Place where we recieve events from the outside world",
    }

    Outboxes = {
        "signal": "Control signalling",
        "outbox": "Notification and topology output",
        "display_signal": "Requests to Pygame Display service",
    }

    def __init__(self,
                 screensize=(800, 600),
                 fullscreen=False,
                 caption="3D Topology Viewer",
                 particleTypes=None,
                 initialTopology=None,
                 laws=None,
                 simCyclesPerRedraw=1,
                 border=0):
        """x.__init__(...) initializes x; see x.__class__.__doc__ for signature"""

        super(TopologyViewer3D, self).__init__()

        glutInit(sys.argv)

        tracker = _cat.coordinatingassistanttracker.getcat()
        try:
            self.display = tracker.retrieveService("ogl_display")[0]
        except KeyError:
            self.display = OpenGLDisplay(width=screensize[0],
                                         height=screensize[1],
                                         fullscreen=fullscreen,
                                         title=caption)
            self.display.activate()
            OpenGLDisplay.setDisplayService(self.display, tracker)
        self.display = OpenGLDisplay.getDisplayService()[0]
        self.link((self, "display_signal"), (self.display, "notify"))
        self.link((self.display, "signal"), (self, "control"))

        self.border = border

        if particleTypes == None:
            self.particleTypes = {
                "-": CuboidParticle3D,
                "cuboid": CuboidParticle3D,
                "sphere": SphereParticle3D,
                "teapot": TeapotParticle3D
            }
        else:
            self.particleTypes = particleTypes

        if initialTopology == None:
            initialTopology = ([], [])
        self.initialNodes = list(initialTopology[0])
        self.initialBonds = list(initialTopology[1])

        self.hitParticles = []
        self.multiSelectMode = False
        self.selectedParticles = []
        self.grabbed = False
        self.rotationMode = False

        if laws == None:
            self.laws = Kamaelia.Support.Particles.SimpleLaws(bondLength=2)
        else:
            self.laws = laws

        self.physics = ParticleSystem(self.laws, [], 0)
        self.biggestRadius = 0

        # Do interaction
        self.simCyclesPerRedraw = simCyclesPerRedraw
        self.lastIdleTime = time.time()

        # Tell if new node is added; if true, new id needs adding to OpenGLDisplay list
        self.isNewNode = False

        # For hierarchy structure
        self.maxLevel = 0
        self.currentLevel = 0
        self.previousParentParticleID = self.currentParentParticleID = ''
        self.viewerOldPos = Vector()
        self.levelViewerPos = {}
        # The Physics particle system of current display level for display
        self.currentDisplayedPhysics = ParticleSystem(self.laws, [], 0)

        # For double click
        self.lastClickPos = (0, 0)
        self.lastClickTime = time.time()
        self.dClickRes = 0.3

    def initialiseComponent(self):
        """Initialises."""
        self.addListenEvents([
            pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION,
            pygame.KEYDOWN, pygame.KEYUP
        ])
        # For key holding handling
        pygame.key.set_repeat(100, 100)

        for node in self.initialNodes:
            self.addParticle(*node)

        for source, dest in self.initialBonds:
            self.makeBond(source, dest)

    def main(self):
        """Main loop."""
        # Make display request for event listening purpose
        self.size = Vector(0, 0, 0)
        disprequest = {
            "OGL_DISPLAYREQUEST": True,
            "objectid": id(self),
            "callback": (self, "callback"),
            "events": (self, "events"),
            "size": self.size
        }
        # send display request
        self.send(disprequest, "display_signal")
        # Wait for response on displayrequest and get identifier of the viewer
        while not self.dataReady("callback"):
            yield 1
        self.identifier = self.recv("callback")

        self.initialiseComponent()

        while True:
            # Process incoming messages
            if self.dataReady("inbox"):
                message = self.recv("inbox")
                self.doCommand(message)

                # Wait for response on displayrequest and get identifier of the particle
                if self.isNewNode:
                    while not self.dataReady("callback"):
                        yield 1
                    self.physics.particles[-1].identifier = self.recv(
                        "callback")
                    self.isNewNode = False
            else:
                self.lastIdleTime = 0

            yield 1

            if self.lastIdleTime + 1.0 < time.time():
                #Freeze selected particles so that they are not subject to the physics law
                for particle in self.selectedParticles:
                    particle.freeze()
                # Do interaction between particles
                self.currentDisplayedPhysics.run(self.simCyclesPerRedraw)
                # Unfreeze selected particles
                for particle in self.selectedParticles:
                    particle.unFreeze()

                # Draw particles if new or updated
                for particle in self.currentDisplayedPhysics.particles:
                    if particle.needRedraw:
                        self.drawParticles(particle)

                self.handleEvents()

                # Perform transformation
                for particle in self.currentDisplayedPhysics.particles:
                    transform_update = particle.applyTransforms()
                    if transform_update is not None:
                        self.send(transform_update, "display_signal")

                self.lastIdleTime = time.time()
            else:
                yield 1
            if self.dataReady("control"):
                msg = self.recv("control")
                if isinstance(msg, Axon.Ipc.shutdownMicroprocess):
                    self.quit(msg)

    def quit(self, msg=Axon.Ipc.shutdownMicroprocess()):
        """Cause termination."""
        print('Shut down...')
        self.send(msg, "signal")
        self.scheduler.stop()

    def draw(self):
        """\
        Dummy method reserved for future use
        
        Invoke draw() and save its commands to a newly generated displaylist.
        
        The displaylist name is then sent to the display service via a
        "DISPLAYLIST_UPDATE" request.
        """
        pass

    def drawParticles(self, *particles):
        """\
            Sends particles drawing opengl command to the display service.
        """
        for particle in particles:
            # Display list id
            displaylist = glGenLists(1)
            # Draw object to its displaylist
            glNewList(displaylist, GL_COMPILE)
            particle.draw()
            glEndList()

            # Send displaylist
            dl_update = {
                "DISPLAYLIST_UPDATE": True,
                "objectid": id(particle),
                "displaylist": displaylist
            }
            self.send(dl_update, "display_signal")

    def addListenEvents(self, events):
        """\
            Sends listening request for pygame events to the display service.
            The events parameter is expected to be a list of pygame event constants.
        """
        for event in events:
            self.send({
                "ADDLISTENEVENT": event,
                "objectid": id(self)
            }, "display_signal")

    def removeListenEvents(self, events):
        """\
            Sends stop listening request for pygame events to the display service.
            The events parameter is expected to be a list of pygame event constants.
        """
        for event in events:
            self.send({
                "REMOVELISTENEVENT": event,
                "objectid": id(self)
            }, "display_signal")

    def handleEvents(self):
        """Handle events."""
        while self.dataReady("events"):
            event = self.recv("events")
            if event.type == pygame.MOUSEBUTTONDOWN or event.type == pygame.MOUSEMOTION or event.type == pygame.MOUSEBUTTONUP:
                self.handleMouseEvents(event)
            elif event.type == pygame.KEYDOWN or event.type == pygame.KEYUP:
                self.handleKeyEvents(event)

            # Scroll if self.display.viewerposition changes
            if self.display.viewerposition.copy() != self.viewerOldPos:
                self.scroll()
                self.viewerOldPos = self.display.viewerposition.copy()

    def handleMouseEvents(self, event):
        """Handle mouse events."""
        if event.type == pygame.MOUSEBUTTONDOWN or pygame.MOUSEMOTION and self.grabbed:
            if not self.rotationMode:
                for particle in self.hitParticles:
                    p1 = Vector(*particle.pos).copy()
                    p1.x += 10
                    p2 = Vector(*particle.pos).copy()
                    p2.y += 10
                    # Get the position of mouse
                    z = Intersect.ray_Plane(Vector(0, 0, 0), event.direction, [
                        Vector(*particle.pos) -
                        Vector(0, 0, self.display.viewerposition.z),
                        p1 - Vector(0, 0, self.display.viewerposition.z),
                        p2 - Vector(0, 0, self.display.viewerposition.z)
                    ])
                    newpoint = event.direction * z
        if event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:
                # Handle double click
                clickPos = event.pos
                currentTime = time.time()
                elapsedTime = currentTime - self.lastClickTime
                # If it's a double-click
                if clickPos == self.lastClickPos and elapsedTime < self.dClickRes:
                    self.gotoDisplayLevel(1)
                else:  # Single click
                    if not self.rotationMode:  # Select particle
                        for particle in self.currentDisplayedPhysics.particles:
                            if particle.identifier in event.hitobjects:
                                self.grabbed = True
                                self.hitParticles.append(particle)
                                self.selectParticle(particle)
                        # If click places other than particles in non multiSelectMode, deselect all
                        if not self.hitParticles and not self.multiSelectMode:
                            self.deselectAll()
                self.lastClickPos = clickPos
                self.lastClickTime = currentTime
            elif event.button == 3:  # Right-clicked
                self.gotoDisplayLevel(-1)
            elif event.button == 4:  # Scrolled-up: zoom out
                if self.selectedParticles:
                    particles = self.selectedParticles
                else:
                    particles = self.currentDisplayedPhysics.particles
                for particle in particles:
                    posVector = Vector(*particle.pos)
                    posVector.z -= 1
                    particle.pos = posVector.toTuple()
            elif event.button == 5:  # Scrolled-down: zoom in
                if self.selectedParticles:
                    particles = self.selectedParticles
                else:
                    particles = self.currentDisplayedPhysics.particles
                for particle in particles:
                    posVector = Vector(*particle.pos)
                    posVector.z += 1
                    particle.pos = posVector.toTuple()
        if event.type == pygame.MOUSEBUTTONUP:
            if event.button == 1:
                for particle in self.hitParticles:
                    self.grabbed = False
                    particle.oldpoint = None
                    self.hitParticles.pop(self.hitParticles.index(particle))
        if event.type == pygame.MOUSEMOTION:
            if not self.rotationMode and self.grabbed:  # Drag particles
                for particle in self.hitParticles:
                    try:
                        if particle.oldpoint is not None:
                            diff = newpoint - particle.oldpoint
                            amount = (diff.x, diff.y)
                            particle.pos = (Vector(*particle.pos) +
                                            Vector(*amount)).toTuple()
                    except NameError:
                        pass

                    # Redraw the link so that the link can move with the particle
                    for p in particle.bondedFrom:
                        p.needRedraw = True
            elif self.rotationMode:  # Rotate particles
                dAnglex = float(event.rel[1])
                dAngley = -float(event.rel[0])
                self.rotateParticles(self.selectedParticles,
                                     (dAnglex, dAngley, 0))

        try:
            for particle in self.hitParticles:
                particle.oldpoint = newpoint
        except NameError:
            pass

    def handleKeyEvents(self, event):
        """Handle keyboard events."""
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                self.quit()
            elif event.key == pygame.K_BACKSPACE:
                self.gotoDisplayLevel(-1)
            elif event.key == pygame.K_RETURN:
                self.gotoDisplayLevel(1)
            elif event.key == pygame.K_LSHIFT or event.key == pygame.K_RSHIFT:
                self.multiSelectMode = True
            elif event.key == pygame.K_LCTRL or event.key == pygame.K_RCTRL:
                self.rotationMode = True
            # Change viewer position
            elif event.key == pygame.K_PAGEUP:
                self.display.viewerposition.z -= 0.5
            elif event.key == pygame.K_PAGEDOWN:
                self.display.viewerposition.z += 0.5
            elif event.key == pygame.K_w:
                self.display.viewerposition.y += 0.5
            elif event.key == pygame.K_s:
                self.display.viewerposition.y -= 0.5
            elif event.key == pygame.K_a:
                self.display.viewerposition.x -= 0.5
            elif event.key == pygame.K_d:
                self.display.viewerposition.x += 0.5
            # Rotate particles
            elif event.key == pygame.K_UP:
                self.rotateParticles(self.selectedParticles, (-20, 0, 0))
            elif event.key == pygame.K_DOWN:
                self.rotateParticles(self.selectedParticles, (20, 0, 0))
            elif event.key == pygame.K_LEFT:
                self.rotateParticles(self.selectedParticles, (0, 20, 0))
            elif event.key == pygame.K_RIGHT:
                self.rotateParticles(self.selectedParticles, (0, -20, 0))
            elif event.key == pygame.K_COMMA:
                self.rotateParticles(self.selectedParticles, (0, 0, 20))
            elif event.key == pygame.K_PERIOD:
                self.rotateParticles(self.selectedParticles, (0, 0, -20))
        # Key exit (release) handling
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_LSHIFT or event.key == pygame.K_RSHIFT:
                # Return to normal mode from multiSelectMode
                self.multiSelectMode = False
            elif event.key == pygame.K_LCTRL or event.key == pygame.K_RCTRL:
                # Return to normal mode from rotationMode
                self.rotationMode = False

    def scroll(self):
        """Scroll the surface by resetting gluLookAt."""
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        self.display.setProjection()

    def rotateParticles(self, particles, dAngle):
        """\
        Rotate the particles around their common centre dAngle degree.
        Particles is a list; dAngle is a triple tuple of degree.
        If particles are given an empty list, rotate all particles instead.
        """
        if particles == []:
            particles = self.currentDisplayedPhysics.particles
        centrePoint = Vector()
        for particle in particles:
            posVector = Vector(*particle.pos)
            centrePoint += posVector
        centrePoint /= len(particles)
        if dAngle[0] != 0:  # Rotate around x axis
            for particle in particles:
                posVector = Vector(*particle.pos)
                relativePosVector = posVector - centrePoint
                radius = (relativePosVector.z * relativePosVector.z +
                          relativePosVector.y * relativePosVector.y)**0.5
                newAngle = (
                    math.atan2(relativePosVector.z, relativePosVector.y) +
                    dAngle[0] * math.pi / 180)
                particle.pos = (posVector.x,
                                radius * math.cos(newAngle) + centrePoint.y,
                                radius * math.sin(newAngle) + centrePoint.z)
                particle.drotation += Vector(dAngle[0], 0, 0)
        if dAngle[1] != 0:  # Rotate around y axis
            for particle in particles:
                posVector = Vector(*particle.pos)
                relativePosVector = posVector - centrePoint
                radius = (relativePosVector.z * relativePosVector.z +
                          relativePosVector.x * relativePosVector.x)**0.5
                newAngle = (
                    math.atan2(relativePosVector.z, relativePosVector.x) +
                    dAngle[1] * math.pi / 180)
                particle.pos = (radius * math.cos(newAngle) + centrePoint.x,
                                posVector.y,
                                radius * math.sin(newAngle) + centrePoint.z)
                particle.drotation += Vector(0, -dAngle[1], 0)
        if dAngle[2] != 0:  # Rotate around z axis
            for particle in particles:
                posVector = Vector(*particle.pos)
                relativePosVector = posVector - centrePoint
                radius = (relativePosVector.x * relativePosVector.x +
                          relativePosVector.y * relativePosVector.y)**0.5
                newAngle = (
                    math.atan2(relativePosVector.y, relativePosVector.x) +
                    dAngle[2] * math.pi / 180)
                particle.pos = (radius * math.cos(newAngle) + centrePoint.x,
                                radius * math.sin(newAngle) + centrePoint.y,
                                posVector.z)
                particle.drotation += Vector(0, 0, dAngle[2])
        # An angle keeps the same with when it minus muptiple 360
        particle.drotation %= 360

    def gotoDisplayLevel(self, dlevel):
        """Switch to another display level."""
        isValid = False
        if self.currentLevel + dlevel > self.maxLevel:
            print("Warning: max hierarchy level has reached!")
        elif self.currentLevel + dlevel < 0:
            print("Warning: The first hierarchy level has reached!")
        else:
            if dlevel < 0:  # Go to the last dlevel level
                self.previousParentParticleID = self.currentParentParticleID
                items = self.currentParentParticleID.split(':')
                for _ in xrange(-dlevel):
                    items.pop()
                self.currentParentParticleID = ':'.join(items)
                isValid = True
            if dlevel == 1:  # It only makes sense if dlevel == 1 when go to next dlevel level
                if len(self.selectedParticles) == 1:
                    hasChildParticles = False
                    for particle in self.physics.particles:
                        if particle.ID.find(
                                self.selectedParticles[0].ID
                        ) == 0 and particle.ID != self.selectedParticles[0].ID:
                            hasChildParticles = True
                            break
                    if hasChildParticles:
                        self.previousParentParticleID = self.currentParentParticleID
                        self.currentParentParticleID = self.selectedParticles[
                            0].ID
                        isValid = True
                    else:
                        print(
                            'Warning: The particle you double-clicked has no children!'
                        )
                else:
                    print(
                        "Tips: To extend a node, please double-click the node you want to extend"
                    )
        # Show the specified display level if valid
        if isValid:
            # Save current level's viewer position
            self.levelViewerPos[
                self.currentLevel, self.
                previousParentParticleID] = self.display.viewerposition.copy()
            # Deselect all
            self.deselectAll()
            # Display next level
            self.currentLevel += dlevel
            # Reset viewer position to previous
            try:
                self.display.viewerposition = self.levelViewerPos[
                    self.currentLevel, self.currentParentParticleID].copy()
            except KeyError:
                self.display.viewerposition = self.levelViewerPos[
                    self.currentLevel,
                    self.currentParentParticleID] = Vector()
            # Remove current displayed particles
            for particle in self.currentDisplayedPhysics.particles:
                self.display.ogl_displaylists.pop(id(particle))
                self.display.ogl_transforms.pop(id(particle))
            self.currentDisplayedPhysics.removeByID(
                *self.currentDisplayedPhysics.particleDict.keys())

            # Add current level's particles to self.currentDisplayedPhysics.particles for display
            self.currentDisplayedPhysics.particles = []
            if self.physics.particles != []:
                for particle in self.physics.particles:
                    if self.currentParentParticleID == '':  # If no parent, it's the top level
                        if ':' not in particle.ID:
                            self.currentDisplayedPhysics.add(particle)
                            particle.oldpos = particle.initialpos
                    # The child particles of self.currentParentParticleID
                    elif particle.ID.find(
                            self.currentParentParticleID
                    ) == 0 and particle.ID.count(':') == self.currentLevel:
                        self.currentDisplayedPhysics.add(particle)
                        particle.oldpos = particle.initialpos

    def doCommand(self, msg):
        """\
        Proceses a topology command tuple:
            [ "ADD", "NODE", <id>, <name>, <positionSpec>, <particle type> ] 
            [ "DEL", "NODE", <id> ]
            [ "ADD", "LINK", <id from>, <id to> ]
            [ "DEL", "LINK", <id from>, <id to> ]
            [ "DEL", "ALL" ]
            [ "GET", "ALL" ]
        """
        if len(msg) >= 2:
            cmd = msg[0].upper(), msg[1].upper()

            # Add default arguments when they are not provided
            if cmd == ("ADD", "NODE"):
                if len(msg) == 4:
                    msg += ['randompos', '-']
                elif len(msg) == 5:
                    msg += ['-']

            if cmd == ("ADD", "NODE") and len(msg) == 6:
                if msg[2] in [p.ID for p in self.physics.particles]:
                    print("Node exists, please use a new node ID!")
                else:
                    if (msg[5] in self.particleTypes):
                        ptype = self.particleTypes[msg[5]]
                        ident = msg[2]
                        name = msg[3]

                        posSpec = msg[4]
                        pos = self._generatePos(posSpec)

                        particle = ptype(position=pos, ID=ident, name=name)
                        particle.originaltype = msg[5]

                        self.addParticle(particle)
                        self.isNewNode = True

            elif cmd == ("DEL", "NODE") and len(msg) == 3:
                ident = msg[2]
                self.removeParticle(ident)

            elif cmd == ("ADD", "LINK") and len(msg) == 4:
                src = msg[2]
                dst = msg[3]
                self.makeBond(src, dst)

            elif cmd == ("DEL", "LINK") and len(msg) == 4:
                src = msg[2]
                dst = msg[3]
                self.breakBond(src, dst)

            elif cmd == ("DEL", "ALL") and len(msg) == 2:
                self.removeParticle(*self.physics.particleDict.keys())
                self.currentLevel = 0
                self.currentParentParticleID = ''

            elif cmd == ("GET", "ALL") and len(msg) == 2:
                topology = [("DEL", "ALL")]
                topology.extend(self.getTopology())
                self.send(("TOPOLOGY", topology), "outbox")

            elif cmd == ("UPDATE_NAME", "NODE") and len(msg) == 4:
                node_id = msg[2]
                new_name = msg[3]
                self.updateParticleLabel(node_id, new_name)
                self.send(("UPDATE_NAME", "NODE", node_id, new_name), "outbox")
            elif cmd == ("GET_NAME", "NODE") and len(msg) == 3:
                node_id = msg[2]
                name = self.getParticleLabel(node_id)
                self.send(("GET_NAME", "NODE", node_id, name), "outbox")
            else:
                print("Command Error: please check your command format!")
        else:
            print("Command Error: not enough parameters!")

    def _generatePos(self, posSpec):
        """\
        generateXY(posSpec) -> (x,y,z) or raises ValueError
        
        posSpec == "randompos" or "auto" -> random (x,y,z) within the surface (specified border distance in from the edege)
        posSpec == "(XXX,YYY,ZZZ)" -> specified x,y,z (positive or negative integers)
        spaces are allowed within the tuple, but quotation is needed in this case.
        E.g., " ( 0 , 0 , -10 ) "
        """
        posSpec = posSpec.lower()
        if posSpec == "randompos" or posSpec == "auto":
            zLim = self.display.nearPlaneDist, self.display.farPlaneDist
            z = -1 * random.randrange(
                int((zLim[1] - zLim[0]) / 20) + self.border,
                int((zLim[1] - zLim[0]) / 8) - self.border, 1)
            yLim = z * math.tan(
                self.display.perspectiveAngle * math.pi /
                360.0), -z * math.tan(
                    self.display.perspectiveAngle * math.pi / 360.0)
            xLim = yLim[0] * self.display.aspectRatio, yLim[
                1] * self.display.aspectRatio
            y = random.randrange(
                int(yLim[0]) + self.border,
                int(yLim[1]) - self.border, 1)
            x = random.randrange(
                int(xLim[0]) + self.border,
                int(xLim[1]) - self.border, 1)
            # Apply camera/ viewer transformation
            x += self.display.viewerposition.x
            y += self.display.viewerposition.y
            z += self.display.viewerposition.z
            return x, y, z
        else:  # given specified position
            posSpec = posSpec.strip()
            # Use triple tuple format for position
            match = re.match(
                "^\( *([+-]?\d+) *, *([+-]?\d+) *, *([+-]?\d+) *\)$", posSpec)
            if match:
                x = int(match.group(1))
                y = int(match.group(2))
                z = int(match.group(3))
                return x, y, z

        raise ValueError("Unrecognised position specification")

    def addParticle(self, *particles):
        """Add particles to the system"""
        for p in particles:
            if p.radius > self.biggestRadius:
                self.biggestRadius = p.radius
            pLevel = p.ID.count(':')
            if self.maxLevel < pLevel:
                self.maxLevel = pLevel
            # Make display request for every particle added
            disprequest = {
                "OGL_DISPLAYREQUEST": True,
                "objectid": id(p),
                "callback": (self, "callback"),
                "events": (self, "events"),
                "size": p.size
            }
            # Send display request
            self.send(disprequest, "display_signal")
        self.physics.add(*particles)

        # Add new particles to self.currentDisplayedPhysics
        for particle in particles:
            if self.currentParentParticleID == '':  # If no parent, it's the top level
                if ':' not in particle.ID:
                    self.currentDisplayedPhysics.add(particle)
                    particle.oldpos = particle.initialpos
            # The child particles of self.currentParentParticleID
            elif particle.ID.find(
                    self.currentParentParticleID) == 0 and particle.ID.count(
                        ':') == self.currentLevel:
                self.currentDisplayedPhysics.add(particle)
                particle.oldpos = particle.initialpos

    def removeParticle(self, *ids):
        """\
        Remove particle(s) specified by their ids.

        Also breaks any bonds to/from that particle.
        """
        for ident in ids:
            self.physics.particleDict[ident].breakAllBonds()
            try:
                self.display.ogl_objects.remove(
                    id(self.physics.particleDict[ident]))
                self.display.ogl_names.pop(id(
                    self.physics.particleDict[ident]))
                self.display.ogl_displaylists.pop(
                    id(self.physics.particleDict[ident]))
                self.display.ogl_transforms.pop(
                    id(self.physics.particleDict[ident]))
            except KeyError:
                pass
        self.physics.removeByID(*ids)
        for ident in ids:
            try:
                self.currentDisplayedPhysics.removeByID(ident)
            except KeyError:
                pass

    def selectParticle(self, particle):
        """Select the specified particle."""
        if self.multiSelectMode:
            if particle not in self.selectedParticles:
                particle.select()
                self.selectedParticles.append(particle)
                self.send("('SELECT', 'NODE', '" + particle.name + "')",
                          "outbox")
            else:
                particle.deselect()
                self.selectedParticles.remove(particle)
                self.send("('DESELECT', 'NODE', '" + particle.name + "')",
                          "outbox")
        else:
            self.deselectAll()
            self.selectedParticles = []
            particle.select()
            self.selectedParticles.append(particle)
            self.send("('SELECT', 'NODE', '" + particle.name + "')", "outbox")

    def deselectAll(self):
        """Deselect all particles."""
        for particle in self.selectedParticles:
            particle.deselect()
        self.selectedParticles = []

    def makeBond(self, source, dest):
        """Make a bond from source to destination particle, specified by IDs"""
        self.physics.particleDict[source].makeBond(self.physics.particleDict,
                                                   dest)
        self.physics.particleDict[source].needRedraw = True

    def breakBond(self, source, dest):
        """Break a bond from source to destination particle, specified by IDs"""
        self.physics.particleDict[source].breakBond(self.physics.particleDict,
                                                    dest)
        self.physics.particleDict[source].needRedraw = True

    def updateParticleLabel(self, node_id, new_name):
        """\
        updateParticleLabel(node_id, new_name) -> updates the given nodes name & visual label if it exists
        
        node_id - an id for an already existing node
        new_name - a string (may include spaces) defining the new node name
        """
        for p in self.physics.particles:
            if p.ID == node_id:
                p.set_label(new_name)
                p.needRedraw = True
                return

    def getParticleLabel(self, node_id):
        """\
        getParticleLabel(node_id) -> particle's name
        
        Returns the name/label of the specified particle.
        """
        for p in self.physics.particles:
            if p.ID == node_id:
                return p.name

    def getTopology(self):
        """getTopology() -> list of command tuples that would build the current topology"""
        topology = []

        # first, enumerate the particles
        for particle in self.physics.particles:
            topology.append(("ADD", "NODE", particle.ID, particle.name,
                             "random", particle.originaltype))

        # now enumerate the linkages
        for particle in self.physics.particles:
            for dst in particle.getBondedTo():
                topology.append(("ADD", "LINK", particle.ID, dst.ID))

        return topology
예제 #4
0
class TopologyViewer3D(Axon.Component.component):
    """\
    TopologyViewer3D(...) -> new TopologyViewer3D component.
    
    A component that takes incoming topology (change) data and displays it live
    using pygame OpenGL. A simple physics model assists with visual layout. Particle
    types, appearance and physics interactions can be customised.
    
    Keyword arguments (in order):
    
    - screensize          -- (width,height) of the display area (default = (800,600))
    - fullscreen          -- True to start up in fullscreen mode (default = False)
    - caption             -- Caption for the pygame window (default = "3D Topology Viewer")
    - particleTypes       -- dict("type" -> klass) mapping types of particle to classes used to render them (default = {"-":CuboidParticle3D})
    - initialTopology     -- (nodes,bonds) where bonds=list((src,dst)) starting state for the topology  (default=([],[]))
    - laws                -- Physics laws to apply between particles (default = SimpleLaws(bondlength=2))
    - simCyclesPerRedraw  -- number of physics sim cycles to run between each redraw (default=1)
    - border              -- Minimum distance from edge of display area that new particles appear (default=0)
    """
    
    Inboxes = { "inbox"          : "Topology (change) data describing an Axon system",
                "control"        : "Shutdown signalling",
                "callback"       : "for the response after a displayrequest",
                "events"         : "Place where we recieve events from the outside world",
              }
              
    Outboxes = { "signal"         : "Control signalling",
                 "outbox"         : "Notification and topology output",
                 "display_signal" : "Requests to Pygame Display service",
               }
                                                     
    
    def __init__(self, screensize         = (800,600),
                       fullscreen         = False, 
                       caption            = "3D Topology Viewer", 
                       particleTypes      = None,
                       initialTopology    = None,
                       laws               = None,
                       simCyclesPerRedraw = 1,
                       border             = 0):
        """x.__init__(...) initializes x; see x.__class__.__doc__ for signature"""
        
        super(TopologyViewer3D, self).__init__()
        
        glutInit(sys.argv)
        
        tracker = _cat.coordinatingassistanttracker.getcat()
        try:
            self.display = tracker.retrieveService("ogl_display")[0]
        except KeyError:
            self.display = OpenGLDisplay(width=screensize[0], height=screensize[1],fullscreen=fullscreen,
                                    title=caption)
            self.display.activate()
            OpenGLDisplay.setDisplayService(self.display, tracker)
        self.display = OpenGLDisplay.getDisplayService()[0]                
        self.link((self,"display_signal"), (self.display,"notify"))
        self.link((self.display,"signal"), (self,"control"))
        
        self.border = border
        
        if particleTypes == None:
            self.particleTypes = {"-":CuboidParticle3D, "cuboid":CuboidParticle3D, "sphere":SphereParticle3D,
                                  "teapot":TeapotParticle3D}
        else:
            self.particleTypes = particleTypes
            
        if initialTopology == None:
            initialTopology = ([],[])
        self.initialNodes   = list(initialTopology[0])
        self.initialBonds   = list(initialTopology[1])
        
        self.hitParticles = []
        self.multiSelectMode = False
        self.selectedParticles = []
        self.grabbed = False
        self.rotationMode = False  
        
        if laws==None:
            self.laws = Kamaelia.Support.Particles.SimpleLaws(bondLength=2)
        else:
            self.laws = laws
            
        self.physics = ParticleSystem(self.laws, [], 0)
        self.biggestRadius = 0
        
        # Do interaction
        self.simCyclesPerRedraw = simCyclesPerRedraw
        self.lastIdleTime = time.time()
        
        # Tell if new node is added; if true, new id needs adding to OpenGLDisplay list
        self.isNewNode = False
        
        # For hierarchy structure
        self.maxLevel = 0
        self.currentLevel = 0
        self.previousParentParticleID = self.currentParentParticleID = ''
        self.viewerOldPos = Vector()
        self.levelViewerPos = {}
        # The Physics particle system of current display level for display
        self.currentDisplayedPhysics = ParticleSystem(self.laws, [], 0)
        
        # For double click
        self.lastClickPos = (0,0)
        self.lastClickTime = time.time()
        self.dClickRes = 0.3
    
    
    def initialiseComponent(self):
        """Initialises."""
        self.addListenEvents( [pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION, pygame.KEYDOWN, pygame.KEYUP ])
        # For key holding handling
        pygame.key.set_repeat(100,100)
        
        for node in self.initialNodes:
            self.addParticle(*node)

        for source,dest in self.initialBonds:
            self.makeBond(source, dest)
    
    
    def main(self):
        """Main loop."""
        # Make display request for event listening purpose
        self.size = Vector(0,0,0)
        disprequest = { "OGL_DISPLAYREQUEST" : True,
                             "objectid" : id(self),
                             "callback" : (self,"callback"),
                             "events" : (self, "events"),
                             "size": self.size
                           }
        # send display request
        self.send(disprequest, "display_signal")        
        # Wait for response on displayrequest and get identifier of the viewer
        while not self.dataReady("callback"):  yield 1
        self.identifier = self.recv("callback")
        
        self.initialiseComponent()
        
        while True:
            # Process incoming messages
            if self.dataReady("inbox"):
                message = self.recv("inbox")
                self.doCommand(message)

                # Wait for response on displayrequest and get identifier of the particle
                if self.isNewNode:
                    while not self.dataReady("callback"):  yield 1
                    self.physics.particles[-1].identifier = self.recv("callback")
                    self.isNewNode = False
            else:
                self.lastIdleTime = 0
            
            yield 1        
            
            if self.lastIdleTime + 1.0 < time.time():
                #Freeze selected particles so that they are not subject to the physics law
                for particle in self.selectedParticles:
                    particle.freeze()
                # Do interaction between particles
                self.currentDisplayedPhysics.run(self.simCyclesPerRedraw)
                # Unfreeze selected particles
                for particle in self.selectedParticles:
                    particle.unFreeze()
                
                # Draw particles if new or updated
                for particle in self.currentDisplayedPhysics.particles:
                    if particle.needRedraw:
                        self.drawParticles(particle)
                
                self.handleEvents()
                
                # Perform transformation
                for particle in self.currentDisplayedPhysics.particles:
                    transform_update = particle.applyTransforms()
                    if transform_update is not None:
                        self.send(transform_update, "display_signal")
                
                self.lastIdleTime = time.time()
            else:
                yield 1
            if self.dataReady("control"):
                msg = self.recv("control")
                if isinstance(msg, Axon.Ipc.shutdownMicroprocess):
                    self.quit(msg)
            
            
    def quit(self,msg=Axon.Ipc.shutdownMicroprocess()):
        """Cause termination."""
        print ('Shut down...')
        self.send(msg, "signal")
        self.scheduler.stop()
    
    
    def draw(self):
        """\
        Dummy method reserved for future use
        
        Invoke draw() and save its commands to a newly generated displaylist.
        
        The displaylist name is then sent to the display service via a
        "DISPLAYLIST_UPDATE" request.
        """
        pass
    
    
    def drawParticles(self, *particles):
        """\
            Sends particles drawing opengl command to the display service.
        """
        for particle in particles:
            # Display list id
            displaylist = glGenLists(1)
            # Draw object to its displaylist
            glNewList(displaylist, GL_COMPILE)
            particle.draw()
            glEndList()
    
            # Send displaylist
            dl_update = { "DISPLAYLIST_UPDATE": True,
                          "objectid": id(particle),
                          "displaylist": displaylist
                        }
            self.send(dl_update, "display_signal")
    
    
    def addListenEvents(self, events):
        """\
            Sends listening request for pygame events to the display service.
            The events parameter is expected to be a list of pygame event constants.
        """
        for event in events:
            self.send({"ADDLISTENEVENT":event, "objectid":id(self)}, "display_signal")
    
    
    def removeListenEvents(self, events):
        """\
            Sends stop listening request for pygame events to the display service.
            The events parameter is expected to be a list of pygame event constants.
        """
        for event in events:
            self.send({"REMOVELISTENEVENT":event, "objectid":id(self)}, "display_signal")        
                       
    
    def handleEvents(self):
        """Handle events."""
        while self.dataReady("events"):
            event = self.recv("events")
            if event.type == pygame.MOUSEBUTTONDOWN or event.type == pygame.MOUSEMOTION or event.type == pygame.MOUSEBUTTONUP:
                self.handleMouseEvents(event)
            elif event.type == pygame.KEYDOWN or event.type == pygame.KEYUP:
                self.handleKeyEvents(event)
                
            # Scroll if self.display.viewerposition changes
            if self.display.viewerposition.copy() != self.viewerOldPos:
                self.scroll()
                self.viewerOldPos = self.display.viewerposition.copy()

    
    def handleMouseEvents(self, event):
        """Handle mouse events."""
        if event.type == pygame.MOUSEBUTTONDOWN or pygame.MOUSEMOTION and self.grabbed:
            if not self.rotationMode:
                for particle in self.hitParticles:
                    p1 = Vector(*particle.pos).copy()
                    p1.x += 10
                    p2 = Vector(*particle.pos).copy()
                    p2.y += 10
                    # Get the position of mouse
                    z = Intersect.ray_Plane(Vector(0,0,0), event.direction, [Vector(*particle.pos)-Vector(0,0,self.display.viewerposition.z), p1-Vector(0,0,self.display.viewerposition.z), p2-Vector(0,0,self.display.viewerposition.z)])
                    newpoint = event.direction * z
        if event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:
                # Handle double click
                clickPos = event.pos
                currentTime = time.time()
                elapsedTime = currentTime - self.lastClickTime
                # If it's a double-click
                if clickPos == self.lastClickPos and elapsedTime<self.dClickRes:
                    self.gotoDisplayLevel(1)
                else: # Single click
                    if not self.rotationMode: # Select particle
                        for particle in self.currentDisplayedPhysics.particles:
                            if particle.identifier in event.hitobjects:
                                self.grabbed = True
                                self.hitParticles.append(particle)
                                self.selectParticle(particle)
                        # If click places other than particles in non multiSelectMode, deselect all
                        if not self.hitParticles and not self.multiSelectMode:
                            self.deselectAll()
                self.lastClickPos = clickPos
                self.lastClickTime = currentTime
            elif event.button == 3: # Right-clicked
                self.gotoDisplayLevel(-1)
            elif event.button == 4: # Scrolled-up: zoom out
                if self.selectedParticles:
                    particles = self.selectedParticles
                else:
                    particles = self.currentDisplayedPhysics.particles
                for particle in particles:
                    posVector = Vector(*particle.pos)
                    posVector.z -= 1
                    particle.pos = posVector.toTuple()
            elif event.button == 5: # Scrolled-down: zoom in
                if self.selectedParticles:
                    particles = self.selectedParticles
                else:
                    particles = self.currentDisplayedPhysics.particles
                for particle in particles:
                    posVector = Vector(*particle.pos)
                    posVector.z += 1
                    particle.pos = posVector.toTuple()
        if event.type == pygame.MOUSEBUTTONUP:
            if event.button == 1:  
                for particle in self.hitParticles:
                    self.grabbed = False
                    particle.oldpoint = None
                    self.hitParticles.pop(self.hitParticles.index(particle))
        if event.type == pygame.MOUSEMOTION: 
            if not self.rotationMode and self.grabbed: # Drag particles
                for particle in self.hitParticles:
                    try:
                        if particle.oldpoint is not None:
                            diff = newpoint-particle.oldpoint
                            amount = (diff.x, diff.y)
                            particle.pos = (Vector(*particle.pos)+Vector(*amount)).toTuple()
                    except NameError: pass
                    
                    # Redraw the link so that the link can move with the particle
                    for p in particle.bondedFrom:
                        p.needRedraw = True
            elif self.rotationMode: # Rotate particles
                dAnglex = float(event.rel[1])
                dAngley = -float(event.rel[0])
                self.rotateParticles(self.selectedParticles, (dAnglex,dAngley,0))
        
        try:
            for particle in self.hitParticles:
                particle.oldpoint = newpoint                    
        except NameError: pass    
    
    
    def handleKeyEvents(self, event):
        """Handle keyboard events."""            
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                self.quit()
            elif event.key == pygame.K_BACKSPACE:
                self.gotoDisplayLevel(-1)
            elif event.key == pygame.K_RETURN:
                self.gotoDisplayLevel(1)
            elif event.key == pygame.K_LSHIFT or event.key == pygame.K_RSHIFT:
                self.multiSelectMode = True
            elif event.key == pygame.K_LCTRL or event.key == pygame.K_RCTRL:
                self.rotationMode = True
            # Change viewer position
            elif event.key == pygame.K_PAGEUP:
                self.display.viewerposition.z -= 0.5
            elif event.key == pygame.K_PAGEDOWN:
                self.display.viewerposition.z += 0.5
            elif event.key == pygame.K_w:
                self.display.viewerposition.y += 0.5
            elif event.key == pygame.K_s:
                self.display.viewerposition.y -= 0.5
            elif event.key == pygame.K_a:
                self.display.viewerposition.x -= 0.5
            elif event.key == pygame.K_d:
                self.display.viewerposition.x += 0.5
            # Rotate particles
            elif event.key == pygame.K_UP:
                self.rotateParticles(self.selectedParticles, (-20,0,0))      
            elif event.key == pygame.K_DOWN:
                self.rotateParticles(self.selectedParticles, (20,0,0))      
            elif event.key == pygame.K_LEFT:
                self.rotateParticles(self.selectedParticles, (0,20,0))    
            elif event.key == pygame.K_RIGHT:
                self.rotateParticles(self.selectedParticles, (0,-20,0))
            elif event.key == pygame.K_COMMA:
                self.rotateParticles(self.selectedParticles, (0,0,20))    
            elif event.key == pygame.K_PERIOD:
                self.rotateParticles(self.selectedParticles, (0,0,-20))
        # Key exit (release) handling
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_LSHIFT or event.key == pygame.K_RSHIFT:
                # Return to normal mode from multiSelectMode
                self.multiSelectMode = False
            elif event.key == pygame.K_LCTRL or event.key == pygame.K_RCTRL:
                # Return to normal mode from rotationMode
                self.rotationMode = False                 
    
    
    def scroll( self ):
        """Scroll the surface by resetting gluLookAt."""
        glMatrixMode(GL_PROJECTION)                 
        glLoadIdentity()
        self.display.setProjection()
        
    
    def rotateParticles( self, particles, dAngle ):
        """\
        Rotate the particles around their common centre dAngle degree.
        Particles is a list; dAngle is a triple tuple of degree.
        If particles are given an empty list, rotate all particles instead.
        """
        if particles == []:
            particles = self.currentDisplayedPhysics.particles
        centrePoint = Vector() 
        for particle in particles:
            posVector = Vector(*particle.pos)
            centrePoint += posVector
        centrePoint /= len(particles)
        if dAngle[0] != 0: # Rotate around x axis
            for particle in particles:
                posVector = Vector(*particle.pos)
                relativePosVector = posVector - centrePoint
                radius = (relativePosVector.z*relativePosVector.z+relativePosVector.y*relativePosVector.y)**0.5
                newAngle = (math.atan2(relativePosVector.z,relativePosVector.y)+dAngle[0]*math.pi/180)
                particle.pos = (posVector.x, radius*math.cos(newAngle)+centrePoint.y, radius*math.sin(newAngle)+centrePoint.z)
                particle.drotation += Vector(dAngle[0],0,0)
        if dAngle[1] != 0: # Rotate around y axis
            for particle in particles:
                    posVector = Vector(*particle.pos)
                    relativePosVector = posVector - centrePoint
                    radius = (relativePosVector.z*relativePosVector.z+relativePosVector.x*relativePosVector.x)**0.5
                    newAngle = (math.atan2(relativePosVector.z,relativePosVector.x)+dAngle[1]*math.pi/180)
                    particle.pos = (radius*math.cos(newAngle)+centrePoint.x, posVector.y, radius*math.sin(newAngle)+centrePoint.z)
                    particle.drotation += Vector(0,-dAngle[1],0)
        if dAngle[2] != 0: # Rotate around z axis
            for particle in particles:
                posVector = Vector(*particle.pos)
                relativePosVector = posVector - centrePoint
                radius = (relativePosVector.x*relativePosVector.x+relativePosVector.y*relativePosVector.y)**0.5
                newAngle = (math.atan2(relativePosVector.y,relativePosVector.x)+dAngle[2]*math.pi/180)
                particle.pos = (radius*math.cos(newAngle)+centrePoint.x, radius*math.sin(newAngle)+centrePoint.y, posVector.z)
                particle.drotation += Vector(0,0,dAngle[2])
        # An angle keeps the same with when it minus muptiple 360 
        particle.drotation %= 360
        
    
    def gotoDisplayLevel( self, dlevel):
        """Switch to another display level."""
        isValid = False
        if self.currentLevel + dlevel > self.maxLevel:
            print ("Warning: max hierarchy level has reached!")
        elif self.currentLevel + dlevel < 0:
            print ("Warning: The first hierarchy level has reached!")
        else:
            if dlevel < 0: # Go to the last dlevel level
                self.previousParentParticleID = self.currentParentParticleID
                items = self.currentParentParticleID.split(':')
                for _ in xrange(-dlevel):
                    items.pop()
                self.currentParentParticleID = ':'.join(items)
                isValid = True
            if dlevel == 1: # It only makes sense if dlevel == 1 when go to next dlevel level
                if len(self.selectedParticles) == 1:
                    hasChildParticles = False
                    for particle in self.physics.particles:
                        if particle.ID.find(self.selectedParticles[0].ID) == 0 and particle.ID != self.selectedParticles[0].ID:
                            hasChildParticles = True
                            break
                    if hasChildParticles:
                        self.previousParentParticleID = self.currentParentParticleID
                        self.currentParentParticleID = self.selectedParticles[0].ID
                        isValid = True
                    else:
                        print ('Warning: The particle you double-clicked has no children!')
                else:
                    print ("Tips: To extend a node, please double-click the node you want to extend")
        # Show the specified display level if valid
        if isValid:                    
            # Save current level's viewer position
            self.levelViewerPos[self.currentLevel, self.previousParentParticleID] = self.display.viewerposition.copy()
            # Deselect all
            self.deselectAll()
            # Display next level
            self.currentLevel += dlevel
            # Reset viewer position to previous
            try:
                self.display.viewerposition = self.levelViewerPos[self.currentLevel, self.currentParentParticleID].copy()
            except KeyError:
                self.display.viewerposition = self.levelViewerPos[self.currentLevel, self.currentParentParticleID] = Vector()
            # Remove current displayed particles
            for particle in self.currentDisplayedPhysics.particles:
                self.display.ogl_displaylists.pop(id(particle))
                self.display.ogl_transforms.pop(id(particle))
            self.currentDisplayedPhysics.removeByID(*self.currentDisplayedPhysics.particleDict.keys())
            
            # Add current level's particles to self.currentDisplayedPhysics.particles for display
            self.currentDisplayedPhysics.particles = []
            if self.physics.particles != []:
                for particle in self.physics.particles:
                    if self.currentParentParticleID == '': # If no parent, it's the top level 
                        if ':' not in particle.ID:
                            self.currentDisplayedPhysics.add( particle )
                            particle.oldpos = particle.initialpos
                    # The child particles of self.currentParentParticleID
                    elif particle.ID.find(self.currentParentParticleID) == 0 and particle.ID.count(':') == self.currentLevel:
                        self.currentDisplayedPhysics.add( particle )
                        particle.oldpos = particle.initialpos
                            
    
    def doCommand(self, msg):
        """\
        Proceses a topology command tuple:
            [ "ADD", "NODE", <id>, <name>, <positionSpec>, <particle type> ] 
            [ "DEL", "NODE", <id> ]
            [ "ADD", "LINK", <id from>, <id to> ]
            [ "DEL", "LINK", <id from>, <id to> ]
            [ "DEL", "ALL" ]
            [ "GET", "ALL" ]
        """  
        if len(msg) >= 2:
            cmd = msg[0].upper(), msg[1].upper()
            
            # Add default arguments when they are not provided
            if cmd == ("ADD", "NODE"):
                if len(msg) == 4:
                    msg += ['randompos', '-']
                elif len(msg) == 5:
                    msg += ['-']

            if cmd == ("ADD", "NODE") and len(msg) == 6:
                if msg[2] in [p.ID for p in self.physics.particles]:
                    print ("Node exists, please use a new node ID!")
                else:
                    if ( msg[5] in self.particleTypes ):
                        ptype = self.particleTypes[msg[5]]
                        ident    = msg[2]
                        name  = msg[3]
                        
                        posSpec = msg[4]
                        pos     = self._generatePos(posSpec)
    
                        particle = ptype(position = pos, ID=ident, name=name)
                        particle.originaltype = msg[5]
                      
                        self.addParticle(particle)
                        self.isNewNode = True

            elif cmd == ("DEL", "NODE") and len(msg) == 3:
                    ident = msg[2]
                    self.removeParticle(ident)        
            
            elif cmd == ("ADD", "LINK") and len(msg) == 4:
                src = msg[2]
                dst = msg[3]
                self.makeBond(src, dst)
                
            elif cmd == ("DEL", "LINK") and len(msg) == 4:
                src = msg[2]
                dst = msg[3]
                self.breakBond(src, dst)
                
            elif cmd == ("DEL", "ALL") and len(msg) == 2:
                self.removeParticle(*self.physics.particleDict.keys())
                self.currentLevel = 0
                self.currentParentParticleID = ''
                
            elif cmd == ("GET", "ALL") and len(msg) == 2:
                topology = [("DEL","ALL")]
                topology.extend(self.getTopology())
                self.send( ("TOPOLOGY", topology), "outbox" )
            
            elif cmd == ("UPDATE_NAME", "NODE") and len(msg) == 4:
                node_id = msg[2]
                new_name = msg[3]
                self.updateParticleLabel(node_id, new_name)
                self.send( ("UPDATE_NAME", "NODE", node_id, new_name), "outbox" )
            elif cmd == ("GET_NAME", "NODE") and len(msg) == 3:
                node_id = msg[2]
                name = self.getParticleLabel(node_id)
                self.send( ("GET_NAME", "NODE", node_id, name), "outbox" )        
            else:
                print ("Command Error: please check your command format!")
        else:
            print ("Command Error: not enough parameters!")
  
    
    def _generatePos(self, posSpec):
        """\
        generateXY(posSpec) -> (x,y,z) or raises ValueError
        
        posSpec == "randompos" or "auto" -> random (x,y,z) within the surface (specified border distance in from the edege)
        posSpec == "(XXX,YYY,ZZZ)" -> specified x,y,z (positive or negative integers)
        spaces are allowed within the tuple, but quotation is needed in this case.
        E.g., " ( 0 , 0 , -10 ) "
        """
        posSpec = posSpec.lower()
        if posSpec == "randompos" or posSpec == "auto" :
            zLim = self.display.nearPlaneDist, self.display.farPlaneDist                        
            z = -1*random.randrange(int((zLim[1]-zLim[0])/20)+self.border,int((zLim[1]-zLim[0])/8)-self.border,1)
            yLim = z*math.tan(self.display.perspectiveAngle*math.pi/360.0), -z*math.tan(self.display.perspectiveAngle*math.pi/360.0)            
            xLim = yLim[0]*self.display.aspectRatio, yLim[1]*self.display.aspectRatio
            y = random.randrange(int(yLim[0])+self.border,int(yLim[1])-self.border,1)
            x = random.randrange(int(xLim[0])+self.border,int(xLim[1])-self.border,1)
            # Apply camera/ viewer transformation
            x += self.display.viewerposition.x
            y += self.display.viewerposition.y
            z += self.display.viewerposition.z
            return x,y,z            
        else: # given specified position
            posSpec = posSpec.strip()
            # Use triple tuple format for position
            match = re.match("^\( *([+-]?\d+) *, *([+-]?\d+) *, *([+-]?\d+) *\)$", posSpec)
            if match:
                x = int(match.group(1))
                y = int(match.group(2))
                z = int(match.group(3))
                return x,y,z            
        
        raise ValueError("Unrecognised position specification")

    
    def addParticle(self, *particles):
        """Add particles to the system"""
        for p in particles:
            if p.radius > self.biggestRadius:
                self.biggestRadius = p.radius
            pLevel = p.ID.count(':')
            if self.maxLevel < pLevel:
                self.maxLevel = pLevel
            # Make display request for every particle added
            disprequest = { "OGL_DISPLAYREQUEST" : True,
                                 "objectid" : id(p),
                                 "callback" : (self,"callback"),
                                 "events" : (self, "events"),
                                 "size": p.size
                               }
            # Send display request
            self.send(disprequest, "display_signal")
        self.physics.add( *particles )
        
        # Add new particles to self.currentDisplayedPhysics
        for particle in particles:
            if self.currentParentParticleID == '': # If no parent, it's the top level 
                if ':' not in particle.ID:
                    self.currentDisplayedPhysics.add( particle )
                    particle.oldpos = particle.initialpos
            # The child particles of self.currentParentParticleID
            elif particle.ID.find(self.currentParentParticleID) == 0 and particle.ID.count(':') == self.currentLevel:
                self.currentDisplayedPhysics.add( particle )
                particle.oldpos = particle.initialpos
    
        
    def removeParticle(self, *ids):
        """\
        Remove particle(s) specified by their ids.

        Also breaks any bonds to/from that particle.
        """
        for ident in ids:
            self.physics.particleDict[ident].breakAllBonds()
            try:
                self.display.ogl_objects.remove(id(self.physics.particleDict[ident]))
                self.display.ogl_names.pop(id(self.physics.particleDict[ident]))
                self.display.ogl_displaylists.pop(id(self.physics.particleDict[ident]))
                self.display.ogl_transforms.pop(id(self.physics.particleDict[ident]))
            except KeyError: pass
        self.physics.removeByID(*ids)
        for ident in ids:
            try:
                self.currentDisplayedPhysics.removeByID(ident)
            except KeyError: pass
    
        
    def selectParticle(self, particle):
        """Select the specified particle."""
        if self.multiSelectMode:
            if particle not in self.selectedParticles:
                particle.select()
                self.selectedParticles.append(particle)
                self.send( "('SELECT', 'NODE', '"+particle.name+"')", "outbox" )
            else:
                particle.deselect()
                self.selectedParticles.remove(particle)
                self.send( "('DESELECT', 'NODE', '"+particle.name+"')", "outbox" )
        else:
            self.deselectAll()
            self.selectedParticles = []
            particle.select()
            self.selectedParticles.append(particle)
            self.send( "('SELECT', 'NODE', '"+particle.name+"')", "outbox" )


    def deselectAll(self):
        """Deselect all particles."""
        for particle in self.selectedParticles:
            particle.deselect()
        self.selectedParticles = []
    
    
    def makeBond(self, source, dest):
        """Make a bond from source to destination particle, specified by IDs"""
        self.physics.particleDict[source].makeBond(self.physics.particleDict, dest)
        self.physics.particleDict[source].needRedraw = True


    def breakBond(self, source, dest):
        """Break a bond from source to destination particle, specified by IDs"""
        self.physics.particleDict[source].breakBond(self.physics.particleDict, dest)
        self.physics.particleDict[source].needRedraw = True
    
        
    def updateParticleLabel(self, node_id, new_name):
        """\
        updateParticleLabel(node_id, new_name) -> updates the given nodes name & visual label if it exists
        
        node_id - an id for an already existing node
        new_name - a string (may include spaces) defining the new node name
        """
        for p in self.physics.particles:
            if p.ID == node_id:
                p.set_label(new_name)
                p.needRedraw = True
                return


    def getParticleLabel(self, node_id):
        """\
        getParticleLabel(node_id) -> particle's name
        
        Returns the name/label of the specified particle.
        """
        for p in self.physics.particles:
            if p.ID == node_id:
                return p.name
    
    
    def getTopology(self):
        """getTopology() -> list of command tuples that would build the current topology"""
        topology = []
        
        # first, enumerate the particles
        for particle in self.physics.particles:
            topology.append( ( "ADD","NODE",
                               particle.ID,
                               particle.name,
                               "random",
                               particle.originaltype
                           ) )
                           
        # now enumerate the linkages
        for particle in self.physics.particles:
            for dst in particle.getBondedTo():
                topology.append( ( "ADD","LINK", particle.ID, dst.ID ) )
            
        return topology