polishing

master
MrPlatnum 2025-09-14 15:28:45 +02:00
parent 95d92cc4d9
commit 5b1daac97b
9 changed files with 170 additions and 222 deletions

32
package-lock.json generated
View File

@ -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": {

View File

@ -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",

View File

@ -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: `
<div class="max-w-4xl mx-auto p-5 bg-white rounded-lg shadow-lg mt-8">
<div class="text-center mb-8">
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<h2 class="text-3xl font-bold text-gray-800 mb-2">Assessment Suite Completed Successfully</h2>
<p class="text-gray-600 max-w-2xl mx-auto">
Thank you for your participation in this research study. Your data has been successfully
collected and is ready for download.
</p>
<div class="max-w-2xl mx-auto p-8 bg-white rounded-lg shadow-lg mt-10 text-center">
<div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-5">
<svg class="w-12 h-12 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<!-- Results Summary -->
<div class="bg-gray-50 rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Session Summary</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="text-center">
<div class="text-2xl font-bold text-blue-600">{{completedTests}}</div>
<div class="text-sm text-gray-600">Tests Completed</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-600">{{totalInteractions}}</div>
<div class="text-sm text-gray-600">Total Interactions</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-purple-600">{{sessionDuration}}</div>
<div class="text-sm text-gray-600">Session Duration</div>
</div>
</div>
</div>
<h2 class="text-3xl font-bold text-gray-900 mb-3">
Umfrage abgeschlossen
</h2>
<p class="text-gray-600 text-lg max-w-md mx-auto">
Ihre Teilnahme war erfolgreich. Vielen Dank für Ihren Beitrag zu unserer Forschung.
</p>
<!-- Download Section -->
<div class="text-center mb-6">
<button
(click)="downloadResults()"
class="inline-flex items-center px-6 py-3 bg-blue-600 text-white font-medium
rounded-lg hover:bg-blue-700 transition-colors shadow-lg">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
Download Results as CSV
</button>
</div>
<!-- Data Preview -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2">Data Preview:</h4>
<pre class="text-xs text-gray-600 font-mono overflow-auto max-h-80"
[innerHTML]="dataPreview">
</pre>
</div>
<!-- Navigation -->
<div class="text-center mt-8">
<button
(click)="restartAssessment()"
class="px-4 py-2 text-blue-600 border border-blue-600 rounded-lg
hover:bg-blue-50 transition-colors mr-4">
Start New Assessment
</button>
</div>
</div>
`,
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']);
}
}
export class CompletionComponent {
constructor() { }
}

View File

