view eventLogger.mm @ 49:178642d134a7 tip

xtra files
author Robert Tubb <rt300@eecs.qmul.ac.uk>
date Wed, 01 May 2013 17:34:33 +0100
parents a1e75b94c505
children
line wrap: on
line source
//
//  eventLogger.mm
//  oscSenderExample
//
//  Created by Robert Tubb on 05/11/2012.
//
//

//---------------------------------------------------------------------------
#include "eventLogger.h"
#include "grid.h"

extern Grid theGridView;

EventLogger eventLogger;
extern PresetManager presetManager;
//---------------------------------------------------------------------------
void lEvent::draw(){

    double size = 2.0;
    TwoVector pos = theGridView.coordToPixel(TwoVector(val1,val2)); // euch -rely on grid view?
    
    if(eventType == SCROLL){
        ofSetColor(255,123,56);
        ofRect(pos.x, pos.y,size,size);
    }else if(eventType == EVALUATION_POINT){
        size = sliderID/150.0; // slider id is the hover time. sorry sorry.
        if(size > 40) size = 40;
        ofSetColor(25,123,216);
        ofNoFill();
        ofEllipse(pos.x, pos.y, size, size);

    }
}

//---------------------------------------------------------------------------
EventLogger::EventLogger(){

    consentGiven = true; // unless told otherwise firstAppOpen
    loggingEnabled = true;
    serverConnectionOK = false;
    questionnaireCompleted = false;
    questionnaireUploaded = false;
    logUploadInProgress = false;
    ofxiPhoneDeviceType iOSdeviceType = ofxiPhoneGetDeviceType();
    cout << "Device: " << iOSdeviceType << '\n';
    
    nextUploadQty = UPLOAD_CHUNK_SIZE; // amount of data uploaded is always more than UPLOAD_CHUNK_SIZE events
    serverComms = [[ServerComms alloc] init];
    
}
//---------------------------------------------------------------------------
void EventLogger::startLoadAll(){
    
    loadExistingLogFile(ofxiPhoneGetDocumentsDirectory() + EVENT_LOG_FILENAME);
    
    
    sessionStartTime = ofGetSystemTime();
    testConnection();
    
    // if we have a back log of events upload them
    if(theEvents.size() > nextUploadQty && ! logUploadInProgress){
        //try to upload
        uploadEventLog(true);
    }
    
    if(questionnaireCompleted && !questionnaireUploaded){
        uploadQuestionnaire();
    }
    
    //TODO if questionnaire wasn't reached but still opened then set the timer to something sensible
    
    //timer.setInteractionTime(savedInteractionTime);
    //timer.setOrderFromPrevious(interfaceOrder);
    
   // for now sod it, start from scratch
    if(!questionnaireCompleted){
        cout<<"Questionnaire NOT completed - first APP open called\n";
        firstEverAppOpen();
    }

}
//---------------------------------------------------------------------------
void EventLogger::testConnection(){
    Json::Value root;
    root["x"] = "y";
    cout << "testConnection\n";
    sendToServer("testConnection", root, true);
    
}
//---------------------------------------------------------------------------
// this reads the persistent log file , checks if we've used the app before and
// if we've answered questionnaire or not

// should just store stuff. the start up state should be handled by testAPp

