Need help with some simple 2d-geometry classes

I have created a bunch of simple 2d-geometry classes (point, line, polygon) and some functions for these, and while my test code does execute there are a couple things that are not working.

Questions:
Why isn’t the rotate functions executed on the Line2d objects and the Point2d objects?
Why does my polygon end up empty?
Are my repr and str functions appropiate? (I know they aren’t really needed - but I thought it was a good idea to get into a habit of creating these functions for any future custom classes I make)
What would be a appropiate repr and str functions for the Polygon2d class?

(code runs)

## CLASSES

# 2D Point
class Point2d:
    
    def __init__(self, u, v):
        """ Create a Point2d object using UV coords
        Example: p = Point2d(1,-2) """
        
        self.u = u
        self.v = v
        
    def __repr__(self):
        return "Point2d(%s, %s)" % (self.u, self.v)
        
    def __str__(self):
        return "%s, %s" % (self.u, self.v)
        
    def rotate(self, other, angle):
        """ Rotates a Point2d object around another Point2d object.
        Example: self.rotate(other, 45)
        
        Dependencies: python.math """
        
        print("ran rotate on point")
        
        # Short notation
        oX = other.x
        oY = other.y
        pX = self.x
        pY = self.y
        
        # Calculate
        angle = angle * math.pi / 180.0 # Radians
        self.x = math.cos(angle) * (pX-oX) - math.sin(angle) * (pY-oY) + oX
        self.y = math.sin(angle) * (pX-oX) + math.cos(angle) * (pY-oY) + oY

        
# 2D Line
class Line2d:

    def __init__(self, pointA, pointB):
        """ Create a Line2d object using two Point2d objects
        Example: l = Line2d(pointA, pointB) """
        
        self.pointA = pointA
        self.pointB = pointB
        
    def __repr__(self):
        return "Line2d(Point2d(%s, %s), Point2d(%s, %s))" % (self.pointA.u, self.pointA.u, self.pointB.u, self.pointB.v)
        
    def __str__(self):
        return "(%s, %s), (%s, s%)" % (self.pointA.u, self.pointA.v, self.pointB.u, self.pointB.v)
        
    # Get angle using arctan
    def getAngle(self):
        """ Gets the arctan angle between the Line2d's two Point2d objects 
        Example: angle = self.getAngle()
        
        Dependencies: python.math 
        Returns: float """
        
        # Short notation
        pA = self.Point2d
        pB = self.Point2d
        
        # Get distances
        if pA.u >= pB.u:
            distX = (pA.u - pB.u)
            distY = (pA.v - pB.v)
        else:
            distX = (pB.u - pA.u)
            distY = (pB.v - pA.v)
        
        # Calculate arctan angle and return it       
        return math.degrees(math.atan2(distY, distX))        
        
    # Rotate the line
    def rotate(self, other, angle):
        """ Rotates a Line2d object around a Point2d object. 
        Example: self.rotate(other, 45)
        
        Dependencies: python.math """

        print("ran rotate on line")
        
        self.pointA.rotate(other, angle)
        self.pointB.rotate(other, angle)
      
        
