/*
statisticscontact . cpp
Copyright ( c ) 2003 - 2004 by Marc Cramdal < marc . cramdal @ gmail . com >
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* This program is free software ; you can redistribute it and / or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation ; either version 2 of the License , or *
* ( at your option ) any later version . *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*/
# include <stdlib.h>
# include <tqvaluelist.h>
# include <tquuid.h>
# include <kdebug.h>
# include <klocale.h>
# include "kopetemetacontact.h"
# include "kopeteonlinestatus.h"
# include "statisticscontact.h"
# include "statisticsdb.h"
StatisticsContact : : StatisticsContact ( Kopete : : MetaContact * mc , StatisticsDB * db ) : m_metaContact ( mc ) , m_db ( db ) , m_oldStatus ( Kopete : : OnlineStatus : : Unknown )
{
m_isChatWindowOpen = false ;
m_oldStatusDateTime = TQDateTime : : currentDateTime ( ) ;
// Last*Changed are always false at start
m_timeBetweenTwoMessagesChanged = false ;
m_lastTalkChanged = false ;
m_lastPresentChanged = false ;
m_messageLengthChanged = false ;
}
/**
* \ brief saves contact statistics
*/
StatisticsContact : : ~ StatisticsContact ( )
{
if ( m_statisticsContactId . isEmpty ( ) )
return ;
commonStatsSave ( " timebetweentwomessages " , TQString : : number ( m_timeBetweenTwoMessages ) ,
TQString : : number ( m_timeBetweenTwoMessagesOn ) , m_timeBetweenTwoMessagesChanged ) ;
commonStatsSave ( " messagelength " , TQString : : number ( m_messageLength ) , TQString : : number ( m_messageLengthOn ) , m_messageLengthChanged ) ;
commonStatsSave ( " lasttalk " , m_lastTalk . toString ( ) , " " , m_lastTalkChanged ) ;
commonStatsSave ( " lastpresent " , m_lastPresent . toString ( ) , " " , m_lastPresentChanged ) ;
}
void StatisticsContact : : initialize ( Kopete : : Contact * c )
{
// Generate statisticsContactId or get it from database
TQStringList buffer = m_db - > query ( TQString ( " SELECT statisticid FROM contacts "
" WHERE contactid LIKE '%1'; "
) . arg ( c - > contactId ( ) ) ) ;
if ( buffer . isEmpty ( ) )
{
// Check if we don't have old data
if ( ! c - > metaContact ( ) - > metaContactId ( ) . isEmpty ( ) & &
! m_db - > query ( TQString ( " SELECT metacontactid FROM commonstats "
" WHERE metacontactid LIKE '%1'; "
) . arg ( c - > metaContact ( ) - > metaContactId ( ) ) ) . isEmpty ( ) )
{
// Use old style id
m_statisticsContactId = c - > metaContact ( ) - > metaContactId ( ) ;
}
else
{
// Create new id
m_statisticsContactId = TQUuid : : createUuid ( ) . toString ( ) ;
}
// Assign contactId to m_statisticsContactId
m_db - > query ( TQString ( " INSERT INTO contacts (statisticid, contactid) VALUES('%1', '%2'); "
) . arg ( m_statisticsContactId ) . arg ( c - > contactId ( ) ) ) ;
}
else
{
m_statisticsContactId = buffer [ 0 ] ;
}
kdDebug ( ) < < k_funcinfo < < " m_statisticsContactId: " < < m_statisticsContactId < < endl ;
commonStatsCheck ( " timebetweentwomessages " , m_timeBetweenTwoMessages , m_timeBetweenTwoMessagesOn , 0 , - 1 ) ;
commonStatsCheck ( " messagelength " , m_messageLength , m_messageLengthOn , 0 , 0 ) ;
// Check for last talk
TQString lastTalk ;
TQString dummy = " " ;
commonStatsCheck ( " lasttalk " , lastTalk , dummy ) ;
if ( lastTalk . isEmpty ( ) )
{
m_lastTalk . setTime_t ( 0 ) ;
m_lastTalkChanged = true ;
}
else
m_lastTalk = TQDateTime : : fromString ( lastTalk ) ;
// Get last time a message was received
m_lastMessageReceived = TQDateTime : : currentDateTime ( ) ;
// Check for lastPresent
TQString lastPresent = " " ;
commonStatsCheck ( " lastpresent " , lastPresent , dummy ) ;
if ( lastPresent . isEmpty ( ) )
{
m_lastPresent . setTime_t ( 0 ) ;
m_lastPresentChanged = true ;
}
else
m_lastPresent = TQDateTime : : fromString ( lastPresent ) ;
}
void StatisticsContact : : contactAdded ( Kopete : : Contact * c )
{
if ( ! m_statisticsContactId . isEmpty ( ) )
{
// Check if contact is allready in database if not add it
if ( m_db - > query ( TQString ( " SELECT id FROM contacts "
" WHERE statisticid LIKE '%1' AND contactid LIKE '%2'; "
) . arg ( m_statisticsContactId ) . arg ( c - > contactId ( ) ) ) . isEmpty ( ) )
{
// Assign contactId to m_statisticsContactId
m_db - > query ( TQString ( " INSERT INTO contacts (statisticid, contactid) VALUES('%1', '%2'); "
) . arg ( m_statisticsContactId ) . arg ( c - > contactId ( ) ) ) ;
}
kdDebug ( ) < < k_funcinfo < < " m_statisticsContactId: " < < m_statisticsContactId < < endl ;
}
else
{
// This is first contact, we need to initialize this object
initialize ( c ) ;
}
}
void StatisticsContact : : contactRemoved ( Kopete : : Contact * c )
{
if ( m_statisticsContactId . isEmpty ( ) )
return ;
kdDebug ( ) < < k_funcinfo < < " m_statisticsContactId: " < < m_statisticsContactId < < endl ;
m_db - > query ( TQString ( " DELETE FROM contacts WHERE statisticid LIKE '%1' AND contactid LIKE '%2'; "
) . arg ( m_statisticsContactId ) . arg ( c - > contactId ( ) ) ) ;
}
void StatisticsContact : : removeFromDB ( )
{
if ( m_statisticsContactId . isEmpty ( ) )
return ;
kdDebug ( ) < < k_funcinfo < < " m_statisticsContactId: " < < m_statisticsContactId < < endl ;
m_db - > query ( TQString ( " DELETE FROM contacts WHERE statisticid LIKE '%1'; " ) . arg ( m_statisticsContactId ) ) ;
m_db - > query ( TQString ( " DELETE FROM contactstatus WHERE metacontactid LIKE '%1'; " ) . arg ( m_statisticsContactId ) ) ;
m_db - > query ( TQString ( " DELETE FROM commonstats WHERE metacontactid LIKE '%1'; " ) . arg ( m_statisticsContactId ) ) ;
m_statisticsContactId = TQString ( ) ;
}
void StatisticsContact : : commonStatsSave ( const TQString name , const TQString statVar1 , const TQString statVar2 , const bool statVarChanged )
{
// Only update the database if there was a change
if ( ! statVarChanged ) return ;
if ( m_statisticsContactId . isEmpty ( ) )
return ;
m_db - > query ( TQString ( " UPDATE commonstats SET statvalue1 = '%1', statvalue2='%2' "
" WHERE statname LIKE '%3' AND metacontactid LIKE '%4'; " ) . arg ( statVar1 ) . arg ( statVar2 ) . arg ( name ) . arg ( m_statisticsContactId ) ) ;
}
void StatisticsContact : : commonStatsCheck ( const TQString name , int & statVar1 , int & statVar2 , const int defaultValue1 , const int defaultValue2 )
{
TQString a = TQString : : number ( statVar1 ) ;
TQString b = TQString : : number ( statVar2 ) ;
commonStatsCheck ( name , a , b , TQString : : number ( defaultValue1 ) , TQString : : number ( defaultValue2 ) ) ;
statVar1 = a . toInt ( ) ;
statVar2 = b . toInt ( ) ;
}
void StatisticsContact : : commonStatsCheck ( const TQString name , TQString & statVar1 , TQString & statVar2 , const TQString defaultValue1 , const TQString defaultValue2 )
{
if ( m_statisticsContactId . isEmpty ( ) )
return ;
TQStringList buffer = m_db - > query ( TQString ( " SELECT statvalue1,statvalue2 FROM commonstats WHERE statname LIKE '%1' AND metacontactid LIKE '%2'; " ) . arg ( name , m_statisticsContactId ) ) ;
if ( ! buffer . isEmpty ( ) )
{
statVar1 = buffer [ 0 ] ;
statVar2 = buffer [ 1 ] ;
}
else
{
m_db - > query ( TQString ( " INSERT INTO commonstats (metacontactid, statname, statvalue1, statvalue2) VALUES('%1', '%2', 0, 0); " ) . arg ( m_statisticsContactId , name ) ) ;
statVar1 = defaultValue1 ;
statVar2 = defaultValue2 ;
}
}
/**
* \ brief records informations from the new message
*
* Currently it does :
* < ul >
* < li > Recalculate the average time between two messages
* It should only calculate this time if a chatwindow is open ( sure , it isn ' t
* perfect , because we could let a chatwindow open a whole day but that ' s at this * time the nicest way , maybe we could check the time between the two last messages
* and if it is greater than , say , 10 min , do as there where no previous message )
* So we do this when the chatwindow is open . We don ' t set m_isChatWindowOpen to true
* when a new chatwindow is open , but when a new message arrives . However , we set it
* to false when the chatwindow is closed ( see StatisticsPlugin : : slotViewClosed ) .
*
* Then it is only a question of some calculations .
*
* < li > Recalculate the average message length
*
* < li > Change last - talk datetime
* < / ul >
*
*/
void StatisticsContact : : newMessageReceived ( Kopete : : Message & m )
{
kdDebug ( ) < < " statistics: new message received " < < endl ;
TQDateTime currentDateTime = TQDateTime : : currentDateTime ( ) ;
if ( m_timeBetweenTwoMessagesOn ! = - 1 & & m_isChatWindowOpen )
{
m_timeBetweenTwoMessages = ( m_timeBetweenTwoMessages * m_timeBetweenTwoMessagesOn + m_lastMessageReceived . secsTo ( currentDateTime ) ) / ( 1 + m_timeBetweenTwoMessagesOn ) ;
}
setIsChatWindowOpen ( true ) ;
m_timeBetweenTwoMessagesOn + = 1 ;
m_lastMessageReceived = currentDateTime ;
// Message length
m_messageLength = ( m . plainBody ( ) . length ( ) + m_messageLength * m_messageLengthOn ) / ( 1 + m_messageLengthOn ) ;
m_messageLengthOn + + ;
// Last talked
/// @todo do this in message sent too. So we need setLastTalk()
m_lastTalk = currentDateTime ;
m_messageLengthChanged = true ;
m_lastTalkChanged = true ;
m_timeBetweenTwoMessagesChanged = true ;
}
/**
* \ brief Update the database for this contact when required .
*/
void StatisticsContact : : onlineStatusChanged ( Kopete : : OnlineStatus : : StatusType status )
{
if ( m_statisticsContactId . isEmpty ( ) )
return ;
TQDateTime currentDateTime = TQDateTime : : currentDateTime ( ) ;
/// We don't want to log if oldStatus is unknown
/// the change could not be a real one; see StatisticsPlugin::slotMySelfOnlineStatusChanged
if ( m_oldStatus ! = Kopete : : OnlineStatus : : Unknown )
{
kdDebug ( ) < < " statistics - status change for " < < metaContact ( ) - > metaContactId ( ) < < " : " < < TQString : : number ( m_oldStatus ) < < endl ;
m_db - > query ( TQString ( " INSERT INTO contactstatus "
" (metacontactid, status, datetimebegin, datetimeend) "
" VALUES('%1', '%2', '%3', '%4' " " ); " ) . arg ( m_statisticsContactId ) . arg ( Kopete : : OnlineStatus : : statusTypeToString ( m_oldStatus ) ) . arg ( TQString : : number ( m_oldStatusDateTime . toTime_t ( ) ) ) . arg ( TQString : : number ( currentDateTime . toTime_t ( ) ) ) ) ;
}
if ( m_oldStatus = = Kopete : : OnlineStatus : : Online | | m_oldStatus = = Kopete : : OnlineStatus : : Away )
// If the last status was Online or Away, the last time contact was present is the time he goes offline
{
m_lastPresent = currentDateTime ;
m_lastPresentChanged = true ;
}
m_oldStatus = status ;
m_oldStatusDateTime = currentDateTime ;
}
bool StatisticsContact : : wasStatus ( TQDateTime dt , Kopete : : OnlineStatus : : StatusType status )
{
if ( m_statisticsContactId . isEmpty ( ) )
return false ;
TQStringList values = m_db - > query ( TQString ( " SELECT status, datetimebegin, datetimeend "
" FROM contactstatus WHERE metacontactid LIKE '%1' AND datetimebegin <= %2 AND datetimeend >= %3 "
" AND status LIKE '%4' "
" ORDER BY datetimebegin; "
) . arg ( m_statisticsContactId ) . arg ( dt . toTime_t ( ) ) . arg ( dt . toTime_t ( ) ) . arg ( Kopete : : OnlineStatus : : statusTypeToString ( status ) ) ) ;
if ( ! values . isEmpty ( ) ) return true ;
return false ;
}
TQString StatisticsContact : : statusAt ( TQDateTime dt )
{
if ( m_statisticsContactId . isEmpty ( ) )
return " " ;
TQStringList values = m_db - > query ( TQString ( " SELECT status, datetimebegin, datetimeend "
" FROM contactstatus WHERE metacontactid LIKE '%1' AND datetimebegin <= %2 AND datetimeend >= %3 "
" ORDER BY datetimebegin; "
) . arg ( m_statisticsContactId ) . arg ( dt . toTime_t ( ) ) . arg ( dt . toTime_t ( ) ) ) ;
if ( ! values . isEmpty ( ) ) return Kopete : : OnlineStatus ( Kopete : : OnlineStatus : : statusStringToType ( values [ 0 ] ) ) . description ( ) ;
else return " " ;
}
TQString StatisticsContact : : mainStatusDate ( const TQDate & date )
{
if ( m_statisticsContactId . isEmpty ( ) )
return " " ;
TQDateTime dt1 ( date , TQTime ( 0 , 0 , 0 ) ) ;
TQDateTime dt2 ( date . addDays ( 1 ) , TQTime ( 0 , 0 , 0 ) ) ;
kdDebug ( ) < < " dt1: " < < dt1 . toString ( ) < < " dt2: " < < dt2 . toString ( ) < < endl ;
TQString request = TQString ( " SELECT status, datetimebegin, datetimeend, metacontactid "
" FROM contactstatus WHERE metacontactid = '%1' AND "
" (datetimebegin >= %2 AND datetimebegin <= %3 OR "
" datetimeend >= %4 AND datetimeend <= %5) "
" ORDER BY datetimebegin; "
) . arg ( m_statisticsContactId ) . arg ( dt1 . toTime_t ( ) ) . arg ( dt2 . toTime_t ( ) ) . arg ( dt1 . toTime_t ( ) ) . arg ( dt2 . toTime_t ( ) ) ;
kdDebug ( ) < < request < < endl ;
TQStringList values = m_db - > query ( request ) ;
unsigned int online = 0 , offline = 0 , away = 0 ;
for ( uint i = 0 ; i < values . count ( ) ; i + = 4 )
{
unsigned int datetimebegin = values [ i + 1 ] . toInt ( ) , datetimeend = values [ i + 2 ] . toInt ( ) ;
kdDebug ( ) < < " statistics: id " < < values [ i + 3 ] < < " status " < < values [ i ] < < " datetimeend " < < TQString : : number ( datetimeend ) < < " datetimebegin " < < TQString : : number ( datetimebegin ) < < endl ;
if ( datetimebegin < = dt1 . toTime_t ( ) ) datetimebegin = dt1 . toTime_t ( ) ;
if ( datetimeend > = dt2 . toTime_t ( ) ) datetimeend = dt2 . toTime_t ( ) ;
if ( values [ i ] = = Kopete : : OnlineStatus : : statusTypeToString ( Kopete : : OnlineStatus : : Online ) )
online + = datetimeend - datetimebegin ;
else if ( values [ i ] = = Kopete : : OnlineStatus : : statusTypeToString ( Kopete : : OnlineStatus : : Away ) )
away + = datetimeend - datetimebegin ;
else if ( values [ i ] = = Kopete : : OnlineStatus : : statusTypeToString ( Kopete : : OnlineStatus : : Offline ) )
offline + = datetimeend - datetimebegin ;
}
if ( online > away & & online > offline ) return i18n ( " Online " ) ;
else if ( away > online & & away > offline ) return i18n ( " Away " ) ;
else if ( offline > online & & offline > away ) return i18n ( " Offline " ) ;
return " " ;
}
// TQDateTime StatisticsContact::nextOfflineEvent()
// {
// return nextEvent(Kopete::OnlineStatus::Offline);
// }
//
// TQDateTime StatisticsContact::nextOnlineEvent()
// {
// return nextEvent(Kopete::OnlineStatus::Online);
// }
// TQDateTime StatisticsContact::nextEvent(const Kopete::OnlineStatus::StatusType& status)
// {
//
// }
TQValueList < TQTime > StatisticsContact : : mainEvents ( const Kopete : : OnlineStatus : : StatusType & status )
{
TQStringList buffer ;
TQValueList < TQTime > mainEvents ;
if ( m_statisticsContactId . isEmpty ( ) )
return mainEvents ;
TQDateTime currentDateTime = TQDateTime : : currentDateTime ( ) ;
buffer = m_db - > query ( TQString ( " SELECT datetimebegin, datetimeend, status FROM contactstatus WHERE metacontactid LIKE '%1' ORDER BY datetimebegin " ) . arg ( m_statisticsContactId ) ) ;
// Only select the events for which the previous is not Unknown AND the status is status.
TQStringList values ;
for ( uint i = 0 ; i < buffer . count ( ) ; i + = 3 )
{
if ( buffer [ i + 2 ] = = Kopete : : OnlineStatus : : statusTypeToString ( status )
& & abs ( buffer [ i + 1 ] . toInt ( ) - buffer [ i ] . toInt ( ) ) > 120 )
{
values . push_back ( buffer [ i ] ) ;
}
}
// No entries for this contact ...
if ( ! values . count ( ) ) return mainEvents ;
// First we compute the average number of events/day : avEventsPerDay;
int avEventsPerDay = 0 ;
TQDateTime dt1 , dt2 ;
dt1 . setTime_t ( values [ 0 ] . toInt ( ) ) ;
dt2 . setTime_t ( values [ values . count ( ) - 1 ] . toInt ( ) ) ;
avEventsPerDay = tqRound ( ( double ) values . count ( ) / ( double ) dt1 . daysTo ( dt2 ) ) ;
kdDebug ( ) < < " statistics: average events per day : " < < avEventsPerDay < < endl ;
// We want to work on hours
TQValueList < int > hoursValues ;
for ( uint i = 0 ; i < values . count ( ) ; i + + )
{
TQDateTime dt ;
dt . setTime_t ( values [ i ] . toInt ( ) ) ;
hoursValues . push_back ( TQTime ( 0 , 0 , 0 ) . secsTo ( dt . time ( ) ) ) ;
}
// Sort the list
qHeapSort ( hoursValues ) ;
// Then we put some centroids (centroids in [0..24[)
TQValueList < int > centroids ;
int incr = tqRound ( ( double ) hoursValues . count ( ) / ( double ) avEventsPerDay ) ;
incr = incr ? incr : 1 ;
for ( uint i = 0 ; i < hoursValues . count ( ) ; i + = incr )
{
centroids . push_back ( hoursValues [ i ] ) ;
kdDebug ( ) < < " statistics: add a centroid : " < < centroids [ centroids . count ( ) - 1 ] < < endl ;
}
// We need to compute the centroids
centroids = computeCentroids ( centroids , hoursValues ) ;
// Convert to TQDateTime
for ( uint i = 0 ; i < centroids . count ( ) ; i + + )
{
kdDebug ( ) < < " statistics: new centroid : " < < centroids [ i ] < < endl ;
TQTime dt ( 0 , 0 , 0 ) ;
dt = dt . addSecs ( centroids [ i ] ) ;
mainEvents . push_back ( dt ) ;
}
return mainEvents ;
}
TQValueList < int > StatisticsContact : : computeCentroids ( const TQValueList < int > & centroids , const TQValueList < int > & values )
{
kdDebug ( ) < < " statistics: enter compute centroids " < < endl ;
TQValueList < int > whichCentroid ; // whichCentroid[i] = j <=> values[i] has centroid j for closest one
TQValueList < int > newCentroids ;
for ( uint i = 0 ; i < values . count ( ) ; i + + )
// Iterates over the values. For each one we need to get the closest centroid.
{
int value = values [ i ] ;
int distanceToNearestCentroid = abs ( centroids [ 0 ] - value ) ;
int nearestCentroid = 0 ;
for ( uint j = 1 ; j < centroids . count ( ) ; j + + )
{
if ( abs ( centroids [ j ] - value ) < distanceToNearestCentroid )
{
distanceToNearestCentroid = abs ( centroids [ j ] - value ) ;
nearestCentroid = j ;
}
}
whichCentroid . push_back ( nearestCentroid ) ;
}
// Recompute centroids
newCentroids = centroids ;
for ( uint i = 0 ; i < newCentroids . count ( ) ; i + + )
{
kdDebug ( ) < < " statistics: compute new centroids " < < i < < endl ;
int weight = 0 ;
for ( uint j = 0 ; j < values . count ( ) ; j + + )
{
int value = values [ j ] ;
if ( whichCentroid [ j ] = = i )
{
newCentroids [ i ] = tqRound ( ( double ) ( value + newCentroids [ i ] * weight ) / ( double ) ( weight + 1 ) ) ;
weight + + ;
}
}
}
// Should we recompute or are we OK ?
int dist = 0 ;
for ( uint i = 0 ; i < newCentroids . count ( ) ; i + + )
dist + = abs ( newCentroids [ i ] - centroids [ i ] ) ;
if ( dist > 10 )
return computeCentroids ( newCentroids , values ) ;
else
{
return newCentroids ;
}
}