Skip to content

Commit

Permalink
✨ User's choice for Tobii parsing (#15)
Browse files Browse the repository at this point in the history
* 🐛 Interval parsing fixed

* ♻️ Preparation for user prompting after file classification

* ✨ Tobii Modal for User parsing choice
- users can now choose form of data parsing
- new modal content
- new promise system
- pausing parsing process and sending request from worker to the main thread
- needs polishing and testing!!! this is to save progress

* 🐛 Bugfix of user input
- added onDestroy import
- there was a `data.value` instead of `data` in worker message receiver, thus causing undefined Error later on

* 💄 Improved Modal UI
- new margin bottom
- better labels and information
- removed redundant file

* 📝 Added JSDocs to Tobii deserializer
- commented methods and members
- methods grouped to 2 main groups - initialization and deserialization

* ♻️ Refactor of TobiiDeserializer
- simplification of methods (use of map and filter instead of for loops)
- spreading logic of deserializer method between other, new methods

* 💄 ScarfPlot participant overflow improvement
- ellipsis instead of overflow

* 💄 Prevent Modal accidental closing when dragging out of body area

* 📈 Changed Tobii data rendering
- revisits of stimuli are no longer rendered as independent stimuli; instead, they are added behind already rendered segments in one stimulus

* 💄 Animation in AOI modification fix
  • Loading branch information
misavojte committed Jan 29, 2024
1 parent b7c11cc commit f1d3b66
Show file tree
Hide file tree
Showing 9 changed files with 409 additions and 115 deletions.
351 changes: 265 additions & 86 deletions src/lib/class/Eye/EyeDeserializer/TobiiEyeDeserializer.ts

Large diffs are not rendered by default.

