view visualclient2/visclient.py @ 54:c0b34039917a tip

Server: added an exposed function to log the start time of a performance (for log-to-audio sync)
author Mathieu Barthet <mathieu.barthet@eecs.qmul.ac.uk>
date Wed, 14 Oct 2015 19:20:08 +0100
parents 2db17c224664
children 874ac833c8e3
line wrap: on
line source
#!/usr/bin/env python
# encoding: utf-8
"""
visclient.py

Created by George Fazekas on 2012-06-17.
Copyright (c) 2012 . All rights reserved.
"""

import sys,os,math,time,copy
import pygame as pg
from pygame.locals import *
from pygame import gfxdraw as gd
import httplib as ht

import gradients
from gradients import genericFxyGradient

from threading import Thread
from random import random

import colorsys as cs

scol   = (0,255,0,255)
ecol   = (0,0,0,255)

xavg=0
yavg=0
countavg=0
moods = []


# X,Y=1140,900
# X,Y = 600,400
X,Y = 800,600
# X,Y = 1024,768

# Fullscreen resolution:
# XF,YF = 1280,900
# XF,YF = 1440,900
XF,YF = 1344,900
# display calibrated

# detect display resolution
import subprocess
screenres = subprocess.Popen('xrandr | grep "\*" | cut -d" " -f4',shell=True, stdout=subprocess.PIPE).communicate()[0]
screenres = map(lambda x: int(x.strip()), screenres.split('x'))
XF,YF = screenres
print "Screen resolution: ",XF,YF


# NBLOBS = 18
# BLOBSIZE = 25
# G=110
# FADE = 15
# DIST = 0.15 # blob equivalence tolerance
# FRAMERATE = 60

# new parameters
NBLOBS = 18
BLOBSIZE = 25
G=110
FADE = 25
DIST = 0.10 # blob equivalence tolerance
FRAMERATE = 120


# Connection:
# IP = "127.0.0.1:8030"
# IP = "192.168.2.158:8030"
IP = "138.37.95.215" # this is the IP of kakapo<=>golden
HTTP_TIMEOUT = 3
SERVER_UPDATE_INTERVAL = 0.8


class Indicator(object):
	
	
	def __init__(self,bg,pos,invisible=False):

		self.off_color = pg.Color(110,0,0)
		self.on_color = pg.Color(0,120,0)
		if invisible :
			self.off_color = pg.Color(0,0,0)
			self.on_color = pg.Color(0,0,120)
		self.visible = True
		self.ison = True
		self.x,self.y = pos
		self.xs = int(self.x * X)
		self.ys = int(Y - (self.y * Y))
		self.c = self.off_color
		self.size = 6
		self.bg = bg
		
	def reinit(self,bg):
		self.bg = bg
		self.xs = int(self.x * X)
		self.ys = int(Y - (self.y * Y))
		
	def draw(self):
		if self.visible :
			# pg.draw.circle(self.bg, self.c, (self.xs,self.ys),self.size,0)
			# gd.aacircle(self.bg, self.xs, self.ys, self.size+1, self.c)
			gd.filled_circle(self.bg, self.xs, self.ys, self.size, self.c)
			gd.aacircle(self.bg, self.xs, self.ys, self.size, self.c)
			
		
	def toggle(self):
		if self.ison == True :
			self.off()
		else :
			self.on()
		return self
			
	def on(self):
		self.c = self.on_color			
		self.ison = True
		return self
		
	def off(self):
		self.c = self.off_color
		self.ison = False
		return self
		
	def now(self,screen):
		# for i in self.indicators.itervalues() :
		# 	i.draw()
		self.draw()
		screen.blit(self.bg, (0, 0))
		pg.display.flip()
		return self

