implementation module monitor

//	**************************************************************************************************
//
//	This module defines a process creation function that tracks the number of typed keys in a typing
//	session. 
//	This module has been written in Clean 2.0 and uses the Clean Standard Object I/O library 1.2.2
//	
//	**************************************************************************************************

import StdEnv, StdIO
from StdFunc import seq

::	Count
	=	{	oks			:: Int		// nr of OK keys
		,	bads		:: Int		// nr of correction keys
		}
::	Monitor
	=	{	count		:: Count	// current  registration of keys
		,	counts		:: [Count]	// previous registration of keys
		,	time		:: Int		// current tracking time since start (in seconds)
		,	tracking	:: Bool		// monitor is currently tracking
		}
::	MonitorMessage					// The message type of the monitor process
	=	BeginSession				// Begin a typist session
	|	KeyHit Char					// Register key stroke
	|	EndSession					// End a typist session
	|	Quit						// Close the monitor process

openMonitor :: ItemPos (RId MonitorMessage) (PSt .l) -> PSt .l
openMonitor pos monitorId pst
	# (font,   pst) = accPIO (accScreenPicture openDialogFont) pst
	# (metrics,pst) = accPIO (accScreenPicture (getFontMetrics font)) pst
	# (ids,    pst) = accPIO (openIds 2) pst
	= openProcesses (monitorProcess pos font metrics monitorId ids) pst

monitorProcess :: ItemPos Font FontMetrics (RId MonitorMessage) [Id] -> Process
monitorProcess pos font metrics monitorId ids=:[wId,tId]
	= Process SDI initLocal initIO []
where
	initcount	= {oks=0,bads=0}
	initcounts	= []
	initLocal	= {count=initcount,counts=initcounts,time=0,tracking=False}
	
//	initIO initialises the monitor process.
	initIO pst
		# (error,pst)		= openWindow undef window pst
		| error<>NoError	= abort "monitor could not open window."
		# (error,pst)		= openTimer undef timer pst
		| error<>NoError	= abort "monitor could not open timer."
		# (error,pst)		= openReceiver undef receiver pst
		| error<>NoError	= abort "monitor could not open receiver."
		| otherwise			= pst
	where
	//	window is the single document of the monitor process.
		window	= Window "Monitor" NilLS
					[	WindowId			wId
					,	WindowPos			pos
					,	WindowViewDomain	pDomain
					,	WindowLook			True (monitorlook initLocal)
					,	WindowInit			(noLS (appPIO (appWindowPicture wId (setPenFont font))))
					]
		
		pDomain	= {corner1=zero,corner2={x=WindowWidth,y=WindowHeight}}
	
	//	The timer gathers per second the number of good and bad key hits.
	//	The monitor window is updated by drawing only the new diagram bars.
		timer	= Timer ticksPerSecond NilLS
					[	TimerId				tId
					,	TimerSelectState	Unable
					,	TimerFunction		(noLS1 (showKeyHits False))
					]
		
	//	The receiver is the interface of the monitor process to the typist process.
		receiver :: *Receiver MonitorMessage .ls (PSt Monitor)
		receiver= Receiver monitorId (noLS1 receive) []
	
//	monitorlook defines the look of the monitor window. 
	monitorlook :: Monitor SelectState UpdateState *Picture -> *Picture
	monitorlook {counts,tracking} _ _ picture
		# picture	= drawBackground  picture
		# picture	= drawSecondsLine picture
		# picture	= drawKeyHitsLine picture
		# picture	= seq (snd (smap drawKeyHitColumn (0,counts))) picture
		| tracking	= picture
		| otherwise	= drawTotalAndAverage font metrics counts picture
	
	showKeyHits :: Bool NrOfIntervals (PSt Monitor) -> PSt Monitor
	showKeyHits final dt monitor=:{ls=local=:{count,counts,time},io}
		# io		= appWindowPicture wId (seq drawfs) io
		# io		= setWindowLook wId final (True,monitorlook newlocal) io
		= {monitor & ls=newlocal,io=io}
	where
		missedcounts= if (dt>1) (repeatn (dt-1) {oks=0,bads=0}) []
		newcounts	= [count:missedcounts]
		newlocal	= {local & count=initcount,counts=counts++newcounts,time=time+dt}
		drawfs		= snd (smap drawKeyHitColumn (time,newcounts))
	
	receive :: MonitorMessage (PSt Monitor) -> PSt Monitor