# 2D polygon
class Polygon2d:
    
    def __init__(self, lineList):
        """ Create a Polygon2D object using a list of Line2d objects
        Example: l = Polygon2D(list) """
        
        self.lineList = lineList
        self.pos = len(lineList)
        
    def __repr__(self): ## WARN: PLACEHOLDER - INCORRECT - DO NOT USE
        return lolk 
        
    def __str__(self): ## WARN: PLACEHOLDER - INCORRECT - DO NOT USE
        return "Polygon2d(%s, %s)" % (self.lineList)
        
    def __iter__(self):
        return self
        
    def next(self):
        if self.pos <= 0:
            raise StopIteration
        self.pos = self.pos - 1
        return self.lineList[self.pos]
    
    # Rotate the hull
    def rotate(self, other, angle):
        """ Rotates a Polygon2d object around a Point2d object 
        Example: self.rotate(other, 45)
        
        Dependencies: python.math """

        print("ran rotate on polygon")
        
        for Line2d in self:
            print Line2d
            Line2d.rotate(other, angle)
    
    # Calculate the area of the bounding box
    def getBoundsArea(self):
        """ Gets the MAR (minimum-area rectangle) of the polygon 
        Example: area = self.getBoundsArea() 
        
        Returns: float """
    
        # Get max/min bounds
        xMax, xMin, yMax, yMin = (0.0,)*4
        for Line2d in self:
            for Point2d in Line2d:
                if Point2d.u > xMax:
                    xMax = Point2d.u
                if Point2d.u < xMin:
                    xMin = Point2d.u
                if Point2d.v > yMax:
                    yMax = Point2d.v
                if Point2d.v < yMin:
                    yMin = Point2d.v
        
        # Calculate and return area
        return (abs(xMax-xMin) * abs(yMax-yMin))
        
        
    # Get the coords of all Point2d objects in the polygon
    def getCoords(self):
        """ Gets all the coordinates for the Point2d objects in the polygon
        Example: coordList = self.getCoords()
        
        Returns: tuple list of floats """
    
        # Loop through all Point2d objects and get the coords, then append to list
        coordList = []
        for Line2d in self:
            for Point2d in Line2d:
                coordList.append((Point2d.u, Point2d.v))

        # Remove duplicates and return list
        return list(set(coordList))
        
## TEST

if __name__ == "__main__":
    
    # Create points
    p1 = Point2d(0.0, 0.0)
    p2 = Point2d(1.0, 0.0)
    p3 = Point2d(1.0, 1.0)
    p4 = Point2d(0.0, 1.0)
    
    # Create lines
    l1 = (p1, p2)
    l2 = (p2, p3)
    l3 = (p3, p4)
    l4 = (p4, p1)
    
    # Create polygon
    poly = Polygon2d([l1, l2, l3, l4])
    
    # Write polygon coords
    print("coords before are %s") % poly.getCoords()
    
    # Rotate polygon
    origin = Point2d(0.0, 0.0)
    poly.rotate(origin, 90)
    
    # Write polygon coords
    print("coords after are %s") % poly.getCoords()

So you’ve got a whole bunch of things that are busted.

Your iter / next method on Polygon2d isn’t actually yielding values properly.
So when you try to get all the lines in your poly it doesn’t return anything useful.

Once you fix that, you’ll realize you never actually instantiated any Line2d classes, so your rotate operation will fail on the tuples.

Your Line2d class has an error in its str method, so that will fail when you try to print from there.
Also you attempt to iterate through the points in your Line2d objects, but Line2d is not an iterator. So you’d need to add an iter method.

Fixing that will point out that you’re Point2d class doesn’t work either.
For some reason you’ve declared the point coordinates as (u, v) in your init method, but (x, y) in your rotate method.

Fixing that will cause another error, if you’ve not actually imported math, but I’m assuming you’ve got that in another part of your code already?

If this is for python2 (which it appears so, given the print statements), always inherit your classes from object.

Also some other minor things, in your Polygon2d.rotate method, you reuse Line2d as your for loop variable, that is shadowing the class name from above, and is bad practice.

Updated version.

in your polygon loop, you’re redefining Line2d, why not just “for line in self:” ?

[QUOTE=R.White;27914]Updated version.[/QUOTE]

Awesome! Thank you so much!
Your help has been truly invaluable!

I noticed a bug with the code: Since the rotation are being done on all lines (4x in this case) and every line contains 2x points, this means that all points in the polygon gets rotated twice as much as they should.
So I corrected the Polygon2d.rotate() function and added a new function for retrieving the positions of the lines (just to make sure that both the coords as well as the lines rotate correctly and dont get scrambled).

CODE:

import math
 
## CLASSES
 
