Skip to content

Commit

Permalink
feat(questionResponses): Citizen science uses responses to store answ…
Browse files Browse the repository at this point in the history
…ers to questions.
  • Loading branch information
peichins committed Mar 27, 2019
1 parent 93be50e commit bf8fa1c
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 224 deletions.
28 changes: 8 additions & 20 deletions src/app/citizenScience/bristlebird/bristlebird.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,27 +107,15 @@ class BristlebirdController {
this.showAudio = CitizenScienceCommon.bindShowAudio($scope);

//TODO: replace hardcoded value with routed study id
self.study_id = 1;
Question.questions(self.study_id).then(x => {

$scope.study_id = 1;
Question.questions($scope.study_id).then(x => {
console.log("questions loaded", x);
//TODO: update to allow multiple questions
$scope.labels = x.data.data[0].questionData.labels;

SampleLabels.init(x.data.data[0].id, $scope.study_id);
});

SampleLabels.init($scope.csProject, $scope.samples, $scope.labels);

/**
* Retrieve settings about this citizen science project
*/
CsLabels.getSettings($scope.csProject).then(
function (settings) {
$scope.settings = settings;
if ($scope.settings.hasOwnProperty("sampleDuration")) {
self.sampleDuration = $scope.settings.sampleDuration;
}
}
);
//SampleLabels.init($scope.csProject, $scope.samples, $scope.labels);

/**
* When the currentItem changes, change the current audio file / spectrogram to match it
Expand All @@ -143,9 +131,9 @@ class BristlebirdController {
var backgroundPath = self.backgroundPaths[parseInt(item.id) % (self.backgroundPaths.length - 1)];
backgroundImage.currentBackground = backgroundPath;
$scope.$broadcast("update-selected-labels", SampleLabels.getLabelsForSample(item.id));
// record that this sample has been viewed
SampleLabels.setValue(item.id);
$scope.numSamplesViewed = SampleLabels.getNumSamplesViewed();

// todo: check where this is used
//$scope.numSamplesViewed = SampleLabels.getNumSamplesViewed();
}
}, true);

Expand Down
3 changes: 2 additions & 1 deletion src/app/citizenScience/datasetProgress/datasetProgress.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ angular.module("bawApp.components.progress", ["bawApp.citizenScience.csSamples"]
}

$scope.$watch(() => CsSamples.currentItem(), (newVal, oldVal) => {
SampleLabels.registerCurrentSampleId(CsSamples.currentItem().id);
var newDatasetItemId = newVal.id;
SampleLabels.submitAndClear(newDatasetItemId);
});

/**
Expand Down
12 changes: 0 additions & 12 deletions src/app/citizenScience/labels/citizenScienceLabels.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,6 @@ csLabels.factory("CsLabels", [
});
},

/**
* Gets all settings associated with the specified citizen science project
* @param project string
* @returns {HttpPromise}
*/
getSettings: function (project) {
return $http.get(self.apiUrl(
"settings",
project
));
}

};

return self.publicFunctions;
Expand Down
243 changes: 56 additions & 187 deletions src/app/citizenScience/responses/citizenScienceSampleLabels.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,60 @@ var sampleLabels = angular.module("bawApp.citizenScience.sampleLabels",
["bawApp.citizenScience.common", "baw"]);

/**
*
* Handles applying labels to and removing labels from samples
* The "join" between labels and samples is stored in JSON in the following structure
* Keeps track of the labels applied to the current sample.
* Sends as a data for a questionResponse in the following structure
* {
* 'sampleId': {
* 'labelId': {
* 'value': [1,0],
* 'timestamp': [timestamp]
* }
* ...
* }
* ...
* 'labelsIds': [1,3,4,7]
* }
*
*
*/
sampleLabels.factory("SampleLabels", [
"CitizenScienceCommon",
"$http",
"DatasetItem",
function SampleLabels(CitizenScienceCommon, $http, DatasetItem) {
"QuestionResponse",
function SampleLabels(CitizenScienceCommon, $http, QuestionResponse) {

var self = this;

self.datasetId = 3;
// the data for questionResponses. Each question will have a unique key
self.data = {};
self.hasResponse = false;

/**
* checks the local storage for sampleLabels
*/
self.init = function (citizenScienceProject) {

self.localStorageKey = citizenScienceProject + "_sampleLabels";

var data = localStorage.getItem(self.localStorageKey);
/**
* happens once when the questions are loaded by the citizen science study controller
* @param questionid int
* @param studyId int
*/
self.init = function (questionId = false, studyId = false) {

if (data !== null) {
self.data = JSON.parse(data);
} else {
self.data = {};
if (studyId !== false) {
self.data.studyId = studyId;
}
if (questionId !== false) {
self.data.questionId = questionId;
}

self.csProject = citizenScienceProject;

return self.data;

};

self.currentSampleId = 0;

/**
* stringifies the object that acts as a join between samples and labels,
* then stores that json string in local storage
*/
self.writeToStorage = function () {
var value = JSON.stringify(self.data);
localStorage.setItem(self.localStorageKey, value);
};


/**
* submits all responses to the server
* will merge with existing responses using timestamp of each response to save the latest
* @param sampleId
* @param labelId
* @param value
* Happens whenever we get a new dataset item
* @param newDatasetItemId int
*/
self.submitResponse = function () {
self.setup = function (newDatasetItemId) {

//TODO
self.data.datasetItemId = newDatasetItemId;
self.data.labels = new Set();
// hasResponse will be stay true if a value has been added and then removed
// until this init function is called.
self.hasResponse = false;

};


self.functions = {

init : self.init,
Expand All @@ -86,18 +67,10 @@ sampleLabels.factory("SampleLabels", [
* @param labelId
* @returns {boolean}
*/
getValue : function (sampleId, labelId) {
getValue : function (labelId) {

if (sampleId === null) {
sampleId = self.currentSampleId;
}
return self.data.labels.has(labelId);

if (self.data[sampleId] !== undefined) {
if (self.data[sampleId][labelId] !== undefined) {
return self.data[sampleId][labelId].value;
}
}
return false;
},

/**
Expand All @@ -106,160 +79,56 @@ sampleLabels.factory("SampleLabels", [
* @param labelId int; if omitted, we are not applying a label but noting that the sample has been viewed
* @param value int [0,1]
*/
setValue : function (sampleId, labelId, value) {


if (sampleId === null) {
sampleId = self.currentSampleId;
}

if (sampleId <= 0) {
console.warn("bad sampleId supplied");
return;
}

if (self.data[sampleId] === undefined) {
self.data[sampleId] = {};
}
setValue : function (labelId, value) {

if (labelId !== undefined) {

if (self.data[sampleId][labelId] === undefined) {
self.data[sampleId][labelId] = {};
self.hasResponse = true;
if (value) {
self.data.labels.add(labelId);
} else {
self.data.labels.delete(labelId);
}

self.data[sampleId][labelId].value = value;
self.data[sampleId][labelId].timestamp = new Date();

}

self.writeToStorage();
self.submitResponse(sampleId, labelId, value);

},

/**
* returns an object that holds all the labels that have been applied to
* the given sample and their values. (if a label has been removed then it will be stored
* as false. If it has never been applied, it will not be present).
* @param sampleId
* @returns {*}
*/
getLabelsForSample : function (sampleId) {

if (typeof(self.data[sampleId]) !== "object") {
self.data[sampleId] = {};
}

return self.data[sampleId];
},

/**
* Returns whether any responses have been recorded for a sample
* @param sampleId
* sends the response to the server using the questionResponse service
* and reinitialises with a new datasetItemId
* @param newDatasetItemId
*/
hasResponse : function (sampleId = -1) {
submitAndClear : function (newDatasetItemId) {

if (sampleId < 0) {
sampleId = self.currentSampleId;
if (self.data.datasetItemId) {
// convert labels to data json
self.data.data = {"labels": [...self.data.labels]};
QuestionResponse.createQuestionResponse(self.data.questionId, self.data.datasetItemId, self.data.studyId, self.data.data);
}

if (!self.data.hasOwnProperty(sampleId)) {
return false;
}

return (Object.keys(self.data[sampleId]).length > 0);
// todo: is it better to do this in the promise success, incase it doesn't work? but then it should be linkedto the current dataset item shown.
self.setup(newDatasetItemId);

},

/**
* returns the number of samples that have responses
* If a sample is viewed, but no labels are applied, an element
* should be added to the data object as an empty object
* returns an object that holds all the labels that have been applied to
* the given sample and their values. (if a label has been removed then it will be stored
* as false. If it has never been applied, it will not be present).
* @param sampleId
* @returns {*}
*/
getNumSamplesViewed : function () {
if (self.data !== undefined) {
return Object.keys(self.data).length;
} else {
return 0;
}

},

registerCurrentSampleId : function (currentSampleId) {
self.currentSampleId = currentSampleId;
},
getLabelsForSample : function () {

/**
* Dev function to delete all applied labels
*/
clearLabels : function () {
self.data = {};
self.writeToStorage();
return [...self.data.labels];
},

/**
* Combines the SampleLabels data with the samples and labels
* to return the full report of which labels have been applied to which samples
* @return {{}|*}
* Returns whether any responses have been recorded for the current dataset item and question
* @param sampleId
*/
getData : function (labels) {

var d = self.data;

var keys = Object.keys(d);

if (keys.length === 0) {
return d;
}

var currentKey = -1;

var addItemDetails = function (response) {

var datasetItemId = keys[currentKey];

d[datasetItemId] = {
"sample" : response.data.data[0],
"labels" : JSON.parse(JSON.stringify(d[datasetItemId]))
};

for (var labelId in d[datasetItemId].labels) {

if (d[datasetItemId].labels.hasOwnProperty(labelId)) {

d[datasetItemId].labels[labelId] = {
"label": labels.find(l => true),
"response": JSON.parse(JSON.stringify(d[datasetItemId].labels[labelId]))
};

}

}

requestNextItem();


};

var requestFailed = function (response) {
requestNextItem();
};

var requestNextItem = function () {
currentKey++;
// recurse to do the next dataset item
if (currentKey < keys.length) {
DatasetItem.datasetItem(self.datasetId, keys[currentKey]).then(addItemDetails, requestFailed);
}
};

// request the first dataset item and add it to the returned object.
// when that is finished it will do the next one.
requestNextItem();


return d;
hasResponse : function () {
return self.hasResponse;
}

};
Expand Down
Loading

0 comments on commit bf8fa1c

Please sign in to comment.