Source

fungus / code_swarm.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
#!/usr/bin/env python
# encoding: utf-8

"""Simple code_swarm scenes. 

Should become live-feedable at some point. 

usage: 
    - python code_swarm.py [options] [action]
      show a code swarm

options: 
    --time sec - the time in seconds the code_swarm should last. 

actions: 
    --maildir dir - show a swarm from maildir files inside a folder (doesn't recurse into subdirs). 
    
    -- activity activity_file - show a swarm from a code_swarm activity file.

Plans: 
- Show Blobs at random positions. - done - 
- Make Blobs grow when called repeatedly. -> use Pyglet sprites for scaling. - done -
- Add Blob movement: files and coders move closer for some time when called together. - done -
- Show names of coders instead of Blobs. - done - 
- Add repulsion between all files which are currently partnered (their authors additionally do general repulsion to them). - done - 
- Readout a code_swarm activity file and let all blobs appear when called. 
- Add change-sizes (size of diff) instead of only changed files. I want to see who changed how much, not only what and when. -> scaling depends on change-size. 
- Allow using different images for files. 

Structure: 
- Blob -> every dot
    * Short-term associated blobs -> move closer for some time. 
    * Fade-out after some time. remove from visible list when faded out. 
    * Unique name -> coder or filename. 
- Author -> Every programmer
    * Show names instead of Blobs (text sprite). 
    * Have partners: all files. 
- File -> Every file
    * Repulsion between files with the same partner. 
- Scene
    * show_change(project, author, file) 
	- check if author and file exist (if not name in [blob.name for blob in self.blobs]: create blob), then add them to each other as temporary pair. 
	- the name for the file consists of project + filepath. 
- Feeder -> the data to show
    * readout data from somewhere
    * provide the data of changes to show for each step. 
    * Set a timeframe and use that to select changes. 

"""

#### Call the correct fungus_game when called from the command line ####

if __name__ == "__main__": 
    # Call this Scene via fungus_game
    from fungus_core import call_this_scene
    # pass the commandline args
    from sys import argv
    call_this_scene(__file__, argv)


#### Options ####

## Maildir parsing
# Remove host from email addresses? 
EMAIL_WITHOUT_HOST = True

RECURSIVE_TRACKING = False

WEAK_RECURSION = True

#### Constants ####

#: Countdown which slows down the shutdown - the number of frames the program should run after teh code_swarm is finished. 
COUNTDOWN = 30

#: Strengths of random movement
RANDOM_MOVEMENT = 0.5

#: Speed towards the partner. They move this part of their distance together each step. 
SPEED_TOWARDS_PARTNER = 0.02

# Speed of partner repulsion; multiplied with the INVERSE distance. 
SPEED_AWAY_FROM_PARTNER = 10

#: Repulsion between active files - they move this part of their INVERSE distance away from each other each step. 
SPEED_OF_PARTNER_REPULSION = 2

#: The square root of the ratio between the speed of movement for files and for coders (file speed / coder speed)
SPEED_RATIO_AUTHOR_FILE_SQRT = 4.0

#: How close files should come to authors, in multiples of the radius. 
SAFE_DISTANCE = 60.0

#: The minumum to which the safe circle gets filled - over time - used to determine the adaptive minimum distance between files of one coder. 
SAFE_CIRCLE_FILL = 0.6

#: The number of frames for which a file is being associated to an author after an edit.
TIME_OF_PARTNERSHIP = 30

#: The time a partner is considered active
TIME_OF_PARTNER_ACTIVITY = 10

#: If we want to give all files of one author minor (but expensive CPUwise) repulsion between each other, so they form nicer circles. 
SIMULATE_MINOR_FILE_REPULSION = False
#: Do we want repulsion if we have few partners? 
SIMULATE_MINOR_FILE_REPULSION_IF_CHEAP = True
SIMULATE_MINOR_FILE_REPULSION_IF_CHEAP_THRESHOLD = 5

#: How much the opacity gets reduced when a file no longer active.
INACTIVITY_OPACITY_REDUCTION = 100

#: How much teh blobs should fade out per frame
OPACITY_REDUCTION_PER_FRAME = 6

