From 7bedaafbc90ba68957faddf85371fd069f3da499 Mon Sep 17 00:00:00 2001 From: Phil Eichinski Date: Fri, 7 Jun 2019 16:53:08 +1000 Subject: [PATCH] feat(citSci): started adding capability for questions of different shapes --- .../citizenScience/bristlebird/bristlebird.js | 82 ++++------ .../bristlebird/listen.tpl.html | 21 +-- .../labels/citizenScienceLabels.js | 146 +++++++++--------- .../responses/citizenScienceSampleLabels.js | 25 ++- src/app/citizenScience/responses/responses.js | 36 +---- src/app/citizenScience/thumbLabels/labels.js | 16 +- .../thumbLabels/labels.tpl.html | 21 +-- .../yesnoLabels/_yesnoLabels.scss | 141 +++++++++++++++++ src/app/citizenScience/yesnoLabels/label.js | 56 +++++++ src/app/citizenScience/yesnoLabels/labels.js | 24 +++ .../yesnoLabels/labels.tpl.html | 47 ++++++ src/baw.paths.nobuild.js | 11 +- src/components/models/models.js | 3 +- src/components/models/study.js | 17 ++ src/components/services/question.js | 2 +- src/components/services/services.js | 1 + src/components/services/study.js | 33 ++++ 17 files changed, 487 insertions(+), 195 deletions(-) create mode 100644 src/app/citizenScience/yesnoLabels/_yesnoLabels.scss create mode 100644 src/app/citizenScience/yesnoLabels/label.js create mode 100644 src/app/citizenScience/yesnoLabels/labels.js create mode 100644 src/app/citizenScience/yesnoLabels/labels.tpl.html create mode 100644 src/components/models/study.js create mode 100644 src/components/services/study.js diff --git a/src/app/citizenScience/bristlebird/bristlebird.js b/src/app/citizenScience/bristlebird/bristlebird.js index 1943518a..c3bc760c 100644 --- a/src/app/citizenScience/bristlebird/bristlebird.js +++ b/src/app/citizenScience/bristlebird/bristlebird.js @@ -22,11 +22,13 @@ class BristlebirdController { $location, CitizenScienceCommon, CsSamples, - CsLabels, SampleLabels, backgroundImage, paths, - Question) { + Question, + $routeParams, + StudyService + ) { var self = this; @@ -34,15 +36,7 @@ class BristlebirdController { * The name of the css project as it appears in the dataset definition * @type {string} */ - $scope.csProject = "ebb"; - - /** - * The duration of each audio sample - * currently all samples will be the same duration (not set per sample in the dataset) - * @type {number} - */ - self.sampleDuration = 25; - + $scope.csProject = $routeParams.studyName; /** * The current sample object, including sample id @@ -55,8 +49,6 @@ class BristlebirdController { // to be populated after getting samples from dataset $scope.media = null; - - // load populate the onboarding steps as they are loaded. $scope.onboarding = {}; $scope.onboarding.steps = [ @@ -94,37 +86,7 @@ class BristlebirdController { }; - - /** - * Labels that the user can select. - * applies one or more tags which are not shown to the user. - * example response from server - * [{ - * "tags": ["ebb", "type1"], - * "name": "Eastern Bristlebird", - * "examples": [{ - * "annotationId": 124730 - * }, { - * "annotationId": 124727 - * }, { - * "annotationId": 98378 - * }] - * }, - * { - * "tags": ["ground_parrot", "type1"], - * "name": "Ground Parrot", - * "examples": [{ - * "annotationId": 124622 - * }] - * }, - * { - * "tags": ["quoll", "type1"], - * "label": "Spotted Quoll", - * "examples": [] - * }]; - */ - - $scope.labels = []; + $scope.questionData = {}; // the model passed to ngAudio $scope.audioElementModel = CitizenScienceCommon.getAudioModel(); @@ -132,14 +94,28 @@ class BristlebirdController { this.showAudio = CitizenScienceCommon.bindShowAudio($scope); //TODO: replace hardcoded value with routed study id - $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); + + StudyService.studyByName($routeParams.studyName).then(x => { + var studies = x.data.data; + if (studies.length === 0) { + console.warn("No study " + $routeParams.studyName + " exists"); + return; + } else if (studies.length > 1) { + console.warn("More than one study found. Using the first one"); + } + $scope.study = studies[0]; + $scope.study_id = $scope.study.id; + + Question.questions($scope.study_id).then(x => { + console.log("questions loaded", x); + //TODO: update to allow multiple questions + $scope.questionData = x.data.data[0].questionData; + + SampleLabels.init(x.data.data[0], $scope.study_id); + }); }); + //SampleLabels.init($scope.csProject, $scope.samples, $scope.labels); /** @@ -194,8 +170,7 @@ angular "bawApp.components.citizenScienceThumbLabels", "bawApp.components.onboarding", "bawApp.components.background", - "bawApp.citizenScience.csSamples", - "bawApp.citizenScience.csLabels" + "bawApp.citizenScience.csSamples" ]) .controller( "BristlebirdController", @@ -205,11 +180,12 @@ angular "$location", "CitizenScienceCommon", "CsSamples", - "CsLabels", "SampleLabels", "backgroundImage", "conf.paths", "Question", + "$routeParams", + "Study", BristlebirdController ]) .controller( diff --git a/src/app/citizenScience/bristlebird/listen.tpl.html b/src/app/citizenScience/bristlebird/listen.tpl.html index 0e8aa2e7..dc0dc3a2 100644 --- a/src/app/citizenScience/bristlebird/listen.tpl.html +++ b/src/app/citizenScience/bristlebird/listen.tpl.html @@ -47,22 +47,11 @@

Eastern Bristlebird Search - - - - - + + + + + diff --git a/src/app/citizenScience/labels/citizenScienceLabels.js b/src/app/citizenScience/labels/citizenScienceLabels.js index e6ad44c8..d952321d 100644 --- a/src/app/citizenScience/labels/citizenScienceLabels.js +++ b/src/app/citizenScience/labels/citizenScienceLabels.js @@ -1,73 +1,73 @@ -var csLabels = angular.module("bawApp.citizenScience.csLabels", ["bawApp.citizenScience.common"]); - - -/** - * Manages the data for labels that will be applied to cs samples - */ -csLabels.factory("CsLabels", [ - "CitizenScienceCommon", - "$http", - function CsLabels(CitizenScienceCommon, $http) { - - var self = this; - self.useLocalData = true; - self.sheets_api_url = "http://" + window.location.hostname + ":8081"; - self.local_api_url = "/public/citizen_science"; - - - /** - * Constructs a url for the request by concatenating the arguments, joined by "/" - * and appending to the relevant baseURL. Allows experimenting with different sources - * for the data without changing everything - * @returns {string|*} - */ - self.apiUrl = function () { - // convert to array - var base_url, url; - if (self.useLocalData) { - base_url = self.local_api_url; - } else { - base_url = self.sheets_api_url; - } - var args = Array.prototype.slice.call(arguments); - - url = [base_url].concat(args).join("/"); - - if (self.useLocalData) { - url = url + ".json"; - } - - return url; - }; - - - self.publicFunctions = { - - - /** - * Gets all labels associated with the specified citizen science project - * @param project string - */ - getLabels: function (project) { - var response = $http.get(self.apiUrl( - "labels", - project - )); - - return response.then(function (response) { - var labels = []; - if (Array.isArray(response.data)) { - labels = response.data; - } - - return labels; - }); - }, - - }; - - return self.publicFunctions; - - }]); - - +// var csLabels = angular.module("bawApp.citizenScience.csLabels", ["bawApp.citizenScience.common"]); +// +// +// /** +// * Manages the data for labels that will be applied to cs samples +// */ +// csLabels.factory("CsLabels", [ +// "CitizenScienceCommon", +// "$http", +// function CsLabels(CitizenScienceCommon, $http) { +// +// var self = this; +// self.useLocalData = true; +// self.sheets_api_url = "http://" + window.location.hostname + ":8081"; +// self.local_api_url = "/public/citizen_science"; +// +// +// /** +// * Constructs a url for the request by concatenating the arguments, joined by "/" +// * and appending to the relevant baseURL. Allows experimenting with different sources +// * for the data without changing everything +// * @returns {string|*} +// */ +// self.apiUrl = function () { +// // convert to array +// var base_url, url; +// if (self.useLocalData) { +// base_url = self.local_api_url; +// } else { +// base_url = self.sheets_api_url; +// } +// var args = Array.prototype.slice.call(arguments); +// +// url = [base_url].concat(args).join("/"); +// +// if (self.useLocalData) { +// url = url + ".json"; +// } +// +// return url; +// }; +// +// +// self.publicFunctions = { +// +// +// /** +// * Gets all labels associated with the specified citizen science project +// * @param project string +// */ +// getLabels: function (project) { +// var response = $http.get(self.apiUrl( +// "labels", +// project +// )); +// +// return response.then(function (response) { +// var labels = []; +// if (Array.isArray(response.data)) { +// labels = response.data; +// } +// +// return labels; +// }); +// }, +// +// }; +// +// return self.publicFunctions; +// +// }]); +// +// diff --git a/src/app/citizenScience/responses/citizenScienceSampleLabels.js b/src/app/citizenScience/responses/citizenScienceSampleLabels.js index af33946c..b02049a1 100644 --- a/src/app/citizenScience/responses/citizenScienceSampleLabels.js +++ b/src/app/citizenScience/responses/citizenScienceSampleLabels.js @@ -21,6 +21,8 @@ sampleLabels.factory("SampleLabels", [ // the data for questionResponses. Each question will have a unique key self.data = {}; self.hasResponse = false; + self.allowEmpty = true; + self.allowMulti = true; @@ -29,15 +31,29 @@ sampleLabels.factory("SampleLabels", [ * @param questionid int * @param studyId int */ - self.init = function (questionId = false, studyId = false) { + self.init = function (question = false, studyId = false) { if (studyId !== false) { self.data.studyId = studyId; } - if (questionId !== false) { - self.data.questionId = questionId; + if (question !== false) { + self.data.questionId = question.id; + + if (question.data.hasOwnProperty("allowEmpty")) { + self.allowEmpty = question.data.allowEmpty; + } + + if (question.data.hasOwnProperty("allowMulti")) { + self.allowMulti = question.data.allowMulti; + } + if (question.data.labels.length === 1) { + // for binary yes/no there is only one label, therefore no multi select + self.allowMulti = false; + } + } + return self.data; }; @@ -70,6 +86,9 @@ sampleLabels.factory("SampleLabels", [ if (labelId !== undefined) { self.hasResponse = true; if (value) { + if (!self.allowMulti) { + self.data.labels.clear(); + } self.data.labels.add(labelId); } else { self.data.labels.delete(labelId); diff --git a/src/app/citizenScience/responses/responses.js b/src/app/citizenScience/responses/responses.js index e6f84bf4..92bd9692 100644 --- a/src/app/citizenScience/responses/responses.js +++ b/src/app/citizenScience/responses/responses.js @@ -1,37 +1,14 @@ +/* This controls the screen where all responses are listed for admin */ class ResponsesController { constructor($scope, SampleLabels, - CsLabels) { + Question) { var self = this; - SampleLabels.init("ebb"); - - self.labels = false; - - CsLabels.getLabels("ebb").then(labels => { - $scope.responseData = SampleLabels.getData(labels); - self.labels = labels; - }); - - - - - $scope.$watch("responseData", function (newVal, oldVal) { - $scope.responseDataString = JSON.stringify(newVal, null, 4); - },true); - - $scope.deleteResponses = function () { - if (confirm("Are you sure you want to delete all the responses?")) { - SampleLabels.clearLabels(); - CsLabels.getLabels("ebb").then(labels => { - $scope.responseData = SampleLabels.getData(labels); - self.labels = labels; - }); - } - }; - + // todo: display table of responses. + console.log(self); } @@ -40,14 +17,13 @@ class ResponsesController { angular .module("bawApp.citizenScience.responses", [ - "bawApp.citizenScience.sampleLabels", - "bawApp.citizenScience.csLabels" + "bawApp.citizenScience.sampleLabels" ]) .controller( "ResponsesController", [ "$scope", "SampleLabels", - "CsLabels", + "Question", ResponsesController ]); \ No newline at end of file diff --git a/src/app/citizenScience/thumbLabels/labels.js b/src/app/citizenScience/thumbLabels/labels.js index 31244d0f..9288f1b9 100644 --- a/src/app/citizenScience/thumbLabels/labels.js +++ b/src/app/citizenScience/thumbLabels/labels.js @@ -37,9 +37,13 @@ angular.module("bawApp.components.citizenScienceThumbLabels", $scope.examplesPosition = "0px"; $scope.$watch(function () { - return self.labels; + return self.questionData; }, function (newVal, oldVal) { - self.fetchAnnotationData(newVal); + if (newVal !== null && typeof newVal === "object") { + if (newVal.hasOwnProperty("labels")) { + self.fetchAnnotationData(); + } + } }); /** @@ -48,9 +52,10 @@ angular.module("bawApp.components.citizenScienceThumbLabels", * full "anotation" object that contains the AudioEvent model as well Media model * @param labels Object */ - self.fetchAnnotationData = function (labels) { + self.fetchAnnotationData = function () { // transform labels structure into a single array of annotationsIds + var labels = self.questionData.labels; var annotationIds = [].concat.apply([], labels.map(l => l.examples)).map(e => e.annotationId); if (annotationIds.length === 0) { @@ -92,7 +97,7 @@ angular.module("bawApp.components.citizenScienceThumbLabels", // add annotations back into labels object response.annotations.forEach(function (annotation) { - self.labels.forEach(function (l) { + self.questionData.labels.forEach(function (l) { l.examples.forEach(function (e) { if (e.annotationId === annotation.id) { e.annotation = annotation; @@ -101,7 +106,6 @@ angular.module("bawApp.components.citizenScienceThumbLabels", }); }); - console.log(self.labels); }, function (httpResponse) { console.error("Failed to load citizen science example item response.", httpResponse); @@ -109,6 +113,6 @@ angular.module("bawApp.components.citizenScienceThumbLabels", }; }], bindings: { - labels: "=", + questionData: "=", } }); \ No newline at end of file diff --git a/src/app/citizenScience/thumbLabels/labels.tpl.html b/src/app/citizenScience/thumbLabels/labels.tpl.html index c28fc77f..980d678f 100644 --- a/src/app/citizenScience/thumbLabels/labels.tpl.html +++ b/src/app/citizenScience/thumbLabels/labels.tpl.html @@ -1,13 +1,16 @@ -
+ +
- + > + +
+
-
\ No newline at end of file diff --git a/src/app/citizenScience/yesnoLabels/_yesnoLabels.scss b/src/app/citizenScience/yesnoLabels/_yesnoLabels.scss new file mode 100644 index 00000000..be3a9152 --- /dev/null +++ b/src/app/citizenScience/yesnoLabels/_yesnoLabels.scss @@ -0,0 +1,141 @@ +$thumb-height: 100; + +$triangle: ''; + +.thumb-labels-container { + text-align: center; + margin-top: 10px; + background: rgba(0, 0, 0, 0.3); + position: relative; + + /* flexbox layout of thumbs */ + display: flex; + flex-wrap: wrap; + justify-content: center; + + .citizen-science-thumb-label { + + position: relative; + z-index: 51; + + &.active { + + /* make room under the row for the info */ + padding-bottom: 400px; + + .citizen-science-thumb { + + outline: rgba(255, 255, 255, 0.5) solid 2px; + + &:after { + content: url("data:image/svg+xml;utf8,#{$triangle}"); + position: absolute; + bottom: -15px; + z-index: 52; + left: 0; + right: 0; + opacity: 0.9 + } + + } + + } + + .citizen-science-thumb { + position: relative; + height: $thumb-height + px; + margin: 5px; + + img { + height: 100%; + } + + .thumb-tick { + position: absolute; + top:5px; + right:8px; + color: #fff; + text-shadow: 1px 1px 2px #000; + } + + } + + citizen-science-label-examples { + + position: absolute; + z-index: 50; + box-shadow: 5px 5px 10px 10px rgba(0, 0, 0, 0.3); + border-radius: 5px; + background: rgba(0, 0, 0, 0.9); + padding: 5px; + top: ($thumb-height + 10) + px; + + .citizen-science-thumb-example { + + margin-top: 10px; + display: flex; + justify-content: space-evenly; + + .img { + flex-grow:1; + background-size: cover; + background-position: center center; + box-shadow: 0px 0px 1px 10px rgba(0, 0, 0, 0.5) inset; + max-width: 60%; + min-width: 30%; + } + + @media (max-width: 520px) { + .img { + display: none; + } + } + + annotation-item { + + padding-left: 20px; + padding-right: 10px; + + } + + } + + } + + } + + .label-examples-annotations { + + h3 { + text-align: center; + font-size: 1.2em; + max-width: 350px; + padding-left: 50px; + padding-right: 50px; + position: relative; + margin-left: auto; + margin-right: auto; + + span { + max-width: calc(100% - 100px); + } + + .arrow { + font-size: 1.4em; + position: absolute; + + &.prev { + left: 0px; + } + + &.next { + right: 0px; + } + + } + + } + + } + +} \ No newline at end of file diff --git a/src/app/citizenScience/yesnoLabels/label.js b/src/app/citizenScience/yesnoLabels/label.js new file mode 100644 index 00000000..d23b04cd --- /dev/null +++ b/src/app/citizenScience/yesnoLabels/label.js @@ -0,0 +1,56 @@ +angular.module("bawApp.components.citizenScienceThumbLabels.label", + [ + "bawApp.components.citizenScienceThumbLabels.examples", + "bawApp.citizenScience.sampleLabels" + ]) + .component("citizenScienceLabel", { + templateUrl: "citizenScience/thumbLabels/label.tpl.html", + controller: [ + "$scope", + "SampleLabels", + function ($scope, SampleLabels) { + + /** + * A label is "selected" if it has been applied to the current sample + * A label is "active" if it has been clicked to show details + */ + + var self = this; + + $scope.isSelected = function() { + return SampleLabels.getValue(self.label.id); + }; + + $scope.isShowingDetails = function () { + return self.currentDetailsLabelId.value === self.label.id; + }; + + /** + * toggles whether the details pane is showing for the current label + */ + $scope.toggleShowDetails = function () { + if ($scope.isShowingDetails()) { + self.currentDetailsLabelId.value = -1; + } else { + self.currentDetailsLabelId.value = self.label.id; + } + console.log("showing details for label:", self.currentDetailsLabelId.value); + + }; + + /** + * callback when this label is either attached or detached from the current sample + * @param isSelected Boolean + */ + self.onToggleSelected = function (isSelected) { + SampleLabels.setValue(self.label.id, isSelected); + }; + + }], + bindings: { + + label: "=", + currentDetailsLabelId: "=" + + } + }); \ No newline at end of file diff --git a/src/app/citizenScience/yesnoLabels/labels.js b/src/app/citizenScience/yesnoLabels/labels.js new file mode 100644 index 00000000..01b94b0d --- /dev/null +++ b/src/app/citizenScience/yesnoLabels/labels.js @@ -0,0 +1,24 @@ +angular.module("bawApp.components.citizenScienceThumbLabels", + [ + "bawApp.components.citizenScienceThumbLabels.label" + ]) + .component("citizenScienceLabels", { + templateUrl: "citizenScience/yesnoLabels/labels.tpl.html", + controller: [ + "$scope", + "$http", + "CitizenScienceCommon", + "annotationLibraryCommon", + "baw.models.AudioEvent", + "$q", + function ($scope) { + + var self = this; + // yesno questions only have one label + $scope.label = questionData.labels[0].name + + }], + bindings: { + questionData: "=", + } + }); \ No newline at end of file diff --git a/src/app/citizenScience/yesnoLabels/labels.tpl.html b/src/app/citizenScience/yesnoLabels/labels.tpl.html new file mode 100644 index 00000000..4512152b --- /dev/null +++ b/src/app/citizenScience/yesnoLabels/labels.tpl.html @@ -0,0 +1,47 @@ +
+ + + + +
+ + +
+ + + + + {{$ctrl.label.name}} +
+ + + + + + + +
+ + + + + +
+ +
+ diff --git a/src/baw.paths.nobuild.js b/src/baw.paths.nobuild.js index be77019b..cf63b605 100644 --- a/src/baw.paths.nobuild.js +++ b/src/baw.paths.nobuild.js @@ -92,6 +92,11 @@ module.exports = function (environment) { "show": "/responses/{responseId}", "create": "/responses" }, + "study": { + "list": "/studies", + "show": "/studies/{studyId}", + "filter": "/studies/filter" + }, }, "links": { "projects": "/projects", @@ -191,9 +196,9 @@ module.exports = function (environment) { "libraryItem": "/library/{recordingId}/audio_events/{audioEventId}", "visualize": "/visualize", "citizenScience": { - "listenId":"/citsci/bristlebird/listen/{sampleNum}", - "listen":"/citsci/bristlebird/listen", - "responses": "/citsci/bristlebird/responses" + "listenId":"/citsci/{studyName}/listen/{sampleNum}", + "listen":"/citsci/{studyName}/listen", + "responses": "/citsci/{studyName}/responses" }, "demo": { "d3": "/demo/d3", diff --git a/src/components/models/models.js b/src/components/models/models.js index 0169e4ea..c115aa4f 100644 --- a/src/components/models/models.js +++ b/src/components/models/models.js @@ -27,7 +27,8 @@ angular.module( "bawApp.models.datasetItem", "bawApp.models.progressEvent", "bawApp.models.question", - "bawApp.models.questionResponse" + "bawApp.models.questionResponse", + "bawApp.models.study" ]); diff --git a/src/components/models/study.js b/src/components/models/study.js new file mode 100644 index 00000000..f75dd869 --- /dev/null +++ b/src/components/models/study.js @@ -0,0 +1,17 @@ +angular + .module("bawApp.models.study", []) + .constant("baw.models.study.defaultDatasetId", 1) + .factory("baw.models.study", [ + "baw.models.ApiBase", + function (ApiBase) { + + class Study extends ApiBase { + constructor(resource) { + super(resource); + this.customSettings = this.customSettings || null; + } + + } + + return Study; + }]); diff --git a/src/components/services/question.js b/src/components/services/question.js index ae67604d..f4255787 100644 --- a/src/components/services/question.js +++ b/src/components/services/question.js @@ -24,7 +24,7 @@ angular }; resource.question = function getQuestion(questionId) { - var url = $url.formatUri(paths.api.routes.datasetItem.showAbsolute, {questionId: questionId}); + var url = $url.formatUri(paths.api.routes.question.showAbsolute, {questionId: questionId}); return $http.get(url).then(x => QuestionModel.makeFromApi(x)); }; diff --git a/src/components/services/services.js b/src/components/services/services.js index 0ee5eb57..e7f97aff 100644 --- a/src/components/services/services.js +++ b/src/components/services/services.js @@ -38,6 +38,7 @@ angular.module( "bawApp.services.progressEvent", "bawApp.services.question", "bawApp.services.questionResponse", + "bawApp.services.study" ]); diff --git a/src/components/services/study.js b/src/components/services/study.js new file mode 100644 index 00000000..c2291727 --- /dev/null +++ b/src/components/services/study.js @@ -0,0 +1,33 @@ +angular + .module("bawApp.services.study", []) + .factory( + "Study", + [ + "$resource", + "$http", + "bawResource", + "$url", + "conf.paths", + "baw.models.study", + function ($resource, $http, bawResource, $url, paths, StudyModel) { + + var resource = bawResource( + paths.api.routes.study.list, + {studyId: "@studyId"}, + {}); + + + resource.study = function getStudy(studyId) { + var url = $url.formatUri(paths.api.routes.study.showAbsolute, {studyId: studyId}); + return $http.get(url).then(x => StudyModel.makeFromApi(x)); + }; + + resource.studyByName = function getStudyByName(studyName) { + var url = $url.formatUri(paths.api.routes.study.filterAbsolute, {filter_name: studyName}); + return $http.get(url).then(x => StudyModel.makeFromApi(x)); + }; + + return resource; + } + ] + ); \ No newline at end of file