diff --git a/public/sounds/calibration_finished.ogg b/public/sounds/calibration_finished.ogg new file mode 100644 index 0000000..7f1bf17 Binary files /dev/null and b/public/sounds/calibration_finished.ogg differ diff --git a/public/sounds/neutral_finished.ogg b/public/sounds/neutral_finished.ogg new file mode 100644 index 0000000..be6e8d7 Binary files /dev/null and b/public/sounds/neutral_finished.ogg differ diff --git a/public/sounds/recording.ogg b/public/sounds/recording.ogg new file mode 100644 index 0000000..94d8eae Binary files /dev/null and b/public/sounds/recording.ogg differ diff --git a/public/sounds/recording_finished.ogg b/public/sounds/recording_finished.ogg new file mode 100644 index 0000000..36aecfc Binary files /dev/null and b/public/sounds/recording_finished.ogg differ diff --git a/public/sounds/success.ogg b/public/sounds/success.ogg new file mode 100644 index 0000000..761468a Binary files /dev/null and b/public/sounds/success.ogg differ diff --git a/src/components/pages/Calibration.vue b/src/components/pages/Calibration.vue index 91dc26c..33be077 100644 --- a/src/components/pages/Calibration.vue +++ b/src/components/pages/Calibration.vue @@ -21,9 +21,13 @@
@@ -51,7 +55,38 @@ + label="Square size (mm)" + class="mr-3"/> + + + + + +
+ The origin of the world frame is the top-left black-to-black corner of the board (red dot with a blue outline in the picture on the right). +

+ When positioned perpendicular to the floor, transformations are applied so that in the processed data: +
    +
  • The forward axis of the world frame is perpendicular to the board (coming out).
  • +
  • The vertical axis of the world frame is parallel to the board (going up).
  • +
+
+ When positioned lying on the floor, transformations are applied so that in the processed data: +
    +
  • The forward axis of the world frame is parallel to the board (along the shorter side).
  • +
  • The vertical axis of the world frame is perpendicular to the board (going up).
  • +