void EventLogger::loadExistingLogFile(const string &jsonFile){
    Json::Value root;
    Json::Reader reader;
    
    /////////////
    // read file
    
    ifstream theFile(jsonFile.c_str());
    stringstream fileText;
    string line;
    if(!theFile){
        cout<<"No event log file found - first APP open called\n";
        firstEverAppOpen();
        return;
    }else{
        while(theFile){
            theFile >> line;
            // cout << line;  // lots!!!!
            fileText << line;
        }
        theFile.close();
    }

    cout << "size of log JSON string:" << fileText.str().length() << "BYTES \n";
    
    bool parsingSuccessful = reader.parse( fileText.str(), root );
    
    if ( !parsingSuccessful )
    {
        // report to the user the failure and their locations in the document.
        std::cout  << "Failed to parse event log JSON: \n"
        << reader.getFormattedErrorMessages();
        return;
    }
    
    /////////////////
    // now put user deets into variables
    
    userName = root["userName"].asString();
    deviceID = root["deviceID"].asLargestInt();
    nextUploadNumber = root["uploadNumber"].asInt();
    savedInteractionTime = root["savedInteractionTime"].asLargestInt();
    questionnaireCompleted = root["questionnaireCompleted"].asBool();
    questionnaireUploaded = root["questionnaireUploaded"].asBool();
    interfaceOrder = root["interfaceOrder"].asInt();
    

    // check for unuploaded evts
    const Json::Value jlogs = root["events"];
    
    for ( int index = 0; index < jlogs.size(); ++index ) theEvents.push_back(lEvent(jlogs[index]));

    
    //////////////
    
    if(questionnaireCompleted && !questionnaireUploaded){
        // then read it in and upload it
        Json::Value JQ = root["questionnaire"];
        Json::Value JArray = JQ["qAnswers"];
        if(JArray.size() < 2){
            cout << "Error - status of questionnaire is wierd\n";
        }
        for ( unsigned int i = 0; i < JArray.size(); i++ )
        {
            questionnaireAnswers.push_back(JArray[i].asInt());
        }
        questionnaireComments = JQ["comments"].toStyledString();
        //uploadQuestionnaire();
    }

    cout << "Total interaction time: " << savedInteractionTime << '\n';
   
}

//---------------------------------------------------------------------------

void EventLogger::firstEverAppOpen(){
    
    nextUploadNumber = 0;
    deviceID = ofGetSystemTimeMicros();
    savedInteractionTime = 0;
    questionnaireCompleted = false;
    questionnaireUploaded = false;
    
}


//---------------------------------------------------------------------------
void EventLogger::questionnaireAnswersObtained(vector<int> answers, const char* userComments){
    
    questionnaireCompleted = true;
    questionnaireAnswers = answers;
    questionnaireComments = userComments;
    uploadQuestionnaire();
    logEvent(QUESTIONNAIRE_COMPLETED);
    
}
//---------------------------------------------------------------------------
void EventLogger::uploadQuestionnaire(){
    // show indicator
    cout << "^^^^^^^^ UPLOADING QUESTIONNAIRE ^^^^^^^^ \n";
    cout << questionnaireToJson() << "\n";
    sendToServer("questionnaire", questionnaireToJson(), true);

}
//---------------------------------------------------------------------------
bool EventLogger::sendToServer(string functionName, Json::Value jsonData, bool async = false){

    Json::FastWriter writer;
    string jsontext = writer.write( jsonData );
    
    // remove newline
    if (!jsontext.empty() && jsontext[jsontext.length()-1] == '\n') {
        jsontext.erase(jsontext.length()-1);
    }
    ostringstream jd;
    jd << jsontext;
    NSString *theData = [NSString stringWithUTF8String:jd.str().c_str()];
    NSString *theType = [NSString stringWithUTF8String:functionName.c_str()];

    if(async){
        [serverComms doPostRequest:theType withData:theData];
    }else{
        bool success = [serverComms doSyncPostRequest:theType withData:theData];
        return success;
    }
    
}
//-----------------------------
void EventLogger::questionnaireOK(){
    questionnaireUploaded = true;
    questionnaireComments = "";
}
//-----------------------------
void EventLogger::eventlogOK(){
    // COMMENT THIS IF UPLAODING FROM IPAD TO XCODE
    
    // it's a bad idea to do this in another thread...
    theEvents.clear();
    cout << "EVENT LOG UPLOAD SUCCESS\n";
    nextUploadNumber++;
    logUploadInProgress = false;
}
//-----------------------------
void EventLogger::testConnectionOK(){
    cout << "^^^^^^^^ server connection OK ^^^^^^^^ \n";
    serverConnectionOK = true;
}
//-----------------------------
void EventLogger::questionnaireNotOK(){
    cout << "XXXXX questionnaire  NOT OK XXXXXXX \n";
    questionnaireUploaded = false;
}
//-----------------------------
void EventLogger::eventlogNotOK(){
    // try later
    cout << "XXXXX event log  NOT OK XXXXXXX \n";
    nextUploadQty += UPLOAD_CHUNK_SIZE;
    logUploadInProgress = false;
}
//-----------------------------
void EventLogger::testConnectionNotOK(){
    cout << "XXXXX server connection NOT OK XXXXXXX \n";
    serverConnectionOK = false;
    // alert?
    
}


