diff --git a/package-lock.json b/package-lock.json index 7355847..1ad9d31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@angular/cli": "^18.0.6", "@angular/compiler-cli": "^18.0.0", "@types/jasmine": "~5.1.0", + "@types/three": "^0.180.0", "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.21", "jasmine-core": "~5.1.0", @@ -36,6 +37,7 @@ "karma-jasmine-html-reporter": "~2.1.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", + "three": "^0.180.0", "typescript": "~5.4.2" } }, @@ -2553,6 +2555,13 @@ "node": ">=0.1.90" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.1.tgz", @@ -4412,6 +4421,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -4629,6 +4645,29 @@ "@types/node": "*" } }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.180.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz", + "integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.22.0" + } + }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -4636,6 +4675,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/webxr": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.23.tgz", + "integrity": "sha512-GPe4AsfOSpqWd3xA/0gwoKod13ChcfV67trvxaW2krUbgb9gxQjnCx8zGshzMl8LSHZlNH5gQ8LNScsDuc7nGQ==", + "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", @@ -4827,6 +4873,13 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webgpu/types": { + "version": "0.1.64", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.64.tgz", + "integrity": "sha512-84kRIAGV46LJTlJZWxShiOrNL30A+9KokD7RB3dRCIqODFjodS5tCD5yyiZ8kIReGVZSDfA3XkkwyyOIF6K62A==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -7193,6 +7246,13 @@ "node": ">=0.8.0" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -9478,6 +9538,13 @@ "node": ">= 8" } }, + "node_modules/meshoptimizer": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", + "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", + "dev": true, + "license": "MIT" + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -13077,6 +13144,13 @@ "tslib": "^2" } }, + "node_modules/three": { + "version": "0.180.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", + "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", + "dev": true, + "license": "MIT" + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", diff --git a/package.json b/package.json index 9e256cf..ca644a8 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@angular/cli": "^18.0.6", "@angular/compiler-cli": "^18.0.0", "@types/jasmine": "~5.1.0", + "@types/three": "^0.180.0", "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.21", "jasmine-core": "~5.1.0", @@ -38,6 +39,7 @@ "karma-jasmine-html-reporter": "~2.1.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", + "three": "^0.180.0", "typescript": "~5.4.2" } } 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 94a81db..99161e9 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 @@ -10,7 +10,7 @@ import { MetricsTrackerService } from '../../../../services/metrics-tracker.serv templateUrl: './demographics-feedback.component.html', styleUrls: ['./demographics-feedback.component.css'] }) -export class DemographicsFeedbackComponent implements OnInit, OnDestroy { +export class DemographicsFeedbackComponent implements OnInit { private metricsService = inject(MetricsTrackerService); @Output() testComplete = new EventEmitter(); @@ -88,7 +88,7 @@ export class DemographicsFeedbackComponent implements OnInit, OnDestroy { submittedAt: new Date().toISOString() }; - this.metricsService.sendMetricsToServer('FullTestSuite', formData).subscribe({ + this.metricsService.sendMetricsToServer(formData).subscribe({ next: () => { this.isSubmitting = false; this.isSubmitted = true; @@ -123,8 +123,4 @@ export class DemographicsFeedbackComponent implements OnInit, OnDestroy { return score * 2.5; } - - ngOnDestroy() { - this.metricsService.resetMetrics(); - } } 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 577044c..b0c1db4 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 @@ -48,7 +48,7 @@ export class SpatialPositionAssessmentComponent implements AfterViewInit, OnDest mv.addEventListener('ar-status', (e: any) => { if (e.detail.status === 'session-started') { setTimeout(() => this.captureAnchor(), 500); - this.metricsService.startTracking(mv); + this.metricsService.startTracking(mv, "position"); } }); } 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 760ea34..638d0d5 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 @@ -49,8 +49,8 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDes 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); + setTimeout(() => this.getInitialAnchor(), 500); + this.metricsService.startTracking(mv, "stability"); } }); } @@ -155,7 +155,6 @@ export class SpatialStabilityAssessmentComponent implements AfterViewInit, OnDes clearInterval(this.countdownInterval); this.countdownInterval = null; } - this.metricsService.resetMetrics(); } finishAssessment() { 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 0d7c0f0..48a748c 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 @@ -58,7 +58,8 @@ export class TextLegibilityAssessmentComponent implements AfterViewInit, OnDestr const mv = this.modelViewerRef.nativeElement; mv.addEventListener('ar-status', (event: any) => { if (event.detail.status === 'session-started') { - this.metricsService.startTracking(mv); + console.log('AR session started. Delaying startTracking call to ensure session is ready.'); + this.metricsService.startTracking(mv, "text-legibility"); } }); } diff --git a/src/app/services/metrics-tracker.service.ts b/src/app/services/metrics-tracker.service.ts index 7a8a943..ce7ee2c 100644 --- a/src/app/services/metrics-tracker.service.ts +++ b/src/app/services/metrics-tracker.service.ts @@ -1,257 +1,236 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; -import { tap } from 'rxjs'; +import { interval, Subscription, tap } from 'rxjs'; import { v4 as uuidv4 } from 'uuid'; - +import * as THREE from 'three'; export interface InteractionEvent { - timestamp: number; - type: string; - elementId?: string; - elementTag?: string; - elementClasses?: string; - value?: any; + 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; + timestamp: number; + alpha: number | null; + beta: number | null; + gamma: number | null; } - export interface ArTrackingData { - timestamp: number; - anchor: string | null; - cameraPosition: { x: number; y: number; z: number; w?: number } | null; - cameraOrientation: { x: number; y: number; z: number; w: number } | null; - cameraEuler: { pitch: number; yaw: number; roll: number } | null; - cameraOrbit: { theta: number; phi: number; radius: number } | null; - cameraTarget: { x: number; y: number; z: number } | null; + timestamp: number; + modelIsVisible: boolean | null; + modelWorldPosition: { x: number; y: number; z: number } | null; + modelInitialScale: { x: number; y: number; z: number } | null; + cameraFov: number | null; + cameraAspect: number | null; + viewerPosition?: { x: number; y: number; z: number; w: number } | null; + viewerOrientation?: { x: number; y: number; z: number; w: number } | null; } +export interface DeviceInformation { + screenWidth: number; + screenHeight: number; + devicePixelRatio: number; + userAgent: string; +} export interface MetricsLog { - interactions: InteractionEvent[]; - deviceOrientations: DeviceOrientation[]; - arData: ArTrackingData[]; + testName: string; + interactions: InteractionEvent[]; + deviceOrientations: DeviceOrientation[]; + arTrackingData: ArTrackingData[]; } - @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class MetricsTrackerService { - private http = inject(HttpClient); - private serverUrl = '/api/log'; - private deviceId: string; + private http = inject(HttpClient); + private serverUrl = '/api/log'; + private deviceId: string; + private allTestLogs: MetricsLog[] = []; + private deviceInfo: DeviceInformation | null = null; + private activeLog: MetricsLog | null = null; - private metricsLog: MetricsLog = { - interactions: [], - deviceOrientations: [], - arData: [] - }; + private modelViewerElement: any = null; + private trackingSubscription: Subscription | null = null; + private lastDeviceOrientation: DeviceOrientationEvent | null = null; + constructor() { + this.handleDeviceOrientation = this.handleDeviceOrientation.bind(this); - private xrFrameSubscriptionId: number | null = null; - private lastDeviceOrientation: DeviceOrientationEvent | null = null; - private modelViewerElement: any = null; - private xrFrameCount = 0; - - - constructor() { - this.handleDeviceOrientation = this.handleDeviceOrientation.bind(this); - this.onXrFrame = this.onXrFrame.bind(this); - - - const storedId = localStorage.getItem('device-uuid'); - this.deviceId = storedId || uuidv4(); - if (!storedId) { - localStorage.setItem('device-uuid', this.deviceId); - } - - - if (typeof window !== 'undefined') { - window.addEventListener('deviceorientation', this.handleDeviceOrientation, true); - } + const storedId = localStorage.getItem('device-uuid'); + this.deviceId = storedId || uuidv4(); + if (!storedId) { + localStorage.setItem('device-uuid', this.deviceId); } - - private handleDeviceOrientation(event: DeviceOrientationEvent): void { - this.lastDeviceOrientation = event; + if (typeof window !== 'undefined') { + window.addEventListener('deviceorientation', this.handleDeviceOrientation, true); } + } + private handleDeviceOrientation(event: DeviceOrientationEvent): void { + this.lastDeviceOrientation = event; + } - public logInteraction(event: Event): void { - const timestamp = Date.now(); - const target = event.target as HTMLElement; - let interaction: InteractionEvent; + public logInteraction(event: Event): void { + if (!this.activeLog) return; // Don't log if no test is active + const target = event.target as HTMLElement; + let interaction: InteractionEvent; - if (target) { - // Standard DOM event with a target - interaction = { - timestamp, - type: event.type, - elementId: target.id || undefined, - elementTag: target.tagName, - elementClasses: target.className, - value: (target as any).value ?? undefined - }; - } else if (event instanceof CustomEvent) { - // CustomEvent, likely without a target - interaction = { - timestamp, - type: event.type, - value: event.detail - }; - } else { - // Fallback for other events without a target - interaction = { - timestamp, - type: event.type - }; - } - - this.metricsLog.interactions.push(interaction); + if (target) { + interaction = { timestamp: Date.now(), type: event.type, elementId: target.id, elementTag: target.tagName, elementClasses: target.className, value: (target as any).value ?? undefined }; + } else if (event instanceof CustomEvent) { + interaction = { timestamp: Date.now(), type: event.type, value: event.detail }; + } else { + console.warn("logInteraction called with an unknown event type:", event); + return; } + this.activeLog.interactions.push(interaction); + } + + private getInternalThreeObjects(modelViewerElement: any): { scene: any; renderer: any } { + let scene = null; + let renderer = null; - - public startTracking(modelViewerElement: any): void { - this.stopTracking(); - if (!modelViewerElement) { - console.error("startTracking called with no modelViewerElement."); - return; - } - this.modelViewerElement = modelViewerElement; - - - if (this.modelViewerElement.xrSession) { - this.startXrTracking(this.modelViewerElement.xrSession); - } - } - - - private startXrTracking(xrSession: any): void { - if (this.xrFrameSubscriptionId === null) { - this.xrFrameCount = 0; - this.xrFrameSubscriptionId = xrSession.requestAnimationFrame(this.onXrFrame); - } - } - - - private onXrFrame(time: DOMHighResTimeStamp, frame: any): void { - const session = frame.session; - this.xrFrameSubscriptionId = session.requestAnimationFrame(this.onXrFrame); - - - this.xrFrameCount++; - if (this.xrFrameCount % 30 !== 0) { - return; - } - - - const referenceSpace = this.modelViewerElement?.renderer.xr.getReferenceSpace(); - if (!referenceSpace) return; - - const viewerPose = frame.getViewerPose(referenceSpace); - const timestamp = Date.now(); - let position = null, orientation = null, euler = null; - - - if (viewerPose) { - const { transform } = viewerPose; - position = { x: transform.position.x, y: transform.position.y, z: transform.position.z, w: transform.position.w }; - orientation = { x: transform.orientation.x, y: transform.orientation.y, z: transform.orientation.z, w: transform.orientation.w }; - euler = this.quaternionToEuler(orientation.x, orientation.y, orientation.z, orientation.w); - } - - - const anchorRaw = this.modelViewerElement.getAnchor ? this.modelViewerElement.getAnchor() : null; - const anchor = (typeof anchorRaw === 'string' && anchorRaw.includes('not placed')) ? null : anchorRaw; - const orbit = this.modelViewerElement.getCameraOrbit ? this.modelViewerElement.getCameraOrbit() : null; - const target = this.modelViewerElement.getCameraTarget ? this.modelViewerElement.getCameraTarget() : null; - - - const arData: ArTrackingData = { - timestamp, - anchor, - cameraPosition: position, - cameraOrientation: orientation, - cameraEuler: euler, - 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); - - - if (this.lastDeviceOrientation) { - this.metricsLog.deviceOrientations.push({ - timestamp, - alpha: this.lastDeviceOrientation.alpha, - beta: this.lastDeviceOrientation.beta, - gamma: this.lastDeviceOrientation.gamma - }); - } - } - - - public stopTracking(): void { - if (this.xrFrameSubscriptionId !== null) { - const xrSession = this.modelViewerElement?.xrSession; - if (xrSession) { - xrSession.cancelAnimationFrame(this.xrFrameSubscriptionId); + for (let p = modelViewerElement; p != null; p = Object.getPrototypeOf(p)) { + const privateAPI = Object.getOwnPropertySymbols(p); + const rendererSym = privateAPI.find((s) => s.toString() === 'Symbol(renderer)'); + const sceneSym = privateAPI.find((s) => s.toString() === 'Symbol(scene)'); + + if (rendererSym && modelViewerElement[rendererSym]) { + renderer = modelViewerElement[rendererSym].threeRenderer; + } + if (sceneSym && modelViewerElement[sceneSym]) { + scene = modelViewerElement[sceneSym]; + } + if (renderer && scene) { + break; } - this.xrFrameSubscriptionId = null; } - this.modelViewerElement = null; + return { scene, renderer }; } + public startTracking(modelViewerElement: any, testName: string): void { + this.stopTracking(); - public sendMetricsToServer(testName: string, formData?: any) { - const payload = { + if (!modelViewerElement) { + console.error("startTracking called with no modelViewerElement."); + return; + } + this.modelViewerElement = modelViewerElement; + + const newLog: MetricsLog = { testName, + interactions: [], + deviceOrientations: [], + arTrackingData: [] + }; + this.allTestLogs.push(newLog); + this.activeLog = newLog; + + if (!this.deviceInfo) { + this.deviceInfo = { + screenWidth: window.screen.width, + screenHeight: window.screen.height, + devicePixelRatio: window.devicePixelRatio, + userAgent: navigator.userAgent + }; + } + + const { scene, renderer } = this.getInternalThreeObjects(this.modelViewerElement); + + if (!scene || !renderer) { + console.error("Could not access internal Three.js scene or renderer. Tracking cannot start."); + return; + } + + console.log(`Starting tracking for test: ${testName}`); + this.trackingSubscription = interval(500).subscribe(async () => { + if (!this.activeLog) return; + + const timestamp = Date.now(); + const model = scene?._model; + const camera = scene?.camera; + const session = renderer?.xr?.getSession(); + + let modelWorldPosition = null; + if (model) { + model.updateWorldMatrix(true, false); + modelWorldPosition = new THREE.Vector3(); + modelWorldPosition.setFromMatrixPosition(model.matrixWorld); + } + + const arData: ArTrackingData = { + timestamp, + modelIsVisible: model ? model.visible : null, + modelWorldPosition: modelWorldPosition ? { ...modelWorldPosition } : null, + modelInitialScale: model ? { ...model.scale } : null, + cameraFov: camera ? camera.fov : null, + cameraAspect: camera ? camera.aspect : null + }; + + if (session) { + const frame = await new Promise(resolve => session.requestAnimationFrame((time: any, frame: any) => resolve(frame))); + const referenceSpace = renderer.xr.getReferenceSpace(); + if (frame && referenceSpace) { + const viewerPose = frame.getViewerPose(referenceSpace); + if (viewerPose) { + const { transform } = viewerPose; + arData.viewerPosition = { x: transform.position.x, y: transform.position.y, z: transform.position.z, w: transform.position.w }; + arData.viewerOrientation = { x: transform.orientation.x, y: transform.orientation.y, z: transform.orientation.z, w: transform.orientation.w }; + } + } + } + + this.activeLog.arTrackingData.push(arData); + + if (this.lastDeviceOrientation) { + const orientationData: DeviceOrientation = { timestamp, alpha: this.lastDeviceOrientation.alpha, beta: this.lastDeviceOrientation.beta, gamma: this.lastDeviceOrientation.gamma }; + this.activeLog.deviceOrientations.push(orientationData); + } + }); + } + + public stopTracking(): void { + if (this.trackingSubscription) { + this.trackingSubscription.unsubscribe(); + this.trackingSubscription = null; + } + this.modelViewerElement = null; + this.activeLog = null; + } + + public sendMetricsToServer(formData?: any) { + const payload = { deviceId: this.deviceId, - metricsLog: this.metricsLog, + deviceInfo: this.deviceInfo, + testLogs: this.allTestLogs, // Send the entire collection of logs ...(formData && { formData }) }; - - this.stopTracking(); - return this.http.post(this.serverUrl, payload).pipe( - tap({ - next: () => this.resetMetrics(), - error: (err) => console.error(`Failed to send metrics for '${testName}':`, err) - }) - ); - } - - - public resetMetrics(): void { - this.metricsLog = { interactions: [], deviceOrientations: [], arData: [] }; - } - private quaternionToEuler(x: number, y: number, z: number, w: number): { pitch: number, yaw: number, roll: number } { - const sinr_cosp = 2 * (w * x + y * z); - const cosr_cosp = 1 - 2 * (x * x + y * y); - const roll = Math.atan2(sinr_cosp, cosr_cosp); + this.stopTracking(); + return this.http.post(this.serverUrl, payload).pipe( + tap({ + next: () => { + console.log(`Metrics for session sent successfully.`); + this.resetAllMetrics(); + }, + error: (err) => console.error(`Failed to send metrics:`, err) + }) + ); + } - - const sinp = 2 * (w * y - z * x); - const pitch = Math.abs(sinp) >= 1 ? (Math.PI / 2) * Math.sign(sinp) : Math.asin(sinp); - - - const siny_cosp = 2 * (w * z + x * y); - const cosy_cosp = 1 - 2 * (y * y + z * z); - const yaw = Math.atan2(siny_cosp, cosy_cosp); - - - return { - pitch: pitch * (180 / Math.PI), - yaw: yaw * (180 / Math.PI), - roll: roll * (180 / Math.PI) - }; - } + public resetAllMetrics(): void { + this.allTestLogs = []; + this.deviceInfo = null; + this.activeLog = null; + } }