// SettingsPanel Component for WiFi-DensePose UI
import { poseService } from '../services/pose.service.js';
import { wsService } from '../services/websocket.service.js';
export class SettingsPanel {
constructor(containerId, options = {}) {
this.containerId = containerId;
this.container = document.getElementById(containerId);
if (!this.container) {
throw new Error(`Container with ID '${containerId}' not found`);
}
this.config = {
enableAdvancedSettings: true,
enableDebugControls: true,
enableExportFeatures: true,
allowConfigPersistence: true,
...options
};
this.settings = {
// Connection settings
zones: ['zone_1', 'zone_2', 'zone_3'],
currentZone: 'zone_1',
autoReconnect: true,
connectionTimeout: 10000,
// Pose detection settings
confidenceThreshold: 0.3,
keypointConfidenceThreshold: 0.1,
maxPersons: 10,
maxFps: 30,
// Rendering settings
renderMode: 'skeleton',
showKeypoints: true,
showSkeleton: true,
showBoundingBox: false,
showConfidence: true,
showZones: true,
showDebugInfo: false,
// Colors
skeletonColor: '#00ff00',
keypointColor: '#ff0000',
boundingBoxColor: '#0000ff',
// Performance settings
enableValidation: true,
enablePerformanceTracking: true,
enableDebugLogging: false,
// Advanced settings
heartbeatInterval: 30000,
maxReconnectAttempts: 10,
enableSmoothing: true
};
this.callbacks = {
onSettingsChange: null,
onZoneChange: null,
onRenderModeChange: null,
onExport: null,
onImport: null
};
this.logger = this.createLogger();
// Initialize component
this.initializeComponent();
}
createLogger() {
return {
debug: (...args) => console.debug('[SETTINGS-DEBUG]', new Date().toISOString(), ...args),
info: (...args) => console.info('[SETTINGS-INFO]', new Date().toISOString(), ...args),
warn: (...args) => console.warn('[SETTINGS-WARN]', new Date().toISOString(), ...args),
error: (...args) => console.error('[SETTINGS-ERROR]', new Date().toISOString(), ...args)
};
}
initializeComponent() {
this.logger.info('Initializing SettingsPanel component', { containerId: this.containerId });
// Load saved settings
this.loadSettings();
// Create DOM structure
this.createDOMStructure();
// Set up event handlers
this.setupEventHandlers();
// Update UI with current settings
this.updateUI();
this.logger.info('SettingsPanel component initialized successfully');
}
createDOMStructure() {
this.container.innerHTML = `
`;
this.addSettingsStyles();
}
addSettingsStyles() {
const style = document.createElement('style');
style.textContent = `
.settings-panel {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
font-family: Arial, sans-serif;
overflow: hidden;
}
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: #f8f9fa;
border-bottom: 1px solid #ddd;
}
.settings-header h3 {
margin: 0;
color: #333;
font-size: 16px;
font-weight: 600;
}
.settings-actions {
display: flex;
gap: 8px;
}
.settings-content {
padding: 20px;
max-height: 400px;
overflow-y: auto;
}
.settings-section {
margin-bottom: 25px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.settings-section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.settings-section h4 {
margin: 0 0 15px 0;
color: #555;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
gap: 10px;
}
.setting-row label {
flex: 1;
color: #666;
font-size: 13px;
font-weight: 500;
}
.setting-input, .setting-select {
flex: 0 0 120px;
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
}
.setting-range {
flex: 0 0 100px;
margin-right: 8px;
}
.setting-value {
flex: 0 0 40px;
font-size: 12px;
color: #666;
text-align: center;
background: #f8f9fa;
padding: 2px 6px;
border-radius: 3px;
border: 1px solid #ddd;
}
.setting-checkbox {
flex: 0 0 auto;
width: 18px;
height: 18px;
}
.setting-color {
flex: 0 0 50px;
height: 30px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.btn {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.btn:hover {
background: #f8f9fa;
border-color: #adb5bd;
}
.btn-sm {
padding: 4px 8px;
font-size: 11px;
}
.settings-toggle {
text-align: center;
padding-top: 15px;
border-top: 1px solid #eee;
}
.settings-footer {
padding: 10px 20px;
background: #f8f9fa;
border-top: 1px solid #ddd;
text-align: center;
}
.settings-status {
font-size: 12px;
color: #666;
}
.advanced-section {
background: #f9f9f9;
margin: 0 -20px 25px -20px;
padding: 20px;
border: none;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
.advanced-section h4 {
color: #dc3545;
}
`;
if (!document.querySelector('#settings-panel-styles')) {
style.id = 'settings-panel-styles';
document.head.appendChild(style);
}
}
setupEventHandlers() {
// Reset button
const resetBtn = document.getElementById(`reset-settings-${this.containerId}`);
resetBtn?.addEventListener('click', () => this.resetSettings());
// Export button
const exportBtn = document.getElementById(`export-settings-${this.containerId}`);
exportBtn?.addEventListener('click', () => this.exportSettings());
// Import button and file input
const importBtn = document.getElementById(`import-settings-${this.containerId}`);
const importFile = document.getElementById(`import-file-${this.containerId}`);
importBtn?.addEventListener('click', () => importFile.click());
importFile?.addEventListener('change', (e) => this.importSettings(e));
// Advanced toggle
const advancedToggle = document.getElementById(`toggle-advanced-${this.containerId}`);
advancedToggle?.addEventListener('click', () => this.toggleAdvanced());
// Setting change handlers
this.setupSettingChangeHandlers();
this.logger.debug('Event handlers set up');
}
setupSettingChangeHandlers() {
// Zone selector
const zoneSelect = document.getElementById(`zone-select-${this.containerId}`);
zoneSelect?.addEventListener('change', (e) => {
this.updateSetting('currentZone', e.target.value);
this.notifyCallback('onZoneChange', e.target.value);
});
// Render mode
const renderModeSelect = document.getElementById(`render-mode-${this.containerId}`);
renderModeSelect?.addEventListener('change', (e) => {
this.updateSetting('renderMode', e.target.value);
this.notifyCallback('onRenderModeChange', e.target.value);
});
// Range inputs with value display
const rangeInputs = ['confidence-threshold', 'keypoint-confidence'];
rangeInputs.forEach(id => {
const input = document.getElementById(`${id}-${this.containerId}`);
const valueSpan = document.getElementById(`${id}-value-${this.containerId}`);
input?.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
valueSpan.textContent = value.toFixed(1);
const settingKey = id.replace('-', '_').replace('_threshold', 'Threshold').replace('_confidence', 'ConfidenceThreshold');
this.updateSetting(settingKey, value);
});
});
// Checkbox inputs
const checkboxes = [
'auto-reconnect', 'show-keypoints', 'show-skeleton', 'show-bounding-box',
'show-confidence', 'show-zones', 'show-debug-info', 'enable-validation',
'enable-performance-tracking', 'enable-debug-logging', 'enable-smoothing'
];
checkboxes.forEach(id => {
const input = document.getElementById(`${id}-${this.containerId}`);
input?.addEventListener('change', (e) => {
const settingKey = this.camelCase(id);
this.updateSetting(settingKey, e.target.checked);
});
});
// Number inputs
const numberInputs = [
'connection-timeout', 'max-persons', 'max-fps',
'heartbeat-interval', 'max-reconnect-attempts'
];
numberInputs.forEach(id => {
const input = document.getElementById(`${id}-${this.containerId}`);
input?.addEventListener('change', (e) => {
const settingKey = this.camelCase(id);
this.updateSetting(settingKey, parseInt(e.target.value));
});
});
// Color inputs
const colorInputs = ['skeleton-color', 'keypoint-color', 'bounding-box-color'];
colorInputs.forEach(id => {
const input = document.getElementById(`${id}-${this.containerId}`);
input?.addEventListener('change', (e) => {
const settingKey = this.camelCase(id);
this.updateSetting(settingKey, e.target.value);
});
});
}
camelCase(str) {
return str.replace(/-./g, match => match.charAt(1).toUpperCase());
}
updateSetting(key, value) {
this.settings[key] = value;
this.saveSettings();
this.notifyCallback('onSettingsChange', { key, value, settings: this.settings });
this.updateStatus(`Updated ${key}`);
this.logger.debug('Setting updated', { key, value });
}
updateUI() {
// Update all form elements with current settings
Object.entries(this.settings).forEach(([key, value]) => {
this.updateUIElement(key, value);
});
}
updateUIElement(key, value) {
const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
// Handle special cases
const elementId = `${kebabKey}-${this.containerId}`;
const element = document.getElementById(elementId);
if (!element) return;
switch (element.type) {
case 'checkbox':
element.checked = value;
break;
case 'range':
element.value = value;
// Update value display
const valueSpan = document.getElementById(`${kebabKey}-value-${this.containerId}`);
if (valueSpan) valueSpan.textContent = value.toFixed(1);
break;
case 'color':
element.value = value;
break;
default:
element.value = value;
}
}
toggleAdvanced() {
const advancedSection = document.getElementById(`advanced-section-${this.containerId}`);
const toggleBtn = document.getElementById(`toggle-advanced-${this.containerId}`);
const isVisible = advancedSection.style.display !== 'none';
advancedSection.style.display = isVisible ? 'none' : 'block';
toggleBtn.textContent = isVisible ? 'Show Advanced' : 'Hide Advanced';
this.logger.debug('Advanced settings toggled', { visible: !isVisible });
}
resetSettings() {
if (confirm('Reset all settings to defaults? This cannot be undone.')) {
this.settings = this.getDefaultSettings();
this.updateUI();
this.saveSettings();
this.notifyCallback('onSettingsChange', { reset: true, settings: this.settings });
this.updateStatus('Settings reset to defaults');
this.logger.info('Settings reset to defaults');
}
}
exportSettings() {
const data = {
timestamp: new Date().toISOString(),
version: '1.0',
settings: this.settings
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `pose-detection-settings-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
this.updateStatus('Settings exported');
this.notifyCallback('onExport', data);
this.logger.info('Settings exported');
}
importSettings(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (data.settings) {
this.settings = { ...this.getDefaultSettings(), ...data.settings };
this.updateUI();
this.saveSettings();
this.notifyCallback('onSettingsChange', { imported: true, settings: this.settings });
this.notifyCallback('onImport', data);
this.updateStatus('Settings imported successfully');
this.logger.info('Settings imported successfully');
} else {
throw new Error('Invalid settings file format');
}
} catch (error) {
this.updateStatus('Error importing settings');
this.logger.error('Error importing settings', { error: error.message });
alert('Error importing settings: ' + error.message);
}
};
reader.readAsText(file);
event.target.value = ''; // Reset file input
}
saveSettings() {
if (this.config.allowConfigPersistence) {
try {
localStorage.setItem(`pose-settings-${this.containerId}`, JSON.stringify(this.settings));
} catch (error) {
this.logger.warn('Failed to save settings to localStorage', { error: error.message });
}
}
}
loadSettings() {
if (this.config.allowConfigPersistence) {
try {
const saved = localStorage.getItem(`pose-settings-${this.containerId}`);
if (saved) {
this.settings = { ...this.getDefaultSettings(), ...JSON.parse(saved) };
this.logger.debug('Settings loaded from localStorage');
}
} catch (error) {
this.logger.warn('Failed to load settings from localStorage', { error: error.message });
}
}
}
getDefaultSettings() {
return {
zones: ['zone_1', 'zone_2', 'zone_3'],
currentZone: 'zone_1',
autoReconnect: true,
connectionTimeout: 10000,
confidenceThreshold: 0.3,
keypointConfidenceThreshold: 0.1,
maxPersons: 10,
maxFps: 30,
renderMode: 'skeleton',
showKeypoints: true,
showSkeleton: true,
showBoundingBox: false,
showConfidence: true,
showZones: true,
showDebugInfo: false,
skeletonColor: '#00ff00',
keypointColor: '#ff0000',
boundingBoxColor: '#0000ff',
enableValidation: true,
enablePerformanceTracking: true,
enableDebugLogging: false,
heartbeatInterval: 30000,
maxReconnectAttempts: 10,
enableSmoothing: true
};
}
updateStatus(message) {
const statusElement = document.getElementById(`settings-status-${this.containerId}`);
if (statusElement) {
statusElement.textContent = message;
// Clear status after 3 seconds
setTimeout(() => {
statusElement.textContent = 'Settings ready';
}, 3000);
}
}
// Public API methods
getSettings() {
return { ...this.settings };
}
setSetting(key, value) {
this.updateSetting(key, value);
}
setCallback(eventName, callback) {
if (eventName in this.callbacks) {
this.callbacks[eventName] = callback;
}
}
notifyCallback(eventName, data) {
if (this.callbacks[eventName]) {
try {
this.callbacks[eventName](data);
} catch (error) {
this.logger.error('Callback error', { eventName, error: error.message });
}
}
}
// Apply settings to services
applyToServices() {
try {
// Apply pose service settings
poseService.updateConfig({
enableValidation: this.settings.enableValidation,
enablePerformanceTracking: this.settings.enablePerformanceTracking,
confidenceThreshold: this.settings.confidenceThreshold,
maxPersons: this.settings.maxPersons
});
// Apply WebSocket service settings
if (wsService.updateConfig) {
wsService.updateConfig({
enableDebugLogging: this.settings.enableDebugLogging,
heartbeatInterval: this.settings.heartbeatInterval,
maxReconnectAttempts: this.settings.maxReconnectAttempts
});
}
this.updateStatus('Settings applied to services');
this.logger.info('Settings applied to services');
} catch (error) {
this.logger.error('Error applying settings to services', { error: error.message });
this.updateStatus('Error applying settings');
}
}
// Get render configuration for PoseRenderer
getRenderConfig() {
return {
mode: this.settings.renderMode,
showKeypoints: this.settings.showKeypoints,
showSkeleton: this.settings.showSkeleton,
showBoundingBox: this.settings.showBoundingBox,
showConfidence: this.settings.showConfidence,
showZones: this.settings.showZones,
showDebugInfo: this.settings.showDebugInfo,
skeletonColor: this.settings.skeletonColor,
keypointColor: this.settings.keypointColor,
boundingBoxColor: this.settings.boundingBoxColor,
confidenceThreshold: this.settings.confidenceThreshold,
keypointConfidenceThreshold: this.settings.keypointConfidenceThreshold,
enableSmoothing: this.settings.enableSmoothing
};
}
// Get stream configuration for PoseService
getStreamConfig() {
return {
zoneIds: [this.settings.currentZone],
minConfidence: this.settings.confidenceThreshold,
maxFps: this.settings.maxFps
};
}
// Cleanup
dispose() {
this.logger.info('Disposing SettingsPanel component');
try {
// Save settings before disposing
this.saveSettings();
// Clear container
if (this.container) {
this.container.innerHTML = '';
}
this.logger.info('SettingsPanel component disposed successfully');
} catch (error) {
this.logger.error('Error during disposal', { error: error.message });
}
}
}