//	Starting a tracking session enables the timer and clears all previous tracking information.
	receive BeginSession monitor
		# local	= {initLocal & tracking=True}
		# io	= enableTimer  tId monitor.io
		# io	= setWindowLook wId True (True,monitorlook local) io
		= {monitor & ls=local,io=io}

//	For each key hit, only administrate whether it is a good or bad key hit.
	receive (KeyHit char) monitor
		= appPLoc (incCount char) monitor
	where
		incCount :: Char Monitor -> Monitor
		incCount c local=:{count}
			| c=='\b'	= {local & count={count & bads=count.bads + 1}}
			| otherwise	= {local & count={count & oks =count.oks  + 1}}

//	Ending a session disables the timer and presents the number and average of key hits. 
	receive EndSession monitor=:{ls=local=:{time}}
		# monitor	= {monitor & ls={local & tracking=False}}
		# monitor	= showKeyHits True (60-time) monitor
		# monitor	= appPIO (disableTimer tId) monitor
		= monitor

//	Quit closes the monitor process.
	receive Quit monitor
		= closeProcess monitor


//	The drawing functions:

drawBackground :: *Picture -> *Picture
drawBackground picture
	= unfill {corner1=zero,corner2={x=WindowWidth,y=WindowHeight}} picture

drawSecondsLine :: *Picture -> *Picture
drawSecondsLine picture
	# picture	= setPenPos {x=GraphX,y=GraphY} picture
	# picture	= draw {vx=GraphWidth,vy=0} picture
	= seq (map drawSecond [0,10..MaxNrOfSeconds]) picture
where
	drawSecond :: Int *Picture -> *Picture
	drawSecond i picture
		# picture	= drawAt {x=x,y=GraphY} {vx=0,vy=AxisMarkSize}		picture
		# picture	= drawAt {x=x,y=GraphY+SecondsOffset} (toString i)	picture
		= picture
	where
		x	= GraphX+i*SecondsWidth

drawKeyHitsLine :: *Picture -> *Picture
drawKeyHitsLine picture
	# picture	= setPenPos {x=GraphX,y=GraphY} picture
	# picture	= draw {vx=0,vy=0-GraphHeight}  picture
	= seq (map drawKeyHit [0,2..MaxNrKeyHits]) picture
where
	drawKeyHit :: Int *Picture -> *Picture
	drawKeyHit i picture
		# picture	= drawAt {x=GraphX-AxisMarkSize,y=y} {vx=AxisMarkSize,vy=0}	picture
		# picture	= drawAt {x=x,y=y} (toString i)								picture
		= picture
	where
		x	= GraphX-KeyHitsOffset
		y	= GraphY-i*KeyHitHeight

drawTotalAndAverage :: Font FontMetrics [Count] *Picture -> *Picture
drawTotalAndAverage font metrics counts picture
	| isEmpty counts	= picture
	| otherwise			= totalAndAverage font metrics counts picture
