fileadelphia / src / fileadelphia / city.py

import math
import os
import pygame
from OpenGL.GL import *
from OpenGL.GLU import *
from stat import S_ISDIR, S_ISREG
from pygame.locals import *
import logging
import sys

from const import VIEW_WIDTH, VIEW_HEIGHT
from states import State, DirectoryInfoState, PermissionDeniedState, FileInfoState
from building import Building
from text import Text
from states import TransitionState

#TODO: increase this
MAX_BUILDINGS_PER_NODE = 20

logger = logging.getLogger('quadtree')
#logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG)

city_texture_inited = False

class QuadTreeNode(object):
    """
    Children are always
    a 4 tuple
        |     |
    -z  |  3  |  2
     ^  |     |
     |  |-------------
     |  |     |
        |  0  |  1
        |     |
        --------------
     (0, 0)   ---> x
    """
    def __init__(self, x, z):
        self.x, self.z = x, z
        self.leaf = True
        self.buildings = []
        self.quads = (None, None, None, None)
        #logger.debug('created quad node (%d, %d)', x, z)
        

    def __str__(self):
        s = "QuadTreeNode(%d, %d, "%(self.x, self.z)
        if self.leaf:
            s += "Leaf, %s"%self.buildings
        else:
            s += "NOT LEAF"
        s += ")"
        return s

    def intersects(self, pos, shrink):
        """
        returns the building that intersects
        with (x, y, z)
        shrink is passed on to the building intersects
        method.
        """
        logger.debug('%s: requesting intersection test with %s', self, pos)
        if self.leaf:
            for building in self.buildings:
                logger.debug('leaft checking %s', building)
                if building.intersects(pos, shrink):
                    logger.debug('MATCH')
                    return building
            logger.debug('leaf NONE')
            return None
        else:
        	quad = self.find_quad(pos[0], pos[2])
        	return quad.intersects(pos, shrink)

    def add_building(self, building):
        #TODO: split on building > MAX_BUILDINGS_PER_NODE
        if self.leaf:
            self.buildings.append(building)
            if len(self.buildings) > MAX_BUILDINGS_PER_NODE:
                ##logger.debug('splitting %s', self)
                self.split()
        else:
            quad = self.find_quad(building.nearest[0], building.nearest[2])
            assert quad != self
            ##logger.debug('add_building: adding to %s', quad)
            quad.add_building(building)

    def split(self):
        #print self.x, self.z
        self.quads = (
            QuadTreeNode(self.x/2, self.z/2),
            QuadTreeNode(self.x+self.x/2, self.z/2),
            QuadTreeNode(self.x+self.x/2, self.z+self.z/2),
            QuadTreeNode(self.x/2, self.z+self.z/2)
        )
        self.leaf = False

        for building in self.buildings:
            quad = self.find_quad(building.nearest[0], building.nearest[2])
            #logger.debug('split: found quad %s', quad)
            quad.add_building(building)

        self.buildings = []

    def find_immediate_quad(self, x, z):
        ##logger.debug('find_immediate_quad: requested quad for point (%d, %d)', x, z)
        if self.leaf:
            return self

        if x < self.x:
            if z > self.z:
                return self.quads[0]
            else:
                return self.quads[3]
        else:
            if z > self.z:
                return self.quads[1]
            else:
                return self.quads[2]

    def find_quad(self, x, z):
        ##logger.debug('find_quad: requested quad for point (%d, %d)', x, z)
        if self.leaf:
            return self
        return self.find_immediate_quad(x, z).find_quad(x, z)

    def buildings_in_region(self, pos, radius):
        """
        tries its best but may not always
        be very precise.

        It relies on certain assumptions.
        First since the buildings are never too close
        as long as we stick to a decent radius
        there are never too many buildings to render.
        """
        logger.debug('%s: buildings in region %s, %d', self, pos, radius)
        x, z = pos
        if self.leaf:
            if self.distance(self.x, self.z, x, z) < radius**2:
                logger.debug('we are a leaf, take all of them')
                return self.buildings
            else:
                logger.debug('we are a leaf in a galaxy far far away, take none')
                return []
        else:
