diff --git a/package-lock.json b/package-lock.json index b763483..7355847 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@angular/router": "^18.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", + "uuid": "^13.0.0", "zone.js": "~0.14.3" }, "devDependencies": { @@ -25,6 +26,7 @@ "@angular/cli": "^18.0.6", "@angular/compiler-cli": "^18.0.0", "@types/jasmine": "~5.1.0", + "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.21", "jasmine-core": "~5.1.0", "karma": "~6.4.0", @@ -4627,6 +4629,13 @@ "@types/node": "*" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", @@ -12380,6 +12389,16 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", @@ -13410,13 +13429,16 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/validate-npm-package-license": { diff --git a/package.json b/package.json index a2512e6..9e256cf 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@angular/router": "^18.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", + "uuid": "^13.0.0", "zone.js": "~0.14.3" }, "devDependencies": { @@ -27,6 +28,7 @@ "@angular/cli": "^18.0.6", "@angular/compiler-cli": "^18.0.0", "@types/jasmine": "~5.1.0", + "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.21", "jasmine-core": "~5.1.0", "karma": "~6.4.0", diff --git a/src/app/components/completion/completion.component.ts b/src/app/components/completion/completion.component.ts index 1802490..7063431 100644 --- a/src/app/components/completion/completion.component.ts +++ b/src/app/components/completion/completion.component.ts @@ -1,153 +1,31 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { Router } from '@angular/router'; -import { ArLoggerService } from '../../services/ar-logger.service'; -import { DataExportService } from '../../services/data-export.service'; @Component({ selector: 'app-completion', standalone: true, imports: [CommonModule], template: ` -
-
-
- - - -
-

Assessment Suite Completed Successfully

-

- Thank you for your participation in this research study. Your data has been successfully - collected and is ready for download. -

+
+ +
+ + +
- -
-

Session Summary

-
-
-
{{completedTests}}
-
Tests Completed
-
-
-
{{totalInteractions}}
-
Total Interactions
-
-
-
{{sessionDuration}}
-
Session Duration
-
-
-
+

+ Umfrage abgeschlossen +

+ +

+ Ihre Teilnahme war erfolgreich. Vielen Dank für Ihren Beitrag zu unserer Forschung. +

- -
- -
- - -
-

Data Preview:

-
-        
-
- - -
- -
`, styles: [] }) -export class CompletionComponent implements OnInit { - completedTests = 0; - totalInteractions = 0; - sessionDuration = '0m 0s'; - dataPreview = ''; - - constructor( - private router: Router, - private logger: ArLoggerService, - private dataExportService: DataExportService - ) {} - - ngOnInit(): void { - this.loadSessionSummary(); - this.generateDataPreview(); - } - - private loadSessionSummary(): void { - const testData = this.logger.getTestData(); - - // Count completed tests - this.completedTests = Object.values(testData.results) - .filter((result: any) => result.completedSuccessfully).length; - - // Count total interactions - this.totalInteractions = Object.values(testData.results) - .reduce((total: number, result: any) => { - return total + (result.interactionCount || 0); - }, 0); - - // Calculate session duration - const duration = Date.now() - new Date(testData.device.timestamp).getTime(); - const minutes = Math.floor(duration / 60000); - const seconds = Math.floor((duration % 60000) / 1000); - this.sessionDuration = `${minutes}m ${seconds}s`; - } - - private generateDataPreview(): void { - const testData = this.logger.getTestData(); - const preview = JSON.stringify(testData, null, 2); - - // Truncate for display - if (preview.length > 2000) { - this.dataPreview = preview.substring(0, 2000) + '...\n\n[Preview truncated - complete data available in CSV export]'; - } else { - this.dataPreview = preview; - } - } - - downloadResults(): void { - const csvData = this.logger.exportToCSV(); - const csvContent = csvData.map(row => row.join(',')).join('\n'); - - const blob = new Blob([csvContent], { type: 'text/csv' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - - a.setAttribute('hidden', ''); - a.setAttribute('href', url); - a.setAttribute('download', `ceiling_ar_assessment_${this.logger.getTestData().device.sessionId}.csv`); - - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - - window.URL.revokeObjectURL(url); - } - - restartAssessment(): void { - // Reset services and navigate back to consent - this.router.navigate(['/consent']); - } -} \ No newline at end of file +export class CompletionComponent { + constructor() { } +} diff --git a/src/app/components/consent/consent.component.html b/src/app/components/consent/consent.component.html index b14a44e..ce0e513 100644 --- a/src/app/components/consent/consent.component.html +++ b/src/app/components/consent/consent.component.html @@ -1,62 +1,75 @@ -
-
-

- Einverständniserklärung -

- -
-

- Zweck: Diese empirische Studie untersucht optimale Design-Parameter für - deckenbasierte Augmented-Reality-Anwendungen. -

+
+ + +
+

Inkompatibles Gerät

+

+ Danke, dass Sie an dieser Studie teilnehmen wollen. Leider ist die Studie nur für Android-Geräte verfügbar. +

+
+ + +
+
+

+ Einverständniserklärung +

-
-

Datenerhebungsprotokoll:

-
    -
  • Gerätespezifikationen (Display-Abmessungen, Browser-Version, Betriebssystem)
  • -
  • WebXR-Interaktionsmetriken und Bewegungsverfolgungsdaten
  • -
  • Parameteranpassungsmuster und Timing
  • -
  • Geräteorientierung und Betrachtungswinkel-Parameter
  • -
  • Erste Eingabeerkennung und natürliche Gestenanalyse
  • -
  • Optionale demografische Informationen
  • -
+
+

+ Zweck: Diese empirische Studie untersucht optimale Design-Parameter für + deckenbasierte Augmented-Reality-Anwendungen. +

+ +
+

Datenerhebungsprotokoll:

+
    +
  • Gerätespezifikationen (Display-Abmessungen, Browser-Version, Betriebssystem)
  • +
  • WebXR-Interaktionsmetriken und Bewegungsverfolgungsdaten
  • +
  • Parameteranpassungsmuster und Timing
  • +
  • Geräteorientierung und Betrachtungswinkel-Parameter
  • +
  • Erste Eingabeerkennung und natürliche Gestenanalyse
  • +
  • Optionale demografische Informationen
  • +
+
+ +

+ Datenschutz: Alle erhobenen Daten werden anonymisiert und ausschließlich + für wissenschaftliche Analysen verwendet. Es werden keine personenbezogenen Daten erfasst. +

+ +

+ Freiwillige Teilnahme: Die Teilnahme ist vollständig freiwillig. + Sie können jederzeit ohne Konsequenzen zurücktreten. +

-

- Datenschutz: Alle erhobenen Daten werden anonymisiert und ausschließlich - für wissenschaftliche Analysen verwendet. Es werden keine personenbezogenen Daten erfasst. -

- -

- Freiwillige Teilnahme: Die Teilnahme ist vollständig freiwillig. - Sie können jederzeit ohne Konsequenzen zurücktreten. -

+
+ +
-
- +
+
-
- -
diff --git a/src/app/components/consent/consent.component.ts b/src/app/components/consent/consent.component.ts index 08266fa..d686adb 100644 --- a/src/app/components/consent/consent.component.ts +++ b/src/app/components/consent/consent.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; @@ -11,23 +11,38 @@ import { ArLoggerService } from '../../services/ar-logger.service'; templateUrl: './consent.component.html', styleUrls: ['./consent.component.css'] }) -export class ConsentComponent { +export class ConsentComponent implements OnInit { consentGiven = false; + isIosDevice = false; constructor( private router: Router, private logger: ArLoggerService ) {} + ngOnInit(): void { + this.isIosDevice = this.isIOS(); + + if (this.isIosDevice) { + console.log("iOS device detected. Study will be disabled."); + } + } + + isIOS(): boolean { + const win = window as any; + return /iPad|iPhone|iPod/.test(navigator.userAgent) && !win.MSStream; + } + + onConsentChange(event: Event): void { const target = event.target as HTMLInputElement; this.consentGiven = target.checked; } startTestSuite(): void { - if (this.consentGiven) { + if (this.consentGiven && !this.isIosDevice) { this.logger.initializeSession(); this.router.navigate(['/test-suite']); } } -} \ No newline at end of file +} 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 fe56c57..813bb8e 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 @@ -23,7 +23,8 @@ import '../../../../../assets/scripts/model-viewer'; styleUrls: ['./spatial-position-assessment.component.css'], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) -export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDestroy { // <-- Implement OnDestroy +export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDestroy { + @ViewChild('modelViewer') modelViewerRef!: ElementRef; @Output() testComplete = new EventEmitter(); @Output() redoTest = new EventEmitter(); @@ -41,10 +42,10 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest ngAfterViewInit() { const mv = this.modelViewerRef.nativeElement; - this.metricsService.startTracking(mv); mv.addEventListener('ar-status', (e: any) => { if (e.detail.status === 'session-started') { setTimeout(() => this.captureAnchor(), 500); + this.metricsService.startTracking(mv); } }); } 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 c626cc4..95e7268 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 @@ -46,13 +46,11 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDes } ngAfterViewInit() { - const modelViewer = this.modelViewerRef.nativeElement; - - this.metricsService.startTracking(modelViewer); - - modelViewer.addEventListener('ar-status', (event: any) => { + const mv = this.modelViewerRef.nativeElement; + mv.addEventListener('ar-status', (event: any) => { if (event.detail.status === 'session-started' && !this.isModelPlaced) { setTimeout(() => this.getInitialAnchor(), 1000); + this.metricsService.startTracking(mv); } }); } 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 3ffbd12..eeaa943 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 @@ -30,7 +30,6 @@ export class TextLegibilityAssessmentComponent implements AfterViewInit, OnDestr private metricsService = inject(MetricsTrackerService); - // --- Component State (Restored) --- minSize = 2; maxSize = 64; currentSize = 16; @@ -49,7 +48,11 @@ export class TextLegibilityAssessmentComponent implements AfterViewInit, OnDestr ngAfterViewInit() { const mv = this.modelViewerRef.nativeElement; - this.metricsService.startTracking(mv); + mv.addEventListener('ar-status', (event: any) => { + if (event.detail.status === 'session-started') { + this.metricsService.startTracking(mv); + } + }); } public logInteraction(event: Event) { @@ -105,7 +108,7 @@ export class TextLegibilityAssessmentComponent implements AfterViewInit, OnDestr finishAssessment() { this.phase = 'finished'; - + const finalResults = { minReadableSize: this.minSizeResult, maxReadableSize: this.maxSizeResult, diff --git a/src/app/services/metrics-tracker.service.ts b/src/app/services/metrics-tracker.service.ts index 203d025..026eb31 100644 --- a/src/app/services/metrics-tracker.service.ts +++ b/src/app/services/metrics-tracker.service.ts @@ -1,7 +1,9 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { interval, Subscription, tap } from 'rxjs'; +import { v4 as uuidv4 } from 'uuid'; +// --- Data Interfaces --- export interface InteractionEvent { timestamp: number; type: string; @@ -46,6 +48,8 @@ export class MetricsTrackerService { private http = inject(HttpClient); private serverUrl = '/api/log'; + private deviceId: string; + private metricsLog: MetricsLog = { interactions: [], deviceOrientations: [], @@ -55,10 +59,24 @@ export class MetricsTrackerService { private trackingSubscription: Subscription | null = null; private lastDeviceOrientation: DeviceOrientationEvent | null = null; + constructor() { + this.handleDeviceOrientation = this.handleDeviceOrientation.bind(this); + + const storedId = localStorage.getItem('device-uuid'); + if (storedId) { + this.deviceId = storedId; + } else { + this.deviceId = uuidv4(); + localStorage.setItem('device-uuid', this.deviceId); + } + console.log('Device ID for this session:', this.deviceId); + } + private handleDeviceOrientation(event: DeviceOrientationEvent): void { this.lastDeviceOrientation = event; } - // --- Public API --- + + public logInteraction(event: Event): void { if (event.target) { const target = event.target as HTMLElement; @@ -71,21 +89,18 @@ export class MetricsTrackerService { value: (target as any).value ?? undefined }; this.metricsLog.interactions.push(interaction); - } - else if (event instanceof CustomEvent && event.detail) { + } 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 { + } else { console.warn("logInteraction called with an unknown event type:", event); } } - - + public startTracking(modelViewerElement: any): void { if (this.trackingSubscription || !modelViewerElement) return; @@ -112,9 +127,8 @@ export class MetricsTrackerService { }; this.metricsLog.arData.push(arData); - let orientation: DeviceOrientation | null = null; if (this.lastDeviceOrientation) { - orientation = { + const orientation: DeviceOrientation = { timestamp: timestamp, alpha: this.lastDeviceOrientation.alpha, beta: this.lastDeviceOrientation.beta, @@ -128,10 +142,11 @@ export class MetricsTrackerService { public sendMetricsToServer(testName: string, formData?: any) { const payload = { testName, + deviceId: this.deviceId, metricsLog: this.metricsLog, ...(formData && { formData }) }; - console.log(payload) + console.log("Sending final payload:", payload); return this.http.post(this.serverUrl, payload).pipe( tap({ next: (response) => { @@ -149,9 +164,10 @@ export class MetricsTrackerService { } this.trackingSubscription?.unsubscribe(); this.trackingSubscription = null; + this.lastDeviceOrientation = null; } public resetMetrics(): void { this.metricsLog = { interactions: [], deviceOrientations: [], arData: [] }; } -} \ No newline at end of file +}