#### Imports ####

from fungus_core import Sprite
from fungus_core import pyglet_label
from fungus_scene import BaseScene

from os.path import join, dirname

from random import random, choice, randint

# let the blobs grow slowly to their real size. 
from math import sqrt
# Also we need the lengths of the safe circle for adaptive file repulsion. 
from math import pi

# Reduce CPU usage: sleep
from time import sleep
# Also get the current time to be able to calculate time deltas in the feeder. 
from time import time
# And ctime to show time as localtime
from time import ctime

#### An example scene ####

### Things needed for the scene

IMAGE_BASE_PATH = join(dirname(__file__), "graphics")

class Feeder(object): 
    """The feeder keeps track of activities and supplies the correct activities when they get requested.
    
    to get the current data simply call Feeder.data
    
    TODO: 
	- readout data from somewhere
	- Set a timeframe and use that to select changes. for data
	
    """
    def __init__(self, track = []): 
	#: Internal data with time to play. Source for self.get_data()
	self._data = None # [(time_to_play, item, related_item)]
	# for codeswarms: item = author, related_item = file. 
	#: The time when the data property was last accessed. 
	self.last_access_time = None
	#: The time up to which we want to show the data
	self.time_delta = 0
	#: The maximum time for feeding
	self.max_time = None
	#: The maximum datetime - here the simulation can stop. 
	self.max_timestamp = None
	#: IDs to track. 
	self.track = track
	self.track_2nd = []
	
    def read_codeswarm_activity(self, path, time_to_play = 180): 
	"""Read the data from all mails in a maildir folder and turn it into a code_swarm.
	@param path: Absolute path to the activity xml file
	@type path: string
	@param time_to_play: Duration of the feed (seconds)
	@type time_to_play: number
	@return: None (saves data in self._data with time_to_play)
	"""
	# Set the maximum time
	self.max_time = time_to_play
	
	from xml.dom import minidom
	xml = minidom.parse(path)
	data = [] # [(date, author, filename)]
	struct = xml.childNodes[0].childNodes
	# Only read the items which have attributes (ignore the white space in between...). 
	for val in [child for child in struct if child.hasAttributes()]: 
	    att = val.attributes
	    va = att.values()
	    date = float(va[0].value) / 1000.0
	    author = va[1].value
	    filename = va[2].value
	    data.append((date, author, filename))
	
	# Clean data, including normalization and datesorting. 
	data = self.clean_data(data, time_to_play)
	
	# Store the data
	self._data = data
	
	
    def read_maildir(self, path, time_to_play = 180): 
	"""Read the data from all mails in a maildir folder and turn it into a code_swarm.
	@param path: Absolute path to the folder
	@type path: string
	@param time_to_play: Duration of the feed (seconds)
	@type time_to_play: number
	@return: None (saves data in self._data with time_to_play)
	"""
	from os import listdir
	from os.path import join, isfile
	# the general parser
	from email.parser import Parser
	# date handling
	from email.utils import parsedate, parseaddr, getaddresses
	from time import mktime
	
	# Set the maximum time
	self.max_time = time_to_play
	
	# Now read and parse all maildir files with their real date
	files = [join(path, f) for f in listdir(path) if isfile(join(path, f))]
	data = [] #: Preliminary data without normed time
	
	print "...parsing ", len(files), "emails..."
	# Now create the parser
	parser = Parser()
	# And parse all mais
	for p in files:
	    print files.index(p), "of", len(files)
	    f = open(p)
	    # parse the mail - we're only interested in the headers (the second param is headersonly). 
	    mail = parser.parse(f, True)
	    f.close()
	    # if the mail has from, to and date, append it to the list
	    try:
		# turn the date into seconds since epoch
		date = mail.get("Date")
		date = mktime(parsedate(date))
		# use the email address only (if we have one)
		fro = parseaddr(mail.get("From"))[1]
		
		# get all tos
		tos = mail.get_all('to', [])
		ccs = mail.get_all('cc', [])
		resent_tos = mail.get_all('resent-to', [])
		resent_ccs = mail.get_all('resent-cc', [])
		# only the mail addresses
		all_recipients = [addr for name, addr in getaddresses(tos + ccs + resent_tos + resent_ccs)]
		
		# turn the mail into a set of simply tuples and append these to the data
		for to in all_recipients: 
		    if EMAIL_WITHOUT_HOST: 
			mail = (date, fro.split("@")[0], to.split("@")[0])
		    else: 
			mail = (date, fro, to)
		    # Now we add all data. 
		    data.append(mail)

	    except: 
		continue
	
	data = self.clean_data(data, time_to_play)
	
	# Now store them in self._data
	self._data = data
	print "...finished parsing email data..."
	
    
    def clean_data(self, data, time_to_play = 180): 
	"""Cleanup data - normalize tiem and such."""
	# then normalize all times, so we have one stream 
	# beginning at 0 and ending at the specified "time". 
	first_date = min([date for date, fro, to in data])
	self.max_timestamp = max([date for date, fro, to in data])
	# multiply all dates by this to normalize them to the time_to_play.
	multiplier = time_to_play / (self.max_timestamp - first_date)
	# Normalize all dates, so they begin at 0 and extend for "time" seconds. 
	data = [((date - first_date)*multiplier, date, fro, to) for date, fro, to in data]
	# and sort the data in place
	data.sort()
	
	# If we want to track only one, we split the communication - and if we want to be recursive, we also track all contacts. 
	if self.track: 
	    data_tmp = []
	    for reldate, date, fro, to in data: 
		if fro in self.track: 
		    data_tmp.append((reldate, date, fro, to))
		    if RECURSIVE_TRACKING: 
			self.track.append(to)
		    else:
			self.track_2nd.append(to)
		elif WEAK_RECURSION and fro in self.track_2nd: 
		    data_tmp.append((reldate, date, fro, to))
	    data = data_tmp
	
	# Update the max timestamp again, so we really leave once we're done, even though we might have removed the previously last events. 
	self.max_timestamp = max([date for reldate, date, fro, to in data])
	
	return data
    
    def get_data(self): 
	"""@return: current changes which should be displayed at once."""
	if self._data is None: 
	    return self.random_data()
	else: 
	    # If data was never accessed before, set the current time for last access. 
	    if self.last_access_time is None: 
		self.last_access_time = time()
	    # increase the time up to which we want to show the data
	    self.time_delta += time() - self.last_access_time
	    # Update the last access time
	    # get all datapoints up to the 
	    data = [(date, fro, to) for reltime, date, fro, to in self._data if reltime <= self.time_delta]
	    # remove all these datapoints from the private data. 
	    self._data = [(reltime, date, fro, to) for reltime, date, fro, to in self._data if reltime > self.time_delta]
	    
	    self.last_access_time = time()
	    return data
	    
	    
    def random_data(self): 
	"""Create random data (for testing),"""
	# Dummy: Feed random data. 
	
	#: The data to show: [(coder, filename)]
	data = []
	
	# Random normal file, likely new
	data.append(("coder", str(randint(5, 1000))))
	# Three random often changed files
	data.append(("coder", str(randint(0, 5))))
	data.append(("integrator", str(randint(-2, 3))))
	data.append(("documenter", str(randint(-3, 2))))
	
	# and randomly a big code drop, either by coder or by integrator. 
	# integrator drop. 
	if random() < 0.002: 
	    for i in range(60): 
		data.append(("integrator", str(randint(5, 200))))
	# coder drop. 
	elif random() < 0.002: 
	    for i in range(60): 
		data.append(("coder", str(randint(5, 1000))))
	# dual drop. 
	elif random() < 0.002: 
	    for i in range(60): 
		data.append(("integrator", str(randint(5, 200))))
		data.append(("coder", str(randint(5, 1000))))
	
	return [(time(), author, f) for author, f in data]
    
    data = property(get_data)
    