//---------------------------------------------------------------------------

bool EventLogger::uploadEventLog(bool async){

    // show indicator
    logUploadInProgress = true;
    cout << "^^^^^^^^  ATTEMPTING TO UPLOAD " << theEvents.size() << " EVENTS ^^^^^^^^ .\n";
    if(!async){
        bool success = sendToServer("eventlog", logsToJson(), async);
        if(!success){
            // try later : NOPE has maximum size
            nextUploadQty += UPLOAD_CHUNK_SIZE;
        }else{
            
            // if success - clear memory
            // IF UPLAODING FROM IPAD TO XCODE COMMENT OUT
            theEvents.clear();
            cout << "UPLOAD SUCCESS\n";
            nextUploadNumber++;
        }
        logUploadInProgress = false;
        return success;
    }else{
        sendToServer("eventlog", logsToJson(), async);
    }
}
//----------------------------------------------------------------------------
bool eventWasInRegion(deque<lEvent>::iterator eiter, TwoVector regionTopLeft, TwoVector regionBottomRight){
    if( ((*eiter).val1 > regionTopLeft.x ) && ((*eiter).val2 > regionTopLeft.y ) && ((*eiter).val1 < regionBottomRight.x ) && ((*eiter).val2 < regionBottomRight.y )){
        return true;
    }else{
        return false;
    }
    
}
//----------------------------------------------------------------------------
void EventLogger::clearTrail(){
    eventsToDraw.clear();
}
//----------------------------------------------------------------------------
vector<lEvent *> EventLogger::getEvaluationPointsInRange(const TwoVector min, const TwoVector max){
    vector<lEvent *> results;
    if(eventsToDraw.size() <= 0){
        return results;
        
    }
    
    deque<lEvent>::iterator eiter;
    deque<lEvent>::iterator preveiter;
    for(eiter = --eventsToDraw.end(); eiter >= eventsToDraw.begin(); eiter--){
        if((*eiter).eventType == EVALUATION_POINT){
            if( ((*eiter).val1 > min.x ) && ((*eiter).val2 > min.y ) && ((*eiter).val1 < max.x ) && ((*eiter).val2 < max.y )){
                results.push_back(&(*eiter));
            }
        }
    }
    return results;
}
//----------------------------------------------------------------------------
void EventLogger::drawTrail(const TwoVector min, const TwoVector max){
    if(eventsToDraw.size() <= 0){
        return;
        
    }
    deque<lEvent>::iterator eiter;
    deque<lEvent>::iterator preveiter;
    int i = 0;
    
    
    preveiter = --eventsToDraw.end();
    TwoVector start = TwoVector(ofGetWidth()*0.5,ofGetHeight()*0.5);
    TwoVector end = theGridView.coordToPixel(TwoVector((*preveiter).val1,(*preveiter).val2));
    ofSetColor(255,255,255,96);
    ofLine(start.x,start.y, end.x, end.y);
    
    for(eiter = --eventsToDraw.end(); eiter >= eventsToDraw.begin(); eiter--){

        //cout << i << '\n';
        if( (*eiter).eventType == SCROLL
           || (*eiter).eventType == EVALUATION_POINT
           || (*eiter).eventType == RANDOMISE
           || (*eiter).eventType == CHANGE_SLIDER)
        {
            i++;
            if(i > SCROLL_TRAIL_LENGTH){
                return;
            }
            if(eventWasInRegion(eiter, min, max) || eventWasInRegion(preveiter, min, max)){
                // draw a line between prev and this
                if(eiter != eventsToDraw.begin()){
                    TwoVector start = theGridView.coordToPixel(TwoVector((*preveiter).val1,(*preveiter).val2));
                    TwoVector end = theGridView.coordToPixel(TwoVector((*eiter).val1,(*eiter).val2));
                    ofSetColor(255,255,255,96);
                    ofLine(start.x,start.y, end.x, end.y);
                
                }
            
            }
            preveiter = eiter;
        }
        if( (*eiter).eventType == EVALUATION_POINT){
            if( ((*eiter).val1 > min.x ) && ((*eiter).val2 > min.y ) && ((*eiter).val1 < max.x ) && ((*eiter).val2 < max.y )){
                // draw it
                (*eiter).draw();
            }
        }
    }
    
}