where
	totalAndAverage :: Font FontMetrics [Count] *Picture -> *Picture
	totalAndAverage font metrics=:{fMaxWidth} counts picture
		# (totalW,  picture)	= getFontStringWidth font totalT				picture
		# (averageW,picture)	= getFontStringWidth font averageT				picture
		# (sumW,    picture)	= getFontStringWidth font sumT					picture
		# picture				= drawAverage	height							picture
		# picture				= setPenPos		{x=GraphX,y=summaryY}			picture
		# picture				= draw			totalT							picture
		# picture				= movePenPos	{vx=0-totalW,vy=lineHeight}		picture
		# picture				= draw			averageT						picture
		# picture				= setPenPos		{x=GraphX+(max totalW averageW)+fMaxWidth,y=summaryY}
																				picture
		# picture				= draw			sumT							picture
		# picture				= movePenPos	{vx=0-sumW,vy=lineHeight}		picture
		# picture				= draw			(toString (round 1 average))	picture
		= picture
	where
		lineHeight				= fontLineHeight metrics
		summaryY				= WindowHeight-SummaryMargin
		seconds					= length counts
		total					= foldr (+) 0 (map (\{oks,bads}->oks-bads) counts)
		average					= toReal total/toReal seconds
		height					= toInt (average*toReal KeyHitHeight)
		sumT					= toString total
		averageT				= "Average:"
		totalT					= "Total:"
		
		drawAverage :: Int *Picture -> *Picture
		drawAverage height picture
			| height<=0	= picture
			| otherwise	= appXorPicture (drawLine {x=GraphX,y=y} {x=GraphX+GraphWidth,y=y}) picture
		where
			y			= GraphY-height

drawKeyHitColumn :: (Int,Count) -> (Int,IdFun *Picture)
drawKeyHitColumn (i,count)
	= (i+1,drawColumn i count)
where
	drawColumn :: Int Count *Picture -> *Picture
	drawColumn i {oks,bads} picture
		# picture	= fill			 {corner1={x=leftX,y=yOk-1},corner2={x=rightX,y=GraphY}} picture
		# picture	= setPenColour	 Red													 picture
		# picture	= fill			 {corner1={x=leftX,y=yBad}, corner2={x=rightX,y=GraphY}} picture
		# picture	= drawSeparation {x=leftX,y=yBad} {x=rightX,y=yBad} bads				 picture
		# picture	= setPenColour	 Black													 picture
		= picture
	where
		yBad		= GraphY-bads*KeyHitHeight
		yOk			= GraphY-hits*KeyHitHeight
		leftX		= GraphX+i *  SecondsWidth
		rightX		= leftX +     SecondsWidth
		hits		= oks+bads
		
		drawSeparation :: Point2 Point2 Int *Picture -> *Picture
		drawSeparation a b badHits picture
			| badHits==0	= picture
			| otherwise		= drawLine a b (setPenColour White picture)


//	Application constants:

KeyHitHeight	:== 10;		KeyHitsOffset :== 20
MaxNrKeyHits	:== 10
SecondsWidth	:== 4;		SecondsOffset :== 15
MaxNrOfSeconds	:== 60
	
SummaryMargin	:== 20
	
TopMargin		:== 20
LeftMargin		:== 60		// note: LeftMargin  >KeyHitsOffset
RightMargin		:== 30
BottomMargin	:== 60		// note: BottomMargin>SecondsOffset+SummaryMargin
	
GraphWidth		:== MaxNrOfSeconds*SecondsWidth
GraphHeight		:== MaxNrKeyHits  *KeyHitHeight
GraphX			:== LeftMargin
GraphY			:== TopMargin +GraphHeight
AxisMarkSize	:== 3
WindowWidth		:== LeftMargin+RightMargin +GraphWidth
WindowHeight	:== TopMargin +BottomMargin+GraphHeight

		
//	General functions:

round :: !Int !Real -> Real
round decimals r
	| decimals<=0	= toReal (toInt r)
	# p				= toReal (10^decimals)
	| otherwise		= toReal (toInt (r*p))/p

smap :: ((.s,.x) -> (.s,.y)) !(!.s,![.x]) -> (!.s,![.y])
smap f (s,[x:xs])
	# (s,y ) = f (s,x)
	# (s,ys) = smap f (s,xs)
	= (s,[y:ys])
smap _ (s,_)
	= (s,[])