class MovingBlob(object):
	
	black_color = pg.Color(0,0,0)

	def __init__(self,bg,x,y,color=(255,255,255),mood=None,fade=FADE):
		self.x = x
		self.y = y
		self.xs = x * X
		self.ys = Y - (y * Y)
		self.bg = bg
		self.size = BLOBSIZE
		self.time = time.time()
		self.alpha = 255
		self.c = color
		self.count = 1
		self.visible = True
		self.FADE = fade
		self.mood = mood
		if mood and mood.color :
			self.c = mood.color
			self.title = mood.word
		self.speed_factor = 80.0
		self.target_x = 0.0
		self.target_y = 0.0
		
	def set_target(self,x,y):
		self.target_x = x
		self.target_y = y
		
	def draw(self):
		if not self.visible : return

		global xavg,yavg,moods
		# xspeed = (xavg - self.x) / self.speed_factor
		# yspeed = (yavg - self.y) / self.speed_factor
		# self.x = self.x + xspeed
		# self.y = self.y + yspeed
		self.x = self.x + (self.target_x - self.x) / self.speed_factor
		self.y = self.y + (self.target_y - self.y) / self.speed_factor
		
		#self.size = countavg//TODO
		self.xs = self.x * X
		self.ys = Y - (self.y * Y)
		#d=int(self.size)

		d = cd = sys.float_info.max
		
		for mood in moods :
			d = mood.get_distance(self.x,self.y)
			if d < cd :
				cd = d
				self.mood = mood
				self.c = mood.color
				self.title = mood.word

		# self.bg.blit(gradients.radial(self.size, (self.c[0],self.c[1],self.c[2],self.alpha), (0,0,0,self.alpha)), (self.xs-BLOBSIZE,self.ys-BLOBSIZE))
		d=int(self.size)
		# print d
		self.bg.blit(gradients.radial(self.size, (self.c[0],self.c[1],self.c[2],self.alpha), (0,0,0,self.alpha)), (self.xs-d,self.ys-d))
		gd.aacircle(self.bg, int(self.xs), int(self.ys), int(self.size), self.black_color)
		gd.aacircle(self.bg, int(self.xs), int(self.ys), int(self.size-1), self.black_color)
		
			
		font = pg.font.Font(None, 36)
		text = font.render(self.title, 1, self.c)
		textpos = text.get_rect()
		#print textpos.width	
		if self.xs > X- textpos.width:	
			if self.ys > Y- textpos.height:	
				self.bg.blit(text, (self.xs - textpos.width,self.ys - textpos.height))
			else:
				self.bg.blit(text, (self.xs - textpos.width,self.ys))
		
		else :	
			if self.ys > Y- textpos.height:	
				self.bg.blit(text, (self.xs,self.ys - textpos.height))
			else: 
				self.bg.blit(text, (self.xs,self.ys))
		#self.bg.blit(gradients.radial(self.size, (self.c[0],self.c[1],self.c[2],self.alpha), (0,0,0,self.alpha)), (self.xs-d,self.ys-d))
		#self.alpha = 255 - int(self.age()*self.FADE) 
		#if self.alpha < 5 : 
		#	self.alpha = 1
		#	self.visible = False
				
	def age(self):
		return time.time() - self.time
		
	def resize(self,count):
		if self.count == count :
			return None
		self.time = time.time()
		self.count = count
		# self.size = int(BLOBSIZE * int(self.count/1.5))
		self.to = int(BLOBSIZE * int(self.count/1.5))
		if self.to < BLOBSIZE :
			self.to = BLOBSIZE
		if self.to and self.size != self.to:
			self.start_animate()
			# print "resize to",count,self.to			
		
	def get_distance(self,x,y):
		return math.sqrt( math.pow((self.x-x),2) + math.pow((self.y-y),2) )
		
	def fade(self,fade):
		if not fade : self.alpha = 255
		self.FADE = fade
		
	def reset_time(self):
		self.time = time.time()

	def reinit(self,bg):
		self.bg = bg
		self.xs = int(self.x * X)
		self.ys = int(Y - (self.y * Y))
		
	def start_animate(self):
		self.thread = Thread(target = self.animate)
		self.thread.daemon = True
		self.thread.start()
		
	def animate(self):
		'''Animate the way bubbles are grown.'''
		tolerance = 5
		# while self.size > self.to-tolerance and self.size < self.to+tolerance :		
		while self.size != self.to :
			self.size = int(self.size + int(self.to-self.size) * 0.1)
			time_inc = 20.0 / (pow(1.2, abs(self.to-self.size)) * 200.0)
			time.sleep(0.02+time_inc)
			# print "sizing to ", self.to


