shapes.py 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244
  1. from numpy import linspace, sin, cos, pi, array, asarray, ndarray, sqrt, abs
  2. import pprint, copy, glob, os
  3. from MatplotlibDraw import MatplotlibDraw
  4. drawing_tool = MatplotlibDraw()
  5. def point(x, y, check_inside=False):
  6. if isinstance(x, (float,int)) and isinstance(y, (float,int)):
  7. pass
  8. else:
  9. raise TypeError('x=%s,y=%s must be float,float, not %s,%s' %
  10. (x, y, type(x), type(y)))
  11. if check_inside:
  12. ok, msg = drawing_tool.inside((x,y), exception=True)
  13. if not ok:
  14. print msg
  15. return array((x, y), dtype=float)
  16. def arr2D(x, check_inside=False):
  17. if isinstance(x, (tuple,list,ndarray)):
  18. if len(x) == 2:
  19. pass
  20. else:
  21. raise ValueError('x=%s has length %d, not 2' % (x, len(x)))
  22. else:
  23. raise TypeError('x=%s must be list/tuple/ndarray, not %s' %
  24. (x, type(x)))
  25. if check_inside:
  26. ok, msg = drawing_tool.inside(x, exception=True)
  27. if not ok:
  28. print msg
  29. return asarray(x, dtype=float)
  30. def _is_sequence(seq, length=None,
  31. can_be_None=False, error_message=True):
  32. if can_be_None:
  33. legal_types = (list,tuple,ndarray,None)
  34. else:
  35. legal_types = (list,tuple,ndarray)
  36. if isinstance(seq, legal_types):
  37. if length is not None:
  38. if length == len(seq):
  39. return True
  40. elif error_message:
  41. raise TypeError('%s is %s; must be %s of length %d' %
  42. (str(seq), type(seq),
  43. ', '.join([str(t) for t in legal_types]),
  44. len(seq)))
  45. else:
  46. return False
  47. else:
  48. return True
  49. elif error_message:
  50. raise TypeError('%s is %s; must be %s' %
  51. (str(seq), type(seq),
  52. ','.join([str(t)[5:-1] for t in legal_types])))
  53. else:
  54. return False
  55. def is_sequence(*sequences, **kwargs):
  56. length = kwargs.get('length', 2)
  57. can_be_None = kwargs.get('can_be_None', False)
  58. error_message = kwargs.get('error_message', True)
  59. for x in sequences:
  60. _is_sequence(x, length=length, can_be_None=can_be_None,
  61. error_message=error_message)
  62. ok, msg = drawing_tool.inside(x, exception=True)
  63. if not ok: print msg
  64. def animate(fig, time_points, user_action, moviefiles=False,
  65. pause_per_frame=0.5):
  66. if moviefiles:
  67. # Clean up old frame files
  68. framefilestem = 'tmp_frame_'
  69. framefiles = glob.glob('%s*.png' % framefilestem)
  70. for framefile in framefiles:
  71. os.remove(framefile)
  72. for n, t in enumerate(time_points):
  73. drawing_tool.erase()
  74. user_action(t, fig)
  75. #could demand returning fig, but in-place modifications
  76. #are done anyway
  77. #fig = user_action(t, fig)
  78. #if fig is None:
  79. # raise TypeError(
  80. # 'animate: user_action returns None, not fig\n'
  81. # '(a Shape object with the whole figure)')
  82. fig.draw()
  83. drawing_tool.display()
  84. if moviefiles:
  85. drawing_tool.savefig('%s%04d.png' % (framefilestem, n))
  86. if moviefiles:
  87. return '%s*.png' % framefilestem
  88. class Shape:
  89. """
  90. Superclass for drawing different geometric shapes.
  91. Subclasses define shapes, but drawing, rotation, translation,
  92. etc. are done in generic functions in this superclass.
  93. """
  94. def __init__(self):
  95. """
  96. Until new version of shapes.py is ready:
  97. Never to be called from subclasses.
  98. """
  99. raise NotImplementedError(
  100. 'class %s must implement __init__,\nwhich defines '
  101. 'self.shapes as a list of Shape objects\n'
  102. '(and preferably self._repr string).\n'
  103. 'Do not call Shape.__init__!' % \
  104. self.__class__.__name__)
  105. def __iter__(self):
  106. # We iterate over self.shapes many places, and will
  107. # get here if self.shapes is just a Shape object and
  108. # not the assumed list.
  109. print 'Warning: class %s does not define self.shapes\n'\
  110. 'as a *list* of Shape objects'
  111. return [self] # Make the iteration work
  112. def copy(self):
  113. return copy.deepcopy(self)
  114. def __getitem__(self, name):
  115. """
  116. Allow indexing like::
  117. obj1['name1']['name2']
  118. all the way down to ``Curve`` or ``Point`` (``Text``)
  119. objects.
  120. """
  121. if hasattr(self, 'shapes'):
  122. if name in self.shapes:
  123. return self.shapes[name]
  124. else:
  125. for shape in self.shapes:
  126. if isinstance(self.shapes[shape], (Curve,Point)):
  127. # Indexing of Curve/Point/Text is not possible
  128. raise TypeError(
  129. 'Index "%s" (%s) is illegal' %
  130. (name, self.__class__.__name__))
  131. return self.shapes[shape][name]
  132. else:
  133. raise Exception('This is a bug')
  134. def for_all_shapes(self, func, *args, **kwargs):
  135. if not hasattr(self, 'shapes'):
  136. # When self.shapes is lacking, we either come to
  137. # a special implementation of func or we come here
  138. # because Shape.func is just inherited. This is
  139. # an error if the class is not Curve or Point
  140. if isinstance(self, (Curve, Point)):
  141. return # ok: no shapes and no func
  142. else:
  143. raise AttributeError('class %s has no shapes attribute!' %
  144. self.__class__.__name__)
  145. is_dict = True if isinstance(self.shapes, dict) else False
  146. for shape in self.shapes:
  147. if is_dict:
  148. shape = self.shapes[shape]
  149. if not isinstance(shape, Shape):
  150. if isinstance(shape, dict):
  151. raise TypeError(
  152. 'class %s has a shapes attribute that is just\n'
  153. 'a plain dictionary,\n%s\n'
  154. 'Did you mean to embed this dict in a Compose\n'
  155. 'object?' % (self.__class__.__name__,
  156. str(shape)))
  157. elif isinstance(shape, (list,tuple)):
  158. raise TypeError(
  159. 'class %s has a shapes attribute containing\n'
  160. 'a %s object %s,\n'
  161. 'Did you mean to embed this list in a Compose\n'
  162. 'object?' % (self.__class__.__name__,
  163. type(shape), str(shape)))
  164. else:
  165. raise TypeError(
  166. 'class %s has a shapes attribute %s which is not'
  167. 'a Shape objects\n%s' %
  168. (self.__class__.__name__, type(shape),
  169. pprint.pformat(self.shapes)))
  170. getattr(shape, func)(*args, **kwargs)
  171. def draw(self):
  172. self.for_all_shapes('draw')
  173. return self
  174. def rotate(self, angle, center):
  175. is_sequence(center, length=2)
  176. self.for_all_shapes('rotate', angle, center)
  177. return self
  178. def translate(self, vec):
  179. is_sequence(vec, length=2)
  180. self.for_all_shapes('translate', vec)
  181. return self
  182. def scale(self, factor):
  183. self.for_all_shapes('scale', factor)
  184. return self
  185. def set_linestyle(self, style):
  186. styles = ('solid', 'dashed', 'dashdot', 'dotted')
  187. if style not in styles:
  188. raise ValueError('%s: style=%s must be in %s' %
  189. (self.__class__.__name__ + '.set_linestyle:',
  190. style, str(styles)))
  191. self.for_all_shapes('set_linestyle', style)
  192. return self
  193. def set_linewidth(self, width):
  194. if not isinstance(width, int) and width >= 0:
  195. raise ValueError('%s: width=%s must be positive integer' %
  196. (self.__class__.__name__ + '.set_linewidth:',
  197. width))
  198. self.for_all_shapes('set_linewidth', width)
  199. return self
  200. def set_linecolor(self, color):
  201. if color in drawing_tool.line_colors:
  202. color = drawing_tool.line_colors[color]
  203. elif color in drawing_tool.line_colors.values():
  204. pass # color is ok
  205. else:
  206. raise ValueError('%s: invalid color "%s", must be in %s' %
  207. (self.__class__.__name__ + '.set_linecolor:',
  208. color, list(drawing_tool.line_colors.keys())))
  209. self.for_all_shapes('set_linecolor', color)
  210. return self
  211. def set_arrow(self, style):
  212. styles = ('->', '<-', '<->')
  213. if not style in styles:
  214. raise ValueError('%s: style=%s must be in %s' %
  215. (self.__class__.__name__ + '.set_arrow:',
  216. style, styles))
  217. self.for_all_shapes('set_arrow', style)
  218. return self
  219. def set_filled_curves(self, color='', pattern=''):
  220. if color in drawing_tool.line_colors:
  221. color = drawing_tool.line_colors[color]
  222. elif color in drawing_tool.line_colors.values():
  223. pass # color is ok
  224. else:
  225. raise ValueError('%s: invalid color "%s", must be in %s' %
  226. (self.__class__.__name__ + '.set_filled_curves:',
  227. color, list(drawing_tool.line_colors.keys())))
  228. self.for_all_shapes('set_filled_curves', color, pattern)
  229. return self
  230. def show_hierarchy(self, indent=0, format='std'):
  231. """Recursive pretty print of hierarchy of objects."""
  232. if not isinstance(self.shapes, dict):
  233. print 'cannot print hierarchy when %s.shapes is not a dict' % \
  234. self.__class__.__name__
  235. s = ''
  236. if format == 'dict':
  237. s += '{'
  238. for shape in self.shapes:
  239. if format == 'dict':
  240. shape_str = repr(shape) + ':'
  241. elif format == 'plain':
  242. shape_str = shape
  243. else:
  244. shape_str = shape + ':'
  245. if format == 'dict' or format == 'plain':
  246. class_str = ''
  247. else:
  248. class_str = ' (%s)' % \
  249. self.shapes[shape].__class__.__name__
  250. s += '\n%s%s%s %s' % (
  251. ' '*indent,
  252. shape_str,
  253. class_str,
  254. self.shapes[shape].show_hierarchy(indent+4, format))
  255. if format == 'dict':
  256. s += '}'
  257. return s
  258. def __str__(self):
  259. """Display hierarchy with minimum information (just object names)."""
  260. return self.show_hierarchy(format='plain')
  261. def __repr__(self):
  262. """Display hierarchy as a dictionary."""
  263. return self.show_hierarchy(format='dict')
  264. #return pprint.pformat(self.shapes)
  265. class Curve(Shape):
  266. """General curve as a sequence of (x,y) coordintes."""
  267. def __init__(self, x, y):
  268. """
  269. `x`, `y`: arrays holding the coordinates of the curve.
  270. """
  271. self.x = asarray(x, dtype=float)
  272. self.y = asarray(y, dtype=float)
  273. #self.shapes must not be defined in this class
  274. #as self.shapes holds children objects:
  275. #Curve has no children (end leaf of self.shapes tree)
  276. self.linestyle = None
  277. self.linewidth = None
  278. self.linecolor = None
  279. self.fillcolor = None
  280. self.fillpattern = None
  281. self.arrow = None
  282. def inside_plot_area(self, verbose=True):
  283. """Check that all coordinates are within drawing_tool's area."""
  284. xmin, xmax = self.x.min(), self.x.max()
  285. ymin, ymax = self.y.min(), self.y.max()
  286. t = drawing_tool
  287. inside = True
  288. if xmin < t.xmin:
  289. inside = False
  290. if verbose:
  291. print 'x_min=%g < plot area x_min=%g' % (xmin, t.xmin)
  292. if xmax > t.xmax:
  293. inside = False
  294. if verbose:
  295. print 'x_max=%g > plot area x_max=%g' % (xmax, t.xmax)
  296. if ymin < t.ymin:
  297. inside = False
  298. if verbose:
  299. print 'y_min=%g < plot area y_min=%g' % (ymin, t.ymin)
  300. if xmax > t.xmax:
  301. inside = False
  302. if verbose:
  303. print 'y_max=%g > plot area y_max=%g' % (ymax, t.ymax)
  304. return inside
  305. def draw(self):
  306. """
  307. Send the curve to the plotting engine. That is, convert
  308. coordinate information in self.x and self.y, together
  309. with optional settings of linestyles, etc., to
  310. plotting commands for the chosen engine.
  311. """
  312. self.inside_plot_area()
  313. drawing_tool.plot_curve(
  314. self.x, self.y,
  315. self.linestyle, self.linewidth, self.linecolor,
  316. self.arrow, self.fillcolor, self.fillpattern)
  317. def rotate(self, angle, center):
  318. """
  319. Rotate all coordinates: `angle` is measured in degrees and
  320. (`x`,`y`) is the "origin" of the rotation.
  321. """
  322. angle = angle*pi/180
  323. x, y = center
  324. c = cos(angle); s = sin(angle)
  325. xnew = x + (self.x - x)*c - (self.y - y)*s
  326. ynew = y + (self.x - x)*s + (self.y - y)*c
  327. self.x = xnew
  328. self.y = ynew
  329. return self
  330. def scale(self, factor):
  331. """Scale all coordinates by `factor`: ``x = factor*x``, etc."""
  332. self.x = factor*self.x
  333. self.y = factor*self.y
  334. return self
  335. def translate(self, vec):
  336. """Translate all coordinates by a vector `vec`."""
  337. self.x += vec[0]
  338. self.y += vec[1]
  339. return self
  340. def set_linecolor(self, color):
  341. self.linecolor = color
  342. return self
  343. def set_linewidth(self, width):
  344. self.linewidth = width
  345. return self
  346. def set_linestyle(self, style):
  347. self.linestyle = style
  348. return self
  349. def set_arrow(self, style=None):
  350. self.arrow = style
  351. return self
  352. def set_name(self, name):
  353. self.name = name
  354. return self
  355. def set_filled_curves(self, color='', pattern=''):
  356. self.fillcolor = color
  357. self.fillpattern = pattern
  358. return self
  359. def show_hierarchy(self, indent=0, format='std'):
  360. if format == 'dict':
  361. return '"%s"' % str(self)
  362. elif format == 'plain':
  363. return ''
  364. else:
  365. return str(self)
  366. def __str__(self):
  367. """Compact pretty print of a Curve object."""
  368. s = '%d coords' % self.x.size
  369. if not self.inside_plot_area(verbose=False):
  370. s += ', some coordinates are outside plotting area!\n'
  371. props = ('linecolor', 'linewidth', 'linestyle', 'arrow',
  372. 'fillcolor', 'fillpattern')
  373. for prop in props:
  374. value = getattr(self, prop)
  375. if value is not None:
  376. s += ' %s=%s' % (prop, repr(value))
  377. return s
  378. def __repr__(self):
  379. return str(self)
  380. class Point(Shape):
  381. """A point (x,y) which can be rotated, translated, and scaled."""
  382. def __init__(self, x, y):
  383. self.x, self.y = x, y
  384. #self.shapes is not needed in this class
  385. def __add__(self, other):
  386. if isinstance(other, (list,tuple)):
  387. other = Point(other)
  388. return Point(self.x+other.x, self.y+other.y)
  389. # class Point is an abstract class - only subclasses are useful
  390. # and must implement draw
  391. def draw(self):
  392. raise NotImplementedError(
  393. 'class %s must implement the draw method' %
  394. self.__class__.__name__)
  395. def rotate(self, angle):
  396. """Rotate point an `angle` (in degrees) around (`x`,`y`)."""
  397. angle = angle*pi/180
  398. x, y = center
  399. c = cos(angle); s = sin(angle)
  400. xnew = x + (self.x - x)*c - (self.y - y)*s
  401. ynew = y + (self.x - x)*s + (self.y - y)*c
  402. self.x = xnew
  403. self.y = ynew
  404. return self
  405. def scale(self, factor):
  406. """Scale point coordinates by `factor`: ``x = factor*x``, etc."""
  407. self.x = factor*self.x
  408. self.y = factor*self.y
  409. return self
  410. def translate(self, vec):
  411. """Translate point by a vector `vec`."""
  412. self.x += vec[0]
  413. self.y += vec[1]
  414. return self
  415. def show_hierarchy(self, indent=0, format='std'):
  416. s = '%s at (%g,%g)' % (self.__class__.__name__, self.x, self.y)
  417. if format == 'dict':
  418. return '"%s"' % s
  419. elif format == 'plain':
  420. return ''
  421. else:
  422. return s
  423. # no need to store input data as they are invalid after rotations etc.
  424. class Rectangle(Shape):
  425. def __init__(self, lower_left_corner, width, height):
  426. is_sequence(lower_left_corner)
  427. p = lower_left_corner # short form
  428. x = [p[0], p[0] + width,
  429. p[0] + width, p[0], p[0]]
  430. y = [p[1], p[1], p[1] + height,
  431. p[1] + height, p[1]]
  432. self.shapes = {'rectangle': Curve(x,y)}
  433. class Triangle(Shape):
  434. """Triangle defined by its three vertices p1, p2, and p3."""
  435. def __init__(self, p1, p2, p3):
  436. is_sequence(p1, p2, p3)
  437. x = [p1[0], p2[0], p3[0], p1[0]]
  438. y = [p1[1], p2[1], p3[1], p1[1]]
  439. self.shapes = {'triangle': Curve(x,y)}
  440. class Line(Shape):
  441. def __init__(self, start, end):
  442. is_sequence(start, end)
  443. x = [start[0], end[0]]
  444. y = [start[1], end[1]]
  445. self.shapes = {'line': Curve(x, y)}
  446. def compute_formulas(self):
  447. x, y = self.shapes['line'].x, self.shapes['line'].y
  448. # Define equations for line:
  449. # y = a*x + b, x = c*y + d
  450. try:
  451. self.a = (y[1] - y[0])/(x[1] - x[0])
  452. self.b = y[0] - self.a*x[0]
  453. except ZeroDivisionError:
  454. # Vertical line, y is not a function of x
  455. self.a = None
  456. self.b = None
  457. try:
  458. if self.a is None:
  459. self.c = 0
  460. else:
  461. self.c = 1/float(self.a)
  462. if self.b is None:
  463. self.d = x[1]
  464. except ZeroDivisionError:
  465. # Horizontal line, x is not a function of y
  466. self.c = None
  467. self.d = None
  468. def compute_formulas(self):
  469. x, y = self.shapes['line'].x, self.shapes['line'].y
  470. tol = 1E-14
  471. # Define equations for line:
  472. # y = a*x + b, x = c*y + d
  473. if abs(x[1] - x[0]) > tol:
  474. self.a = (y[1] - y[0])/(x[1] - x[0])
  475. self.b = y[0] - self.a*x[0]
  476. else:
  477. # Vertical line, y is not a function of x
  478. self.a = None
  479. self.b = None
  480. if self.a is None:
  481. self.c = 0
  482. elif abs(self.a) > tol:
  483. self.c = 1/float(self.a)
  484. self.d = x[1]
  485. else: # self.a is 0
  486. # Horizontal line, x is not a function of y
  487. self.c = None
  488. self.d = None
  489. def __call__(self, x=None, y=None):
  490. """Given x, return y on the line, or given y, return x."""
  491. self.compute_formulas()
  492. if x is not None and self.a is not None:
  493. return self.a*x + self.b
  494. elif y is not None and self.c is not None:
  495. return self.c*y + self.d
  496. else:
  497. raise ValueError(
  498. 'Line.__call__(x=%s, y=%s) not meaningful' % \
  499. (x, y))
  500. # First implementation of class Circle
  501. class Circle(Shape):
  502. def __init__(self, center, radius, resolution=180):
  503. self.center, self.radius = center, radius
  504. self.resolution = resolution
  505. t = linspace(0, 2*pi, resolution+1)
  506. x0 = center[0]; y0 = center[1]
  507. R = radius
  508. x = x0 + R*cos(t)
  509. y = y0 + R*sin(t)
  510. self.shapes = {'circle': Curve(x, y)}
  511. def __call__(self, theta):
  512. """
  513. Return (x, y) point corresponding to angle theta.
  514. Not valid after a translation, rotation, or scaling.
  515. """
  516. return self.center[0] + self.radius*cos(theta), \
  517. self.center[1] + self.radius*sin(theta)
  518. class Arc(Shape):
  519. def __init__(self, center, radius,
  520. start_angle, arc_angle,
  521. resolution=180):
  522. is_sequence(center)
  523. self.center = center
  524. self.radius = radius
  525. self.start_angle = start_angle*pi/180 # radians
  526. self.arc_angle = arc_angle*pi/180
  527. self.resolution = resolution
  528. t = linspace(self.start_angle,
  529. self.start_angle + self.arc_angle,
  530. resolution+1)
  531. x0 = center[0]; y0 = center[1]
  532. R = radius
  533. x = x0 + R*cos(t)
  534. y = y0 + R*sin(t)
  535. self.shapes = {'arc': Curve(x, y)}
  536. def __call__(self, theta):
  537. """
  538. Return (x,y) point at start_angle + theta.
  539. Not valid after translation, rotation, or scaling.
  540. """
  541. theta = theta*pi/180
  542. t = self.start_angle + theta
  543. x0 = self.center[0]
  544. y0 = self.center[1]
  545. R = self.radius
  546. x = x0 + R*cos(t)
  547. y = y0 + R*sin(t)
  548. return (x, y)
  549. # Alternative for small arcs: Parabola
  550. class Parabola(Shape):
  551. def __init__(self, start, mid, stop, resolution=21):
  552. self.p1, self.p2, self.p3 = start, mid, stop
  553. # y as function of x? (no point on line x=const?)
  554. tol = 1E-14
  555. if abs(self.p1[0] - self.p2[0]) > 1E-14 and \
  556. abs(self.p2[0] - self.p3[0]) > 1E-14 and \
  557. abs(self.p3[0] - self.p1[0]) > 1E-14:
  558. self.y_of_x = True
  559. else:
  560. self.y_of_x = False
  561. # x as function of y? (no point on line y=const?)
  562. tol = 1E-14
  563. if abs(self.p1[1] - self.p2[1]) > 1E-14 and \
  564. abs(self.p2[1] - self.p3[1]) > 1E-14 and \
  565. abs(self.p3[1] - self.p1[1]) > 1E-14:
  566. self.x_of_y = True
  567. else:
  568. self.x_of_y = False
  569. if self.y_of_x:
  570. x = linspace(start[0], end[0], resolution)
  571. y = self(x=x)
  572. elif self.x_of_y:
  573. y = linspace(start[1], end[1], resolution)
  574. x = self(y=y)
  575. else:
  576. raise ValueError(
  577. 'Parabola: two or more points lie on x=const '
  578. 'or y=const - not allowed')
  579. self.shapes = {'parabola': Curve(x, y)}
  580. def __call__(self, x=None, y=None):
  581. if x is not None and self.y_of_x:
  582. return self._L2x(self.p1, self.p2)*self.p3[1] + \
  583. self._L2x(self.p2, self.p3)*self.p1[1] + \
  584. self._L2x(self.p3, self.p1)*self.p2[1]
  585. elif y is not None and self.x_of_y:
  586. return self._L2y(self.p1, self.p2)*self.p3[0] + \
  587. self._L2y(self.p2, self.p3)*self.p1[0] + \
  588. self._L2y(self.p3, self.p1)*self.p2[0]
  589. else:
  590. raise ValueError(
  591. 'Parabola.__call__(x=%s, y=%s) not meaningful' % \
  592. (x, y))
  593. def _L2x(self, x, pi, pj, pk):
  594. return (x - pi[0])*(x - pj[0])/((pk[0] - pi[0])*(pk[0] - pj[0]))
  595. def _L2y(self, y, pi, pj, pk):
  596. return (y - pi[1])*(y - pj[1])/((pk[1] - pi[1])*(pk[1] - pj[1]))
  597. class Circle(Arc):
  598. def __init__(self, center, radius, resolution=180):
  599. Arc.__init__(self, center, radius, 0, 360, resolution)
  600. class Wall(Shape):
  601. def __init__(self, x, y, thickness, pattern='/'):
  602. is_sequence(x, y, length=len(x))
  603. if isinstance(x[0], (tuple,list,ndarray)):
  604. # x is list of curves
  605. x1 = concatenate(x)
  606. else:
  607. x1 = asarray(x, float)
  608. if isinstance(y[0], (tuple,list,ndarray)):
  609. # x is list of curves
  610. y = concatenate(y)
  611. else:
  612. y1 = asarray(y, float)
  613. # Displaced curve (according to thickness)
  614. x2 = x1
  615. y2 = y1 + thickness
  616. # Combine x1,y1 with x2,y2 reversed
  617. from numpy import concatenate
  618. x = concatenate((x1, x2[-1::-1]))
  619. y = concatenate((y1, y2[-1::-1]))
  620. wall = Curve(x, y)
  621. wall.set_filled_curves(color='white', pattern=pattern)
  622. self.shapes = {'wall': wall}
  623. class VelocityProfile(Shape):
  624. def __init__(self, start, height, profile, num_arrows, scaling=1):
  625. # vx, vy = profile(y)
  626. shapes = {}
  627. # Draw left line
  628. shapes['start line'] = Line(start, (start[0], start[1]+height))
  629. # Draw velocity arrows
  630. dy = float(height)/(num_arrows-1)
  631. x = start[0]
  632. y = start[1]
  633. r = profile(y) # Test on return type
  634. if not isinstance(r, (list,tuple,ndarray)) and len(r) != 2:
  635. raise TypeError('VelocityProfile constructor: profile(y) function must return velocity vector (vx,vy), not %s' % type(r))
  636. for i in range(num_arrows):
  637. y = i*dy
  638. vx, vy = profile(y)
  639. if abs(vx) < 1E-8:
  640. continue
  641. vx *= scaling
  642. vy *= scaling
  643. arr = Arrow1((x,y), (x+vx, y+vy), '->')
  644. shapes['arrow%d' % i] = arr
  645. # Draw smooth profile
  646. xs = []
  647. ys = []
  648. n = 100
  649. dy = float(height)/n
  650. for i in range(n+2):
  651. y = i*dy
  652. vx, vy = profile(y)
  653. vx *= scaling
  654. vy *= scaling
  655. xs.append(x+vx)
  656. ys.append(y+vy)
  657. shapes['smooth curve'] = Curve(xs, ys)
  658. self.shapes = shapes
  659. class Text(Point):
  660. def __init__(self, text, position, alignment='center', fontsize=14):
  661. is_sequence(position)
  662. is_sequence(position, length=2, can_be_None=True)
  663. self.text = text
  664. self.position = position
  665. self.alignment = alignment
  666. self.fontsize = fontsize
  667. Point.__init__(self, position[0], position[1])
  668. #no need for self.shapes here
  669. def draw(self):
  670. drawing_tool.text(self.text, (self.x, self.y),
  671. self.alignment, self.fontsize)
  672. def __str__(self):
  673. return 'text "%s" at (%g,%g)' % (self.text, self.x, self.y)
  674. def __repr__(self):
  675. return str(self)
  676. class Text_wArrow(Text):
  677. def __init__(self, text, position, arrow_tip,
  678. alignment='center', fontsize=14):
  679. is_sequence(arrow_tip, length=2, can_be_None=True)
  680. is_sequence(position)
  681. self.arrow_tip = arrow_tip
  682. Text.__init__(self, text, position, alignment, fontsize)
  683. def draw(self):
  684. drawing_tool.text(self.text, self.position,
  685. self.alignment, self.fontsize,
  686. self.arrow_tip)
  687. def __str__(self):
  688. return 'annotation "%s" at (%g,%g) with arrow to (%g,%g)' % \
  689. (self.text, self.x, self.y,
  690. self.arrow_tip[0], self.arrow_tip[1])
  691. def __repr__(self):
  692. return str(self)
  693. class Axis(Shape):
  694. def __init__(self, bottom_point, length, label, below=True,
  695. rotation_angle=0, label_spacing=1./30):
  696. """
  697. Draw axis from bottom_point with `length` to the right
  698. (x axis). Place label below (True) or above (False) axis.
  699. Then return `rotation_angle` (in degrees).
  700. To make a standard x axis, call with ``below=True`` and
  701. ``rotation_angle=0``. To make a standard y axis, call with
  702. ``below=False`` and ``rotation_angle=90``.
  703. A tilted axis can also be drawn.
  704. The `label_spacing` denotes the space between the symbol
  705. and the arrow tip as a fraction of the length of the plot
  706. in x direction.
  707. """
  708. # Arrow is vertical arrow, make it horizontal
  709. arrow = Arrow(bottom_point, length, rotation_angle=-90)
  710. arrow.rotate(rotation_angle, bottom_point)
  711. spacing = drawing_tool.xrange*label_spacing
  712. if below:
  713. spacing = - spacing
  714. label_pos = [bottom_point[0] + length, bottom_point[1] + spacing]
  715. symbol = Text(label, position=label_pos)
  716. symbol.rotate(rotation_angle, bottom_point)
  717. self.shapes = {'arrow': arrow, 'symbol': symbol}
  718. class Gravity(Axis):
  719. """Downward-pointing gravity arrow with the symbol g."""
  720. def __init__(self, start, length):
  721. Axis.__init__(self, start, length, '$g$', below=False,
  722. rotation_angle=-90, label_spacing=1./30)
  723. def test_Axis():
  724. set_coordinate_system(xmin=0, xmax=15, ymin=0, ymax=15, axis=True)
  725. x_axis = Axis((7.5,2), 5, 'x', rotation_angle=0)
  726. y_axis = Axis((7.5,2), 5, 'y', below=False, rotation_angle=90)
  727. system = Compose({'x axis': x_axis, 'y axis': y_axis})
  728. system.draw()
  729. drawing_tool.display()
  730. set_linestyle('dashed')
  731. system.shapes['x axis'].rotate(40, (7.5, 2))
  732. system.shapes['y axis'].rotate(40, (7.5, 2))
  733. system.draw()
  734. drawing_tool.display()
  735. print repr(system)
  736. class Distance_wSymbol(Shape):
  737. """
  738. Arrow with symbol at the midpoint,
  739. for identifying a distance with a symbol.
  740. """
  741. def __init__(self, start, end, symbol, symbol_spacing=1/60., fontsize=14):
  742. start = arr2D(start)
  743. end = arr2D(end)
  744. mid = 0.5*(start + end) # midpoint of start-end line
  745. tangent = end - start
  746. normal = arr2D([-tangent[1], tangent[0]])/\
  747. sqrt(tangent[0]**2 + tangent[1]**2)
  748. symbol_pos = mid + normal*drawing_tool.xrange*symbol_spacing
  749. arrow = Arrow1(start, end, style='<->')
  750. arrow.set_linecolor('black')
  751. arrow.set_linewidth(1)
  752. self.shapes = {'arrow': arrow,
  753. 'symbol': Text(symbol, symbol_pos, fontsize=fontsize)}
  754. class ArcSymbol(Shape):
  755. def __init__(self, symbol, center, radius,
  756. start_angle, arc_angle,
  757. symbol_spacing=1/60.,
  758. resolution=180, fontsize=14):
  759. arc = Arc(center, radius, start_angle, arc_angle,
  760. resolution)
  761. mid = arr2D(arc(arc_angle/2.))
  762. normal = mid - arr2D(center)
  763. normal = normal/sqrt(normal[0]**2 + normal[1]**2)
  764. symbol_pos = mid + normal*drawing_tool.xrange*symbol_spacing
  765. self.shapes = {'arc': arc,
  766. 'symbol': Text(symbol, symbol_pos, fontsize=fontsize)}
  767. class Compose(Shape):
  768. def __init__(self, shapes):
  769. """shapes: list or dict of Shape objects."""
  770. self.shapes = shapes
  771. # can make help methods: Line.midpoint, Line.normal(pt, dir='left') -> (x,y)
  772. # list annotations in each class? contains extra annotations for explaining
  773. # important parameters to the constructor, e.g., Line.annotations holds
  774. # start and end as Text objects. Shape.demo calls shape.draw and
  775. # for annotation in self.demo: annotation.draw() YES!
  776. # Can make overall demo of classes by making objects and calling demo
  777. # Could include demo fig in each constructor
  778. class Arrow1(Shape):
  779. """Draw an arrow as Line with arrow."""
  780. def __init__(self, start, end, style='->'):
  781. arrow = Line(start, end)
  782. arrow.set_arrow(style)
  783. self.shapes = {'arrow': arrow}
  784. class Arrow3(Shape):
  785. """
  786. Build a vertical line and arrow head from Line objects.
  787. Then rotate `rotation_angle`.
  788. """
  789. def __init__(self, bottom_point, length, rotation_angle=0):
  790. self.bottom = bottom_point
  791. self.length = length
  792. self.angle = rotation_angle
  793. top = (self.bottom[0], self.bottom[1] + self.length)
  794. main = Line(self.bottom, top)
  795. #head_length = self.length/8.0
  796. head_length = drawing_tool.xrange/50.
  797. head_degrees = 30*pi/180
  798. head_left_pt = (top[0] - head_length*sin(head_degrees),
  799. top[1] - head_length*cos(head_degrees))
  800. head_right_pt = (top[0] + head_length*sin(head_degrees),
  801. top[1] - head_length*cos(head_degrees))
  802. head_left = Line(head_left_pt, top)
  803. head_right = Line(head_right_pt, top)
  804. head_left.set_linestyle('solid')
  805. head_right.set_linestyle('solid')
  806. self.shapes = {'line': main, 'head left': head_left,
  807. 'head right': head_right}
  808. # rotate goes through self.shapes so this must be initialized first
  809. self.rotate(rotation_angle, bottom_point)
  810. class Wheel(Shape):
  811. def __init__(self, center, radius, inner_radius=None, nlines=10):
  812. self.center = center
  813. self.radius = radius
  814. if inner_radius is None:
  815. self.inner_radius = radius/5.0
  816. else:
  817. self.inner_radius = inner_radius
  818. self.nlines = nlines
  819. outer = Circle(self.center, self.radius)
  820. inner = Circle(self.center, self.inner_radius)
  821. lines = []
  822. # Draw nlines+1 since the first and last coincide
  823. # (then nlines lines will be visible)
  824. t = linspace(0, 2*pi, self.nlines+1)
  825. Ri = self.inner_radius; Ro = self.radius
  826. x0 = self.center[0]; y0 = self.center[1]
  827. xinner = x0 + Ri*cos(t)
  828. yinner = y0 + Ri*sin(t)
  829. xouter = x0 + Ro*cos(t)
  830. youter = y0 + Ro*sin(t)
  831. lines = [Line((xi,yi),(xo,yo)) for xi, yi, xo, yo in \
  832. zip(xinner, yinner, xouter, youter)]
  833. self.shapes = [outer, inner] + lines
  834. class SineWave(Shape):
  835. def __init__(self, xstart, xstop,
  836. wavelength, amplitude, mean_level):
  837. self.xstart = xstart
  838. self.xstop = xstop
  839. self.wavelength = wavelength
  840. self.amplitude = amplitude
  841. self.mean_level = mean_level
  842. npoints = (self.xstop - self.xstart)/(self.wavelength/61.0)
  843. x = linspace(self.xstart, self.xstop, npoints)
  844. k = 2*pi/self.wavelength # frequency
  845. y = self.mean_level + self.amplitude*sin(k*x)
  846. self.shapes = {'waves': Curve(x,y)}
  847. class Spring1(Shape):
  848. spring_fraction = 1./2 # fraction of total length occupied by spring
  849. def __init__(self, start, length, tooth_width, num_teeth=8):
  850. """
  851. Specify a vertical spring, starting at bottom_point and
  852. having a specified lengths. In the middle third of the
  853. spring there are ntooths saw thooth tips.
  854. """
  855. B = start
  856. n = num_teeth - 1 # n counts teeth intervals
  857. # n must be odd:
  858. if n % 2 == 0:
  859. n = n+1
  860. L = length
  861. w = tooth_width
  862. # [0, x, L-x, L], f = (L-2*x)/L
  863. # x = L*(1-f)/2.
  864. shapes = {}
  865. f = Spring1.spring_fraction
  866. t = f*L/n # distance between teeth
  867. s = L*(1-f)/2. # start of spring
  868. P0 = (B[0], B[1]+s)
  869. shapes['line start'] = Line(B, P0)
  870. T1 = P0
  871. T2 = (T1[0] + w, T1[1] + t/2.0)
  872. k = 1
  873. shapes['line%d' % k] = Line(T1,T2)
  874. T1 = T2[:] # copy
  875. for i in range(2*n-3):
  876. T2 = (T1[0] + (-1)**(i+1)*2*w, T1[1] + t/2.0)
  877. k += 1
  878. shapes['line%d' % k] = Line(T1, T2)
  879. T1 = (T2[0], T2[1])
  880. T2 = (T1[0] + w, T1[1] + t/2.0)
  881. k += 1
  882. shapes['line%d' % k] = Line(T1,T2)
  883. P2 = (B[0], B[1]+L)
  884. shapes['line end'] = Line(T2, P2)
  885. self.shapes = shapes
  886. class Spring2(Shape):
  887. spring_fraction = 1./2 # fraction of total length occupied by spring
  888. def __init__(self, start, length, width, num_windings=11):
  889. """
  890. Specify a vertical spring, starting at bottom_point and
  891. having a specified lengths. In the middle third of the
  892. spring there are ntooths saw thooth tips.
  893. """
  894. B = start
  895. n = num_windings - 1 # n counts teeth intervals
  896. if n <= 6:
  897. n = 7
  898. # n must be odd:
  899. if n % 2 == 0:
  900. n = n+1
  901. L = length
  902. w = width
  903. # [0, x, L-x, L], f = (L-2*x)/L
  904. # x = L*(1-f)/2.
  905. shapes = {}
  906. f = Spring2.spring_fraction
  907. t = f*L/n # must be better worked out
  908. s = L*(1-f)/2. # start of spring
  909. P0 = (B[0], B[1]+s)
  910. shapes['line start'] = Line(B, P0)
  911. q = linspace(0, n, n*180 + 1)
  912. x = P0[0] + w*sin(2*pi*q)
  913. y = P0[1] + q*t
  914. shapes['sprial'] = Curve(x, y)
  915. P1 = (B[0], L-s)
  916. P2 = (B[0], B[1]+L)
  917. shapes['line end'] = Line(P1,P2)
  918. self.shapes = shapes
  919. class Dashpot(Shape):
  920. dashpot_fraction = 1./2 # fraction of total length occupied by dashpot
  921. piston_gap_fraction = 1./6
  922. piston_thickness_fraction = 1./8
  923. def __init__(self, start, length, width, piston_pos=None):
  924. """
  925. Specify a vertical dashpot of height `length`, width `width`,
  926. and `start` as bottom/starting point. The piston position,
  927. `piston_pos` can be specified, but default value None places
  928. it at 1/3 from the bottom of the dashpot.
  929. """
  930. B = start
  931. L = length
  932. w = width
  933. # [0, x, L-x, L], f = (L-2*x)/L
  934. # x = L*(1-f)/2.
  935. shapes = {}
  936. f = Dashpot.dashpot_fraction
  937. s = L*(1-f)/2. # start of dashpot
  938. P0 = (B[0], B[1]+s)
  939. P1 = (B[0], B[1]+L-s)
  940. P2 = (B[0], B[1]+L)
  941. shapes['line start'] = Line(B, P0)
  942. shapes['pot'] = Curve([P1[0]-w, P0[0]-w, P0[0]+w, P1[0]+w],
  943. [P1[1], P0[1], P0[1], P1[1]])
  944. piston_thickness = f*L*Dashpot.piston_thickness_fraction
  945. if piston_pos is None:
  946. piston_pos = P0[1] + 1/3.*f*L
  947. print 'Calculated piston position:', piston_pos, P0[1], 1/3.*f*L
  948. if piston_pos < P0[1]:
  949. piston_pos = P0[1]
  950. print 'too small piston position, <', P0[1]
  951. if piston_pos > P1[1]-piston_thickness:
  952. print 'too large piston position, >', P1[1]-piston_thickness
  953. piston_pos = P1[1]-piston_thickness
  954. gap = w*Dashpot.piston_gap_fraction
  955. shapes['piston'] = Compose(
  956. {'line': Line(P2, (B[0], piston_pos + piston_thickness)),
  957. 'rectangle': Rectangle((B[0] - w+gap, piston_pos),
  958. 2*w-2*gap, piston_thickness),
  959. })
  960. shapes['piston']['rectangle'].set_filled_curves(pattern='X')
  961. self.shapes = shapes
  962. # COMPOSITE types:
  963. # MassSpringForce: Line(horizontal), Spring, Rectangle, Arrow/Line(w/arrow)
  964. # must be easy to find the tip of the arrow
  965. # Maybe extra dict: self.name['mass'] = Rectangle object - YES!
  966. def _test1():
  967. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  968. l1 = Line((0,0), (1,1))
  969. l1.draw()
  970. input(': ')
  971. c1 = Circle((5,2), 1)
  972. c2 = Circle((6,2), 1)
  973. w1 = Wheel((7,2), 1)
  974. c1.draw()
  975. c2.draw()
  976. w1.draw()
  977. hardcopy()
  978. display() # show the plot
  979. def _test2():
  980. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  981. l1 = Line((0,0), (1,1))
  982. l1.draw()
  983. input(': ')
  984. c1 = Circle((5,2), 1)
  985. c2 = Circle((6,2), 1)
  986. w1 = Wheel((7,2), 1)
  987. filled_curves(True)
  988. set_linecolor('blue')
  989. c1.draw()
  990. set_linecolor('aqua')
  991. c2.draw()
  992. filled_curves(False)
  993. set_linecolor('red')
  994. w1.draw()
  995. hardcopy()
  996. display() # show the plot
  997. def _test3():
  998. """Test example from the book."""
  999. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1000. l1 = Line(start=(0,0), stop=(1,1)) # define line
  1001. l1.draw() # make plot data
  1002. r1 = Rectangle(lower_left_corner=(0,1), width=3, height=5)
  1003. r1.draw()
  1004. Circle(center=(5,7), radius=1).draw()
  1005. Wheel(center=(6,2), radius=2, inner_radius=0.5, nlines=7).draw()
  1006. hardcopy()
  1007. display()
  1008. def _test4():
  1009. """Second example from the book."""
  1010. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1011. r1 = Rectangle(lower_left_corner=(0,1), width=3, height=5)
  1012. c1 = Circle(center=(5,7), radius=1)
  1013. w1 = Wheel(center=(6,2), radius=2, inner_radius=0.5, nlines=7)
  1014. c2 = Circle(center=(7,7), radius=1)
  1015. filled_curves(True)
  1016. c1.draw()
  1017. set_linecolor('blue')
  1018. r1.draw()
  1019. set_linecolor('aqua')
  1020. c2.draw()
  1021. # Add thick aqua line around rectangle:
  1022. filled_curves(False)
  1023. set_linewidth(4)
  1024. r1.draw()
  1025. set_linecolor('red')
  1026. # Draw wheel with thick lines:
  1027. w1.draw()
  1028. hardcopy('tmp_colors')
  1029. display()
  1030. def _test5():
  1031. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1032. c = 6. # center point of box
  1033. w = 2. # size of box
  1034. L = 3
  1035. r1 = Rectangle((c-w/2, c-w/2), w, w)
  1036. l1 = Line((c,c-w/2), (c,c-w/2-L))
  1037. linecolor('blue')
  1038. filled_curves(True)
  1039. r1.draw()
  1040. linecolor('aqua')
  1041. filled_curves(False)
  1042. l1.draw()
  1043. hardcopy()
  1044. display() # show the plot
  1045. def rolling_wheel(total_rotation_angle):
  1046. """Animation of a rotating wheel."""
  1047. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1048. import time
  1049. center = (6,2)
  1050. radius = 2.0
  1051. angle = 2.0
  1052. pngfiles = []
  1053. w1 = Wheel(center=center, radius=radius, inner_radius=0.5, nlines=7)
  1054. for i in range(int(total_rotation_angle/angle)):
  1055. w1.draw()
  1056. print 'XXXXXXXXXXXXXXXXXXXXXX BIG PROBLEM WITH ANIMATE!!!'
  1057. display()
  1058. filename = 'tmp_%03d' % i
  1059. pngfiles.append(filename + '.png')
  1060. hardcopy(filename)
  1061. time.sleep(0.3) # pause
  1062. L = radius*angle*pi/180 # translation = arc length
  1063. w1.rotate(angle, center)
  1064. w1.translate((-L, 0))
  1065. center = (center[0] - L, center[1])
  1066. erase()
  1067. cmd = 'convert -delay 50 -loop 1000 %s tmp_movie.gif' \
  1068. % (' '.join(pngfiles))
  1069. print 'converting PNG files to animated GIF:\n', cmd
  1070. import commands
  1071. failure, output = commands.getstatusoutput(cmd)
  1072. if failure: print 'Could not run', cmd
  1073. if __name__ == '__main__':
  1074. #rolling_wheel(40)
  1075. #_test1()
  1076. #_test3()
  1077. funcs = [
  1078. #test_Axis,
  1079. test_inclined_plane,
  1080. ]
  1081. for func in funcs:
  1082. func()
  1083. raw_input('Type Return: ')