# 2D Point
class Point2d(object):
    
    def __init__(self, u, v):
        """ Create a Point2d object using UV coords
        Example: p = Point2d(1,-2) """
        
        self.u = u
        self.v = v
        
    def __repr__(self):
        return "Point2d(%s, %s)" % (self.u, self.v)
        
    def __str__(self):
        return "%s, %s" % (self.u, self.v)
        
    def rotate(self, other, angle):
        """ Rotates a Point2d object around another Point2d object.
        Example: self.rotate(other, 45)
        
        Dependencies: python.math """

        # Short notation
        oU = other.u
        oV = other.v
        pU = self.u
        pV = self.v
        
        # Calculate
        angle = angle * math.pi / 180.0 # Radians
        self.u = math.cos(angle) * (pU-oU) - math.sin(angle) * (pV-oV) + oU
        self.v = math.sin(angle) * (pU-oU) + math.cos(angle) * (pV-oV) + oV
        
        
# 2D Line
class Line2d(object):
 
    def __init__(self, pointA, pointB):
        """ Create a Line2d object using two Point2d objects
        Example: l = Line2d(pointA, pointB) """
        
        self.pointA = pointA
        self.pointB = pointB
        
    def __repr__(self):
        return "Line2d(Point2d(%s, %s), Point2d(%s, %s))" % (self.pointA.u, self.pointA.u, self.pointB.u, self.pointB.v)
        
    def __str__(self):
        return "(%s, %s), (%s, %s)" % (self.pointA.u, self.pointA.v, self.pointB.u, self.pointB.v)
        
    def __iter__(self):
        for coord in (self.pointA, self.pointB):
            yield coord
 
    # Get angle using arctan
    def getAngle(self):
        """ Gets the arctan angle between the Line2d's two Point2d objects 
        Example: angle = self.getAngle()
        
        Dependencies: python.math 
        Returns: float """
        
        # Short notation
        pA = self.Point2d
        pB = self.Point2d
        
        # Get distances
        if pA.u >= pB.u:
            distX = (pA.u - pB.u)
            distY = (pA.v - pB.v)
        else:
            distX = (pB.u - pA.u)
            distY = (pB.v - pA.v)
        
        # Calculate arctan angle and return it       
        return math.degrees(math.atan2(distY, distX))     
        
    # Rotate the line - OBSOLETE FUNCTION
    def rotate(self, other, angle):
        """ Rotates a Line2d object around a Point2d object. 
        Example: self.rotate(other, 45)
        
        Dependencies: python.math """
        
        self.pointA.rotate(other, angle)
        self.pointB.rotate(other, angle)
      
        
# 2D polygon
class Polygon2d(object):
    
    def __init__(self, lineList):
        """ Create a Polygon2D object using a list of Line2d objects
        Example: l = Polygon2D(list) """
        
        # As long as you're passing an iterable with two points in it
        # This will work. So tuples, lists, or line2d's
        self.lineList = [Line2d(*line) for line in lineList]
        self.pos = len(lineList)
        
    def __repr__(self): ## WARN: PLACEHOLDER - INCORRECT - DO NOT USE
        return lolk 
        
    def __str__(self): ## WARN: PLACEHOLDER - INCORRECT - DO NOT USE
        return "Polygon2d(%s, %s)" % (self.lineList)
        
    def __iter__(self):
        # Yielding these in reversed order, not sure why
        for line in self.lineList:
            yield line

    def __len__(self):
        return len(self.lineList)
    
    # Rotate the polygon
    def rotate(self, other, angle):
        """ Rotates a Polygon2d object around a Point2d object 
        Example: self.rotate(other, 45)
        
        Dependencies: python.math """

        # Old func rotated all coords 2x resulting in a double rotation of the entire polygon!!!
        # for line in self:
        #    print line
        #    line.rotate(other, angle)
        
        # Get all points in this polygon
        pointList = []
        for line in self:
            for point in line:
                pointList.append(point)
              
        # Remove duplicates
        pointList = list(set(pointList))
        
        # Rotate all points
        for point in pointList:
            point.rotate(other, angle)
    
    # Calculate the area of the bounding box
    def getBoundsArea(self):
        """ Gets the MAR (minimum-area rectangle) of the polygon 
        Example: area = self.getBoundsArea() 
        
        Returns: float """
    
        # Get max/min bounds
        xMax, xMin, yMax, yMin = (0.0,)*4
        for line in self:
            for Point2d in line:
                if Point2d.u > xMax:
                    xMax = Point2d.u
                if Point2d.u < xMin:
                    xMin = Point2d.u
                if Point2d.v > yMax:
                    yMax = Point2d.v
                if Point2d.v < yMin:
                    yMin = Point2d.v
        
        # Calculate and return area
        return (abs(xMax-xMin) * abs(yMax-yMin))
        
        
    # Get the coords of all Point2d objects in the polygon
    def getCoords(self):
        """ Gets all the coordinates for the Point2d objects in the polygon
        Example: coordList = self.getCoords()
        
        Returns: tuple list of floats """
    
        # Loop through all Point2d objects and get the coords, then append to list
        coordList = []
        for line in self:
            for point in line:
                coordList.append((point.u, point.v))
 
        # Remove duplicates and return list
        return list(set(coordList))
        
    # Get all Line2d objects in the polygon
    def getLines(self):
        """ Gets all the Line2d objects in the polygon
        Example: lineList = self.getLines()
        
        Returns: A list of tuples containing two tuples each """
        
        lineList = []
        for line in self:
            lineList.append((line.pointA, line.pointB))
        
        return lineList
    
    
    