class Blob(object):
	
	def __init__(self,bg,x,y,color=(255,255,255),mood=None,fade=FADE):
		self.x = x
		self.y = y
		self.xs = x * X
		self.ys = Y - (y * Y)
		self.bg = bg
		self.size = BLOBSIZE #pixels
		self.time = time.time() #s
		self.const_time = time.time() #s
		self.alpha = 255 #8-bit alpha channel value
		self.c = color #RGB colour 3-tuple
		self.count = 1
		self.visible = True
		self.FADE = fade
		if mood and mood.color :
			self.c = mood.color
			self.title = mood.word	
		self.inactivity_delay = 75.0 #s
		self.proximity_delay = 35.0 #s
		self.proximity_tolerance = 0.13
		self.target_in_proximity = None

	def __cmp__(self,other):
		if other is None :
			return -1		
		d = math.sqrt( math.pow((self.x-other.x),2) + math.pow((self.y-other.y),2) )
		if d < DIST : 
			return 0
		else :
			return -1
			
	def object_in_proximity(self,other):
		if other is None :
			return False		
		d = math.sqrt( math.pow((self.x-other.x),2) + math.pow((self.y-other.y),2) )
		if d < self.proximity_tolerance :
			return True
		else :
			return False
			
	def check_target_proximity(self,target):
		'''Check if the moving bubble is in proximity of this object. As soon as it is there, mark the time.
		We do not want to reset this time just wait until this bubble dies due to inactivity in its region.'''
		prox = self.object_in_proximity(target)
		if self.target_in_proximity is None and prox is True :
			self.target_in_proximity = time.time()
		# if prox is False :
		# 	self.target_in_proximity = None		
		
	def draw(self):
		if not self.visible : return

		d=int(self.size)
		self.bg.blit(gradients.radial(self.size, (self.c[0],self.c[1],self.c[2],self.alpha), (0,0,0,self.alpha)), (self.xs-d,self.ys-d))
		self.alpha = 255 - int(self.age()*self.FADE) 
		if self.alpha < 5 : 
			self.alpha = 1
			self.visible = False
				
	def age(self):
		return time.time() - self.time
		
	def real_age(self):
		return time.time() - self.const_time
		
	def age_weight(self):				
		age_s = time.time() - self.const_time 
		if age_s < self.inactivity_delay :
			return 1.0
		return 1.0 / ((age_s-self.inactivity_delay+1)*10.0)

	def proximity_weight(self):
		if self.target_in_proximity == None :
			return 1.0
		age_s = time.time() - self.target_in_proximity
		if age_s < self.proximity_delay :
			return 1.0
		return 1.0 / ((age_s-self.proximity_delay+1)*10.0)	
		
	def increment(self,count):
		self.time = time.time()
		self.count = count
		# self.size = int(BLOBSIZE * int(self.count/1.5))
		self.to = int(BLOBSIZE * int(self.count/1.5))
		if self.to < 250 :
			self.start_animate()
		
	def get_distance(self,x,y):
		return math.sqrt( math.pow((self.x-x),2) + math.pow((self.y-y),2) )
		
	def fade(self,fade):
		if not fade : self.alpha = 255
		self.FADE = fade
		
	def reset_time(self):
		self.time = time.time()
		
	def start_animate(self):
		self.thread = Thread(target = self.animate)
		self.thread.daemon = True
		self.thread.start()
		
	def animate(self):
		'''Animate the way bubbles are grown.'''
		while self.size < self.to :
			self.size += 1
			time_inc = 20.0 / (pow(1.2, self.to-self.size) * 200.0)
			time.sleep(0.02+time_inc)
			
	def force(self,other):
		'''Calculate the force between this object and another'''
		if other is None :
			return 0.0,0.0,False	 
		captured = False
		ds = math.pow((self.x-other.x),2) + math.pow((self.y-other.y),2)
		if ds < 0.005 :
			return 0.0,0.0,False
		d = math.sqrt(ds)
		if d < 0.07 :
			captured = True
			# return 0.0,0.0,False
		m1,m2 = self.size,other.size
		G = 6.674 * 0.000001
		f = - G * (m1*m2) / ds
		x = f * (other.x-self.x) / d
		y = f * (other.y-self.y) / d
		return x,y,captured