//---------------------------------------------------------------------------
// only called when doing supervised tests
void EventLogger::newUser(){
    // store old stuff
    
    saveSessionToFile();
    presetManager.saveSessionToFile(userName);
    cout<<"setup new user\n";
    deleteLogs();
    nextUploadNumber = 0;
    deviceID = ofGetSystemTimeMicros();
    savedInteractionTime = 0;
    totalInteractionTime = 0;
    sessionStartTime = ofGetSystemTime();
    questionnaireCompleted = false;
    questionnaireUploaded = false; 
    
    ((testApp *)ofGetAppPtr())->showIntro();
    
}
//---------------------------------------------------------------------------
// called from alertView OK in iViewController
void EventLogger::setUsername(const char *u){
    userName = u;
    
}
//---------------------------------------------------------------------------
void EventLogger::thinnedScrollEvent(lEvent newEvent){
    static lEvent previousEvent(EMPTY_EVENT); // initialised as whatever
    static int eventCounter = 0;
    
    // if first event then log it. it won't be, but still.
    if(theEvents.size() == 0){
        theEvents.push_back(newEvent);
        previousEvent = newEvent;
        return;
    }

    // if previous event is more than 300ms ago, or event type has changed, log both of them (lots of events on move+zoom combinations!!)
    int gap = newEvent.eventTime - previousEvent.eventTime;
    if(gap > 300){
        // log previous event as a evaluation point
        lEvent evalEvt(EVALUATION_POINT, previousEvent.val1, previousEvent.val2, gap);
        theEvents.push_back(evalEvt);
        eventsToDraw.push_back(evalEvt);
        if(eventsToDraw.size() > SCROLL_TRAIL_LENGTH){
            eventsToDraw.pop_front();
        }
        // and now new event as scroll
        theEvents.push_back(newEvent);
        eventsToDraw.push_back(newEvent);
        if(eventsToDraw.size() > SCROLL_TRAIL_LENGTH){
            eventsToDraw.pop_front();
        }
        eventCounter = 0;

    }else if(eventCounter >= EVENT_THIN_FACTOR){ // otherwise only record every Nth event
        theEvents.push_back(newEvent);
        eventsToDraw.push_back(newEvent);
        if(eventsToDraw.size() > SCROLL_TRAIL_LENGTH){
            eventsToDraw.pop_front();
        }
        eventCounter = 0;
        
    }
    eventCounter++;
    previousEvent = newEvent;
    
}//---------------------------------------------------------------------------
void EventLogger::thinnedSliderEvent(lEvent newEvent){
    static lEvent previousEvent(EMPTY_EVENT); // initialised as whatever. hopefully won't log
    static int eventCounter = 0;
    
    // if first event then log it.
    if(theEvents.size() == 0){
        theEvents.push_back(newEvent);
        previousEvent = newEvent;
        return;
    }

    // if previous event is more than 300ms ago log both of them 
    int gap = newEvent.eventTime - previousEvent.eventTime;
    if(gap > 300){
        // we need the PREV grid coord though, which is lost!!
        TwoVector c = theGridView.getCoord();
        lEvent evalEvt(EVALUATION_POINT, c.x, c.y, gap);
        theEvents.push_back(evalEvt);
        eventsToDraw.push_back(evalEvt);
        if(eventsToDraw.size() > SCROLL_TRAIL_LENGTH){
            eventsToDraw.pop_front();
        }
        // if prev slider event not logged, log it
        if(theEvents.back().eventTime != previousEvent.eventTime){
            theEvents.push_back(previousEvent);
        }
        
        // also log new slider evt
        theEvents.push_back(newEvent);
        eventCounter = 0;
        
    }else if(eventCounter >= EVENT_THIN_FACTOR){ // otherwise only record every Nth event
        theEvents.push_back(newEvent);
        eventCounter = 0;
        
    }
    eventCounter++;
    previousEvent = newEvent;
}
//---------------------------------------------------------------------------
void EventLogger::thinnedZoomEvent(lEvent newEvent){
    static lEvent previousEvent(EMPTY_EVENT); // initialised as whatever. hopefully won't log
    static int eventCounter = 0;
    
    // if first event then log it. it won't be, but still.
    if(theEvents.size() == 0){
        theEvents.push_back(newEvent);
        previousEvent = newEvent;
        return;
    }
    
    int gap = newEvent.eventTime - previousEvent.eventTime;
    if(gap > 400){
        // if prev event not logged, log it
        if(theEvents.back().eventTime != previousEvent.eventTime){
            theEvents.push_back(previousEvent);
        }
        theEvents.push_back(newEvent);
        eventCounter = 0;

    }else if(eventCounter >= EVENT_THIN_FACTOR){
        theEvents.push_back(newEvent);
        eventCounter = 0;

    }
    eventCounter++;
    previousEvent = newEvent;
}
//-----------------------------------------------------------------------------
void EventLogger::thinnedSnapEvent(lEvent nextEvent){
    static lEvent previousEvent(EMPTY_EVENT);
    if(nextEvent.val1 != previousEvent.val1 || nextEvent.val2 != previousEvent.val2){
        theEvents.push_back(nextEvent);
        cout << "Snapped EVENT\n";
    }
    previousEvent = nextEvent;
}
//---------------------------------------------------------------------------
void EventLogger::logEvent(const leventType& evtType,const TwoVector& centre, const double& scale, const int& sliderID, const double& sliderVal){
    
    
    // scroll has 2 double coords
    // zoom has 1 double scale
    // save preset has 2 coords
    // switch view has view type
    // slider change has int slider index and 1 float value
    
    // get time for key index
    
    // thinFactor
    if(!loggingEnabled) return;
    switch ( evtType ) {
            // data thinning here
        case SCROLL:
            thinnedScrollEvent(lEvent(evtType,centre.x,centre.y));
            //theEvents.push_back(lEvent(evtType,centre.x,centre.y));
            break;
        case ZOOM:
            thinnedZoomEvent(lEvent(evtType,scale));
            break;
        case CHANGE_SLIDER:
            thinnedSliderEvent(lEvent(evtType,sliderVal , 0.0 , sliderID));
            break;
        case SNAPPED_TO_PRESET:
            thinnedSnapEvent(lEvent(evtType,centre.x,centre.y,sliderID));
            
            break;
           // non thinned data
        case SAVE_PRESET:
            theEvents.push_back(lEvent(evtType,centre.x,centre.y));
            // Code
            break;
        case SAVE_DESET:
            theEvents.push_back(lEvent(evtType,centre.x,centre.y));
            break;

        case SCROLL_STOPPED:
            theEvents.push_back(lEvent(evtType,centre.x,centre.y));
            break;
    
        case SWAP_VIEW:
            theEvents.push_back(lEvent(evtType,0.0 , 0.0 , sliderID)); // slider ID is which view
            break;
        // these cases are move events that need to be put in events to draw
        case RANDOMISE:
            theEvents.push_back(lEvent(evtType,centre.x,centre.y));
            eventsToDraw.push_back(lEvent(evtType,centre.x,centre.y));
            if(eventsToDraw.size() > SCROLL_TRAIL_LENGTH){
                eventsToDraw.pop_front();
            }
            break;
        default:
            // default is just an event type with no values
            theEvents.push_back(lEvent(evtType));
            break;
    }
    if(theEvents.size() > nextUploadQty && !logUploadInProgress){
        //try to upload asynchronously
        uploadEventLog(true);
    }
    //sessionTime = (ofGetSystemTime() - sessionStartTime);
    totalInteractionTime = savedInteractionTime + (ofGetSystemTime() - sessionStartTime); // milliseconds

}
//--------------------------------------------------------------------
// called from newUser
void EventLogger::deleteLogs(){
    // the
    theEvents.clear();
    string fname = ofxiPhoneGetDocumentsDirectory() + EVENT_LOG_FILENAME;
    ofFile logFile(fname,ofFile::WriteOnly);
    logFile << "";
    logFile.close();
}
//---------------------------------------------------------------------------

