// Data Export Utility - Export sensor/pose data as JSON or CSV import { sensingService } from '../services/sensing.service.js'; import { toastManager } from './toast.js'; export class DataExport { constructor() { this.buffer = []; this.maxBuffer = 1000; this.recording = false; this._unsub = null; } init() { document.addEventListener('export-data', () => this.showExportDialog()); // Continuously buffer sensing data when available this._unsub = sensingService.onData((data) => { if (this.buffer.length >= this.maxBuffer) { this.buffer.shift(); } this.buffer.push({ timestamp: new Date().toISOString(), ...this.extractFields(data) }); }); } extractFields(data) { // Extract relevant fields from sensing data return { rssi: data.rssi ?? null, variance: data.variance ?? null, motion_band: data.motion_band ?? null, breathing_band: data.breathing_band ?? null, classification: data.classification ?? null, person_count: data.person_count ?? data.persons ?? null, subcarriers: data.subcarrier_count ?? null, source: data.source ?? null }; } showExportDialog() { if (this.buffer.length === 0) { toastManager.warning('No sensor data to export. Connect to a data source first.'); return; } // Create dialog const overlay = document.createElement('div'); overlay.className = 'export-dialog-overlay'; overlay.innerHTML = ` `; overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); overlay.querySelector('.export-cancel').addEventListener('click', () => overlay.remove()); overlay.querySelector('.export-confirm').addEventListener('click', () => { const format = overlay.querySelector('input[name="export-format"]:checked').value; const count = parseInt(overlay.querySelector('#export-count').value, 10) || this.buffer.length; this.exportData(format, count); overlay.remove(); }); document.body.appendChild(overlay); overlay.querySelector('.export-confirm').focus(); } exportData(format, count) { const data = this.buffer.slice(-count); let content, filename, mimeType; if (format === 'json') { content = JSON.stringify(data, null, 2); filename = `ruview-data-${this.timestamp()}.json`; mimeType = 'application/json'; } else { content = this.toCSV(data); filename = `ruview-data-${this.timestamp()}.csv`; mimeType = 'text/csv'; } this.downloadFile(content, filename, mimeType); toastManager.success(`Exported ${data.length} data points as ${format.toUpperCase()}`); } toCSV(data) { if (data.length === 0) return ''; const headers = Object.keys(data[0]); const rows = data.map(row => headers.map(h => { const val = row[h]; if (val === null || val === undefined) return ''; if (typeof val === 'string' && (val.includes(',') || val.includes('"'))) { return `"${val.replace(/"/g, '""')}"`; } return String(val); }).join(',')); return [headers.join(','), ...rows].join('\n'); } downloadFile(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } timestamp() { return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); } dispose() { if (this._unsub) this._unsub(); } }