class Swarmable(object): 
	"""Any object which is part of the code_swarm.
	
	Swarmable offers basic methods which get called into the specific display options of the blobs.
	
	It has to be inherited together with something which offers movement, especially self.x and self.y!
	"""
	def __init__(self, name=None, *args, **kwds): 
	    
	    try: 
		super(Swarmable, self).__init__(*args, **kwds)
	    except: 
		# Lable doesn't carry on *args and **kwds :(
		pyglet_label.__init__(self, *args, **kwds)
		
	    #: The name of the blob. Author name or filename
	    self.name = name 
	    if self.name is None: 
		self.name = random()
	    
	    #: The blobs to close in towards: [[blob, steps]], remove if steps == 0. 
	    self.partners = []
	    
	    # Controlling parameters
	    #: How close should others come in pixels (radius)- 
	    self.safe_distance = SAFE_DISTANCE**2
	    self.safe_circle_length = 2 * pi * SAFE_DISTANCE
	    
	    #: Continuous movement
	    self.dx = 0
	    self.dy = 0
	
	def move_random(self): 
		"""Moce at random."""
		self.x += (2*random() - 1.0) * RANDOM_MOVEMENT
		self.y += (2*random() - 1.0) * RANDOM_MOVEMENT
	
	def add_partner(self, partner): 
		"""Add a blob as partner."""
		# avoid adding ourselves, as that would break everything
		if partner is self: 
		    return
		self.partners.append([partner, TIME_OF_PARTNERSHIP])
	
	def attraction_to_safe_distance(self, partner): 
		    """Pull another a step towards the safe distance."""
		    # The whole way to the other
		    x_to_partner = (partner.x - self.x )
		    y_to_partner = (partner.y - self.y )
		    # keep them in sane ranges
		    sane = 1
		    if x_to_partner > -sane and x_to_partner < 0: 
			x_to_partner = -sane
		    elif x_to_partner < sane and x_to_partner > 0: 
			x_to_partner = sane
		    if y_to_partner > -sane and y_to_partner < 0: 
			y_to_partner = -sane
		    elif y_to_partner < sane and y_to_partner > 0: 
			y_to_partner = sane
		    
		    # repulsion and attraction with relaxation in between: 
		    # If the partner is far away, get it closer. 
		    if self.distance_to(partner) > self.safe_distance*1.1: 
			# Move a step on that way. 
			self.x += x_to_partner * SPEED_TOWARDS_PARTNER / SPEED_RATIO_AUTHOR_FILE_SQRT
			self.y += y_to_partner * SPEED_TOWARDS_PARTNER / SPEED_RATIO_AUTHOR_FILE_SQRT
			# Also make the partner move a step on the way. 
			partner.x -= x_to_partner * SPEED_TOWARDS_PARTNER * SPEED_RATIO_AUTHOR_FILE_SQRT
			partner.y -= y_to_partner * SPEED_TOWARDS_PARTNER * SPEED_RATIO_AUTHOR_FILE_SQRT
		    # else move away
		    elif self.distance_to(partner) < self.safe_distance: 
			# Move a step on that way - ignore the speed setting for the repulsion. 
			self.x -= 1.0 / x_to_partner / SPEED_RATIO_AUTHOR_FILE_SQRT * SPEED_AWAY_FROM_PARTNER
			self.y -= 1.0 / y_to_partner / SPEED_RATIO_AUTHOR_FILE_SQRT * SPEED_AWAY_FROM_PARTNER
			# Also make the partner move a step on the way. 
			partner.x += 1.0 / x_to_partner * SPEED_RATIO_AUTHOR_FILE_SQRT * SPEED_AWAY_FROM_PARTNER
			partner.y += 1.0 / y_to_partner * SPEED_RATIO_AUTHOR_FILE_SQRT * SPEED_AWAY_FROM_PARTNER
		
		
	def minor_repulsion(self, other): 
	    """Pull back a small step from the other."""
	    x_to_other = other.x - self.x
	    y_to_other = other.y - self.y 
	    # keep them in sane ranges
	    sane = 0.25
	    if x_to_other > -sane and x_to_other < 0: 
		x_to_other = -sane
	    elif x_to_other < sane and x_to_other > 0: 
		x_to_other = sane
	    if y_to_other > -sane and y_to_other < 0: 
		y_to_other = -sane
	    elif y_to_other < sane and y_to_other > 0: 
		y_to_other = sane
	    # do the actual repulsion
	    speed = SPEED_OF_PARTNER_REPULSION
	    self.x -= 1.0 / x_to_other * speed
	    self.y -= 1.0 / y_to_other * speed
	
	def minor_repulsion_master(self): 
	    """Control the minor repulsion for all partners."""
	    # Only do that if the partner is sufficiently far away
	    # Always try to have the blobs spaced about equally while having as few updates as possible.
	    
	    #: The adaptive threshold for file repulsion (=> file distance), 
	    adaptive_threshold = SAFE_CIRCLE_FILL * self.safe_circle_length / len(self.partners)
	    
	    # Before you try it yourself: 
	    # Doing an xy_list and calculating everything on that doesn't offer a visible performance gain... 
	    for partner, steps in self.partners: 
		    for other, steps in self.partners: 
			    if not other is partner and abs(other.x - partner.x) < adaptive_threshold and abs(other.y - partner.y) < adaptive_threshold: 
				    partner.minor_repulsion(other)
				    
	
	def get_partners_nearer(self): 
		"""Walk a bit towards each of your partners."""
		
		# if we don't have partners, we can skip this
		if not self.partners: 
		    return
		
		# First add minor repulsion between all partnered files to make them align in a nice ring, if we want it. 
		if SIMULATE_MINOR_FILE_REPULSION: 
		    self.minor_repulsion_master()
		elif SIMULATE_MINOR_FILE_REPULSION_IF_CHEAP and len(self.partners) < SIMULATE_MINOR_FILE_REPULSION_IF_CHEAP_THRESHOLD: 
		    self.minor_repulsion_master()
		    
		# Then pull the partner towards the safe distance (by doing this as second step, they are always closer to the safe_distance. 
		for partner, steps in self.partners: 
		    self.attraction_to_safe_distance(partner)
		
		for i in range(len(self.partners)): 
		    # reduce the steps to go by one. 
		    self.partners[i][1] -= 1
		
		# After TIME_OF_PARTNER_ACTIVITY steps the partner is considered no longer active and its opacity reduced. 
		activity_threshold = TIME_OF_PARTNERSHIP - TIME_OF_PARTNER_ACTIVITY
		for inactive_partner in [inactive for inactive in self.partners if inactive[1] == activity_threshold]: 
		    # make now inactive partners less visible -> inactivity visibility
		    # The fixed value ensures that the file always has a fixed visibility-time after the last time it was changed. 
		    inactive_partner[0].opacity = 250 - TIME_OF_PARTNER_ACTIVITY - INACTIVITY_OPACITY_REDUCTION
		
		# if we have no more steps to go, remove the partner. 
		for inactive_partner in [inactive for inactive in self.partners if inactive[1] <= 0]: 
		    # remove the partners from the partners list. 
		    self.partners.remove(inactive_partner)
		    
		
	def distance_to(self, other): 
		"""Squared distance to the partner. Calculated from the centers."""
		# If we have width and height, use them, else just use x and y. 
		# TODO: Use content_width where necessary. 
		if self.width is not None and self.height is not None and other.width is not None and other.height is not None: 
		    x = (other.x + 0.5*other.width) - (self.x + 0.5*self.width)
		    y = (other.y + 0.5*other.height) - (self.y + 0.5*self.height)
		else: 
		    x = other.x - self.x
		    y = other.y - self.y
		return x**2 + y**2
	    
	

