Skip to content

Commit

Permalink
Merge pull request #19 from mad-lab-fau/17-add-absolute-sampling-times
Browse files Browse the repository at this point in the history
17 add absolute sampling times
  • Loading branch information
roschro-fau authored Jan 22, 2024
2 parents 784f4fa + ece6494 commit 3dfb158
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 106 deletions.
196 changes: 103 additions & 93 deletions src/lib/components/preparation/QrCodeForm.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
<script lang="ts">
import { CAR_STUDY, DEFAULT_SALIVA_DISTANCE, OTHER_STUDY } from '$lib/constants';
import {
CAR_STUDY,
DEFAULT_SALIVA_DISTANCE,
DEFAULT_SALIVA_TIME,
DEFAULT_NUM_SAMPLE_ALARM_TIMES,
OTHER_STUDY
} from "$lib/constants";
import { studyProps, qrCodeProps, qrCodePropsValid } from '$lib/stores/configStore';
import { Step } from '@skeletonlabs/skeleton';
import { onMount, afterUpdate } from 'svelte';
let uniformSalivaDistances = true;
let salivaDistance = DEFAULT_SALIVA_DISTANCE;
let uniformSalivaDistances = false;
let uniformSalivaDistance = DEFAULT_SALIVA_DISTANCE;
let numSampleAlarmTimes = DEFAULT_NUM_SAMPLE_ALARM_TIMES;
let salivaDistances: number[];
let salivaTimes: string[];
onMount(() => {
// to prevent contunuing with invalid settings
// to prevent continuing with invalid settings
qrCodeProps.update((props) => {
return {
...props,
Expand All @@ -25,44 +33,53 @@
$: $qrCodeProps, qrCodePropsValid.set(isValid());
// every time saliva distance is modified, check if input is valid
$: salivaDistance, qrCodePropsValid.set(isValid());
$: uniformSalivaDistance, qrCodePropsValid.set(isValid());
// every time the number of samples changes, regenererate saliva times array
// every time the number of samples with fixed alarm times changes, regenerate saliva times array
$: numSampleAlarmTimes, qrCodePropsValid.set(isValid());
// every time the number of samples changes, regenerate saliva times array
$: $studyProps.numSamples, initializeSalivaTimes();
function initializeSalivaTimes() {
uniformSalivaDistances = false;
if ($studyProps.numSamples > 1) {
// first initialization
if ($qrCodeProps.salivaDistances.length != $studyProps.numSamples - 1) {
salivaDistances = [...Array(Math.floor(Number($studyProps.numSamples)) - 1)];
// use values from storage
} else {
salivaDistances = $qrCodeProps.salivaDistances;
}
if (!$qrCodeProps.salivaDistances || $qrCodeProps.salivaDistances.length != $studyProps.numSamples) {
salivaDistances = [...Array(Math.floor(Number($studyProps.numSamples)))].fill(DEFAULT_SALIVA_DISTANCE);
salivaDistances[0] = 0;
} else {
salivaDistances = [];
// use values from storage
salivaDistances = $qrCodeProps.salivaDistances;
}
if (!$qrCodeProps.salivaAlarmTimes || $qrCodeProps.salivaAlarmTimes.length != $studyProps.numSamples) {
salivaTimes = [...Array(Math.floor(Number($studyProps.numSamples)))].fill(DEFAULT_SALIVA_TIME);
// use values from storage
} else {
salivaTimes = $qrCodeProps.salivaAlarmTimes;
}
if ($qrCodeProps.numSampleAlarmTimes && $qrCodeProps.numSampleAlarmTimes <= $studyProps.numSamples) {
numSampleAlarmTimes = $qrCodeProps.numSampleAlarmTimes;
}
}
function isValid() {
let idList = ['mail'];
let idList = ["mail"];
if (uniformSalivaDistances) {
idList = [...idList, 'distances'];
idList = [...idList, "distances"];
} else {
for (let i = 0; i < salivaDistances.length; i++) {
idList = [...idList, 'distance' + i];
idList = [...idList, "samplesAbsTime"]
for (let i = 0; i < $studyProps.numSamples - numSampleAlarmTimes; i++) {
idList = [...idList, `distance${i}`];
}
for (let i = 0; i < numSampleAlarmTimes; i++) {
idList = [...idList, `time${i}`];
}
}
for (let id of idList) {
let element = document.getElementById(id);
if (element instanceof HTMLInputElement) {
if (!element.reportValidity()) {
return false;
}
if (element instanceof HTMLInputElement && !element.reportValidity()) {
return false;
}
}
if(salivaDistances){
if (salivaDistances || salivaTimes) {
submitQrCodeProps();
}
return true;
Expand All @@ -74,25 +91,21 @@
const submitQrCodeProps = () => {
if (uniformSalivaDistances) {
salivaDistances = salivaDistances.fill(salivaDistance);
} else {
for (let i = 0; i < salivaDistances.length; i++) {
let inputField = document.getElementById('distance' + i);
if (inputField instanceof HTMLInputElement) {
salivaDistances[i] = +inputField.value;
}
}
salivaDistances = salivaDistances.fill(uniformSalivaDistance)
numSampleAlarmTimes = 0;
}
qrCodeProps.update((props) => {
return {
...props,
salivaDistances: salivaDistances
numSampleAlarmTimes: numSampleAlarmTimes,
salivaDistances: salivaDistances,
salivaAlarmTimes: salivaTimes,
};
});
};
</script>

{#if $studyProps.studyType == CAR_STUDY || $studyProps.studyType == OTHER_STUDY}
{#if $studyProps.studyType === CAR_STUDY || $studyProps.studyType === OTHER_STUDY}
<Step locked={!$qrCodePropsValid}>
<svelte:fragment slot="header">Qr Code Details</svelte:fragment>
<form id="qr_code_form">
Expand Down Expand Up @@ -134,77 +147,74 @@

<hr class="my-4">

{#if !uniformSalivaDistances}
<label class="label md:w-1/3">
<span>Number of samples that have to be taken at a fixed time</span>
<input
class="input md:w-1/4"
id="samplesAbsTime"
type="number"
bind:value={numSampleAlarmTimes}
min="0"
max="{$studyProps.numSamples}"
required />
</label>
<hr class="my-4">
{/if}

{#if $studyProps.numSamples > 1}
<p><b>Time between biomarker samples</b></p>
<h4>Times for biomarker samples</h4>
{#if uniformSalivaDistances}
<div
class="h-full max-h-72 md:w-1/4 overflow-y-auto overflow-x-hidden flex flex-col flex-grow px-4"
>
<label class="label">
<p><b>All samples</b></p>
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
<div class="h-full max-h-72 py-2 md:w-1/4 p overflow-y-auto overflow-x-hidden flex flex-col flex-grow px-4">
<label class="label pb-1" for="distances"><span>Time between all samples</span></label>
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
<input
class="input col-span-2"
id="distances"
type="number"
bind:value={salivaDistance}
bind:value={uniformSalivaDistance}
min="1"
max="99"
step="1"
required
/>
required/>
<div class="input-group-shim col-span-1">min</div>
</div>
</label>
</div>
</div>
{/if}

{#if !uniformSalivaDistances}
<div
class="h-full max-h-72 md:w-1/4 overflow-y-auto overflow-x-hidden flex flex-col flex-grow px-4"
>
<label class="label">
{#each salivaDistances as dist, i}
{#if $studyProps.startSampleFromZero}
<p>{$studyProps.samplePrefix}{i} and {$studyProps.samplePrefix}{i + 1}:</p>
{:else}
<div class="h-full md:w-1/3 py-2 overflow-y-auto overflow-x-hidden flex flex-col flex-grow px-4">
{#each Array($studyProps.numSamples - numSampleAlarmTimes) as _, i}
<label class="label pt-2 pb-1" for="distance{i}">
{#if i === 0}
<span>Time span between wake-up alarm and sample {i + Number(!$studyProps.startSampleFromZero)}</span>
{:else}
<p>{$studyProps.samplePrefix}{i + 1} and {$studyProps.samplePrefix}{i + 2}:</p>
<span>Time span between sample {i + Number(!$studyProps.startSampleFromZero) - 1} and sample {i + Number(!$studyProps.startSampleFromZero)}</span>
{/if}
{#if i == salivaDistances.length - 1}
<!-- last field: validate without the need to change focus -->
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
<input
class="input col-span-2"
id="distance{i}"
type="number"
value={dist}
on:input={salivaListChanged}
min="1"
max="999"
step="1"
required
/>
<div class="input-group-shim col-span-1">min</div>
</div>
{:else}
<!-- only validate after full number was entered -->
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
<input
class="input col-span-2"
id="distance{i}"
type="number"
value={dist}
on:focusout={salivaListChanged}
min="1"
max="999"
step="1"
required
/>
<div class="input-group-shim col-span-1">min</div>
</div>
{/if}
{/each}
</label>
</label>
<div class="input-group input-group-divider grid-cols-[auto_2fr_auto]">
<input
class="input col-span-2"
id="distance{i}"
type="number"
bind:value={salivaDistances[i]}
on:input={salivaListChanged}/>
<div class="input-group-shim">min</div>
</div>
{/each}
{#each Array(numSampleAlarmTimes) as _, i}
<label class="label pt-2 pb-1" for="time{i}">
<span>
Alarm time for sample {$studyProps.numSamples - numSampleAlarmTimes + i + Number(!$studyProps.startSampleFromZero)}
</span>
</label>
<div class="input-group input-group-divider grid-cols-[auto_2fr_auto]">
<input
class="input col-span-2"
id="time{i}"
type="time"
bind:value={salivaTimes[i]}
on:input={salivaListChanged}/>
</div>
{/each}
</div>
{/if}
{/if}
Expand Down
5 changes: 4 additions & 1 deletion src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* constants for study configuration */

export const CAR_STUDY = 1;
export const LAB_STUDY = 2;
export const OTHER_STUDY = 3;
export const STUDY_TYPES = [CAR_STUDY, LAB_STUDY, OTHER_STUDY];
export const APP_OPTION = {CAR_STUDY: true, LAB_STUDY: false, OTHER_STUDY: true};
export const DEFAULT_SALIVA_DISTANCE = 15;

export const DEFAULT_NUM_SAMPLE_ALARM_TIMES = 0;
export const DEFAULT_SALIVA_TIME = "12:00"
/* constants to create QR code data */
export const QR_PARSER_APP_ID = "CARWATCH";
export const QR_PARSER_SEPARATOR = ";";
Expand All @@ -15,6 +17,7 @@ export const QR_PARSER_PROPERTY_STUDY_NAME = "N";
export const QR_PARSER_PROPERTY_STUDY_DAYS = "D";
export const QR_PARSER_PROPERTY_NUM_PARTICIPANTS = "S";
export const QR_PARSER_PROPERTY_SALIVA_TIMES = "T";
export const QR_PARSER_PROPERTY_SALIVA_ALARMS = "A";
export const QR_PARSER_PROPERTY_START_SAMPLE = "SS";
export const QR_PARSER_PROPERTY_EVENING = "E";
export const QR_PARSER_PROPERTY_CONTACT = "M";
Expand Down
4 changes: 3 additions & 1 deletion src/lib/stores/configStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ interface BarcodeProperties {

interface QrCodeProperties {
generateQrCodes: boolean;
numSampleAlarmTimes: number;
salivaDistances: number[];
salivaAlarmTimes: string[];
contact: string;
checkDuplicates: boolean;
enableManualScan: boolean;
Expand All @@ -53,7 +55,7 @@ if (browser) {
// Create the base stores for each data type
const defaultStudyProps: StudyProperties = { studyName: 'Test', numDays: 1, numSamples: 2, samplePrefix: "S", readSubjectsFromFile: false, numSubjects: 1, subjectList: [], subjectColumn: 'subject', subjectPrefix: 'VP_', hasEveningSample: false, startSampleFromZero: false, studyType: STUDY_TYPES[0] }
const defaultBarcodeProps: BarcodeProperties = { generateBarcodes: true, hasBarcode: false, addName: false, numCols: 4, numRows: 12, leftMargin: 9.8, rightMargin: 9.8, topMargin: 21.2, bottomMargin: 21.2, colDist: 2.5, rowDist: 0 }
const defaultQrCodeProps: QrCodeProperties = { generateQrCodes: true, salivaDistances: [], contact: '', checkDuplicates: false, enableManualScan: false }
const defaultQrCodeProps: QrCodeProperties = { generateQrCodes: true, numSampleAlarmTimes: 0, salivaDistances: [], salivaAlarmTimes: [], contact: '', checkDuplicates: false, enableManualScan: false }

// Create the stores
export const studyProps = storedStudyProps ? writable<StudyProperties>(JSON.parse(storedStudyProps)) : writable<StudyProperties>(defaultStudyProps);
Expand Down
20 changes: 9 additions & 11 deletions src/routes/download/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
QR_PARSER_PROPERTY_EVENING,
QR_PARSER_PROPERTY_MANUAL_SCAN,
QR_PARSER_PROPERTY_NUM_PARTICIPANTS,
QR_PARSER_PROPERTY_SALIVA_ALARMS,
QR_PARSER_PROPERTY_SALIVA_TIMES,
QR_PARSER_PROPERTY_START_SAMPLE,
QR_PARSER_PROPERTY_STUDY_DAYS,
Expand Down Expand Up @@ -36,11 +37,8 @@
let studyName = $studyProps.studyName;
for (let subject = 1; subject <= $studyProps.numSubjects; subject++) {
for (let day = 1; day <= $studyProps.numDays; day++) {
for (
let sample = startSample;
sample < $studyProps.numSamples + startSample + Number($studyProps.hasEveningSample);
sample++
) {
let lastSampleId = $studyProps.numSamples + startSample + Number($studyProps.hasEveningSample) - 1;
for (let sample = startSample; sample <= lastSampleId; sample++) {
// convert sample to zero padded string with length 2
let sampleString = sample.toString().padStart(2, '0');
let dayString = day.toString().padStart(2, '0');
Expand All @@ -51,7 +49,7 @@
}
// special case: evening sample referred to a "A"
let sampleCaption = sample.toString();
if (sample == $studyProps.numSamples && $studyProps.hasEveningSample) {
if (sample == lastSampleId && $studyProps.hasEveningSample) {
sampleCaption = 'E';
}
caption += $studyProps.subjectList[subject - 1] + '_D' + day + '_' + $studyProps.samplePrefix + sampleCaption;
Expand All @@ -68,11 +66,10 @@
function createQrCodeData() {
// sanitize inputs to prevent decoding issues
let studyName = sanitizeStringForQr($studyProps.studyName);
let distanceList = '';
$qrCodeProps.salivaDistances.forEach((dist) => {
distanceList += `${dist},`;
});
let distances = $qrCodeProps.salivaDistances.slice(0, $studyProps.numSamples - $qrCodeProps.numSampleAlarmTimes);
let distanceList = distances.join(",");
let fixedAlarms = $qrCodeProps.salivaAlarmTimes.slice(0, $qrCodeProps.numSampleAlarmTimes);
let fixedAlarmList = fixedAlarms.join(",").replaceAll(":", "");
let startSample = `${$studyProps.samplePrefix}${$studyProps.startSampleFromZero ? 0 : 1}`;
// create encoding
Expand All @@ -83,6 +80,7 @@
`${QR_PARSER_PROPERTY_NUM_PARTICIPANTS}${QR_PARSER_SPECIFIER}${$studyProps.numSubjects}${QR_PARSER_SEPARATOR}` +
`${QR_PARSER_PROPERTY_START_SAMPLE}${QR_PARSER_SPECIFIER}${startSample}${QR_PARSER_SEPARATOR}` +
`${QR_PARSER_PROPERTY_SALIVA_TIMES}${QR_PARSER_SPECIFIER}${distanceList}${QR_PARSER_SEPARATOR}` +
`${QR_PARSER_PROPERTY_SALIVA_ALARMS}${QR_PARSER_SPECIFIER}${fixedAlarmList}${QR_PARSER_SEPARATOR}` +
`${QR_PARSER_PROPERTY_EVENING}${QR_PARSER_SPECIFIER}${+$studyProps.hasEveningSample}${QR_PARSER_SEPARATOR}` +
`${QR_PARSER_PROPERTY_CONTACT}${QR_PARSER_SPECIFIER}${$qrCodeProps.contact}${QR_PARSER_SEPARATOR}` +
`${QR_PARSER_PROPERTY_DUPLICATES}${QR_PARSER_SPECIFIER}${+$qrCodeProps.checkDuplicates}${QR_PARSER_SEPARATOR}` +
Expand Down

0 comments on commit 3dfb158

Please sign in to comment.