diff visualclient2/visclient.py @ 37:2db17c224664

added visclient2
author gyorgyf
date Fri, 24 Apr 2015 07:09:32 +1000
parents
children 874ac833c8e3
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/visualclient2/visclient.py	Fri Apr 24 07:09:32 2015 +1000
@@ -0,0 +1,1061 @@
+#!/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()
+