Package arcmap :: Module shapes
[hide private]
[frames] | no frames]

Source Code for Module arcmap.shapes

  1  ################################################################################ 
  2  # Authors: Brian Schott (Sir Alaran) 
  3  # Copyright: Brian Schott (Sir Alaran) 
  4  # Date: Sep 29 2009 
  5  # License: 
  6  # 
  7  # This program is free software: you can redistribute it and/or modify 
  8  # it under the terms of the GNU General Public License as published by 
  9  # the Free Software Foundation, either version 3 of the License, or 
 10  # (at your option) any later version. 
 11  # 
 12  # This program is distributed in the hope that it will be useful, 
 13  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 14  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 
 15  # GNU General Public License for more details. 
 16  # 
 17  # You should have received a copy of the GNU General Public License 
 18  # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 19  ################################################################################ 
 20   
 21   
 22  """ 
 23  Contains various geometry related classes and functions 
 24  """ 
 25   
 26   
 27  import math 
 28  import preferences 
 29   
30 -class Point:
31 """ 32 This may just end up being replaced by a tuple 33 """
34 - def __init__(self, x, y):
35 """ 36 Brief Description 37 @type x: int 38 @param x: Placeholder 39 @type y: int 40 @param y: Placeholder 41 """ 42 self.x = x 43 self.y = y
44 45
46 -class Shape:
47 """ 48 Base class for shapes 49 """
50 - def __init__(self):
51 self.__center = None 52 self.friction = 1.0 53 self.restitution = 0.0 54 self.damage = 0.0
55
56 - def adjust(self, x, y, i = -1):
57 """ 58 Brief Description 59 @type x: int 60 @param x: Placeholder 61 @type y: int 62 @param y: Placeholder 63 @type i: int 64 @param i: index of the point to adjust. -1 adjusts the last point 65 """ 66 i = -1 67 raise NameError("Shape::adjust must be overridden in a subclass")
68 69
70 - def boundingBox(self):
71 """ 72 @rtype: (int, int, int, int) 73 @return: a bounding box int the form x, y, x2, y2 where x2 is the 74 x-coordinate of the right edge, y2 as the bottom edge, and so on. 75 """ 76 raise NameError("Shape::boundingBox must be overridden in a subclass")
77
78 - def getCenter(self):
79 """ 80 @rtype: Point 81 @return: The center of the shape 82 """ 83 return self.__center
84
85 - def setCenter(self, center):
86 self.__center = center
87
88 - def intersects(self, p):
89 """ 90 @type p: Point 91 @param p: point to test 92 @rtype: bool 93 @return: True if p intersects the shape, False otherwise 94 """ 95 raise NameError("Shape::intersects must be overridden in a subclass")
96
97 - def shift(self, xoffset, yoffset):
98 """ 99 Moves the shape around by x and y 100 """ 101 raise NameError("Shape::shift must be overridden in a subclass")
102
103 - def getHandles(self):
104 """ 105 @rtype: Point[] 106 @return: List of points that define the center of handles for resizing 107 the shape 108 """ 109 raise NameError("Shape::getHandles must be overridden in a subclass")
110 111
112 -class Polygon(Shape):
113
114 - def __init__(self, points = None):
115 """ 116 @type points: point[] 117 @param points: the points that make up the polygon 118 """ 119 Shape.__init__(self) 120 self.__points = points 121 self.__concave = False 122 if points != None: 123 if len(self.__points) < 3: 124 log.warning("Polygon constructed with fewer than three points."+ 125 " This will not end well.") 126 else: 127 self.__concave = self.__calcConcave() 128 self.calcCenter() 129 else: 130 self.__points = [Point(0, 0)] 131 132 points = None
133
134 - def addPoint(self, p):
135 """ 136 Brief Description 137 @type p: Point 138 @param p: the point to add 139 """ 140 self.__points.append(p) 141 self.__concave = self.__calcConcave() 142 self.calcCenter()
143
144 - def getPoints(self):
145 """ 146 @rtype: Point[] 147 @return: A copy of the polygon's points 148 """ 149 return self.__points[:]
150
151 - def getLastPoint(self):
152 if len(self.__points) > 2: 153 return self.__points[-2] 154 else: 155 return None
156
157 - def delExtraPoint(self):
158 del self.__points[-1]
159
160 - def forceCClockwise(self):
161 """ 162 Rearranges the points of the shape so that they are in counter-clockwise 163 order. 164 """ 165 # Python makes this too easy. 166 self.__points.sort(key=lambda p: math.atan2(p.x - self.getCenter().x, 167 p.y - self.getCenter().y), reverse=True)
168
169 - def calcCenter(self):
170 """ 171 Recalculates the center of the shape. This center is simply the average 172 of the x and y coordinates of all the points 173 """ 174 l = len(self.__points) 175 if l == 0: 176 return 177 totalx = 0 178 totaly = 0 179 for p in self.__points: 180 totalx += p.x 181 totaly += p.y 182 center = Point(totalx / float(l), totaly / float(l)) 183 self.setCenter(center)
184
185 - def convex(self):
186 """ 187 @rtype: bool 188 @return: True if the shape is convex 189 """ 190 if len(self.__points) <= 2: 191 return False 192 else: 193 return not self.__concave
194
195 - def concave(self):
196 """ 197 @rtype: bool 198 @return: True if the shape is convex 199 """ 200 # Separated from the calculating function so that the query is fast 201 return self.__concave
202
203 - def __calcConcave(self):
204 if len(self.__points) <= 2: 205 # This shape isn't even valid 206 return True 207 elif len(self.__points) == 3: 208 # Triangles are always convex 209 return False 210 # I think I adapted this from something I snatched from Wikipedia... 211 # Basically the cross products of the vectors will all point up or all 212 # point down if the turns made at each vertex are in the same direction 213 positiveFlag = negativeFlag = False 214 for i in range(len(self.__points)): 215 j = (i + 1) % len(self.__points) 216 k = (i + 2) % len(self.__points) 217 z = ((self.__points[j].x - self.__points[i].x) 218 * (self.__points[k].y - self.__points[j].y)) 219 z = z - ((self.__points[j].y - self.__points[i].y) 220 * (self.__points[k].x - self.__points[j].x)) 221 if z < 0: 222 positiveFlag = True 223 elif z > 0: 224 negativeFlag = True 225 if positiveFlag and negativeFlag: 226 return True 227 return False
228
229 - def adjust(self, x, y, i = -1):
230 self.__points[i].x = x 231 self.__points[i].y = y 232 self.__concave = self.__calcConcave() 233 self.calcCenter() 234 i = -1
235
236 - def intersects(self, p):
237 # First create a list of vectors that are the right-hand normals of the 238 # vectors that point from point n to point n+1 239 rightVectors = [] 240 for i in range(len(self.__points) - 1): 241 dx = self.__points[i + 1].x - self.__points[i].x 242 dy = self.__points[i + 1].y - self.__points[i].y 243 rightVectors.append(Point(dy, -dx)) 244 # Last point back to the first 245 dx = self.__points[0].x - self.__points[-1].x 246 dy = self.__points[0].y - self.__points[-1].y 247 rightVectors.append(Point(dy, -dx)) 248 249 for i, v in enumerate(rightVectors): 250 # Calculate the vector pointing from the vertex to the test point, 251 # then multiply the components by the right-hand normal. 252 x = (p.x - self.__points[i].x) * v.x 253 y = (p.y - self.__points[i].y) * v.y 254 255 # If the sum of the components is positive, the point is on the 256 # positive side of the normal vector, and thus outside the shape 257 if (x + y) >= 0: 258 return False 259 return True
260
261 - def shift(self, xoffset, yoffset):
262 for p in self.__points: 263 p.x = p.x + xoffset 264 p.y = p.y + yoffset 265 self.calcCenter()
266
267 - def getHandles(self):
268 return self.getPoints()
269 270
271 -class Circle(Shape):
272
273 - def __init__(self, radius, snap = -1):
274 Shape.__init__(self) 275 self.__radius = radius 276 self.__snapFactor = snap 277 snap = -1
278
279 - def adjust(self, x, y, i = -1):
280 c = self.getCenter() 281 newRadius = math.sqrt((c.x - x)**2 + (c.y - y)**2) 282 if self.__snapFactor != -1: 283 newRadius = round(newRadius / self.__snapFactor) \ 284 * self.__snapFactor 285 if newRadius > 0: 286 self.__radius = newRadius
287
288 - def setSnapFactor(self, snap):
289 self.__snapFactor = snap
290
291 - def boundingBox(self):
292 c = self.getCenter() 293 return (c.x - self.__radius, c.x + self.__radius, self.__radius * 2, 294 self.__radius * 2)
295
296 - def getRadius(self):
297 """ 298 @rtype: int 299 @return: the radius of the circle 300 """ 301 return self.__radius
302
303 - def intersects(self, p):
304 c = self.getCenter() 305 dx = p.x - c.x 306 dy = p.y - c.y 307 if ((dx ** 2.0) + (dy ** 2.0)) < (self.__radius ** 2.0): 308 return True 309 else: 310 return False
311
312 - def shift(self, xoffset, yoffset):
313 c = self.getCenter() 314 self.setCenter(Point(c.x + xoffset, c.y + yoffset))
315
316 - def getHandles(self):
317 c = self.getCenter() 318 return [Point(c.x - self.__radius, c.y), 319 Point(c.x + self.__radius, c.y), 320 Point(c.x, c.y - self.__radius), 321 Point(c.x, c.y + self.__radius)]
322 323
324 -def pointInHandle(p, h):
325 """ 326 @type p: Point 327 @param p: the point to test 328 @type h: Point 329 @param h: the center of the handle 330 @rtype: bool 331 @return: True if the point is inside the handle, False if not 332 """ 333 s = preferences.visual["handle_size"] 334 if p.x < h.x - s: 335 return False 336 if p.x > h.x + s: 337 return False 338 if p.y < h.y - s: 339 return False 340 if p.y > h.y + s: 341 return False 342 return True
343 344
345 -def unittest():
346 """ 347 Simple sanity check on the intersects code 348 """ 349 p = Polygon([ 350 Point(1, 1), 351 Point(4, 2), 352 Point(2, 4) 353 ]) 354 assert p.intersects(Point(2, 2)) == True 355 assert p.intersects(Point(2, 1)) == False 356 357 p = Polygon([ 358 Point(1, 1), 359 Point(3, 1), 360 Point(1, 3), 361 Point(3, 3) 362 ]) 363 assert p.intersects(Point(2, 2)) == True 364 assert p.intersects(Point(0, 0)) == False
365 366 if __name__ == "__main__": 367 unittest() 368