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.
+
-
-
-
-
-
-
-
- Download Results as CSV
-
-
-
-
-
-
-
-
-
- Start New Assessment
-
-
`,
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.
-
+
+
+
+
+ Ich stimme der Datenerhebung zu und erkläre mich bereit, an dieser Studie teilzunehmen.
+
+
+
-
-
-
-
- Ich stimme der Datenerhebung zu und erkläre mich bereit, an dieser Studie teilzunehmen.
-
-
+
+
+ Umfrage starten
+
-
-
- Umfrage starten
-
-
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
+}