void EventLogger::exitAndSave(){
    
    if(!consentGiven){
        logEvent(CONSENT_DENIED);
        Json::Value jlogs = logsToJson();
        // try to upload TODO (no - might hang and prevent exit???)
        uploadEventLog(true);
        return;
    }
    logEvent(APP_EXITED);
    savedInteractionTime = savedInteractionTime + (ofGetSystemTime() - sessionStartTime);
    // save user details
    string fname = ofxiPhoneGetDocumentsDirectory() + EVENT_LOG_FILENAME;

    // try to upload
    // do it sync because event list needs to be cleared to prevent saving on device
    uploadEventLog(false);
    
    // write to file
    // json without the logs that were uploaded!
    Json::Value jlogs = logsToJson();
    ofFile logFile(fname,ofFile::WriteOnly);
    logFile << jlogs;
    logFile.close();
    
}
//---------------------------------------------------------------------------

Json::Value EventLogger::logsToJson(){
    // put all logged events into Json formatted string
    Json::Value root;

    vector<lEvent>::iterator eventIter;
    
    root["programVersion"] = PROGRAM_VERSION;
    root["userName"] = userName;
    root["deviceID"] = deviceID;
    root["uploadNumber"] = nextUploadNumber;
    root["iOSdeviceType"] = iOSdeviceType;
    root["savedInteractionTime"] = savedInteractionTime;
    root["questionnaireCompleted"] = questionnaireCompleted;
    root["questionnaireUploaded"] = questionnaireUploaded;
    if(!questionnaireUploaded) root["questionnaire"] = questionnaireToJson();
    
    
    int i = 0;
    for(eventIter = theEvents.begin(); eventIter < theEvents.end(); eventIter++){
        root["events"][i] = (*eventIter).eventToJson();
        i++;
    }
    root["numEventsHere"] = i;
    return root;
}

