From 9446e3b2deb0f9325e28dfa54b5786b6381161c3 Mon Sep 17 00:00:00 2001 From: MrPlatnum Date: Sun, 14 Sep 2025 12:17:51 +0200 Subject: [PATCH] implement metrics tracking --- src/app/app.config.ts | 3 +- .../demographics-feedback.component.html | 421 ++++++++++-------- .../demographics-feedback.component.ts | 124 ++++-- ...spatial-position-assessment.component.html | 38 +- .../spatial-position-assessment.component.ts | 44 +- ...patial-stability-assessment.component.html | 225 +++++----- ...ial-stability-assessment.component.spec.ts | 10 +- .../spatial-stability-assessment.component.ts | 92 ++-- .../text-legibility-assessment.component.html | 79 ++-- .../text-legibility-assessment.component.ts | 103 +++-- src/app/services/metrics-tracker.service.ts | 168 +++++++ src/main.ts | 6 +- 12 files changed, 846 insertions(+), 467 deletions(-) create mode 100644 src/app/services/metrics-tracker.service.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index a1e7d6f..94f5f50 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -2,7 +2,8 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; +import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)] + providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient()] }; diff --git a/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.html b/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.html index bc055c3..7070aba 100644 --- a/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.html +++ b/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.html @@ -1,199 +1,244 @@
-
-
-

Abschließender Fragebogen

-

Ihre Angaben helfen uns bei der wissenschaftlichen Auswertung der Studie.

-
- -
+
+
+

Abschließender Fragebogen

+

Ihre Angaben helfen uns bei der wissenschaftlichen Auswertung der Studie.

+
+ + + +
+

+ 1 + Bewertung der Halteposition +

+

Diese Fragen beziehen sich auf den Test, bei dem Sie das virtuelle Objekt 20 Sekunden lang in Position halten mussten.

- -
-

- 1 - Demografische Angaben -

-
-
- - -
Die Angabe des Alters ist erforderlich.
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-

- 2 - Vorerfahrung mit AR/VR -