class Blob(Swarmable, Sprite): 
	"""One of the moving blobs.
	"""
	def __init__(self, image_path=None, *args, **kwds): 
		"""Initialize the Blob."""
		if image_path is None:
		    image_path=join(IMAGE_BASE_PATH, "blobn.png")
		# Initialize the Sprite
		super(Blob, self).__init__(image_path=image_path, *args, **kwds)
		
		# Start with very small scale
		self.scale = 0.1
	
	def update(self): 
		"""Update the Blobs data and position."""
		# Also become a bit less solid. 
		if self.opacity > OPACITY_REDUCTION_PER_FRAME: 
		    self.opacity -= OPACITY_REDUCTION_PER_FRAME
		elif self.opacity > 0: 
		    self.opacity = 0
	
	def touched(self): 
	    """React to being touched: Grow and become solid."""
	    # Scale up
	    self.scale = sqrt(self.scale)
	    # and appear solid
	    self.opacity = 255
		

class Text(Swarmable, pyglet_label): 
	"""A text-label - the equivalent of Blob for text.
    
	label = pyglet.text.Label('Hello, world',
                          font_name='Times New Roman',
                          font_size=36,
                          x=10, y=10)
	"""
	def __init__(self, name=None, font_name="Times New Roman", font_size=10.0, *args, **kwds): 
		"""Initialize the Blob."""
		# Initialize the Sprite
		
		if name is None: 
		    name = str(random())
		text = name
		try: 
		    super(Text, self).__init__(name=name, text=text, font_name=font_name, font_size=font_size, *args, **kwds)
		except: 
		    pyglet_label.__init__(self, text=text, font_name=font_name, font_size=font_size, *args, **kwds)
		
		# Make the drawing work with fungus. 
		self.blit = self.draw
	    
	# property: opacity
	
	def get_opacity(self): 
	    """Get the opacity - getter for the property."""
	    return self.color[3]
	
	def set_opacity(self, opacity): 
	    """Set the opacity - setter for the property."""
	    self.color = (self.color[0], self.color[1], self.color[2], opacity)
	
	opacity = property(fget=get_opacity, fset=set_opacity)

	def update(self): 
		"""Update the Blobs data and position."""
		self.get_partners_nearer()
		# Also become a bit less solid. 
		if self.opacity > OPACITY_REDUCTION_PER_FRAME: 
		    self.opacity -= OPACITY_REDUCTION_PER_FRAME
		elif self.opacity > 0: 
		    self.opacity = 0
	
	def touched(self): 
	    """React to being touched: Grow and become solid."""
	    # and appear solid
	    self.opacity = 255
	