class BlobTrail(object):
	
	def __init__(self,bg,x,y,color=(255,255,255),mood=None,fade=FADE):
		self.x = x
		self.y = y
		self.xs = x * X
		self.ys = Y - (y * Y)
		self.bg = bg
		self.size = BLOBSIZE
		self.time = time.time()
		self.alpha = 255
		self.c = color
		self.count = 1
		self.visible = True
		self.FADE = fade
		if mood and mood.color :
			self.c = mood.color
			self.title = mood.word	

	def __cmp__(self,other):
		d = math.sqrt( math.pow((self.x-other.x),2) + math.pow((self.y-other.y),2) )
		if d < DIST : 
			return 0
		else :
			return -1
		
	def draw(self):
		if not self.visible : return

		d=int(self.size)
		self.bg.blit(gradients.radial(self.size, (self.c[0],self.c[1],self.c[2],self.alpha), (0,0,0,0)), (self.xs-d,self.ys-d))
		self.alpha = 255 - int(self.age()*self.FADE) 
		if self.alpha < 5 : 
			self.alpha = 1
			self.visible = False
				
	def age(self):
		return time.time() - self.time
		
	def increment(self,count):
		self.time = time.time()
		self.count = count
		# self.size = int(BLOBSIZE * int(self.count/1.5))
		self.to = int(BLOBSIZE * int(self.count/1.5))
		self.start_animate()
		
	def get_distance(self,x,y):
		return math.sqrt( math.pow((self.x-x),2) + math.pow((self.y-y),2) )
		
	def fade(self,fade):
		if not fade : self.alpha = 255
		self.FADE = fade
		
	def reset_time(self):
		self.time = time.time()
		
	def start_animate(self):
		self.thread = Thread(target = self.animate)
		self.thread.daemon = True
		self.thread.start()
		
	def animate(self):
		'''Animate the way bubbles are grown.'''
		while self.size < self.to :
			self.size += 1
			time_inc = 20.0 / (pow(1.2, self.to-self.size) * 200.0)
			time.sleep(0.02+time_inc)


class Mood():

	def __init__(self,word,x,y):
		self.word = word
		self.x = (float(x)/ 2.0) + 0.5
		self.y = (float(y)/ 2.0) + 0.5
		self.color = []
		
	def get_distance(self,x,y):
		return math.sqrt( math.pow((self.x-x),2) + math.pow((self.y-y),2) )
		
	def __repr__(self):
		return "Mood(%s,%3.2f,%3.2f)" %(self.word,self.x,self.y)
	