-
-
-
- -
110
-
{{ demographicsForm.get('arExperience')?.value }}/10
-
-
- -
110
-
{{ demographicsForm.get('vrExperience')?.value }}/10
-
-
-
- -
-
diff --git a/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.ts b/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.ts index 4ad29fd..dc3edb1 100644 --- a/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.ts +++ b/src/app/components/test-suite/assessments/demographics-feedback/demographics-feedback.component.ts @@ -1,6 +1,7 @@ -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, inject, OnInit, OnDestroy, Output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MetricsTrackerService } from '../../../../services/metrics-tracker.service'; @Component({ selector: 'app-demographics-feedback', @@ -9,14 +10,29 @@ import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angula templateUrl: './demographics-feedback.component.html', styleUrls: ['./demographics-feedback.component.css'] }) -export class DemographicsFeedbackComponent implements OnInit { +export class DemographicsFeedbackComponent implements OnInit, OnDestroy { + private metricsService = inject(MetricsTrackerService); + @Output() testComplete = new EventEmitter(); - @Output() redoTest = new EventEmitter(); + @Output() redoTest = new EventEmitter(); demographicsForm!: FormGroup; isSubmitting = false; isSubmitted = false; + susItems = [ + { label: 'Ich denke, dass ich dieses System gerne regelmäßig nutzen würde.', controlName: 'sus1' }, + { label: 'Ich empfinde das System als unnötig komplex.', controlName: 'sus2' }, + { label: 'Ich empfinde das System als einfach zu nutzen.', controlName: 'sus3' }, + { label: 'Ich denke, dass ich technischen Support brauchen würde, um das System zu nutzen.', controlName: 'sus4' }, + { label: 'Ich finde, dass die verschiedenen Funktionen des Systems gut integriert sind.', controlName: 'sus5' }, + { label: 'Ich finde, dass es im System zu viele Inkonsistenzen gibt.', controlName: 'sus6' }, + { label: 'Ich kann mir vorstellen, dass die meisten Leute das System schnell zu beherrschen lernen.', controlName: 'sus7' }, + { label: 'Ich empfinde die Bedienung als sehr umständlich.', controlName: 'sus8' }, + { label: 'Ich habe mich bei der Nutzung des Systems sehr sicher gefühlt.', controlName: 'sus9' }, + { label: 'Ich musste eine Menge Dinge lernen, bevor ich mit dem System arbeiten konnte.', controlName: 'sus10' } + ]; + arVrDevices = [ { value: 'smartphone-ar', label: 'Smartphone-AR' }, { value: 'meta-quest', label: 'Meta Quest/Oculus' }, @@ -37,41 +53,56 @@ export class DemographicsFeedbackComponent implements OnInit { selectedDevices: string[] = []; - constructor(private formBuilder: FormBuilder) {} + constructor(private formBuilder: FormBuilder) { + this.logInteraction = this.logInteraction.bind(this); + } ngOnInit() { this.createForm(); + // Start device orientation tracking for this component + this.metricsService.startDeviceOrientationTracking(); + } + + logInteraction(event: Event) { + this.metricsService.logInteraction(event); } createForm() { this.demographicsForm = this.formBuilder.group({ - // Demografische Daten + comfortHold: ['', Validators.required], + enduranceFiveMinutes: ['', Validators.required], + physicalStrain: ['', Validators.required], + physicalStrain5min: ['', Validators.required], + age: ['', [Validators.required, Validators.min(13), Validators.max(120)]], gender: ['', Validators.required], education: [''], occupation: [''], - // AR/VR Vorerfahrung arExperience: [1, Validators.required], vrExperience: [1, Validators.required], arVrFrequency: [''], - // Physische und visuelle Einschränkungen visionCorrection: [''], - colorVision: [''], dominantHand: [''], - mobilityLimitations: [''], + + smartphoneComfort: [3, Validators.required], + techComfort: [3, Validators.required], + gamingExperience: [3, Validators.required], - // Technologiekompetenz - smartphoneComfort: [5], - techComfort: [5], - gamingExperience: [5], + sus1: ['', Validators.required], + sus2: ['', Validators.required], + sus3: ['', Validators.required], + sus4: ['', Validators.required], + sus5: ['', Validators.required], + sus6: ['', Validators.required], + sus7: ['', Validators.required], + sus8: ['', Validators.required], + sus9: ['', Validators.required], + sus10: ['', Validators.required], - // Feedback zur Studie - overallRating: [0, [Validators.required, Validators.min(1)]], - easeOfUse: [0, [Validators.required, Validators.min(1)]], comments: [''], - wouldRecommend: ['', Validators.required] + }); } @@ -83,21 +114,7 @@ export class DemographicsFeedbackComponent implements OnInit { this.selectedDevices = this.selectedDevices.filter(device => device !== value); } } - - setRating(controlName: string, rating: number) { - this.demographicsForm.get(controlName)?.setValue(rating); - } - - getRatingText(rating: number): string { - const ratings = ['', 'Mangelhaft', 'Ausreichend', 'Gut', 'Sehr Gut', 'Exzellent']; - return ratings[rating] || ''; - } - - getEaseText(rating: number): string { - const ratings = ['', 'Sehr schwierig', 'Schwierig', 'Neutral', 'Einfach', 'Sehr einfach']; - return ratings[rating] || ''; - } - + onSubmit() { if (this.demographicsForm.valid) { this.isSubmitting = true; @@ -105,28 +122,57 @@ export class DemographicsFeedbackComponent implements OnInit { const formData = { ...this.demographicsForm.value, devicesUsed: this.selectedDevices, + susScore: this.calculateSusScore(this.demographicsForm.value), submittedAt: new Date().toISOString() }; - console.log('Demographics and Feedback Data:', formData); + this.metricsService.sendMetricsToServer('FullTestSuite', formData).subscribe({ + next: () => { + this.isSubmitting = false; + this.isSubmitted = true; + // Optionally delay before emitting final completion + setTimeout(() => this.testComplete.emit(), 2000); + }, + error: (err) => { + console.error("FINAL SUBMISSION FAILED:", err); + this.isSubmitting = false; + } + }); - setTimeout(() => { - this.isSubmitting = false; - this.isSubmitted = true; - this.testComplete.emit(); - }, 2000); } else { Object.keys(this.demographicsForm.controls).forEach(key => { this.demographicsForm.get(key)?.markAsTouched(); }); + console.error("Form is invalid. Please fill out all required fields."); } } + private calculateSusScore(formData: any): number { + let score = 0; + score += (formData.sus1 - 1); + score += (formData.sus3 - 1); + score += (formData.sus5 - 1); + score += (formData.sus7 - 1); + score += (formData.sus9 - 1); + + score += (5 - formData.sus2); + score += (5 - formData.sus4); + score += (5 - formData.sus6); + score += (5 - formData.sus8); + score += (5 - formData.sus10); + + return score * 2.5; + } + finishAssessment() { this.testComplete.emit(); } retryCurrent() { - this.redoTest.emit(); + this.redoTest.emit(4); + } + + ngOnDestroy() { + this.metricsService.cleanup(); } } diff --git a/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.html b/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.html index 5363dc5..4ccea7e 100644 --- a/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.html +++ b/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.html @@ -7,10 +7,13 @@ ar-modes="webxr" ar-placement="ceiling" reveal="manual" + [scale]="scale + ' ' + scale + ' ' + scale" camera-orbit="0deg 75deg 2m"> @@ -18,11 +21,19 @@
+
- {{ scale.toFixed(2) }}×
@@ -30,19 +41,32 @@
- {{ verticalOffset >= 0 ? '+' : '' }}{{ verticalOffset.toFixed(2) }}m
- - diff --git a/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.ts b/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.ts index 3dc2a9e..6d4f795 100644 --- a/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.ts +++ b/src/app/components/test-suite/assessments/spatial-position-assessment/spatial-position-assessment.component.ts @@ -1,16 +1,18 @@ import { Component, AfterViewInit, + OnDestroy, ViewChild, ElementRef, ChangeDetectorRef, CUSTOM_ELEMENTS_SCHEMA, EventEmitter, - Output + Output, + inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; - +import { MetricsTrackerService } from '../../../../services/metrics-tracker.service'; import '../../../../../assets/scripts/model-viewer'; @Component({ @@ -21,21 +23,26 @@ import '../../../../../assets/scripts/model-viewer'; styleUrls: ['./spatial-position-assessment.component.css'], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) -export class SpatialPositionAssessmentComponent implements AfterViewInit { +export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDestroy { // <-- Implement OnDestroy @ViewChild('modelViewer') modelViewerRef!: ElementRef; @Output() testComplete = new EventEmitter(); - @Output() redoTest = new EventEmitter(); + @Output() redoTest = new EventEmitter(); + + private metricsService = inject(MetricsTrackerService); public scale = 1; public verticalOffset = 0; - protected isModelPlaced = false; private initialArAnchor: { x: number; y: number; z: number } | null = null; - constructor(private cdr: ChangeDetectorRef) {} + constructor(private cdr: ChangeDetectorRef) { + this.logInteraction = this.logInteraction.bind(this); + } ngAfterViewInit() { const mv = this.modelViewerRef.nativeElement; + this.metricsService.startDeviceOrientationTracking(); + this.metricsService.startArTracking(mv); mv.addEventListener('ar-status', (e: any) => { if (e.detail.status === 'session-started') { setTimeout(() => this.captureAnchor(), 500); @@ -43,6 +50,10 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit { }); } + public logInteraction(event: Event) { + this.metricsService.logInteraction(event); + } + private async captureAnchor() { const mv = this.modelViewerRef.nativeElement; const anchor = mv.getAnchor(); @@ -70,10 +81,31 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit { } confirmPlacement() { + const finalPlacement = { + finalScale: this.scale, + finalVerticalOffset: this.verticalOffset, + initialAnchor: this.initialArAnchor, + finalAnchor: this.modelViewerRef.nativeElement.getAttribute('ar-anchor') + }; + + this.metricsService.logInteraction(new CustomEvent('test-results', { + detail: { + testName: 'SpatialPositionAssessment', + results: finalPlacement + } + })); + + console.log('Spatial position results captured locally.'); + this.testComplete.emit(); } + retryCurrent() { this.redoTest.emit(1); } + + ngOnDestroy() { + this.metricsService.cleanup(); + } } diff --git a/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.html b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.html index b21ecbf..b65280a 100644 --- a/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.html +++ b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.html @@ -1,122 +1,121 @@
- - - + + + - -
- -
-

Finden sie ihre Optionale Betrachtungsposition

-
- -
- - - {{ verticalOffset >= 0 ? '+' : '' }}{{ verticalOffset.toFixed(2) }}m -
- + +
+ +
+

Finden sie ihre Optionale Betrachtungsposition

+
+ +
+ + + {{ verticalOffset >= 0 ? '+' : '' }}{{ verticalOffset.toFixed(2) }}m +
+ + +
+ + +
+ +
+

Position fixiert!

+

Halten Sie das Modell für die gesamte Dauer in möglichst genau dieser Position.

+
+ +
+
- - -
- -
-

Position fixiert!

-

Halten Sie das Modell für die gesamte Dauer in möglichst genau dieser Position.

-
- -
- - -
+
+ + +
+ +
+

{{ remainingTime }}

+

Sekunden verbleibend

+

Halten Sie das Modell in Position.

- - -
- -
-

{{ remainingTime }}

-

Sekunden verbleibend

-

Halten Sie das Modell in Position.

-
- - -
-
-
-
- + +
+
+
+ + +
+ + +
+ +
+

Test abgeschlossen

+
+ +
+
- - -
- -
-

Test abgeschlossen

-
- -
- - -
-
- -
- \ No newline at end of file +
+ +
diff --git a/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.spec.ts b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.spec.ts index 8274f3a..6560862 100644 --- a/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.spec.ts +++ b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RotationSpeedAssessmentComponent } from './spatial-stability-assessment.component'; +import { SpatialStabilityAssessmentComponent } from './spatial-stability-assessment.component'; describe('RotationSpeedAssessmentComponent', () => { - let component: RotationSpeedAssessmentComponent; - let fixture: ComponentFixture; + let component: SpatialStabilityAssessmentComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RotationSpeedAssessmentComponent] + imports: [SpatialStabilityAssessmentComponent] }) .compileComponents(); - fixture = TestBed.createComponent(RotationSpeedAssessmentComponent); + fixture = TestBed.createComponent(SpatialStabilityAssessmentComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.ts b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.ts index c6fb3fe..89daf38 100644 --- a/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.ts +++ b/src/app/components/test-suite/assessments/spatial-stability-assessment/spatial-stability-assessment.component.ts @@ -1,6 +1,18 @@ -import { Component, AfterViewInit, ViewChild, ElementRef, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef, EventEmitter, Output } from '@angular/core'; +import { + Component, + AfterViewInit, + OnDestroy, + ViewChild, + ElementRef, + CUSTOM_ELEMENTS_SCHEMA, + ChangeDetectorRef, + EventEmitter, + Output, + inject +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; +import { MetricsTrackerService } from '../../../../services/metrics-tracker.service'; import '../../../../../assets/scripts/model-viewer'; @@ -12,44 +24,53 @@ import '../../../../../assets/scripts/model-viewer'; styleUrls: ['./spatial-stability-assessment.component.css'], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) -export class SpatialStabilityAssessmentComponent implements AfterViewInit { +export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDestroy { // <-- Implement OnDestroy @ViewChild('modelViewer') modelViewerRef!: ElementRef; @Output() testComplete = new EventEmitter(); - @Output() redoTest = new EventEmitter(); + @Output() redoTest = new EventEmitter(); + + private metricsService = inject(MetricsTrackerService); - public scale = 1; public verticalOffset = 0; public currentPhase = 0; // 0: waiting, 1: adjust, 2: lock, 3: countdown, 4: complete public remainingTime = 20; public progressPercentage = 0; private initialArAnchor: { x: number, y: number, z: number } | null = null; - private lockedPosition: { scale: number, verticalOffset: number } | null = null; + private lockedPosition: { verticalOffset: number, anchor: string } | null = null; private countdownInterval: any = null; protected isModelPlaced = false; - constructor(private cdr: ChangeDetectorRef) {} + constructor(private cdr: ChangeDetectorRef) { + this.logInteraction = this.logInteraction.bind(this); + } ngAfterViewInit() { const modelViewer = this.modelViewerRef.nativeElement; + this.metricsService.startDeviceOrientationTracking(); + this.metricsService.startArTracking(modelViewer); + modelViewer.addEventListener('ar-status', (event: any) => { - if (event.detail.status === 'session-started') { + if (event.detail.status === 'session-started' && !this.isModelPlaced) { setTimeout(() => this.getInitialAnchor(), 1000); } }); } + public logInteraction(event: Event) { + this.metricsService.logInteraction(event); + } + private async getInitialAnchor() { const modelViewer = this.modelViewerRef.nativeElement; const anchorString = modelViewer.getAnchor(); - if (!anchorString.includes('not placed')) { const coords = anchorString.split(' ').map(parseFloat); this.initialArAnchor = { x: coords[0], y: coords[1], z: coords[2] }; this.isModelPlaced = true; this.currentPhase = 1; - console.log('Starting adjustment phase'); + this.cdr.detectChanges(); } else { setTimeout(() => this.getInitialAnchor(), 500); } @@ -65,11 +86,10 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit { lockPosition() { this.lockedPosition = { - scale: this.scale, - verticalOffset: this.verticalOffset + verticalOffset: this.verticalOffset, + anchor: this.modelViewerRef.nativeElement.getAttribute('ar-anchor') }; this.currentPhase = 2; - console.log('Position locked:', this.lockedPosition); } adjustMore() { @@ -86,13 +106,10 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit { this.progressPercentage = ((20 - this.remainingTime) / 20) * 100; if (this.remainingTime <= 0) { - this.completeTest(); + this.completeTest(true); } - this.cdr.detectChanges(); }, 1000); - - console.log('Countdown started'); } cancelTest() { @@ -101,40 +118,57 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit { this.countdownInterval = null; } this.currentPhase = 2; + this.metricsService.logInteraction(new CustomEvent('test-cancelled', { detail: { phase: 'countdown' }})); } - private completeTest() { + private completeTest(wasCompleted: boolean) { if (this.countdownInterval) { clearInterval(this.countdownInterval); this.countdownInterval = null; } - this.currentPhase = 4; // Complete phase - console.log('Test completed successfully!'); + + const finalResults = { + testCompletedSuccessfully: wasCompleted, + initialAnchor: this.initialArAnchor, + lockedPosition: this.lockedPosition, + }; + + this.metricsService.logInteraction(new CustomEvent('test-results', { + detail: { + testName: 'SpatialStability', + results: finalResults + } + })); + + if (wasCompleted) { + this.currentPhase = 4; + } + + if(wasCompleted) { + this.testComplete.emit(); + } } restartTest() { this.currentPhase = 1; - this.remainingTime = 60; + this.remainingTime = 20; this.progressPercentage = 0; this.lockedPosition = null; if (this.countdownInterval) { clearInterval(this.countdownInterval); this.countdownInterval = null; } - } - - - ngOnDestroy() { - if (this.countdownInterval) { - clearInterval(this.countdownInterval); - } + this.metricsService.resetMetrics(); } finishAssessment() { this.testComplete.emit(); } - retryCurrent() { - this.redoTest.emit(1); + ngOnDestroy() { + if (this.countdownInterval) { + clearInterval(this.countdownInterval); + } + this.metricsService.cleanup(); } } diff --git a/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.html b/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.html index 60f587a..e232848 100644 --- a/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.html +++ b/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.html @@ -1,71 +1,92 @@
- + - + -
- + +
+
Minimum setzen
- + {{ currentSize }}px - +
- +
-
Minimum bestätigt: {{ currentSize }}px
- - +
Minimum bestätigt: {{ minSizeResult }}px
+ +
Maximum setzen
- + {{ currentSize }}px - +
- +
-
Maximum bestätigt: {{ currentSize }}px
- +
Maximum bestätigt: {{ maxSizeResult }}px
+
Optimale Größe setzen
- + {{ currentSize }}px - +
- + +
+ + + +
Optimale Größe bestätigt: {{ comfortableSizeResult }}px
+ +
+ + + +
Vielen Dank!
+

Ergebnisse werden für die finale Übermittlung gespeichert...

diff --git a/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.ts b/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.ts index dd64643..c30666b 100644 --- a/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.ts +++ b/src/app/components/test-suite/assessments/text-legibility-assessment/text-legibility-assessment.component.ts @@ -1,16 +1,18 @@ import { Component, AfterViewInit, + OnDestroy, ViewChild, ElementRef, ChangeDetectorRef, CUSTOM_ELEMENTS_SCHEMA, EventEmitter, - Output + Output, + inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; - +import { MetricsTrackerService } from '../../../../services/metrics-tracker.service'; import '../../../../../assets/scripts/model-viewer'; @Component({ @@ -21,97 +23,80 @@ import '../../../../../assets/scripts/model-viewer'; styleUrls: ['./text-legibility-assessment.component.css'], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) -export class TextLegibilityAssessmentComponent implements AfterViewInit { +export class TextLegibilityAssessmentComponent implements AfterViewInit, OnDestroy { @ViewChild('modelViewer') modelViewerRef!: ElementRef; @Output() testComplete = new EventEmitter(); - @Output() redoTest = new EventEmitter(); + @Output() redoTest = new EventEmitter(); + private metricsService = inject(MetricsTrackerService); + + // --- Component State (Restored) --- minSize = 2; maxSize = 64; currentSize = 16; - comfortableSize = 16; - phase: - | 'min' - | 'confirmedMin' - | 'max' - | 'confirmedMax' - | 'comfortable' - | 'confirmedComfort' - | 'finished' = 'min'; + minSizeResult: number | null = null; + maxSizeResult: number | null = null; + comfortableSizeResult: number | null = null; + + phase: 'min' | 'confirmedMin' | 'max' | 'confirmedMax' | 'comfortable' | 'confirmedComfort' | 'finished' = 'min'; private offsetApplied = false; - constructor(private cdr: ChangeDetectorRef) {} + constructor(private cdr: ChangeDetectorRef) { + this.logInteraction = this.logInteraction.bind(this); + } ngAfterViewInit() { - const mv = this.modelViewerRef.nativeElement; - mv.setAttribute('scale', '0.25 0.25 0.25'); + this.metricsService.startDeviceOrientationTracking(); + this.metricsService.startArTracking(this.modelViewerRef.nativeElement); + } - mv.addEventListener('ar-status', async (e: any) => { - if (e.detail.status === 'session-started' && !this.offsetApplied) { - await mv.updateComplete; - const anchor = mv.getAnchor(); - if (!anchor.includes('not placed')) { - const [x, y, z] = anchor.split(' ').map(parseFloat); - mv.setAttribute('ar-anchor', `${x} ${y + 3.0} ${z}`); - this.offsetApplied = true; - } - } - }); + public logInteraction(event: Event) { + this.metricsService.logInteraction(event); } decrease() { - if (this.currentSize > this.minSize) { - this.currentSize--; - this.syncComfortable(); - } + if (this.currentSize > this.minSize) this.currentSize--; } increase() { - if (this.currentSize < this.maxSize) { - this.currentSize++; - this.syncComfortable(); - } - } - - private syncComfortable() { - if (this.phase === 'comfortable') { - this.comfortableSize = this.currentSize; - } + if (this.currentSize < this.maxSize) this.currentSize++; } nextPhase() { switch (this.phase) { case 'min': + this.minSizeResult = this.currentSize; this.phase = 'confirmedMin'; break; case 'confirmedMin': - this.phase = 'max'; this.currentSize = this.maxSize; + this.phase = 'max'; break; case 'max': + this.maxSizeResult = this.currentSize; this.phase = 'confirmedMax'; break; case 'confirmedMax': + this.currentSize = Math.floor((this.minSizeResult! + this.maxSizeResult!) / 2); this.phase = 'comfortable'; - this.currentSize = Math.floor((this.minSize + this.maxSize) / 2); - this.comfortableSize = this.currentSize; break; case 'comfortable': + this.comfortableSizeResult = this.currentSize; this.phase = 'confirmedComfort'; - this.comfortableSize = this.currentSize; break; case 'confirmedComfort': - this.phase = 'finished'; - this.testComplete.emit(); + this.finishAssessment(); break; } } - resetToMid() { this.currentSize = Math.floor((this.minSize + this.maxSize) / 2); this.phase = 'min'; + this.minSizeResult = null; + this.maxSizeResult = null; + this.comfortableSizeResult = null; } retry() { @@ -119,6 +104,28 @@ export class TextLegibilityAssessmentComponent implements AfterViewInit { } finishAssessment() { + this.phase = 'finished'; + + const finalResults = { + minReadableSize: this.minSizeResult, + maxReadableSize: this.maxSizeResult, + comfortableReadableSize: this.comfortableSizeResult + }; + + this.metricsService.logInteraction(new CustomEvent('test-results', { + detail: { + testName: 'TextLegibility', + results: finalResults + } + })); + + console.log(finalResults); + this.testComplete.emit(); } + + + ngOnDestroy() { + this.metricsService.cleanup(); + } } diff --git a/src/app/services/metrics-tracker.service.ts b/src/app/services/metrics-tracker.service.ts new file mode 100644 index 0000000..5d6f69f --- /dev/null +++ b/src/app/services/metrics-tracker.service.ts @@ -0,0 +1,168 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { interval, Subscription, tap } from 'rxjs'; + +export interface InteractionEvent { + timestamp: number; + type: string; + elementId?: string; + elementTag?: string; + elementClasses?: string; + value?: any; +} + +export interface DeviceOrientation { + timestamp: number; + alpha: number | null; + beta: number | null; + gamma: number | null; +} + +export interface ArTrackingData { + timestamp: number; + anchor: string | null; + cameraOrbit: { + theta: number; + phi: number; + radius: number; + } | null; + cameraTarget: { + x: number; + y: number; + z: number; + } | null; +} + +export interface MetricsLog { + interactions: InteractionEvent[]; + deviceOrientations: DeviceOrientation[]; + arData: ArTrackingData[]; +} + +@Injectable({ + providedIn: 'root' +}) +export class MetricsTrackerService { + private http = inject(HttpClient); + private serverUrl = '/api/log'; + + private metricsLog: MetricsLog = { + interactions: [], + deviceOrientations: [], + arData: [] + }; + + private deviceOrientationSubscription: Subscription | null = null; + private arTrackingSubscription: Subscription | null = null; + private lastDeviceOrientation: DeviceOrientationEvent | null = null; + + constructor() { + this.handleDeviceOrientation = this.handleDeviceOrientation.bind(this); + } + + private handleDeviceOrientation(event: DeviceOrientationEvent): void { + this.lastDeviceOrientation = event; + } + + // --- Public API --- + public logInteraction(event: Event): void { + if (event.target) { + const target = event.target as HTMLElement; + const interaction: InteractionEvent = { + timestamp: Date.now(), + type: event.type, + elementId: target.id || undefined, + elementTag: target.tagName, + elementClasses: target.className, + value: (target as any).value ?? undefined + }; + this.metricsLog.interactions.push(interaction); + } + else if (event instanceof CustomEvent && event.detail) { + const interaction: InteractionEvent = { + timestamp: Date.now(), + type: event.type, + value: event.detail + }; + this.metricsLog.interactions.push(interaction); + } + else { + console.warn("logInteraction called with an unknown event type:", event); + } + } + + + public startDeviceOrientationTracking(): void { + if (this.deviceOrientationSubscription || typeof window === 'undefined') return; + + window.addEventListener('deviceorientation', this.handleDeviceOrientation); + + this.deviceOrientationSubscription = interval(500).subscribe(() => { + if (this.lastDeviceOrientation) { + const orientation: DeviceOrientation = { + timestamp: this.lastDeviceOrientation.timeStamp, + alpha: this.lastDeviceOrientation.alpha, + beta: this.lastDeviceOrientation.beta, + gamma: this.lastDeviceOrientation.gamma + }; + this.metricsLog.deviceOrientations.push(orientation); + } + }); + } + + + public startArTracking(modelViewerElement: any): void { + if (this.arTrackingSubscription || !modelViewerElement) return; + + this.arTrackingSubscription = interval(500).subscribe(() => { + if (!modelViewerElement.arActive) { + return; + } + + const anchor = modelViewerElement.getAnchor ? modelViewerElement.getAnchor() : 'getAnchor not available'; + const orbit = modelViewerElement.getCameraOrbit(); + const target = modelViewerElement.getCameraTarget(); + + const arData: ArTrackingData = { + timestamp: Date.now(), + anchor: anchor.includes('not placed') ? null : anchor, + cameraOrbit: orbit ? { theta: orbit.theta, phi: orbit.phi, radius: orbit.radius } : null, + cameraTarget: target ? { x: target.x, y: target.y, z: target.z } : null, + }; + + this.metricsLog.arData.push(arData); + }); + } + + public sendMetricsToServer(testName: string, formData?: any) { + const payload = { + testName, + metricsLog: this.metricsLog, + ...(formData && { formData }) + }; + console.log(payload) + return this.http.post(this.serverUrl, payload).pipe( + tap({ + next: (response) => { + console.log(`Metrics for '${testName}' sent successfully:`, response); + this.resetMetrics(); + }, + error: (err) => console.error(`Failed to send metrics for '${testName}':`, err) + }) + ); + } + + public cleanup(): void { + if (typeof window !== 'undefined') { + window.removeEventListener('deviceorientation', this.handleDeviceOrientation); + } + this.deviceOrientationSubscription?.unsubscribe(); + this.arTrackingSubscription?.unsubscribe(); + this.deviceOrientationSubscription = null; + this.arTrackingSubscription = null; + } + + public resetMetrics(): void { + this.metricsLog = { interactions: [], deviceOrientations: [], arData: [] }; + } +} diff --git a/src/main.ts b/src/main.ts index 681c68e..bd9a082 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,9 +2,11 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { provideRouter } from '@angular/router'; import { routes } from './app/app.routes'; +import { provideHttpClient } from '@angular/common/http'; bootstrapApplication(AppComponent, { providers: [ - provideRouter(routes) + provideRouter(routes), + provideHttpClient() ] -}).catch(err => console.error(err)); \ No newline at end of file +}).catch(err => console.error(err));