# go over all child quads, if they stick to the radius requirement
# and get their buildings
# otherwise invoke on their children
            buildings = []
            for quad in self.quads:
            	dist = self.distance(quad.x, quad.z, x, z)
            	#logger.debug('distance of %s from (%d, %d) is %d. radius is %d', quad, x, z, dist, radius**2)
                if dist < radius**2:
                	buildings.extend(quad.get_buildings())
                else:
                	buildings.extend(quad.buildings_in_region(pos, radius))
            return buildings

    def distance(self, x1, z1, x2, z2):
        return (x2-x1)**2 + (z2-z1)**2

    def get_buildings(self):
        if self.leaf:
        	return self.buildings
        else:
            buildings = []
            for quad in self.quads:
            	buildings.extend(quad.get_buildings())
            return buildings

class City(State):
    def __init__(self, directory):
        global city_texture_inited, textures
        State.__init__(self)
        pygame.display.set_caption("Fileadelphia - %s"%directory)
        self.directory = directory
        self.buildings = []

        if not city_texture_inited:
            city_texture_inited = True
            InitTextGL()
        # layout will update this
        # since it is used every time in display
        # and its best to calculate it only once
        self.buildings_length = 0
        self.texture = textures[0]

        files = os.listdir(self.directory)
        dim = math.sqrt(len(files)*50)
        #dim = 20
        self.layout(files, dim)
        self.intersecting = False
        self.render_center = (0, 0)
        self.render_radius = 50

        self.camera_y = 10
        self.camera_y_min = 1.2

    def layout(self, listing, x_spread):
        x_pos = 4
        z_pos = 0
        depth = 0
        max_x_pos = 0
        for entry in listing:
            path = os.path.join(self.directory, entry)
            try:
                b = Building((x_pos, 0, z_pos), path)
                self.buildings.append(b)
                x_pos += b.length + 4
                depth = max(depth, b.depth)
                if x_pos > x_spread:
                    max_x_pos = max(max_x_pos, x_pos)
                    x_pos = 4
                    z_pos -= depth + 4
            except OSError:
                print 'not showing %s due to OSError'%path

        self.buildings_length = len(self.buildings)
        # z_pos and max_x_pos now have our bounds, time
        # to make a BSP and add the buildings
        print 'x extends to %d, z extends to %d'%(max_x_pos, z_pos)
        self.bsp = QuadTreeNode(max_x_pos/2, z_pos/2)
        for building in self.buildings:
            self.bsp.add_building(building)

        self.hud = Text(""" %s (%s) """%(self.directory,
                                         self.buildings_length and '%d files'%self.buildings_length or 'empty'),
                    15, color=(255, 0, 0))

    def display(self):
        glClearColor(1, 1, 1, 1)

	self.DrawGround()
        bs = self.bsp.buildings_in_region(self.render_center, self.render_radius)
        # if we are seeing too few show more
        # unless we don't have more.
        while self.buildings_length >= 10 and len(bs) < 10:
            bs = self.bsp.buildings_in_region(self.render_center, self.render_radius)
            self.render_radius += 10

        for building in bs:
            building.display()

        glDisable(GL_DEPTH_TEST)
        glMatrixMode(GL_PROJECTION)
        glPushMatrix()
        glLoadIdentity()
        gluOrtho2D(0, VIEW_WIDTH, VIEW_HEIGHT, 0)
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()

        self.hud.render(10, VIEW_HEIGHT-20)

        glEnable(GL_DEPTH_TEST)
        glMatrixMode(GL_PROJECTION)
        glPopMatrix()
        glMatrixMode(GL_MODELVIEW)

        glFlush()

    def update(self, world, delta):
        #print '---------------------------------------------------------------------'
        if self.camera_y > self.camera_y_min:
            self.camera_y -= 0.1
            if self.camera_y < self.camera_y_min:
                self.camera_y = self.camera_y_min
            world.camera.pos = (world.camera.pos[0], self.camera_y, world.camera.pos[2]-0.02)
        self.render_center = (world.camera.pos[0], world.camera.pos[2])
        intersected = False
        #print world.camera.pos, building.nearest, building.farthest
        building = self.bsp.intersects(world.camera.pos, shrink=0.2)
        if building:
            if self.intersecting:
                return
            else:
                self.intersecting = True
            if S_ISDIR(building.info.st_mode) and os.access(building.path, os.R_OK):