## TEST
 
if __name__ == "__main__":
    
    # Create points
    p1 = Point2d(0.0, 0.0)
    p2 = Point2d(1.0, 0.0)
    p3 = Point2d(1.0, 1.0)
    p4 = Point2d(0.0, 1.0)
    
    # Create lines
    l1 = Line2d(p1, p2)
    l2 = Line2d(p2, p3)
    l3 = Line2d(p3, p4)
    l4 = Line2d(p4, p1)
    
    # Create polygon
    poly = Polygon2d([l1, l2, l3, l4])
    
    # Write polygon coords
    # print("coords before are %s") % poly.getCoords()
    
    # Write polygon lines
    print("lines before are %s")%poly.getLines()
    
    # Rotate polygon
    origin = Point2d(0.0, 0.0)
    poly.rotate(origin, 90)
    
    # Write polygon coords
    # print("coords after are %s") % poly.getCoords()
    
    # Write polygon lines
    print("lines after are %s")%poly.getLines()

Couple more questions:
-The init of Polygon2d: Could you ellaborate a little here - how may this object creation fail? My only intention right now is to create the Polygon2d -objects out of lists containing Line2d objects (which in turn is made out of tuples of Point2d objects)
-This is the first time I’ve worked with a custom class that is iterable, and all tutorials I’ve looked at have contained the iter and next -functions (next in Py 3), so I found it strange that you simply commented away the entire next-block and replaced it with a yield. Does this mean that next() is obsolete and that yield is the new way to create iterating classes? Or is there any other reasoning behind this?
-Is there any point in having the str and repr functions unless you are working in a large production and/or with other programmers/TA’s? I understand that it’s probably a good practise to get into the habit of writing them, but how picky should one be?
-What is a good way to write the str and repr functions of a class object containing a lot of sub-objects? (In my case, the Polygon2d contains at least 3x Line2d objects and at least 6x Point2d objects)

init of Polygon2d:
Originally you were just passing in a tuple of points, instead of an actual Line2d instance. Which would then fail if you called rotate on it in your Polygon2d.rotate method.
So I fixed that, before I realized that you had probably meant to just call Line2d on the pair of points in your test, and then double fixed it.
But the initial fix, where I’m iterating through each pair that has been passed in, and creating a Line2d instance from that pair works with both options. You can just as easily swap that back without breaking anything, as long as you actually pass only Line2d’s to the Polygon2d constructor.

iter and / or next.
So the whole point of iter is to return an object that defines next() (next in py3)
Which means if you define it as a generator (which is what we get from using yield), next is defined on that generator.
Otherwise your original idea of returning self, and providing a next method is just as valid.
I just happen to prefer simpler options for this kind of thing, and if I can fall back on a builtin construct I will.