+
+ To align movement with the forward axis of the world frame when the board is lying on the floor, place the board such that its forward axis is parallel to the direction of movement. + For example, for walking, place the board with the longer side perpendicular to the walking direction. Note that this alignment is optional, as the system can operate with the board in any orientation. +
+
@@ -68,6 +103,7 @@ import axios from 'axios' import {mapActions, mapMutations, mapState} from 'vuex' import { apiError, apiSuccess, apiErrorRes, apiInfo} from '@/util/ErrorMessage.js' import MainLayout from '@/layout/MainLayout' +import { playCalibrationFinishedSound } from "@/util/SoundMessage.js"; export default { name: 'Calibration', @@ -79,13 +115,20 @@ export default { rows: 4, cols: 5, squareSize: 35, + placement: 'Perpendicular', busy: false, imgs: null, lastPolledStatus: "", n_cameras_connected: 0, - n_videos_uploaded: 0 + n_videos_uploaded: 0, + isAuditoryFeedbackEnabled: false, } }, + created() { + // Load the initial value from localStorage + const storedValue = localStorage.getItem("auditory_feedback"); + this.isAuditoryFeedbackEnabled = storedValue === "true"; + }, computed: { ...mapState({ session: state => state.data.session, @@ -108,7 +151,8 @@ export default { this.setCalibration({ rows: this.rows, cols: this.cols, - squareSize: this.squareSize + squareSize: this.squareSize, + placement: this.placement }) try { const resUpdate = await axios.get(`/sessions/${this.session.id}/set_metadata/`, { @@ -116,7 +160,7 @@ export default { cb_rows: this.rows, cb_cols: this.cols, cb_square: this.squareSize, - cb_placement: "backWall" + cb_placement: this.placement } }) @@ -147,6 +191,10 @@ export default { apiError(this.n_calibrated_cameras + " device(s) connected to the session and 2+ devices are required, please re-pair your devices using qr code at top of page.", 10000); this.busy = false } else { + // Play sound indicating calibration finished. + if (this.isAuditoryFeedbackEnabled) + playCalibrationFinishedSound(); + apiSuccess(this.n_calibrated_cameras + " devices calibrated successfully.", 5000); this.$router.push(`/${this.session.id}/neutral`) } diff --git a/src/components/pages/Dashboard.vue b/src/components/pages/Dashboard.vue index 98cbe13..410ce43 100644 --- a/src/components/pages/Dashboard.vue +++ b/src/components/pages/Dashboard.vue @@ -46,17 +46,30 @@ Data Menu
-
- +
+
+ +
-
-

- This is a public session. To load your sessions, launch the dashboard from your session list. -

+
- + + + + + + + + + + + + + @@ -158,6 +171,7 @@ import chroma from 'chroma-js'; import { Line as LineChartGenerator } from 'vue-chartjs/legacy' import zoomPlugin from 'chartjs-plugin-zoom'; import IconTooltip from '@/components/ui/IconTooltip.vue'; +import TrialSelect from '@/components/ui/TrialSelect.vue'; import { Chart as ChartJS, @@ -186,6 +200,7 @@ export default { components: { LineChartGenerator, IconTooltip, + TrialSelect, }, // This function is executed once the page has been loaded. created: function () { @@ -193,19 +208,20 @@ export default { this.session_owned = false // If the user is logged in, select session from list of sessions. - if(this.loggedIn) { - // If a session id has been passed as a parameter, set it as the default session. - this.sessionsIds.forEach(sessionId => { - if (sessionId.includes(this.$route.params.id)) { - this.session_selected = sessionId; - this.onSessionSelected(this.session_selected); - this.session_owned = true - } - }); - } + // if(this.loggedIn) { + // // If a session id has been passed as a parameter, set it as the default session. + // this.sessionsIds.forEach(sessionId => { + // if (sessionId.includes(this.$route.params.id)) { + // this.session_selected = sessionId; + // this.onSessionSelected(this.session_selected); + // this.session_owned = true + // } + // }); + // } + }, methods: { - ...mapActions('data', ['loadSession']), + ...mapActions('data', ['loadSession', 'loadSubjects', 'loadExistingSessions']), // Open and close left menu. leftMenu() { if (document.getElementById("body").classList.contains("left-menu-closed")) { @@ -226,26 +242,124 @@ export default { document.getElementById("button-right").style.display = "inline-block"; } }, - // Draw a chart. In future iterations this function should take care of compute the requested data, not only load it. - async drawChart() { - // Show spinner and hide chart until finished. - document.getElementById("spinner-layer").style.display = "block"; - document.getElementById("chart").style.display = "None"; - var index = this.trial_names.indexOf(this.trial_selected); - var id = this.trial_ids[index]; + onResetZoom() { + const chart = this.$refs.chartRef.getCurrentChart(); + if (chart) { + chart.resetZoom(); + } + }, + // Get trials and update trials select when a session is selected. + // async onSessionSelected(sessionName) { + // console.log('onSessionSelected', sessionName) + // // Clear previous toast messages + // clearToastMessages() + // + // // Get value between parentheses (session id). + // var sessionIdSelected = sessionName.match(/\((.*)\)/); + // if (sessionIdSelected !== null) { + // sessionIdSelected = sessionIdSelected.pop(); + // + // this.$router.push({ name: 'Dashboard', params: { id: sessionIdSelected } }).catch(err => {}) + // + // this.current_session_id = sessionIdSelected; + // + // await this.loadSubjects() + // await this.loadSession(this.$route.params.id) + // console.log('this.session=', this.session) + // // console.log(this.selected_trials[0]) + // let subject = null + // if (this.session.subject) { + // for(let i = 0; i < this.subjects.length; i++) { + // if (this.subjects[i].id === this.session.subject) { + // subject = this.subjects[i] + // break + // } + // } + // } + // + // this.selected_trials.push({ + // uuid: this.generateUUID(), + // subject_selected: subject, + // session_selected: this.session, + // trial_selected: this.session.trials.filter(trial => trial.status === 'done' && trial.name !== 'neutral' && trial.name !== 'calibration')[0], + // offset: 0, + // }) + // + // } + // }, + onXQuantitySelected(xQuantitySelected) { + this.x_quantity_selected = xQuantitySelected; + this.chartOptions.scales.x.title.text = xQuantitySelected; + this.drawChart(); + }, + onYQuantitySelected(yQuantitySelected) { + this.y_quantities_selected = yQuantitySelected; + this.drawChart(); + }, + onChartDownload() { + if (this.chart_download_format_selected === 'png') { + const canvas = document.getElementById("chart").getElementsByTagName('canvas')[0]; + const downloadLink = document.createElement('a'); + downloadLink.setAttribute('download', 'chart.png'); + canvas.toBlob(function(blob) { + const url = URL.createObjectURL(blob); + downloadLink.setAttribute('href', url); + downloadLink.click(); + URL.revokeObjectURL(url); + }, 'image/png', 1); + } - try { - const { data } = await axios.get(`/trials/${id}/`); + }, - this.trial = data + placeholderFunction(selected) { + console.log(selected); + }, + addTrialSelection() { + if (!this.loggedIn && this.selected_trials.length > 0) { + this.selected_trials.push({ + uuid: this.generateUUID(), + subject_selected: this.selected_trials[0].subject_selected, + session_selected: this.selected_trials[0].session_selected, + // trial_selected: this.selected_trials[0].session_selected.trials.filter(trial => trial.status === 'done' && trial.name !== 'neutral' && trial.name !== 'calibration')[0], + trial_selected: null, + }) + } else { + this.selected_trials.push({ + uuid: this.generateUUID(), + subject_selected: this.selected_trials.length > 0 ? this.selected_trials[0].subject_selected : null, + // subject_selected : null, + session_selected: null, + trial_selected: null, + }) + } + }, + removeTrialSelection(uuid) { + this.selected_trials = this.selected_trials.filter(trial => trial.uuid !== uuid) + this.loadTrialResults() + }, + captureTrialSelection(trial_selection) { + for (let i = 0; i < this.selected_trials.length; i++) { + if (this.selected_trials[i].uuid === trial_selection.uuid) { + Vue.set(this.selected_trials, i, trial_selection) + break + } + } + this.loadTrialResults() + }, + async loadTrialResults() { + // Show spinner and hide chart until finished. + document.getElementById("spinner-layer").style.display = "block"; + document.getElementById("chart").style.display = "None"; - // load JSON - const json = data.results.filter(element => element.tag == "ik_results") + for (let i=0; i < this.selected_trials.length; i++) { + // let trial_id = this.selected_trials[i].trial_selected.id + if (this.selected_trials[i].trial_selected === null) { continue} + let ik_results = this.selected_trials[i].trial_selected.results.filter(element => element.tag == "ik_results") - if (json && json.length > 0) { + if (ik_results && ik_results.length > 0) { let data - const url = json[0].media + const url = ik_results[0].media if (url.startsWith(axios.defaults.baseURL)) { const res = await axios.get(url) @@ -265,176 +379,84 @@ export default { data = res.data } - // Get indexes where requested data is. - var indexes = [this.x_quantities.indexOf(this.x_quantity_selected)]; - var i = 0; - for (i = 0; i < this.y_quantities_selected.length; i++) { - indexes.push(this.y_quantities.indexOf(this.y_quantities_selected[i]) + 1); - } - // Split file in lines. var lines = data.split("\n"); // Process line by line. First obtain number of rows and number of columns. - var nRows = 0; - var nColumns = 0; - i = 0; - while (lines[i].trim() !== "endheader") { - var splitted = lines[i].trim().split("="); - if (splitted[0] == "nRows") { - nRows = parseInt(splitted[1]); - } else if (splitted[0] == "nColumns") { - nColumns = parseInt(splitted[1]); - } - i++; + let k = 0; + while (lines[k].trim() !== "endheader") { + k++; } // Skip endHeader and possible blank lines. do { - i++; - } while (lines[i].trim() === ""); + k++; + } while (lines[k].trim() === ""); // Get column names. - var columnNames = [] - columnNames.push(this.x_quantity_selected); - columnNames.push(...this.y_quantities_selected); - i++; - - // Get name of selected color scale. - const selectedOption = this.chart_color_scales_options.find(option => { - return option.value === this.chart_color_scales_selected; - }); - - const selectedText = selectedOption ? selectedOption.text : ""; - - // Create an empty dataset per column. - var j = 0; - this.chartData.labels = [] - this.chartData.datasets = [] - var colors = chroma.scale("Viridis").correctLightness().gamma(2).cache(false).colors(this.y_quantities_selected.length); - if (selectedText == "Spectral" || selectedText == "Rainbow" || selectedText == "Red-Yellow-Blue" || selectedText == "Yellow-Green-Blue") - colors = chroma.scale(this.chart_color_scales_selected).colors(this.y_quantities_selected.length); - else if (selectedText == "Yellow-Green") - colors = chroma.scale(this.chart_color_scales_selected).correctLightness().colors(this.y_quantities_selected.length); - else if (selectedText == "Red-Green" || selectedText == "Red-Blue" || selectedText == "Green-Blue") - colors = chroma.scale(this.chart_color_scales_selected).gamma(0.75).cache(false).colors(this.y_quantities_selected.length); - else - colors = chroma.scale(this.chart_color_scales_selected).correctLightness().gamma(2).cache(false).colors(this.y_quantities_selected.length); - - // Add y quantities. - var dataset = {}; - for(j = 0; j < this.y_quantities_selected.length; j++) { - dataset = {}; - dataset["data"] = []; - dataset["label"] = this.y_quantities_selected[j]; - dataset["backgroundColor"] = colors[j]; - dataset["borderColor"] = colors[j]; - dataset["borderWidth"] = this.chart_line_width; - // Handle "none" option to remove points - dataset["pointStyle"] = this.chart_point_style; - if (this.chart_point_style === "none") { - dataset["pointRadius"] = 0; - } else { - dataset["pointRadius"] = this.chart_point_radius; - } - - this.chartData.datasets.push(dataset); - } - - // Insert value from each row - j = 0; - var k = 0; - for (j = 0; j < nRows; j++) { - var lineArray = lines[j + i].trim().split("\t"); - var row = []; - for (k = 0; k < indexes.length; k++) { - if (k === 0) - this.chartData["labels"].push(parseFloat(lineArray[indexes[k]].trim())); - else - this.chartData.datasets[k-1]["data"].push(parseFloat(lineArray[indexes[k]].trim())); - } - } + this.x_quantities = lines[k].trim().split("\t"); + // Create copy for y_quantities and remove time. + this.y_quantities = this.x_quantities.slice(); + this.y_quantities.shift(); + this.x_quantity_selected = this.x_quantities[0] } - // Show spinner and hide chart until finished. - document.getElementById("spinner-layer").style.display = "None"; - document.getElementById("chart").style.display = "block"; - } catch (error) { - apiError(error) - this.trialLoading = false } - }, - onResetZoom() { - const chart = this.$refs.chartRef.getCurrentChart(); - if (chart) { - chart.resetZoom(); - } - }, - // Get trials and update trials select when a session is selected. - onSessionSelected(sessionName) { - // Clear previous toast messages - clearToastMessages() - - // Get value between parentheses (session id). - var sessionIdSelected = sessionName.match(/\((.*)\)/); - if (sessionIdSelected !== null) { - sessionIdSelected = sessionIdSelected.pop(); - this.$router.push({ name: 'Dashboard', params: { id: sessionIdSelected } }) + await this.drawChart() - this.current_session_id = sessionIdSelected; - - var session = this.sessions.filter(function (obj) { - if (obj.id === sessionIdSelected) { - return obj.name; - } - }); - var trials = session[0]['trials']; - // Filter trials by name. - trials = trials.filter(trial => trial.status === 'done' && trial.name !== 'neutral' && trial.name !== 'calibration') - - if (trials.length > 0) { - this.trial_ids = [] - this.trial_names = []; - trials.forEach(element => { - this.trial_names.push(element.name); - this.trial_ids.push(element.id) - }); - this.trial_selected = this.trial_names[0]; - - // Load data from this trial. - this.onTrialSelected(this.trial_selected); - } else { - this.trial_names = [] - apiWarning("There are no dynamic trials associated with this session, thereby nothing to plot.") - } - - } + // Show chart and hide spinner. + document.getElementById("spinner-layer").style.display = "None"; + document.getElementById("chart").style.display = "block"; }, - // Get x-quantities and y-quantities and update respective selects when a trial is selected. - async onTrialSelected(trialName) { - + async drawChart() { // Show spinner and hide chart until finished. document.getElementById("spinner-layer").style.display = "block"; document.getElementById("chart").style.display = "None"; - // Then, when generate chart is clicked, use the available data and calculate the data of - // the columns that are not in database. - this.trial_selected = trialName; - var index = this.trial_names.indexOf(this.trial_selected); - var id = this.trial_ids[index]; - - try { - const { data } = await axios.get(`/trials/${id}/`); + // Get name of selected color scale. + const selectedOption = this.chart_color_scales_options.find(option => { + return option.value === this.chart_color_scales_selected; + }); - this.trial = data + const selectedText = selectedOption ? selectedOption.text : ""; + + // Create an empty dataset per column. + let j = 0; + this.chartData.labels = [] + this.chartData.datasets = [] + var colors = chroma.scale("Viridis").correctLightness().gamma(2).cache(false).colors(this.y_quantities_selected.length); + if (selectedText == "Spectral" || selectedText == "Rainbow" || selectedText == "Red-Yellow-Blue" || selectedText == "Yellow-Green-Blue") + colors = chroma.scale(this.chart_color_scales_selected).colors(this.y_quantities_selected.length); + else if (selectedText == "Yellow-Green") + colors = chroma.scale(this.chart_color_scales_selected).correctLightness().colors(this.y_quantities_selected.length); + else if (selectedText == "Red-Green" || selectedText == "Red-Blue" || selectedText == "Green-Blue") + colors = chroma.scale(this.chart_color_scales_selected).gamma(0.75).cache(false).colors(this.y_quantities_selected.length); + else + colors = chroma.scale(this.chart_color_scales_selected).correctLightness().gamma(2).cache(false).colors(this.y_quantities_selected.length); + + let dashed_line_styles = [ + [], [5, 5], [10, 10], + [20, 5], + [15, 3, 3, 3], + [20, 3, 3, 3, 3, 3, 3, 3], + [12, 3, 3], + ] + + for (let i=0; i < this.selected_trials.length; i++) { + if (!this.selected_trials[i].trial_selected) { + // Show chart and hide spinner. + document.getElementById("spinner-layer").style.display = "None"; + document.getElementById("chart").style.display = "block"; + return + } - // load JSON - const json = data.results.filter(element => element.tag == "ik_results") + // let trial_id = this.selected_trials[i].trial_selected.id + let ik_results = this.selected_trials[i].trial_selected.results.filter(element => element.tag == "ik_results") - if (json && json.length > 0) { + if (ik_results && ik_results.length > 0) { let data - const url = json[0].media + const url = ik_results[0].media if (url.startsWith(axios.defaults.baseURL)) { const res = await axios.get(url) @@ -455,70 +477,116 @@ export default { } // Split file in lines. - var lines = data.split("\n"); + let lines = data.split("\n"); // Process line by line. First obtain number of rows and number of columns. - var i = 0; - while (lines[i].trim() !== "endheader") { - i++; + let nRows = 0; + let nColumns = 0; + let k = 0; + while (lines[k].trim() !== "endheader") { + let splitted = lines[k].trim().split("="); + if (splitted[0] == "nRows") { + nRows = parseInt(splitted[1]); + } else if (splitted[0] == "nColumns") { + nColumns = parseInt(splitted[1]); + } + k++; } // Skip endHeader and possible blank lines. do { - i++; - } while (lines[i].trim() === ""); + k++; + } while (lines[k].trim() === ""); // Get column names. - this.x_quantities = lines[i].trim().split("\t"); - // Create copy for y_quantities and remove time. - this.y_quantities = this.x_quantities.slice(); - this.y_quantities.shift(); + let columnNames = [] + columnNames.push(this.x_quantity_selected); + columnNames.push(...this.y_quantities_selected); + k++; + + let dataset = {} + let start_index = this.chartData.datasets.length + for(j = 0; j < this.y_quantities_selected.length; j++) { + dataset = {}; + dataset["data"] = []; + let session_name = this.selected_trials[i].session_selected.meta['sessionName'] + if ( session_name === null || session_name === undefined ) { + session_name = this.selected_trials[i].session_selected.id.split('-')[0] + } else { + session_name = session_name + ' (' + this.selected_trials[i].session_selected.id.split('-')[0] + ')' + } + + dataset["label"] = "" + + this.selected_trials[i].subject_selected.name + + " : " + session_name + + " : " + this.selected_trials[i].trial_selected.name + + " : " + this.y_quantities_selected[j]; + dataset["backgroundColor"] = colors[j]; + dataset["borderColor"] = colors[j]; + dataset["borderWidth"] = this.chart_line_width; + dataset["borderDash"] = dashed_line_styles[i]; + // Handle "none" option to remove points + dataset["pointStyle"] = this.chart_point_style; + if (this.chart_point_style === "none") { + dataset["pointRadius"] = 0; + } else { + dataset["pointRadius"] = this.chart_point_radius; + } + + this.chartData.datasets.push(dataset); + } + + // Get indexes where requested data is. + let indexes = [this.x_quantities.indexOf(this.x_quantity_selected)]; + var n = 0; + for (n = 0; n < this.y_quantities_selected.length; n++) { + indexes.push(this.y_quantities.indexOf(this.y_quantities_selected[n]) + 1); + } + + // Insert value from each row + j = 0; + let m = 0; + for (j = 0; j < nRows; j++) { + var lineArray = lines[j + k].trim().split("\t"); + // var row = []; + for (m = 0; m < indexes.length; m++) { + if (m > 0) { + this.chartData.datasets[start_index+m-1]["data"].push( + { + x: parseFloat(lineArray[indexes[0]].trim()) + this.selected_trials[i].offset, + y: parseFloat(lineArray[indexes[m]].trim()) + } + ); + } + } + } - this.x_quantity_selected = this.x_quantities[0] - this.drawChart() } - // Show chart and hide spinner. - document.getElementById("spinner-layer").style.display = "None"; - document.getElementById("chart").style.display = "block"; - } catch (error) { - apiError(error) - this.trialLoading = false - } - }, - onXQuantitySelected(xQuantitySelected) { - this.x_quantity_selected = xQuantitySelected; - this.chartOptions.scales.x.title.text = xQuantitySelected; - this.drawChart(); - }, - onYQuantitySelected(yQuantitySelected) { - this.y_quantities_selected = yQuantitySelected; - this.drawChart(); - }, - onChartDownload() { - if (this.chart_download_format_selected === 'png') { - const canvas = document.getElementById("chart").getElementsByTagName('canvas')[0]; - const downloadLink = document.createElement('a'); - downloadLink.setAttribute('download', 'chart.png'); - canvas.toBlob(function(blob) { - const url = URL.createObjectURL(blob); - downloadLink.setAttribute('href', url); - downloadLink.click(); - URL.revokeObjectURL(url); - }, 'image/png', 1); } + // Show chart and hide spinner. + document.getElementById("spinner-layer").style.display = "None"; + document.getElementById("chart").style.display = "block"; + }, - onChartReady(chart, google) { - this.chart_reference = chart; - }, - placeholderFunction(selected) { - console.log(selected); + generateUUID() { + return "10000000-1000-4000-8000-100000000000".replace( + /[018]/g, + c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); }, }, data() { return { + selected_trials: [], + + + subject_selected: "", + + public_session_id: "", current_session_id: "", session_selected: "", trial_selected: "", @@ -598,7 +666,7 @@ export default { } }, legend: { - position: 'right', + position: 'bottom', align: 'center', labels: { font: { @@ -633,82 +701,86 @@ export default { session: state => state.data.session, subjects: state => state.data.subjects, loggedIn: state => state.auth.verified, + user_id: state => state.auth.user_id, }), - sessionsNames() { - var result_sessions = this.sessions.map(function (obj) { - // Check that there are valid trials - var trials = obj['trials']; - // Filter trials by name and status. - trials = trials.filter(trial => trial.status === 'done' && trial.name !== 'calibration') - - if (trials.length > 0) { - return obj.name + " (" + obj.id + ")"; - } else { - return ""; - } - }) - var filtered_sessions = result_sessions.filter(function (value, index, arr) { - return value !== ""; - }); - return filtered_sessions; - }, - sessionsIds() { - var result_sessions = this.sessions.map(function (obj) { - // Check that there are valid trials - var trials = obj['trials']; - // Filter trials by name and status. - trials = trials.filter(trial => trial.status === 'done' && trial.name !== 'calibration') - - if (trials.length > 0) { - if (obj.name) - return obj.name + " (" + obj.id + ")"; - else - if (obj.meta && obj.meta.subject && obj.meta.subject.id) - return obj.meta.subject.id + " (" + obj.id + ")"; - } else { - return ""; - } - }) - var filtered_sessions = result_sessions.filter(function (value, index, arr) { - return value !== ""; - }); - return filtered_sessions; - } + // sessionsNames() { + // var result_sessions = this.sessions.map(function (obj) { + // // Check that there are valid trials + // var trials = obj['trials']; + // // Filter trials by name and status. + // trials = trials.filter(trial => trial.status === 'done' && trial.name !== 'neutral' && trial.name !== 'calibration') + // + // if (trials.length > 0) { + // return obj.name + " (" + obj.id + ")"; + // } else { + // return ""; + // } + // }) + // var filtered_sessions = result_sessions.filter(function (value, index, arr) { + // return value !== ""; + // }); + // return filtered_sessions; + // + // }, + // sessionsIds() { + // var result_sessions = this.sessions.map(function (obj) { + // // Check that there are valid trials + // var trials = obj['trials']; + // // Filter trials by name and status. + // trials = trials.filter(trial => trial.status === 'done' && trial.name !== 'neutral' && trial.name !== 'calibration') + // + // if (trials.length > 0) { + // if (obj.name) + // return obj.name + " (" + obj.id + ")"; + // else + // if (obj.meta && obj.meta.subject && obj.meta.subject.id) + // return obj.meta.subject.id + " (" + obj.id + ")"; + // } else { + // return ""; + // } + // }) + // var filtered_sessions = result_sessions.filter(function (value, index, arr) { + // return value !== ""; + // }); + // return filtered_sessions; + // } }, async mounted () { // Set session as current session. this.current_session_id = this.$route.params.id; - // Show spinner and hide chart until finished. - document.getElementById("spinner-layer").style.display = "block"; - document.getElementById("chart").style.display = "None"; // If not logged in, load session from params and show trials. - await this.loadSession(this.$route.params.id) - - var trials = this.session['trials']; - // Filter trials by name. - trials = trials.filter(trial => trial.status === 'done' && trial.name !== 'neutral' && trial.name !== 'calibration') - if (trials.length > 0) { - this.trial_ids = [] - this.trial_names = []; - trials.forEach(element => { - this.trial_names.push(element.name); - this.trial_ids.push(element.id) - }); - this.trial_selected = this.trial_names[0]; - - // Load data from this trial. - this.onTrialSelected(this.trial_selected); - - } else { - this.trial_names = [] - apiWarning("There are no trials associated to this session. Record a new trial in order to plot information.") - } + await this.loadSubjects({session_id: this.current_session_id}) + await this.loadSession(this.current_session_id) + if (this.current_session_id && this.session?.public) { + this.public_session_id = this.current_session_id + } - // Load data from this trial. - this.onTrialSelected(this.trial_selected); + if (this.current_session_id && this.selected_trials.length === 0) { + let subject = this.subjects.filter(subject => subject.id === this.session.subject)[0] + this.selected_trials.push({ + uuid: this.generateUUID(), + subject_selected: subject, + session_selected: this.session, + trial_selected: this.session.trials.filter(trial => trial.status === 'done' && trial.name !== 'neutral' && trial.name !== 'calibration')[0], + offset: 0, + }) + } + if (!this.current_session_id && this.selected_trials.length == 0) { + this.selected_trials.push({ + uuid: this.generateUUID(), + subject_selected: null, + session_selected: null, + trial_selected: null, + offset: 0, + }) + } + await this.loadTrialResults() + if (this.loggedIn && this.sessions.length <= 1) { + await this.loadExistingSessions({reroute: false, update_sessions: true}) + } }, } diff --git a/src/components/pages/Neutral.vue b/src/components/pages/Neutral.vue index 30d50e8..75e8112 100644 --- a/src/components/pages/Neutral.vue +++ b/src/components/pages/Neutral.vue @@ -366,6 +366,7 @@ import axios from "axios"; import { mapMutations, mapActions, mapState } from "vuex"; import { apiError, apiSuccess, apiErrorRes, apiWarning, apiInfo, clearToastMessages } from "@/util/ErrorMessage.js"; +import { playNeutralFinishedSound } from "@/util/SoundMessage.js"; import MainLayout from "@/layout/MainLayout"; import ExampleImage from "@/components/ui/ExampleImage"; import DialogComponent from '@/components/ui/SubjectDialog.vue' @@ -450,6 +451,7 @@ export default { done: "Confirm", error: "Re-record", stopped: "Processing", + processing: "Processing", }, checkboxRule: (v) => !!v || 'The subject must agree to continue!', @@ -459,8 +461,15 @@ export default { tempFilterFrequency: 'default', // Temporary input holder componentKey: 0, + + isAuditoryFeedbackEnabled: false, }; }, + created() { + // Load the initial value from localStorage + const storedValue = localStorage.getItem("auditory_feedback"); + this.isAuditoryFeedbackEnabled = storedValue === "true"; + }, computed: { ...mapState({ // subjects: (state) => state.data.subjects, @@ -832,6 +841,9 @@ export default { ) { clearToastMessages(); apiInfo("Processing: the subject can relax.", 5000); + + if (this.isAuditoryFeedbackEnabled) + playNeutralFinishedSound() } this.lastPolledStatus = res.data.status; window.setTimeout(this.pollStatus, 1000); diff --git a/src/components/pages/ProfilePage.vue b/src/components/pages/ProfilePage.vue index 124dccf..5c0a1d7 100644 --- a/src/components/pages/ProfilePage.vue +++ b/src/components/pages/ProfilePage.vue @@ -303,6 +303,21 @@
+ + +

+ + +

+
+

Remove your account and all associated data. This includes every session, trial, subject, and result that you have ever created. This process is irreversible. @@ -313,10 +328,9 @@ Delete Account - - - Discard Changes + + Go Back @@ -437,9 +451,15 @@ export default { profile_picture: null, selectedImageFile: null, current_user_page_profile_url: '', - confirm_username: '' + confirm_username: '', + isAuditoryFeedbackEnabled: false, }; }, + created() { + // Load the initial value from localStorage + const storedValue = localStorage.getItem("auditory_feedback"); + this.isAuditoryFeedbackEnabled = storedValue === "true"; + }, methods: { ...mapActions("auth", ["updateProfile", "updateProfilePicture", "set_profile_picture_url", "logout"]), handleShareProfileClick() { @@ -448,9 +468,16 @@ export default { handleEditProfile() { this.editing_profile = true; }, + updateLocalStorage() { + // Update localStorage when the checkbox changes + localStorage.setItem("auditory_feedback", this.isAuditoryFeedbackEnabled); + }, handleEditSettings() { this.editing_settings = true; }, + handleFinished() { + this.editing_settings = false; + }, handleDiscard() { this.editing_profile = false; this.editing_settings = false; diff --git a/src/components/pages/Session.vue b/src/components/pages/Session.vue index cfcfd07..84c0357 100644 --- a/src/components/pages/Session.vue +++ b/src/components/pages/Session.vue @@ -320,7 +320,7 @@ Download data (old) - + Dashboard kinematics @@ -549,6 +549,7 @@ import axios from 'axios' import { mapState, mapMutations, mapActions } from 'vuex' import { apiError, apiErrorRes, apiSuccess } from '@/util/ErrorMessage.js' + import { playRecordingSound, playRecordingFinishedSound } from "@/util/SoundMessage.js"; import Status from '@/components/ui/Status' import * as THREE from 'three' import * as THREE_OC from '@/orbitControls' @@ -672,6 +673,8 @@ trial_modify_tags: false, trial_modify_tags_index: 0, + + isAuditoryFeedbackEnabled: false, } }, filters: { @@ -693,6 +696,7 @@ rows: state => state.data.rows, cols: state => state.data.cols, squareSize: state => state.data.squareSize, + placement: state => state.data.placement, // step Neutral data identifier: state => state.data.identifier, @@ -816,6 +820,11 @@ // // } // } }, + created() { + // Load the initial value from localStorage + const storedValue = localStorage.getItem("auditory_feedback"); + this.isAuditoryFeedbackEnabled = storedValue === "true"; + }, methods: { ...mapMutations('data', [ 'setSessionStep5', @@ -855,23 +864,46 @@ // add to the list this.trialInProcess = res.data this.addTrial(this.trialInProcess) - - - this.recordingStarted = moment() - this.recordingTimePassed = 0 - this.recordingTimer = window.setTimeout(this.recordTimerHandler, 500) - - this.state = 'recording' - - // Wait for cameras to start actually recording. - await new Promise(r => setTimeout(r, 1500)); - + // Get n_cameras_connected. const res_status = await axios.get(`/sessions/${this.session.id}/status/`, {}) - this.n_videos_uploaded = res_status.data.n_videos_uploaded this.n_cameras_connected = res_status.data.n_cameras_connected - + + // If no calibrated cameras... + if (this.n_calibrated_cameras === 0) + throw new Error("There are no calibrated cameras for this trial."); + + // Transition to recording state + this.state = 'recording'; + + // Check if the appropriate number of cameras is connected. + const startTime = Date.now(); + while (this.n_cameras_connected !== this.n_calibrated_cameras) { + console.log("WAITING CAMERA CONNECTION...") + if (Date.now() - startTime > 5000) { // 5-second timeout + const res_stop = await axios.get(`/sessions/${this.session.id}/stop/`, {}) + const res_cancel = await axios.get(`/sessions/${this.session.id}/cancel_trial/`, {}) + this.cancelPoll() + this.state = 'ready' + this.trialInProcess.status = "error" + throw new Error("Connected cameras do not match calibrated cameras. Timeout while waiting for cameras to connect."); + } + + // Retry fetching the status + await new Promise(r => setTimeout(r, 500)); // Wait before retrying + const retryRes = await axios.get(`/sessions/${this.session.id}/status/`, {}); + this.n_cameras_connected = retryRes.data.n_cameras_connected; + } + + // Start recording timer. + this.recordingStarted = moment() + this.recordingTimePassed = 0 + this.recordingTimer = window.setTimeout(this.recordTimerHandler, 500) + + // Play sound indicating the subject can start motion. + if (this.isAuditoryFeedbackEnabled) + playRecordingSound() } catch (error) { apiError(error) } @@ -886,7 +918,11 @@ try { const res = await axios.get(`/sessions/${this.session.id}/stop/`, {}) - + + // Play sound indicating the subject can stop motion. + if (this.isAuditoryFeedbackEnabled) + playRecordingFinishedSound(); + this.trialInProcess.status = res.data.status this.state = 'processing' diff --git a/src/components/ui/TrialSelect.vue b/src/components/ui/TrialSelect.vue new file mode 100644 index 0000000..ae664a8 --- /dev/null +++ b/src/components/ui/TrialSelect.vue @@ -0,0 +1,197 @@ + + \ No newline at end of file diff --git a/src/store/data.js b/src/store/data.js index d5750dd..5b14f52 100644 --- a/src/store/data.js +++ b/src/store/data.js @@ -31,6 +31,7 @@ export default { rows: 4, cols: 5, squareSize: 35, + placement: 'Perpendicular', // step 3 trialId: '', @@ -220,10 +221,11 @@ export default { setConnectDevices (state, { cameras }) { state.cameras = cameras }, - setCalibration (state, { rows, cols, squareSize }) { + setCalibration (state, { rows, cols, squareSize, placement }) { state.rows = rows state.cols = cols state.squareSize = squareSize + state.placement = placement }, setTrialId (state, trialId) { state.trialId = trialId @@ -439,19 +441,23 @@ export default { } } }, - async loadSubjects({ state, commit }) { + async loadSubjects({ state, commit }, {session_id}) { try { let subjects = [] let start = 0 - let quantity = 20 + let quantity = 100 let moreDataAvailable = true while (moreDataAvailable) { - let res = await axios.get('/subjects/', { - params: { + let r_params = { start: start, quantity: quantity - } + } + if (session_id) { + r_params.session_id = session_id + } + let res = await axios.get('/subjects/', { + params: r_params }) let tagPromises = [] @@ -470,8 +476,8 @@ export default { tagPromises.push(tagPromise); } - subjects = subjects.concat(res.data) - if (res.data.length < quantity) { + subjects = subjects.concat(res.data.subjects) + if (res.data.subjects.length < quantity) { moreDataAvailable = false } else { start += quantity diff --git a/src/util/SoundMessage.js b/src/util/SoundMessage.js new file mode 100644 index 0000000..3c7e7e9 --- /dev/null +++ b/src/util/SoundMessage.js @@ -0,0 +1,40 @@ +/** + * Error message processing + * @module util/ErrorMessage + */ +import Vue from 'vue' + +function playCalibrationFinishedSound() { + const audio = new Audio('/sounds/calibration_finished.ogg'); + audio.play().catch((error) => { + console.error('Error playing sound:', error); + }); +} + +function playNeutralFinishedSound() { + const audio = new Audio('/sounds/neutral_finished.ogg'); + audio.play().catch((error) => { + console.error('Error playing sound:', error); + }); +} + +function playRecordingSound() { + const audio = new Audio('/sounds/recording.ogg'); + audio.play().catch((error) => { + console.error('Error playing sound:', error); + }); +} + +function playRecordingFinishedSound() { + const audio = new Audio('/sounds/recording_finished.ogg'); + audio.play().catch((error) => { + console.error('Error playing sound:', error); + }); +} + +export { + playCalibrationFinishedSound, + playNeutralFinishedSound, + playRecordingSound, + playRecordingFinishedSound +}