class VisualClient(object):
	'''Main visualisation client.'''
	global moods,blobTrail

	def __init__(self):
		# self.conn = ht.HTTPConnection("192.168.2.184:8030")
		# self.conn = ht.HTTPConnection("138.37.95.215")
		self.s_age = 10
		self.s_dist = DIST
		self.s_ninp = 18
		
		pg.init()
		pg.font.init()

		# fontObj = pg.font.Font("freesansbold.ttf",18)
		white = ( 255, 255, 255)
		black = ( 0,0,0)
		
		self.fpsClock = pg.time.Clock()
		self.screen = pg.display.set_mode((X, Y))
		pg.display.set_caption('Mood Conductor')
		self.bg = pg.Surface(self.screen.get_size())
		self.bg = self.bg.convert()
		self.bg.fill((0,0,0))
		pg.display.set_gamma(100.0)
		
		
		self.scol   = (0,255,0,255)
		self.ecol   = (0,0,0,255)
		coordstxt = "test"
		
		self.blobs = []
		#self.moods = []
		self.bt=[]

		self.movingBlob = None

		self.read_mood_data()
		
		self.FADE = FADE
		
		self.indicators = {
		"conn":Indicator(self.bg,(0.98,0.02)), 			# connection active
		"update":Indicator(self.bg,(0.96,0.02)), 		# update thread executing
		"data":Indicator(self.bg,(0.94,0.02)), 			# data status changed
		"receive":Indicator(self.bg,(0.92,0.02)), 		# data received
		"grow":Indicator(self.bg,(0.90,0.02)), 			# blob growth active
		"ignore":Indicator(self.bg,(0.88,0.02),True), 	# little AI: ignore some clusters in certain condition
		"suspend":Indicator(self.bg,(0.86,0.02),True), 	# prevent adding new blobs (key: d)
		"config":Indicator(self.bg,(0.84,0.02),True), 	# sending config data
		"fade":Indicator(self.bg,(0.82,0.02),True)} 	# fade on/off (key: s)
		
		self.thread = None		
		self.running = False
		self.fullscreen = False
		self.init_reconnect = False
		self.suspend = False
		
		pass
		
	
	def read_mood_data(self):
		'''Read the mood position and color information form csv file.'''
		# file = 'moods.csv'
		file = '../tags/mc_moodtags_lfm_curated2.csv'
		with open(file) as mf:
			data = mf.readlines()[1:]
			for line in data :
				l = line.split(',')
				l = map(lambda x:x.replace("'","").strip(),l)
				mood = Mood(l[0],l[1],l[2])
				moods.append(mood)
		print moods
		for mood in moods:
			print"['%s',%2.2f,%2.2f,0,0]," %(mood.word,mood.x,mood.y)

		with open('colors.txt') as ff:
			data = ff.readlines()[1:]
			data = map(lambda x: x.split(','),data)
			for mood in moods :
				d = cd = sys.float_info.max
				for colors in data :
					d = mood.get_distance(float(colors[0]),float(colors[1]))
					if d < cd :
						cd = d
						# mood.color = tuple(map(lambda x: int(pow(math.atan((float(x)/7.0)),12.5)),(colors[2],colors[3],colors[4])))
						mood.color = self.set_color(tuple(map(lambda x: int(x),(colors[2],colors[3],colors[4]))))
		return True
	
	def set_color(self,color):
		'''Move to HLS colour space and manipulate saturation there.'''
		# TODO: ideally, we need a non-linear compressor of the lightness and saturation values
		r,g,b = map(lambda x: (1.0*x/255.0), color)
		h,l,s = cs.rgb_to_hls(r,g,b)
		s = 1.0 #1.0 - (1.0 / pow(50.0,s))
		l = 1.0 - (1.0 / pow(20.0,l)) #0.6
		r,g,b = map(lambda x: int(x*255), cs.hls_to_rgb(h,l,s))
		return r,g,b
		
	def start_update_thread(self):
		'''Start the thread that reads data from the server.'''
		time.sleep(SERVER_UPDATE_INTERVAL)
		self.thread = Thread(target = self.update_thread)
		self.thread.daemon = True
		self.running = True
		self.thread.start()
		print "OK. Update thread started."
		
	def stop_update_thread(self):
		'''Stop the thread and allow some time fot the connections to close.'''
		self.running = False
		try :
			self.thread.join(2)
			self.indicators["conn"].off()
			self.indicators["update"].off()
		except :
			print "No update thread to join."

	def update_thread(self):
		'''The server update thread'''
		print "Thread reporting..."
		while self.running :
			# self.update()
			try :
				self.update()
				# self.indicators["update"].visible = True
			except Exception, e:
				if str(e).strip() : print "Exception: ", str(e), type(e), len(str(e).strip())
				self.indicators["conn"].off()
				# self.indicators["update"].visible = False
			try :
				time.sleep(SERVER_UPDATE_INTERVAL)
			except :
				if str(e).strip() : print "Exception: ", str(e), type(e), len(str(e).strip())
				self.indicators["conn"].off()

		
	def update(self):
		'''Update the blob list from the server. This should be in a thread.'''
		# global xavg, yavg, countavg
		# indicate connection health by toggling an indictor
		self.indicators["update"].toggle()
		
		# delete invisibles
		for blob in self.blobs :
			if not blob.visible :
				self.blobs.remove(blob)
				
		# get new coordinates from the server
		self.conn.putrequest("GET","/moodconductor/result", skip_host=True)
		self.conn.putheader("Host", "www.isophonics.net")
		self.conn.endheaders()
		res = self.conn.getresponse()
		data = res.read()
		data = eval(data)		
		if not data : 
			self.conn.close()
			self.indicators["data"].toggle()
			return False

		tempx = 0
		tempy = 0
		tempcount = 0

		for d in data :
			# coordstxt = "x:%s y:%s c:%s" %d
			x,y,count = d
			
			tempx = tempx + x*count
			tempy = tempy + y*count
			tempcount = tempcount + count

			self.add_blob(x,y,count)
			self.indicators["receive"].toggle()
		
		xavg = tempx/tempcount
		yavg = tempy/tempcount
		countavg = tempcount/len(data)
		# print xavg, yavg, countavg
		# if not self.blobs :
		# 	self.add_blob(xavg,yavg,countavg)
		# self.indicators["receive"].toggle()

		self.conn.close()
		self.blobs = self.blobs[:NBLOBS]
		return True
		
		
	def add_blob(self,x,y,count=1):
		'''Insert a blob to the list of blobs'''
		# find mood correxponding to x,y
		if self.suspend :
			return None
		cmood = None
		d = cd = sys.float_info.max
		for mood in moods :
			d = mood.get_distance(x,y)
			if d < cd :
				cd = d
				cmood = mood
		# create new blob or increase click count on existing one
		new = Blob(self.bg,x,y,mood=cmood,fade=self.FADE)
		if self.movingBlob == None :
			self.movingBlob = MovingBlob(self.bg,x,y,mood=cmood,fade=self.FADE)
		if not new in self.blobs :
			self.blobs.insert(0,new)
		elif count > self.blobs[self.blobs.index(new)].count:
			self.blobs[self.blobs.index(new)].increment(count)
			self.indicators["grow"].toggle()
		pass

		
	def draw(self):
		'''Draw all objects'''
		global xavg, yavg, countavg
		
		self.bg.fill((0,0,0,1))
		# self.bg.blit(gradients.radial(19, self.scol, self.ecol), (rect_x,rect_y))
		forces = []
		l = copy.copy(self.blobs)
		l.reverse()
		xt,yt = 0.0,0.0
		bs = 1
		c = 1
		# captured_by = None
		
		# calculate exponential weighted average of the visible blobs
		ignore = False
		for blob in l :
			blob.draw()
			c = c + blob.count
			# aw = blob.age_weight()
			aw = blob.proximity_weight()
			if aw < 1.0 : ignore = True
			w = math.pow(blob.size+(blob.alpha/2.0),7) * aw
			xt = xt + blob.x * w
			yt = yt + blob.y * w
			bs = bs + w
			if self.movingBlob != None :
				blob.check_target_proximity(self.movingBlob)
		xavg = xt / bs
		yavg = yt / bs
		# countavg = bs/(len(l)+1)
		countavg = int(c/(len(l)+1))
		if ignore :
			self.indicators["ignore"].on()
		else :
			self.indicators["ignore"].off()
		
		# compute gravity force
		# 	if self.movingBlob != None :
		# 		x,y,c = blob.force(self.movingBlob)
		# 		forces.append((x,y))
		# 		if c : captured_by = blob
		# tx,ty = reduce(lambda a,b:(a[0]+b[0],a[1]+b[1]), forces, (0.5,0.5))

		# print tx,ty
		# if tx>1.0 : tx = 1.0
		# if tx<0.0 : tx = 0.0
		# if ty>1.0 : ty = 1.0
		# if ty<0.0 : ty = 0.0
		
		# xavg,yavg = tx,ty		
		
		# if tx <= 1.0 and tx >= 0.0 and ty <= 1.0 and ty >= 0.0 :
		# 	xavg,yavg = tx,ty
		# 	countavg = 15
		# 	print tx,ty
		# else :
		# 	print "out of bounds:",tx,ty
		# if captured_by != None :
		# 	xavg,yavg = captured_by.x,captured_by.y
		l = copy.copy(self.bt)
		l.reverse()
		for trail in l:
			trail.draw()
		if self.movingBlob != None :
			# self.movingBlob.resize(countavg)
			self.movingBlob.set_target(xavg,yavg)
			self.movingBlob.draw()
			new = BlobTrail(self.bg,self.movingBlob.x,self.movingBlob.y,mood=self.movingBlob.mood,fade=18)
			self.bt.insert(0,new)
		# axis
		pg.draw.line(self.bg, (G,G,G), (int(X/2.0),0),(int(X/2.0),Y), 1)
		pg.draw.line(self.bg, (G,G,G), (0,int(Y/2.0)),(X,int(Y/2.0)), 1)
		
		# indicators
		for i in self.indicators.itervalues() :
			i.draw()
			
	def read_keys(self):
		'''Read keys'''
		for event in pg.event.get() :
			# Quit (event)
			if event.type == QUIT:
				self.quit()
			elif event.type == KEYDOWN :
				# Post Quit: Esc, q
				if event.key == K_ESCAPE or event.key == K_q :
					pg.event.post(pg.event.Event(QUIT))
				# Reset: Space
				elif event.key == K_SPACE :
					if not self.blobs :
						self.bt = []
					self.blobs = []
				# Random : r
				elif event.key == K_r :
					self.add_blob(random(),random(),count=5)
				# Connection : c
				elif event.key == K_c :
					self.init_reconnect = True
					self.indicators["conn"].off()
				# Fullscreen: f
				elif event.key == K_f :
					# pg.display.toggle_fullscreen()
					self.toggle_screen_mode()
				# Toggle suspend: d
				elif event.key == K_d :
					if self.suspend :
						print "suspend off"
						self.indicators["suspend"].off()
						self.suspend = False
					else:
						print "suspend on"
						self.indicators["suspend"].on()
						self.suspend = True
				# Toggle fade: s
				elif event.key == K_s :
					if self.FADE > 0:
						print "fade off"
						self.indicators["fade"].off()
						self.FADE = 0
						for blob in self.blobs :
							blob.fade(0)
					else:
						print "fade on"
						self.indicators["fade"].on()
						self.FADE = FADE
						for blob in self.blobs :
							blob.fade(FADE)
							blob.reset_time()
				# inc age
				elif event.key == K_1 :
					self.s_age += 1
					self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
				# dec age
				elif event.key == K_2 :
					self.s_age -= 1
					self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
				# inc dist
				elif event.key == K_3 :
					self.s_dist += 0.025
					self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
				# dec dist
				elif event.key == K_4 :
					self.s_dist -= 0.025
					if self.s_dist < 0.025 : self.s_dist = 0.025
					self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
				# inc ninp
				elif event.key == K_5 :
					self.s_ninp += 1
					self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
				# dec ninp
				elif event.key == K_6 :
					self.s_ninp -= 1
					if self.s_ninp < 2 : self.s_ninp = 2
					self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
				# choose different app and restart server
				elif event.key == K_9 :
					self.choose_app()
				
				pass
		pass
		
	def toggle_screen_mode(self):
		'''Go back and forth between full screen mode.'''
		if self.fullscreen == False:
			globals()['_X'] = globals()['X']
			globals()['_Y'] = globals()['Y']
			globals()['X'] = XF
			globals()['Y'] = YF			
			self.screen = pg.display.set_mode((X, Y))
			# self.screen = pg.display.set_mode((0,0),pg.FULLSCREEN)
			self.fullscreen = True
			self.bg = pg.Surface(self.screen.get_size())
			self.bg = self.bg.convert()
			self.bg.fill((0,0,0))
			for i in self.indicators.itervalues() :
				i.reinit(self.bg)
				i.draw()
			if self.movingBlob != None :
				self.movingBlob.reinit(self.bg)
				self.movingBlob.draw()
		else :
			globals()['X'] = globals()['_X']
			globals()['Y'] = globals()['_Y']
			self.screen = pg.display.set_mode((X, Y))			
			self.fullscreen = False
			self.bg = pg.Surface(self.screen.get_size())
			self.bg = self.bg.convert()
			self.bg.fill((0,0,0))
			for i in self.indicators.itervalues() :
				i.reinit(self.bg)
				i.draw()
			if self.movingBlob != None :
				self.movingBlob.reinit(self.bg)
				self.movingBlob.draw()
		pg.display.toggle_fullscreen()
		

				
	def run(self):
		
		# setup connection
		self.connect()
				
		# main loop
		while True :
			# pg.draw.circle(screen, pg.Color(255,0,0), (300,50),20,0)
			# screen.blit(gradients.radial(99, scol, ecol), (401, 1))

			self.read_keys()

			# Draw 
			self.draw()

			# update display
			self.screen.blit(self.bg, (0, 0))
			pg.display.flip()
			self.fpsClock.tick(FRAMERATE)
			
			if self.init_reconnect:
				self.reconnect()
			
		return True
		
	def choose_app(self):
		'''Experimental function for chaging served apps remotely. Disabled for now...'''
		return False
		try :
			print "Changing app and restarting... the connection will be lost."
			self.conn.putrequest("GET","/moodconductor/changeapp", skip_host=True)
			self.conn.putheader("Host", "www.isophonics.net")
			self.conn.endheaders()
			res = self.conn.getresponse()
			res.read()
		except :
			pass
		
	def configure_server(self):
		'''Send the server some configuration data.'''
		# age = 10.0
		# dist = DIST
		# ninp = 18
		self.update_server_config(self.s_age,self.s_dist,self.s_ninp)
				

	def update_server_config(self,age,dist,ninp,retry = 3):
		'''Send the server some configuration data.'''
		self.indicators["config"].on().now(self.screen)
		try :
			print "Sending configuration data."
			self.conn.putrequest("GET","/moodconductor/config?age=%(age)s&dist=%(dist)s&ninp=%(ninp)s" %locals(), skip_host=True)
			self.conn.putheader("Host", "www.isophonics.net")
			self.conn.endheaders()
			res = self.conn.getresponse()
			res.read()
			if not res.status == 200 :
				print "Server response:", res.status, res.reason
				self.indicators["conn"].off()
			time.sleep(0.5)
			self.conn.putrequest("GET","/moodconductor/getconf", skip_host=True)
			self.conn.putheader("Host", "www.isophonics.net")
			self.conn.endheaders()
			res = self.conn.getresponse()
			if not res.status == 200 :
				print "Server response:", res.status, res.reason
				self.indicators["conn"].off()
				# self.indicators["config"].on()
			print "Server configuration:", res.read()
			self.indicators["config"].off()
			self.indicators["conn"].on()
		except:
			print "Failed to send configuration data."
			time.sleep(2)
			retry -= 1
			self.update_server_config(age,dist,ninp,retry)
	
		
	def connect(self,retry = 5):
		'''Connect to the server and test connection.'''
		if not retry :
			print "Server unreachable"
			pg.quit()
			raise SystemExit
		if retry < 5 :
			time.sleep(3)
		try :
			print "connecting to server..."
			self.conn = ht.HTTPConnection(IP,timeout=HTTP_TIMEOUT)
			self.indicators["conn"].on()
		except :
			self.indicators["conn"].off()
			self.connect(retry = retry-1)
			print "connection failed."		
			
		try:
			print "Testing connection."
			# self.conn.putrequest("GET","/moodconductor/index.html", skip_host=True)
			self.conn.putrequest("GET","/moodconductor/test", skip_host=True)
			self.conn.putheader("Host", "www.isophonics.net")
			self.conn.endheaders()
			res = self.conn.getresponse()
			print res.read()
			if res.status == 200 :
				self.indicators["conn"].on()
			else :
				print "Server response:", res.status, res.reason
				self.indicators["conn"].off()
				if not hasattr(self,"noretry") and raw_input("Go offline? ") in ['y',''] :
					return False
				else :
					print "Failed. retrying."
					self.noretry = None
					self.connect(retry = retry-1)
		except Exception as e:
			print "Exception while testing connection.", e
			self.indicators["conn"].off()
			# comment out in offline mode
			if not hasattr(self,"noretry") and raw_input("Go offline? ") in ['y',''] :
				return False
			else :
				self.noretry = None
				self.connect(retry = retry-1)
		self.configure_server()
		print "OK. Starting update thread..."
		self.start_update_thread()
		return True
		
		
		
	def reconnect(self):
		'''Called when c is pressed.'''
		self.indicators["config"].on().now(self.screen)
		self.init_reconnect = False
		
		self.stop_update_thread()
		time.sleep(1)
		try :
			self.conn.close()
		except :
			self.indicators["conn"].off()
		try :
			self.conn = ht.HTTPConnection(IP,timeout=HTTP_TIMEOUT)
			self.conn.connect()
			self.indicators["conn"].on()
			print "Reconnected."
		except :
			self.indicators["conn"].off()
			print "Error while reconnecting."
		self.start_update_thread()
		self.indicators["config"].off().now(self.screen)
		

	def quit(self):
		print "Quitting.."
		self.indicators["conn"].off()		
		self.stop_update_thread()
		self.conn.close()
		pg.quit()
		sys.exit()


def main():
	
	v = VisualClient()
	v.run()

if __name__ == '__main__':
	pass
	main()