repr can definitely be useful, because they make classes a whole lot easier to debug with simple print statements or logging.
I think we can all agree that <main.CustomClass instance at 0x0000000012B14F88> isn’t overly helpful in the grand scheme of things.

So the convention for repr is that eval(repr(CustomClass())) == CustomClass()
Which means if you’re passing in values to your class, you should probably include them in the repr value.

str on the other hand will actually allow other objects treat your instances as strings.
Also keep in mind that if you’ve defined a str method, it will take precedence when printing the instance of your object.


class CustomClass(object):
    """
    An example Container.
    """
    def __init__(self, *args):
        self.args = args
        
    def __repr__(self):
        args = ','.join(['{0!r}'.format(arg) for arg in self])
        return '{cls_name}({args})'.format(cls_name=self.__class__.__name__, args=args)
    
    def __eq__(self, other):
        return list(self) == list(other)
        
    def __iter__(self):
        for item in self.args:
            yield item

cc = CustomClass('hello', 'world')
print cc
# CustomClass("hello","world")
print eval(repr(cc)) == cc
# True
print cc.__iter__()
# <generator object __iter__ at 0x0000000035C685E8>

cc = CustomClass(*xrange(5))
print cc
# CustomClass(0,1,2,3,4)


print eval(repr(cc)) == cc
# True
print cc.__iter__()
# <generator object __iter__ at 0x0000000035C685E8>


So in the example, we’ve got an iter that returns a generator, and a repr method that works no matter how many args we’ve passed it.

You can clean things up a bit by giving your classes custom operators so you can do things like addition and subtraction without lots of boilerplate access.

Here’s a cheap-ass example:


from collections import namedtuple
import math


class Vector2(object):
    """
    Generic vector operations.
    """
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return type(self)(*(a + b for a, b in zip(self, other)))

    def __sub__(self, other):
        return type(self)(*(a - b for a, b in zip(self, other)))

    def __mul__(self, other):
        if hasattr(other, '__iter__'):
            return type(self)(*(a * b for a, b in zip(self, other)))
        return type(self)(*map(lambda a: a * other, self))

    def __div__(self, other):
        if hasattr(other, '__iter__'):
            return type(self)(*(a / b for a, b in zip(self, other)))
        return type(self)(*map(lambda a: a / other, self))

    def __iter__(self):
        yield self.x
        yield self.y

    def __repr__(self):
        return str(tuple(i for i in self))

test = Vector2(1.0,2.0)
test2 = Vector2(3.0,4.0)
print test / test2
print test + test2
(0.3333333333333333, 0.5)
(4.0, 6.0)


The slightly confusing (type(self)) stuff lets this work in other dimensions as well if you subclass it:


class Vector3(Vector2):

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    def __iter__(self):
        yield self.x
        yield self.y
        yield self.z

test4 = Vector3(1,0,0)
test5 = Vector3(.5, .5, .5)
print test4 + test5
(1.5, 0.5, 0.5)

One last thing that you might want to think about is whether operations like rotate() belong instances or if they should be separate. The usual convention is that Instance methods modify an instance in place while class methods create new instances: that way you can tell that


rotated = Vector2.Rotate(origin, degrees)

creates a new Vector2 while


test = Vector2( 1, 0)
test.rotate ( origin, degrees)

alters ‘test’ in place.

Thanks for your answers!
You guys makes working with classes a fun activity ^^

EDIT:

R.White:
I added that repr and eq functions to my Polygon2d class, got an error about incorrect amount of arguments, realized that Polygon2d is passed a list of Line2d objects so I changed it to

def __repr__(self):
    args = ','.join(['{0!r}'.format(arg) for arg in self])
    return '{cls_name}([{args}])'.format(cls_name=self.__class__.__name__, args=args)

[] around {args}
Then I create a Polygon2d object and compare it to itself using the repr function

print eval(repr(poly)) == poly

…but it returns False

So I tested changing eq to return self == other, which resulted in a recursion error.
Then I changed eq to return (self, other)
…and it turns out that “other” has an extra pair of () around it. Other than that they are equal according to the script editor. I’m not sure where those () are comming from.