#TODO replace with transition from directory info to city
                if sys.platform == 'win32':
                    try:
                        world.push_state(TransitionState(DirectoryInfoState(building.path), City(building.path)))
                    except WindowsError:
                        world.push_state(PermissionDeniedState(building.path))
                else:
                    world.push_state(TransitionState(DirectoryInfoState(building.path), City(building.path)))
                return
            elif S_ISDIR(building.info.st_mode):
                world.push_state(PermissionDeniedState(building.path))
                return
            elif S_ISREG (building.info.st_mode):
                world.push_state(TransitionState(self, FileInfoState(building.path)))
                return
        if not intersected:
            self.intersecting = False

    def handle_keypress(self, event, x, y, world):
        if event.key == K_BACKSPACE:
            world.pop_state()
            return True
        elif event.key == K_UP and pygame.key.get_mods() & KMOD_ALT:
            parent = os.path.dirname(self.directory)
            # in linux, for '/', parent and path are the same
            if parent == self.directory or not parent:
                pass
            else:
                # see if the directory just below
                # on the stack is the parent
                # otherwise switch to the parent.
                if len(world.state_stack) > 1:
                    try:
                        p = world.state_stack[-2].directory
                        if p == parent:
                            world.pop_state()
                        else:
                            world.push_state(City(parent))
                    except AttributeError:
                        world.push_state(City(parent))
                else:
                    world.push_state(City(parent))
            return True

        return False

    def DrawGround(self):
        glBindTexture(GL_TEXTURE_2D, self.texture)
        glPushMatrix()
        glBegin(GL_QUADS)                # Start Drawing The Ground

        # Bottom Face
        glTexCoord2f(65.0,65.0); glVertex3f(-100, 0, -100)    # Top Right Of The Texture and Quad
        glTexCoord2f(0.0, 65.0); glVertex3f(100, 0, -100)    # Top Left Of The Texture and Quad
        glTexCoord2f(0.0, 0.0); glVertex3f(100, 0, 100)    # Bottom Left Of The Texture and Quad
        glTexCoord2f(65.0, 0.0); glVertex3f(-100, 0, 100)    # Bottom Right Of The Texture and Quad

        glEnd()
        glPopMatrix()
        glBindTexture(GL_TEXTURE_2D, 0)

def InitTextGL():
    LoadTextures(2)
    glEnable(GL_TEXTURE_2D)
    glClearColor(0.0, 0.0, 0.0, 0.0)
    glClearDepth(1.0)
    glDepthFunc(GL_LESS)
    glEnable(GL_DEPTH_TEST)
    glShadeModel(GL_SMOOTH)

def CreateTexture(imagename, number):
    global textures

    image = pygame.image.load(imagename)
    iw = image.get_width()
    ih = image.get_height()
    image_surface = pygame.image.tostring(image, "RGBA", 1)

    glBindTexture(GL_TEXTURE_2D, int(textures[number]))

    glPixelStorei(GL_UNPACK_ALIGNMENT,1)
    glTexImage2D(GL_TEXTURE_2D, 0, 3, iw, ih, 0, GL_RGBA, GL_UNSIGNED_BYTE, image_surface)
    #glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP)
    #glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
    #glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)

def LoadTextures(number):
    global textures
    textures = glGenTextures(number)
    print textures
    CreateTexture("texture_3.jpg", 0)
    CreateTexture("texture_2.png", 1)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.