55 changes: 40 additions & 15 deletions src/lib/class/Eye/EyePipeline/EyePipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class EyePipeline {
writer: EyeWriter = new EyeWriter()
deserializer: AbstractEyeDeserializer | null = null

requestUserInputCallback: () => Promise<string>
rowIndex = 0

get isAllProcessed(): boolean {
Expand All @@ -34,23 +35,31 @@ export class EyePipeline {
return name
}

constructor(fileNames: string[]) {
constructor(
fileNames: string[],
requestUserInputCallback: () => Promise<string>
) {
this.fileNames = fileNames
this.requestUserInputCallback = requestUserInputCallback
}

async addNewStream(stream: ReadableStream): Promise<DataType | null> {
const parser = new EyeParser(stream)
const firstTextChunk = await parser.getTextChunk()
const settings = await this.classify(firstTextChunk)
const splitter = new EyeSplitter(settings)
this.processChunk(firstTextChunk, settings, splitter)
const userStringInput: string =
settings.type === 'tobii-with-event'
? await this.requestUserInputCallback()
: ''
this.processChunk(firstTextChunk, settings, splitter, userStringInput)

while (!parser.isDone) {
const chunk = await parser.getTextChunk()
this.processChunk(chunk, settings, splitter)
this.processChunk(chunk, settings, splitter, userStringInput)
}

this.releaseAfterFile(splitter, settings)
this.releaseAfterFile(splitter, settings, userStringInput)

if (this.isAllProcessed) {
const refiner = new EyeRefiner()
Expand All @@ -59,12 +68,17 @@ export class EyePipeline {
return null
}

releaseAfterFile(splitter: EyeSplitter, settings: EyeSettingsType): void {
releaseAfterFile(
splitter: EyeSplitter,
settings: EyeSettingsType,
userStringInput: string
): void {
const dataFromSplitter = splitter.release()
for (let i = 0; i < dataFromSplitter.length; i++) {
this.processRow(
dataFromSplitter[i].split(settings.columnDelimiter),
settings
settings,
userStringInput
)
}
this.fileCount++
Expand All @@ -80,15 +94,24 @@ export class EyePipeline {
processChunk(
chunk: string,
settings: EyeSettingsType,
splitter: EyeSplitter
splitter: EyeSplitter,
userStringInput: string
): void {
const rows = splitter.splitChunk(chunk)
for (let i = 0; i < rows.length; i++) {
this.processRow(rows[i].split(settings.columnDelimiter), settings)
this.processRow(
rows[i].split(settings.columnDelimiter),
settings,
userStringInput
)
}
}

private processRow(row: string[], settings: EyeSettingsType): void {
private processRow(
row: string[],
settings: EyeSettingsType,
userStringInput: string
): void {
const headerRowId = settings.headerRowId

if (this.rowIndex < headerRowId) {
Expand All @@ -100,7 +123,8 @@ export class EyePipeline {
this.deserializer = this.getDeserializer(
row,
this.currentFileName,
settings
settings,
userStringInput
)
this.rowIndex++
return
Expand All @@ -118,15 +142,16 @@ export class EyePipeline {
private getDeserializer(
row: string[],
fileName: string,
settings: EyeSettingsType
settings: EyeSettingsType,
userStringInput: string
): AbstractEyeDeserializer {
switch (settings.type) {
case 'begaze':
return new BeGazeEyeDeserializer(row)
case 'tobii':
return this.getTobiiReducer(row, false)
return this.getTobiiReducer(row, userStringInput)
case 'tobii-with-event':
return this.getTobiiReducer(row, false) // TODO: REPAIR if TRUE
return this.getTobiiReducer(row, userStringInput)
case 'gazepoint':
return new GazePointEyeDeserializer(row, fileName)
case 'ogama':
Expand All @@ -144,8 +169,8 @@ export class EyePipeline {

private getTobiiReducer(
row: string[],
parseThroughInterval: boolean
userInput: string
): TobiiEyeDeserializer {
return new TobiiEyeDeserializer(row, parseThroughInterval)
return new TobiiEyeDeserializer(row, userInput)
}
}
40 changes: 39 additions & 1 deletion src/lib/class/WorkerService/EyeWorkerService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { addErrorToast, addInfoToast } from '$lib/stores/toastStore.ts'
import ModalContentTobiiParsingInput from '$lib/components/Modal/ModalContent/ModalContentTobiiParsingInput.svelte'
import { modalStore } from '$lib/stores/modalStore.ts'
import {
addErrorToast,
addInfoToast,
toastStore,
} from '$lib/stores/toastStore.ts'
import type { DataType } from '$lib/type/Data/DataType.ts'

/**
Expand Down Expand Up @@ -100,6 +106,9 @@ export class EyeWorkerService {
case 'fail':
this.handleError(event.data.data)
break
case 'request-user-input':
this.handleUserInputProcess()
break
default:
console.error('EyeWorkerService.handleMessage() - event:', event)
}
Expand All @@ -110,4 +119,33 @@ export class EyeWorkerService {
console.error(event.error)
console.error('EyeWorkerService.handleError() - event:', event)
}

handleUserInputProcess(): void {
this.requestUserInput()
.then(userInput => {
this.worker.postMessage({ type: 'user-input', data: userInput })
modalStore.close()
})
.catch(() => {
addInfoToast(
'User input was not provided. The file will be processed as Tobii without events'
)
this.worker.postMessage({ type: 'user-input', data: '' })
})
}

/**
* Requests user input for further processing,
* to determine how to parse stimuli in the file.
*
* The user input is then sent to the worker which resumes processing.
*/
requestUserInput(): Promise<string> {
return new Promise((resolve, reject) => {
modalStore.open(ModalContentTobiiParsingInput, 'Tobii Parsing Input', {
valuePromiseResolve: resolve,
valuePromiseReject: reject,
})
})
}
}
14 changes: 5 additions & 9 deletions src/lib/components/Modal/Modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,13 @@
</script>

{#if modal}
<div class="modal-overlay" on:click={handleClose} aria-hidden="true" in:fly>
<div
class="modal"
role="dialog"
aria-modal="true"
on:click|stopPropagation
in:fly
>
<div class="modal-overlay" aria-hidden="true" in:fly>
<div class="modal" role="dialog" aria-modal="true" in:fly>
<div class="modal-header">
<h3>{modal.title}</h3>
<button on:click={handleClose}>×</button>
<button on:click={handleClose}>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="body">
<svelte:component this={modal.component} {...modal.props} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import type { ExtendedInterpretedDataType } from '$lib/type/Data/InterpretedData/ExtendedInterpretedDataType.ts'
import { onMount } from 'svelte'
import { flip } from 'svelte/animate'
import { fade } from 'svelte/transition'
import GeneralPositionControl from '$lib/components/General/GeneralPositionControl/GeneralPositionControl.svelte'
import GeneralEmpty from '$lib/components/General/GeneralEmpty/GeneralEmpty.svelte'
Expand Down Expand Up @@ -113,8 +114,8 @@
</tr>
</thead>
<tbody>
{#each aoiObjects as aoi (aoi.id)}
<tr class="gr-line" animate:flip>
{#each aoiObjects as aoi (aoi.id + selectedStimulus)}
<tr class="gr-line" animate:flip in:fade>
<td class="original-name">{aoi.originalName}</td>
<td>
<input
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script lang="ts">
import GeneralRadio from '$lib/components/General/GeneralRadio/GeneralRadio.svelte'
import GeneralButtonMajor from '$lib/components/General/GeneralButton/GeneralButtonMajor.svelte'
import { onDestroy } from 'svelte'
export let valuePromiseResolve: (value: string) => void
export let valuePromiseReject: (reason?: any) => void
let value: string
onDestroy(() => {
valuePromiseReject(new Error('Modal closed without value'))
})
</script>

<div class="content">
<p>
GazePlotter detected Tobii Pro Lab data with the Event column. How should
the stimulus be determined?
</p>
<GeneralRadio
options={[
{ value: '', label: 'Media record column' },
{
value: 'IntervalStart;IntervalEnd',
label: `Event's '%NAME% IntervalStart' and '%NAME% IntervalEnd'`,
},
{
value: '_start;_end',
label: `Event's '%NAME%_start' and '%NAME%_end'`,
},
]}
legend="Stimulus will be determined by"
bind:userSelected={value}
/>
</div>
<GeneralButtonMajor on:click={() => valuePromiseResolve(value)}>
Apply
</GeneralButtonMajor>

<style>
.content {
margin-bottom: 2rem;
}
</style>
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,11 @@
align-items: center;
}
.chylabs > div {
overflow-wrap: break-word;
width: 125px;
line-height: 1;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
text {
font-family: sans-serif;
Expand Down
12 changes: 11 additions & 1 deletion src/lib/worker/eyePipelineWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { EyePipeline } from '$lib/class/Eye/EyePipeline/EyePipeline.ts'
let fileNames: string[] = []
let pipeline: EyePipeline | null = null
let streams: ReadableStream[] = []
let userInputResolver: (value: string) => void

const isStringArray = (data: unknown): data is string[] => {
if (!Array.isArray(data)) return false
Expand All @@ -21,6 +22,13 @@ const isReadableStream = (data: unknown): data is ReadableStream => {
return typeof (data as ReadableStream).getReader === 'function'
}

const requestUserInput = (): Promise<string> => {
self.postMessage({ type: 'request-user-input' })
return new Promise(resolve => {
userInputResolver = resolve
})
}

self.onmessage = async e => await processEvent(e)

async function processEvent(e: MessageEvent): Promise<void> {
Expand All @@ -29,7 +37,7 @@ async function processEvent(e: MessageEvent): Promise<void> {
case 'file-names':
if (!isStringArray(data)) throw new Error('File names are not string[]')
fileNames = data
pipeline = new EyePipeline(fileNames)
pipeline = new EyePipeline(fileNames, requestUserInput)
return
case 'test-stream':
if (!isReadableStream(data))
Expand All @@ -39,6 +47,8 @@ async function processEvent(e: MessageEvent): Promise<void> {
return await evalStream(data)
case 'buffer':
return await evalBuffer(data)
case 'user-input':
return userInputResolver(data)
default:
throw new Error('Unknown const type in worker', data)
}
Expand Down

0 comments on commit f1d3b66

Please sign in to comment.