Second python lab sample solution

lab2.answer.py  [download]

# Weird import for compatibility across Python 2 and Python 3
try:   
	import tkinter   
except ImportError:
	import Tkinter as tkinter   

import math

#
# PLANE CLASS
#

class Plane ():
	# Sort of classvars
	_width = 40
	_height = 30
	_labelx = 20
	_labely = 10

	def __init__(self, map, x, y, altitude, direction, speed):
		# Stash args as ivars
		self._map = map
		self._x = x
		self._y = y
		self._altitude = altitude
		self._direction = direction
		self._speed = speed

		# Install us in map
		self._map.append (self)

		# Make our graphical object
		# NB self._shape is just an ID number not an object,
		# so we HAVE a shape, not ARE a shape
		self._shape = canvas.create_rectangle (
			self._x, self._y,
			self._x + self._width, self._y + self._height,
			fill="blue",
			outline="",
			width="3",
			tags="plane")

		# Also make text item for our altitude label
		self._label = canvas.create_text (
			self._x + self._labelx, self._y + self._labely,
			text=str(altitude))
			
		# Bind callback
		canvas.tag_bind (self._shape, '<Button-1>', self._onclick)
		canvas.tag_bind (self._label, '<Button-1>', self._onclick)

	# Mouse callback for this particular plane
	def _onclick (self, event):
		# Unselect and dehighlight previously selected plane if any
		canvas.itemconfigure ("selected", outline="")
		canvas.dtag ("plane", "selected")

		# Then select and fill me
		canvas.addtag_withtag ("selected", self._shape)
		canvas.itemconfigure (self._shape, outline="black")

		# Also remember me in case map needs to delete me
		map.selectedPlane = self

	# Map must also delete us from its master list
	def delete (self):
		canvas.delete(self._shape)
		canvas.delete(self._label)

	# Timer callback dispatched to us
	def tick (self):
		# Convert polar to rectangular
		self._x += map.speedFactor * self._speed * math.cos (math.radians(360 + 90 - self._direction))
		self._y -= map.speedFactor * self._speed * math.sin (math.radians(360 + 90 - self._direction))

		# Wrap at canvas edges
		if self._x < 0:
			self._x += canvas.winfo_width()
		elif self._x > canvas.winfo_width():
			self._x -= canvas.winfo_width()

		if self._y < 0:
			self._y += canvas.winfo_width()
		elif self._y > canvas.winfo_width():
			self._y -= canvas.winfo_width()

		# Change location of actual shape to what we just chose
		canvas.coords (self._shape, 
			self._x, self._y, 
			self._x + self._width, self._y + self._height)
		
		canvas.coords (self._label,
			self._x + self._labelx, self._y + self._labely)
		
#
# HELICOPTER CLASS
#

class Helicopter(Plane):
	def __init__(self, map, x, y, direction, speed):
		# Altitude always = 1000 
		Plane.__init__ (self, map, x, y, 1000, direction, speed)

		# We tweak the one created by Plane's constructor
		canvas.itemconfigure (self._shape, fill="red")

#
# MAP CLASS
#

class Map():
	# Just a cheapo enum type
	CREATEPLANE, CREATEHELICOPTER = range(0, 2)

	def __init__(self):
		self._planes = list()

		self.animationRunning = False

		self.speedFactor = 1

		# UI state flag for upcoming action
		self.nextAction = None
		self.selectedPlane = None

	def append(self, plane):
		self._planes.append (plane)

	# Button callback
	def deleteSelected(self):
		if self.selectedPlane != None:
			self.selectedPlane.delete()
			self._planes.remove(self.selectedPlane)
			self.selectedPlane = None
		self.nextAction = None

	# Receive tick, pass it to our planes
	def tick(self):
		for p in self._planes:
			p.tick()

	# Set up callback for map
	def mapCallback (self, event):
		# Convert from window coordinates to canvas coordinates
		canvas = event.widget
		x = canvas.canvasx(event.x)
		y = canvas.canvasy(event.y)

		if self.nextAction == self.CREATEPLANE:
			Plane (self, x, y, 9900, 1, 1)

		elif self.nextAction == self.CREATEHELICOPTER:
			Helicopter (self, x, y, 1, 1)

		# Cancel the flag
		self.nextAction = None

#
# MAIN PROGRAM 
#

# Main window
top = tkinter.Tk()

# Map
map = Map()

# Canvas, in global variable
canvas = tkinter.Canvas (top, height=400, width=400, background = "lightblue")
canvas.pack (fill="both", expand=1)
canvas.bind('<Button-1>', map.mapCallback)

# Create some planes in the map
Plane (map, 30, 20, 2000, 0, 1)
Plane (map, 100, 20, 3300, 45, 5)
Plane (map, 140, 40, 4400, 90, 2)
Helicopter (map, 200, 30, 180, 1)
Helicopter (map, 240, 60, 300, 3)

# Control panel area
controlPanel = tkinter.Frame (top, borderwidth=2, background="grey")
controlPanel.pack (side="top")

# Buttons in controlPanel
def startButtonCallback ():
	map.animationRunning = True
	tick()
startButton = tkinter.Button (controlPanel)
startButton["text"] = "Start animation"
startButton["command"] = startButtonCallback
startButton.pack(side="left")

def stopButtonCallback ():
	map.animationRunning = False
stopButton = tkinter.Button (controlPanel)
stopButton["text"] = "Stop animation"
stopButton["command"] = stopButtonCallback
stopButton.pack(side="left")

deleteButton = tkinter.Button (controlPanel)
deleteButton["text"] = "Delete selected"
deleteButton["command"] = map.deleteSelected
deleteButton.pack(side="left")

def planeButtonCallback ():
	map.nextAction = map.CREATEPLANE
planeButton = tkinter.Button (controlPanel)
planeButton["text"] = "Create plane"
planeButton["command"] = planeButtonCallback
planeButton.pack(side="left")
	
def helicopterButtonCallback ():
	map.nextAction = map.CREATEHELICOPTER
helicopterButton = tkinter.Button (controlPanel)
helicopterButton["text"] = "Create helicopter"
helicopterButton["command"] = helicopterButtonCallback
helicopterButton.pack(side="left")

def speedBarCallback (self):
	# Apparently it is called with self = current value
	map.speedFactor = float(self)
speedBar = tkinter.Scale(controlPanel, from_=0.1, to=3.0, resolution = 0.1, orient="horizontal")
speedBar.set (1.0)
speedBar["label"] = "Speed"
speedBar["command"] = speedBarCallback
speedBar.pack (side="left")

# Set up callback in 100 ms
# Receive tick, pass it to objects that need it,
# then set up the next callback
def tick ():
	if map.animationRunning:
		map.tick()
		top.after(100, tick)

top.mainloop()