class Author(Text): 
    """A coder."""
    def __init__(self, *args, **kwds): 
	super(Author, self).__init__(*args, **kwds)
	
class File(Blob): 
    """A coder."""
    def __init__(self, *args, **kwds): 
	super(File, self).__init__(*args, **kwds)

### The Scene itself. 

class Scene(BaseScene): 
    """A dummy scene - mostly just the Scene API."""
    def __init__(self, core, *args, **kwds): 
        """Initialize the scene with a core object for basic functions."""
        
        ## Get the necessary attributes for any scene. 
        # This gets the 'visible', 'colliding' and 'overlay' lists 
        # as well as the scene switch 'switch_to_scene' 
        # which can be assigned a scene to switch to. 
        super(Scene, self).__init__(core, *args, **kwds)
        
	self.swarm_type = None
	
	#: For maildir: should we track a specific ID? 
	self.track = []
	
	#: The path which we want to read out
	self.path = None
	
	#: The default time in seconds we want a code_swarm to last
	self.time = 180
	
	#: Some pre-shutdown wait time. The number of frames the scene will still run. 
	self.countdown = None
	
	## Parse the commandline arguments
	if "--maildir" in args[0]: 
	    self.swarm_type = "maildir"
	    self.path = args[0][args[0].index("--maildir") + 1]
	    args[0].remove("--maildir")
	    args[0].remove(self.path)
	
	if "--activity" in args[0]: 
	    self.swarm_type = "activity"
	    self.path = args[0][args[0].index("--activity") + 1]
	    args[0].remove("--activity")
	    args[0].remove(self.path)
	
	while "--track" in args[0]: 
	    track = args[0][args[0].index("--track") + 1]
	    args[0].remove("--track")
	    args[0].remove(track)
	    self.track = track.split(",")
	
	if "--time" in args[0]: 
	    self.time = args[0][args[0].index("--time") + 1]
	    args[0].remove("--time")
	    args[0].remove(self.time)
	    self.time = float(self.time)
	
	## The data feeder
	if self.swarm_type == "maildir": 
	    self.feeder = Feeder(track=self.track)
	    # add a maildir folder to the data
	    self.feeder.read_maildir(self.path, time_to_play = self.time)
	elif self.swarm_type == "activity": 
	    self.feeder = Feeder(track=self.track)
	    # add the actitivity file to the data
	    self.feeder.read_codeswarm_activity(self.path, time_to_play = self.time)
	else: 
	    self.feeder = Feeder()
	
        ## The blobs
        #: All authors
	self.blobs = []
	
	#: The batch for updating all files together. 
	self.file_batch = self.core.batch()
	# Show the file_batch -> always show all files. 
	self.visible.append(self.file_batch)
	#: The batch for updating all authors together. 
	self.author_batch = self.core.batch()
	self.visible.append(self.author_batch)
	
	# Also add a time widget
	self.time = self.core.load_text("Time", font_size = 10.0, x=self.core.win.width - 200, y=10)
	self.visible.append(self.time)
	
	# And a time widget with relative time (from start)
	self.reltime = self.core.load_text("rel time", font_size = 10.0, x=10, y=10)
	self.visible.append(self.reltime)

    def show_change(self, author_name, filepath): 
	"""Show a change by an author to a file."""
	# If we don't yet have the called ones, add them. 
	# TODO: This is prone to race conditions! Use a lock!
	
	if not author_name in [blob.name for blob in self.blobs]: 
	    x, y = self.get_starting_position()
	    author = Author(name=author_name, x=x, y=y, batch=self.author_batch)
	    self.blobs.append(author)
	
	if self.swarm_type == "maildir": 
	    if not filepath in [blob.name for blob in self.blobs]: 
		x, y = self.get_starting_position()
		author = Author(name=filepath, x=x, y=y, batch=self.author_batch)
		self.blobs.append(author)
	else: 
	    if not filepath in [blob.name for blob in self.blobs]: 
		x, y = self.get_starting_position()
		f = File(name=filepath, x=x, y=y, batch=self.file_batch)
		self.blobs.append(f)
	
	# Now scale them up a bit. This throws a value error 
	# if the blob isn't in the list, 
	# we don't catch it, because now the Blob has to be in the list. 
	# First the file
	f = self.find_blob_by(filepath)
	# Increase the scale by 0.5 TODO: replace with logarithmic scale. 
	f.touched()
	# Then the author
	author = self.find_blob_by(author_name)
	# Increase the scale by 0.5 TODO: replace with logarithmic scale. 
	author.touched()
	
	# Also add the files as partner to the author
	author.add_partner(f)	
	
	
    def find_blob_by(self, name): 
	"""Locate a blob by its name."""
	blob_names = [blob.name for blob in self.blobs]
	return self.blobs[blob_names.index(name)]
	    
     
    def keep_on_screen(self, blob): 
		if blob.x < 0: 
			blob.x = 0
			 
		if blob.y < 0: 
			blob.y = 0
		
		if blob.width is not None and blob.height is not None: 
		    if blob.x + blob.width > self.core.win.width: 
			    blob.x = self.core.win.width - blob.width
			    
		    if blob.y + blob.height > self.core.win.height: 
			    blob.y = self.core.win.height - blob.height


		
    def get_starting_position(self): 
		"""Select a starting position based on the config parameters."""
		# Start at a random position
		x = random() * self.core.win.width
		y = random() * self.core.win.height
		return x, y
		

    def update(self): 
        """Update the stats of all scene objects. 

Don't blit them, though. That's done by the Game itself.

To show something, add it to the self.visible list. 
To add a collider, add it to the self.colliding list. 
To add an overlay sprite, add it to the self.overlay list. 
"""
	### Just Testing
	
	if self.countdown is not None: 
	    if self.countdown <= 0: 
		self.core.win.has_exit = True
	    else: 
		self.countdown -= 1
	
	data = self.feeder.data
	
	for date, author_name, filename in data: 
	    if author_name and filename: 
		self.show_change(author_name, filename)
	    self.time.text = ctime(date)
	    # Shut down once we reach the maximum timestamp. 
	    if date == self.feeder.max_timestamp: 
		self.countdown = COUNTDOWN
	
	self.reltime.text = ctime(time())
	
	# Update each blob
        [blob.update() for blob in self.blobs if blob.opacity > 0]
	
	# sleep for a blink, so we don't always max out the CPU
	sleep(0.01)