shapes.py 38 KB

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