//---------------------------------------------------------------------------

Json::Value EventLogger::questionnaireToJson(){
    // put all answers into Json formatted string
    Json::Value root;
    
    vector<int>::iterator aIter;

    Json::Value questionnaire;
    
    int i = 0;
    for(aIter = questionnaireAnswers.begin(); aIter < questionnaireAnswers.end(); aIter++){
        questionnaire[i] = (*aIter);
        i++;
    }

    root["qAnswers"] = questionnaire;
    root["comments"] = questionnaireComments;
    root["userName"] = userName;
    root["deviceID"] = deviceID;
    root["iOSdeviceType"] = iOSdeviceType;
    root["programVersion"] = PROGRAM_VERSION;
    
    return root;
}

//---------------------------------------------------------------------------
void EventLogger::printAll(){
    cout << "-----------------ALL LOGGED EVENTS----------------- \n";
    vector<lEvent>::iterator evIter;
    cout << logsToJson() << "\n";
    cout << "---------------------QUESTIONNAIRE---------------- \n";
    cout << questionnaireToJson() << "\n";
};
//---------------------------------------------------------------------------

void EventLogger::saveSessionToFile(){
    string fname = ofxiPhoneGetDocumentsDirectory() + userName + '_' + EVENT_LOG_FILENAME;
    
    // write to file
    // json without the logs that were uploaded!
    Json::Value jlogs = logsToJson();
    ofFile logFile(fname,ofFile::WriteOnly);
    logFile << jlogs;
    logFile.close();

}
//----------------------------------------------------------------------------
// TODO this path thing
vector<TwoVector> EventLogger::getRecentPath(int numEvents){
    vector<TwoVector> thePath;
    
    TwoVector lastScrollPos;
    vector<lEvent>::iterator eventIndex;
    eventIndex = theEvents.end();
    int numScrolls = 0;
    while(numScrolls < numEvents){
        // go back check for scrolls, check for end of array etc.
        thePath.push_back(lastScrollPos);
        numScrolls++;
    }
    return thePath;
}