@ -1,62 +1,75 @@
<div class="max-w-4xl mx-auto p-5 bg-white rounded-lg shadow-lg mt-8">
<div class="bg-yellow-50 border border-yellow-200 p-6 rounded-lg mb-6">
<h2 class="text-2xl font-semibold text-gray-800 mb-4">
Einverständniserklärung
</h2>
<div class="space-y-4 text-gray-700">
<p>
<strong>Zweck:</strong> Diese empirische Studie untersucht optimale Design-Parameter für
deckenbasierte Augmented-Reality-Anwendungen.
</p>
<div class="max-w-4xl mx-auto p-5 mt-8">
<!-- iOS Incompatibility Message -->
<div *ngIf="isIosDevice" class="bg-red-100 border-l-4 border-red-500 text-red-700 p-6 rounded-lg shadow-lg text-center">
<h2 class="text-xl font-bold mb-2">Inkompatibles Gerät</h2>
<p class="text-base">
Danke, dass Sie an dieser Studie teilnehmen wollen. Leider ist die Studie nur für Android-Geräte verfügbar.
</p>
</div>
<!-- Standard Consent Form (shown only on non-iOS devices) -->
<div *ngIf="!isIosDevice" class="bg-white rounded-lg shadow-lg">
<div class="bg-yellow-50 border border-yellow-200 p-6 rounded-lg mb-6">
<h2 class="text-2xl font-semibold text-gray-800 mb-4">
Einverständniserklärung
</h2>
<div>
<p class="font-semibold mb-2">Datenerhebungsprotokoll:</p>
<ul class="list-disc list-inside space-y-1 ml-4">
<li>Gerätespezifikationen (Display-Abmessungen, Browser-Version, Betriebssystem)</li>
<li>WebXR-Interaktionsmetriken und Bewegungsverfolgungsdaten</li>
<li>Parameteranpassungsmuster und Timing</li>
<li>Geräteorientierung und Betrachtungswinkel-Parameter</li>
<li>Erste Eingabeerkennung und natürliche Gestenanalyse</li>
<li>Optionale demografische Informationen</li>
</ul>
<div class="space-y-4 text-gray-700">
<p>
<strong>Zweck:</strong> Diese empirische Studie untersucht optimale Design-Parameter für
deckenbasierte Augmented-Reality-Anwendungen.
</p>
<div>
<p class="font-semibold mb-2">Datenerhebungsprotokoll:</p>
<ul class="list-disc list-inside space-y-1 ml-4">
<li>Gerätespezifikationen (Display-Abmessungen, Browser-Version, Betriebssystem)</li>
<li>WebXR-Interaktionsmetriken und Bewegungsverfolgungsdaten</li>
<li>Parameteranpassungsmuster und Timing</li>
<li>Geräteorientierung und Betrachtungswinkel-Parameter</li>
<li>Erste Eingabeerkennung und natürliche Gestenanalyse</li>
<li>Optionale demografische Informationen</li>
</ul>
</div>
<p>
<strong>Datenschutz:</strong> Alle erhobenen Daten werden anonymisiert und ausschließlich
für wissenschaftliche Analysen verwendet. Es werden keine personenbezogenen Daten erfasst.
</p>
<p>
<strong>Freiwillige Teilnahme:</strong> Die Teilnahme ist vollständig freiwillig.
Sie können jederzeit ohne Konsequenzen zurücktreten.
</p>
</div>
<p>
<strong>Datenschutz:</strong> Alle erhobenen Daten werden anonymisiert und ausschließlich
für wissenschaftliche Analysen verwendet. Es werden keine personenbezogenen Daten erfasst.
</p>
<p>
<strong>Freiwillige Teilnahme:</strong> Die Teilnahme ist vollständig freiwillig.
Sie können jederzeit ohne Konsequenzen zurücktreten.
</p>
<div class="mt-6">
<label class="flex items-start space-x-3 cursor-pointer">
<input
type="checkbox"
class="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
[checked]="consentGiven"
(change)="onConsentChange($event)"
>
<span class="text-gray-700">
Ich stimme der Datenerhebung zu und erkläre mich bereit, an dieser Studie teilzunehmen.
</span>
</label>
</div>
</div>
<div class="mt-6">
<label class="flex items-start space-x-3 cursor-pointer">
<input
type="checkbox"
class="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
[checked]="consentGiven"
(change)="onConsentChange($event)"
>
<span class="text-gray-700">
Ich stimme der Datenerhebung zu und erkläre mich bereit, an dieser Studie teilzunehmen.
</span>
</label>
<div class="text-center">
<button
(click)="startTestSuite()"
[disabled]="!consentGiven"
class="px-7 py-3 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700
disabled:bg-gray-400 disabled:cursor-not-allowed transition-all duration-200
transform hover:scale-105 disabled:hover:scale-100"
>
Umfrage starten
</button>
</div>
</div>
<div class="text-center">
<button
(click)="startTestSuite()"
[disabled]="!consentGiven"
class="px-7 py-3 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700
disabled:bg-gray-400 disabled:cursor-not-allowed transition-all duration-200
transform hover:scale-105 disabled:hover:scale-100"
>
Umfrage starten
</button>
</div>
</div>

View File

@ -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']);
}
}
}
}

View File

@ -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<any>;
@Output() testComplete = new EventEmitter<void>();
@Output() redoTest = new EventEmitter<number>();
@ -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);
}
});
}

View File

@ -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);
}
});
}

View File

@ -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,

View File

@ -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: [] };
}
}
}