801 lines
32 KiB
JavaScript
801 lines
32 KiB
JavaScript
/**
|
||
* Settings Manager Module
|
||
* Coordinates preset, calibration, and reference management
|
||
*/
|
||
|
||
import { PresetManager } from './settings/preset-manager.js';
|
||
import { CalibrationManager } from './settings/calibration-manager.js';
|
||
import { ReferenceManager } from './settings/reference-manager.js';
|
||
import { LaserManager } from './settings/laser-manager.js';
|
||
import { Debouncer, ButtonState, downloadJSON } from './utils.js';
|
||
import { renderIcons } from './icons.js';
|
||
import {
|
||
createPlotlyPlot,
|
||
togglePlotlyFullscreen,
|
||
downloadPlotlyImage,
|
||
cleanupPlotly
|
||
} from './plotly-utils.js';
|
||
import { apiGet, buildUrl } from './api-client.js';
|
||
import { API, TIMING, NOTIFICATION_TYPES } from './constants.js';
|
||
|
||
const { SUCCESS, ERROR, WARNING } = NOTIFICATION_TYPES;
|
||
|
||
export class SettingsManager {
|
||
constructor(notifications, websocket, acquisition) {
|
||
this.notifications = notifications;
|
||
this.websocket = websocket;
|
||
this.acquisition = acquisition;
|
||
|
||
this.isInitialized = false;
|
||
this.elements = {};
|
||
this.headerElements = {};
|
||
this.debouncer = new Debouncer();
|
||
|
||
// Sub-managers
|
||
this.presetManager = new PresetManager(notifications);
|
||
this.calibrationManager = new CalibrationManager(notifications);
|
||
this.referenceManager = new ReferenceManager(notifications);
|
||
this.laserManager = new LaserManager(notifications);
|
||
|
||
// Plots modal state
|
||
this.currentPlotsData = null;
|
||
|
||
// Bind handlers
|
||
this.handleViewPlots = this.handleViewPlots.bind(this);
|
||
this.handleViewCurrentPlots = this.handleViewCurrentPlots.bind(this);
|
||
}
|
||
|
||
async init() {
|
||
try {
|
||
this.cacheDom();
|
||
this.initSubManagers();
|
||
this.setupEventHandlers();
|
||
await this.loadInitialData();
|
||
|
||
this.isInitialized = true;
|
||
console.log('Settings Manager initialized');
|
||
} catch (err) {
|
||
console.error('Settings Manager init failed:', err);
|
||
this.notify(ERROR, 'Ошибка настроек', 'Не удалось инициализировать модуль настроек');
|
||
}
|
||
}
|
||
|
||
cacheDom() {
|
||
this.elements = {
|
||
// Presets
|
||
presetDropdown: document.getElementById('presetDropdown'),
|
||
setPresetBtn: document.getElementById('setPresetBtn'),
|
||
currentPreset: document.getElementById('currentPreset'),
|
||
|
||
// Calibration
|
||
currentCalibration: document.getElementById('currentCalibration'),
|
||
startCalibrationBtn: document.getElementById('startCalibrationBtn'),
|
||
calibrationSteps: document.getElementById('calibrationSteps'),
|
||
calibrationStandards: document.getElementById('calibrationStandards'),
|
||
progressText: document.getElementById('progressText'),
|
||
calibrationNameInput: document.getElementById('calibrationNameInput'),
|
||
saveCalibrationBtn: document.getElementById('saveCalibrationBtn'),
|
||
calibrationDropdown: document.getElementById('calibrationDropdown'),
|
||
setCalibrationBtn: document.getElementById('setCalibrationBtn'),
|
||
viewPlotsBtn: document.getElementById('viewPlotsBtn'),
|
||
deleteCalibrationBtn: document.getElementById('deleteCalibrationBtn'),
|
||
clearCalibrationBtn: document.getElementById('clearCalibrationBtn'),
|
||
viewCurrentPlotsBtn: document.getElementById('viewCurrentPlotsBtn'),
|
||
|
||
// Modal
|
||
plotsModal: document.getElementById('plotsModal'),
|
||
plotsGrid: document.getElementById('plotsGrid'),
|
||
downloadAllBtn: document.getElementById('downloadAllBtn'),
|
||
|
||
// References
|
||
referenceNameInput: document.getElementById('referenceNameInput'),
|
||
referenceDescriptionInput: document.getElementById('referenceDescriptionInput'),
|
||
createReferenceBtn: document.getElementById('createReferenceBtn'),
|
||
referenceDropdown: document.getElementById('referenceDropdown'),
|
||
setReferenceBtn: document.getElementById('setReferenceBtn'),
|
||
clearReferenceBtn: document.getElementById('clearReferenceBtn'),
|
||
deleteReferenceBtn: document.getElementById('deleteReferenceBtn'),
|
||
previewReferenceBtn: document.getElementById('previewReferenceBtn'),
|
||
currentReferenceInfo: document.getElementById('currentReferenceInfo'),
|
||
currentReferenceName: document.getElementById('currentReferenceName'),
|
||
currentReferenceTimestamp: document.getElementById('currentReferenceTimestamp'),
|
||
currentReferenceDescription: document.getElementById('currentReferenceDescription'),
|
||
currentReferenceCalibration: document.getElementById('currentReferenceCalibration'),
|
||
|
||
// Laser controls
|
||
laserManualMode: document.getElementById('laserManualMode'),
|
||
laserTemp1: document.getElementById('laserTemp1'),
|
||
laserTemp2: document.getElementById('laserTemp2'),
|
||
laserCurrent1: document.getElementById('laserCurrent1'),
|
||
laserCurrent2: document.getElementById('laserCurrent2'),
|
||
laserMinCurrent1: document.getElementById('laserMinCurrent1'),
|
||
laserMaxCurrent1: document.getElementById('laserMaxCurrent1'),
|
||
laserDeltaCurrent1: document.getElementById('laserDeltaCurrent1'),
|
||
laserScanTemp1: document.getElementById('laserScanTemp1'),
|
||
laserScanTemp2: document.getElementById('laserScanTemp2'),
|
||
laserScanCurrent2: document.getElementById('laserScanCurrent2'),
|
||
laserDeltaTime: document.getElementById('laserDeltaTime'),
|
||
laserTau: document.getElementById('laserTau'),
|
||
laserStartBtn: document.getElementById('laserStartBtn'),
|
||
laserStopBtn: document.getElementById('laserStopBtn'),
|
||
|
||
// Status
|
||
presetCount: document.getElementById('presetCount'),
|
||
calibrationCount: document.getElementById('calibrationCount'),
|
||
referenceCount: document.getElementById('referenceCount'),
|
||
systemStatus: document.getElementById('systemStatus')
|
||
};
|
||
|
||
this.headerElements = {
|
||
preset: document.getElementById('headerPresetSummary'),
|
||
calibration: document.getElementById('headerCalibrationSummary'),
|
||
reference: document.getElementById('headerReferenceSummary')
|
||
};
|
||
}
|
||
|
||
initSubManagers() {
|
||
this.presetManager.init(this.elements);
|
||
this.calibrationManager.init(this.elements);
|
||
this.referenceManager.init(this.elements);
|
||
this.laserManager.init({
|
||
manualMode: this.elements.laserManualMode,
|
||
temp1: this.elements.laserTemp1,
|
||
temp2: this.elements.laserTemp2,
|
||
current1: this.elements.laserCurrent1,
|
||
current2: this.elements.laserCurrent2,
|
||
minCurrent1: this.elements.laserMinCurrent1,
|
||
maxCurrent1: this.elements.laserMaxCurrent1,
|
||
deltaCurrent1: this.elements.laserDeltaCurrent1,
|
||
scanTemp1: this.elements.laserScanTemp1,
|
||
scanTemp2: this.elements.laserScanTemp2,
|
||
scanCurrent2: this.elements.laserScanCurrent2,
|
||
deltaTime: this.elements.laserDeltaTime,
|
||
tau: this.elements.laserTau,
|
||
startBtn: this.elements.laserStartBtn,
|
||
stopBtn: this.elements.laserStopBtn
|
||
});
|
||
|
||
// Setup callbacks
|
||
this.presetManager.onPresetChanged = async () => {
|
||
const status = await this.loadStatus();
|
||
this.calibrationManager.reset();
|
||
await this.calibrationManager.loadWorkingCalibration();
|
||
await this.ensureCurrentPreset(status);
|
||
this.updateReferenceSummary(this.referenceManager.getCurrentReference());
|
||
};
|
||
|
||
this.calibrationManager.onCalibrationSaved = async () => {
|
||
const status = await this.loadStatus();
|
||
await this.ensureCurrentPreset(status);
|
||
};
|
||
|
||
this.calibrationManager.onCalibrationSet = async () => {
|
||
const status = await this.loadStatus();
|
||
await this.ensureCurrentPreset(status);
|
||
};
|
||
|
||
this.referenceManager.onReferenceUpdated = (reference) => {
|
||
this.updateReferenceSummary(reference);
|
||
};
|
||
|
||
this.referenceManager.onShowPlots = (plotData) => {
|
||
this.showPlotsModal(plotData);
|
||
};
|
||
}
|
||
|
||
setupEventHandlers() {
|
||
this.elements.viewPlotsBtn?.addEventListener('click', this.handleViewPlots);
|
||
this.elements.viewCurrentPlotsBtn?.addEventListener('click', this.handleViewCurrentPlots);
|
||
}
|
||
|
||
async loadInitialData() {
|
||
await this.presetManager.loadPresets();
|
||
const status = await this.loadStatus();
|
||
await this.calibrationManager.loadWorkingCalibration();
|
||
await this.ensureCurrentPreset(status);
|
||
}
|
||
|
||
async loadStatus() {
|
||
try {
|
||
const status = await apiGet(API.SETTINGS.STATUS);
|
||
await this.updateStatusDisplay(status);
|
||
return status;
|
||
} catch (e) {
|
||
console.error('Status load failed:', e);
|
||
if (this.elements.systemStatus) {
|
||
this.elements.systemStatus.textContent = 'Не подключено';
|
||
}
|
||
if (this.elements.referenceCount) {
|
||
this.elements.referenceCount.textContent = '-';
|
||
}
|
||
this.notify(ERROR, 'Ошибка статуса', 'Не удалось получить текущее состояние системы');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async fetchCurrentPreset() {
|
||
try {
|
||
return await apiGet(API.SETTINGS.PRESET_CURRENT);
|
||
} catch (e) {
|
||
console.error('Current preset fetch failed:', e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async ensureCurrentPreset(status) {
|
||
let preset = this.presetManager.getCurrentPreset();
|
||
if (preset) {
|
||
return preset;
|
||
}
|
||
|
||
const fallbackPreset = status?.current_preset ?? await this.fetchCurrentPreset();
|
||
if (!fallbackPreset) {
|
||
return null;
|
||
}
|
||
|
||
this.presetManager.setCurrentPresetDirect(fallbackPreset);
|
||
this.calibrationManager.setCurrentPreset(fallbackPreset);
|
||
await this.referenceManager.setCurrentPreset(fallbackPreset);
|
||
await this.calibrationManager.loadWorkingCalibration();
|
||
|
||
if (!status && this.elements.systemStatus) {
|
||
this.elements.systemStatus.textContent = 'Не подключено';
|
||
}
|
||
|
||
const effectiveStatus = {
|
||
...(status ?? { available_presets: 0, available_calibrations: 0, available_references: 0 }),
|
||
current_preset: fallbackPreset,
|
||
available_references: this.referenceManager?.availableReferences?.length ?? 0,
|
||
};
|
||
|
||
if (!status) {
|
||
if (this.elements.presetCount) {
|
||
this.elements.presetCount.textContent = effectiveStatus.available_presets ?? '-';
|
||
}
|
||
if (this.elements.calibrationCount) {
|
||
this.elements.calibrationCount.textContent = effectiveStatus.available_calibrations ?? '-';
|
||
}
|
||
if (this.elements.referenceCount) {
|
||
this.elements.referenceCount.textContent = effectiveStatus.available_references ?? '-';
|
||
}
|
||
}
|
||
|
||
this.updateHeaderSummary(effectiveStatus);
|
||
|
||
return fallbackPreset;
|
||
}
|
||
|
||
async updateStatusDisplay(status) {
|
||
this.presetManager.updateStatus(status);
|
||
this.calibrationManager.updateStatus(status);
|
||
|
||
const preset = this.presetManager.getCurrentPreset();
|
||
this.calibrationManager.setCurrentPreset(preset);
|
||
await this.referenceManager.setCurrentPreset(preset);
|
||
|
||
if (this.elements.presetCount) {
|
||
this.elements.presetCount.textContent = status?.available_presets ?? '-';
|
||
}
|
||
if (this.elements.calibrationCount) {
|
||
this.elements.calibrationCount.textContent = status?.available_calibrations ?? '-';
|
||
}
|
||
if (this.elements.referenceCount) {
|
||
const count = typeof status?.available_references === 'number'
|
||
? status.available_references
|
||
: this.referenceManager?.availableReferences?.length;
|
||
this.elements.referenceCount.textContent = typeof count === 'number' ? count : '-';
|
||
}
|
||
this.updateHeaderSummary(status);
|
||
this.updateReferenceSummary(this.referenceManager.getCurrentReference());
|
||
}
|
||
|
||
async handleViewPlots() {
|
||
this.debouncer.debounce('view-plots', async () => {
|
||
const name = this.elements.calibrationDropdown.value;
|
||
const preset = this.presetManager.getCurrentPreset();
|
||
if (!name || !preset) return;
|
||
|
||
try {
|
||
ButtonState.set(this.elements.viewPlotsBtn, { state: 'loading', icon: 'loader', text: 'Загрузка...' });
|
||
|
||
const url = buildUrl(API.SETTINGS.CALIBRATION_STANDARDS_PLOTS(name), {
|
||
preset_filename: preset.filename
|
||
});
|
||
const plotsData = await apiGet(url);
|
||
|
||
this.showPlotsModal(plotsData);
|
||
} catch (e) {
|
||
console.error('Load plots failed:', e);
|
||
this.notify(ERROR, 'Ошибка графиков', 'Не удалось загрузить графики калибровки');
|
||
} finally {
|
||
ButtonState.set(this.elements.viewPlotsBtn, { state: 'normal', icon: 'bar-chart-3', text: 'Показать графики' });
|
||
}
|
||
}, TIMING.DEBOUNCE_CALIBRATION);
|
||
}
|
||
|
||
async handleViewCurrentPlots() {
|
||
this.debouncer.debounce('view-current-plots', async () => {
|
||
const working = this.calibrationManager.getWorkingCalibration();
|
||
if (!working || !working.active) return;
|
||
|
||
try {
|
||
ButtonState.set(this.elements.viewCurrentPlotsBtn, { state: 'loading', icon: 'loader', text: 'Загрузка...' });
|
||
|
||
const plotsData = await apiGet(API.SETTINGS.WORKING_CALIBRATION_PLOTS);
|
||
this.showPlotsModal(plotsData);
|
||
} catch (e) {
|
||
if (e.status === 404) {
|
||
this.notify(WARNING, 'Нет данных', 'Нет активной калибровки или стандартов для отображения');
|
||
} else {
|
||
console.error('Load current plots failed:', e);
|
||
this.notify(ERROR, 'Ошибка графиков', 'Не удалось загрузить графики текущей калибровки');
|
||
}
|
||
} finally {
|
||
ButtonState.set(this.elements.viewCurrentPlotsBtn, { state: 'normal', icon: 'bar-chart-3', text: 'Графики текущей калибровки' });
|
||
}
|
||
}, TIMING.DEBOUNCE_CALIBRATION);
|
||
}
|
||
|
||
showPlotsModal(plotsData) {
|
||
const modal = this.elements.plotsModal;
|
||
if (!modal) return;
|
||
|
||
this.currentPlotsData = plotsData;
|
||
|
||
if (plotsData.reference_name) {
|
||
this.renderReferencePlot(plotsData);
|
||
} else {
|
||
this.renderCalibrationPlots(plotsData.individual_plots, plotsData.preset);
|
||
}
|
||
|
||
const title = modal.querySelector('.modal__title');
|
||
if (title) {
|
||
if (plotsData.reference_name) {
|
||
title.innerHTML = `
|
||
<span data-icon="target"></span>
|
||
${plotsData.reference_name} - ${plotsData.preset.mode.toUpperCase()}
|
||
`;
|
||
} else {
|
||
title.innerHTML = `
|
||
<span data-icon="bar-chart-3"></span>
|
||
${plotsData.calibration_name} - ${plotsData.preset.mode.toUpperCase()} Standards
|
||
`;
|
||
}
|
||
renderIcons(title);
|
||
}
|
||
|
||
this.setupModalCloseHandlers(modal);
|
||
|
||
modal.classList.add('modal--active');
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
renderReferencePlot(plotsData) {
|
||
const container = this.elements.plotsGrid;
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '';
|
||
|
||
if (!plotsData.plot || plotsData.plot.error) {
|
||
container.innerHTML = '<div class="plot-error">Не удалось загрузить график эталона</div>';
|
||
return;
|
||
}
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'chart-card';
|
||
card.innerHTML = `
|
||
<div class="chart-card__header">
|
||
<div class="chart-card__title">
|
||
<span data-icon="target" class="chart-card__icon"></span>
|
||
${plotsData.reference_name}
|
||
</div>
|
||
<div class="chart-card__actions">
|
||
<button class="chart-card__action" data-action="fullscreen" title="На весь экран">
|
||
<span data-icon="expand"></span>
|
||
</button>
|
||
<button class="chart-card__action" data-action="download" title="Скачать">
|
||
<span data-icon="download"></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="chart-card__content">
|
||
<div class="chart-card__plot" id="reference-plot"></div>
|
||
</div>
|
||
<div class="chart-card__meta">
|
||
<div class="chart-card__timestamp">Дата: ${new Date(plotsData.timestamp).toLocaleString()}</div>
|
||
<div class="chart-card__sweep">${plotsData.description || 'Эталон открытого пространства'}</div>
|
||
</div>
|
||
`;
|
||
|
||
card.addEventListener('click', (e) => {
|
||
const action = e.target.closest?.('[data-action]')?.dataset.action;
|
||
if (!action) return;
|
||
e.stopPropagation();
|
||
|
||
const plotEl = card.querySelector('.chart-card__plot');
|
||
if (action === 'fullscreen') this.toggleFullscreen(card);
|
||
if (action === 'download') this.downloadReferencePlot(plotsData, plotEl);
|
||
});
|
||
|
||
container.appendChild(card);
|
||
renderIcons(card);
|
||
|
||
const plotEl = card.querySelector('.chart-card__plot');
|
||
this.renderPlotly(plotEl, plotsData.plot, plotsData.reference_name);
|
||
}
|
||
|
||
renderCalibrationPlots(individualPlots, preset) {
|
||
const container = this.elements.plotsGrid;
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '';
|
||
|
||
if (!individualPlots || !Object.keys(individualPlots).length) {
|
||
container.innerHTML = '<div class="plot-error">Нет доступных графиков калибровки</div>';
|
||
return;
|
||
}
|
||
|
||
Object.entries(individualPlots).forEach(([name, plot]) => {
|
||
if (plot.error) {
|
||
const err = document.createElement('div');
|
||
err.className = 'chart-card';
|
||
err.innerHTML = `
|
||
<div class="chart-card__header">
|
||
<div class="chart-card__title">
|
||
<span data-icon="alert-circle" class="chart-card__icon"></span>
|
||
${name.toUpperCase()} Standard
|
||
</div>
|
||
</div>
|
||
<div class="chart-card__content">
|
||
<div class="plot-error">Error: ${plot.error}</div>
|
||
</div>
|
||
`;
|
||
renderIcons(err);
|
||
container.appendChild(err);
|
||
return;
|
||
}
|
||
|
||
const card = this.createCalibrationChartCard(name, plot, preset);
|
||
container.appendChild(card);
|
||
});
|
||
|
||
renderIcons(container);
|
||
}
|
||
|
||
createCalibrationChartCard(standardName, plotConfig, preset) {
|
||
const card = document.createElement('div');
|
||
card.className = 'chart-card';
|
||
card.dataset.standard = standardName;
|
||
|
||
const title = `Стандарт ${standardName.toUpperCase()}`;
|
||
|
||
card.innerHTML = `
|
||
<div class="chart-card__header">
|
||
<div class="chart-card__title">
|
||
<span data-icon="bar-chart-3" class="chart-card__icon"></span>
|
||
${title}
|
||
</div>
|
||
<div class="chart-card__actions">
|
||
<button class="chart-card__action" data-action="fullscreen" title="На весь экран">
|
||
<span data-icon="expand"></span>
|
||
</button>
|
||
<button class="chart-card__action" data-action="download" title="Скачать">
|
||
<span data-icon="download"></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="chart-card__content">
|
||
<div class="chart-card__plot" id="calibration-plot-${standardName}"></div>
|
||
</div>
|
||
<div class="chart-card__meta">
|
||
<div class="chart-card__timestamp">Стандарт: ${standardName.toUpperCase()}</div>
|
||
<div class="chart-card__sweep">Пресет: ${preset?.filename || 'Неизвестно'}</div>
|
||
</div>
|
||
`;
|
||
|
||
card.addEventListener('click', (e) => {
|
||
const action = e.target.closest?.('[data-action]')?.dataset.action;
|
||
if (!action) return;
|
||
e.stopPropagation();
|
||
|
||
const plotEl = card.querySelector('.chart-card__plot');
|
||
if (action === 'fullscreen') this.toggleFullscreen(card);
|
||
if (action === 'download') this.downloadCalibrationStandard(standardName, plotEl);
|
||
});
|
||
|
||
const plotEl = card.querySelector('.chart-card__plot');
|
||
this.renderPlotly(plotEl, plotConfig, title);
|
||
renderIcons(card);
|
||
|
||
return card;
|
||
}
|
||
|
||
renderPlotly(container, plotConfig, title) {
|
||
if (!container || !plotConfig || plotConfig.error) {
|
||
container.innerHTML = `<div class="plot-error">Не удалось загрузить график: ${plotConfig?.error || 'Неизвестная ошибка'}</div>`;
|
||
return;
|
||
}
|
||
|
||
const layoutOverrides = {
|
||
title: { text: title, font: { size: 16, color: '#f1f5f9' } },
|
||
xaxis: {
|
||
...plotConfig.layout?.xaxis,
|
||
gridcolor: '#334155',
|
||
zerolinecolor: '#475569',
|
||
color: '#cbd5e1',
|
||
fixedrange: false
|
||
},
|
||
yaxis: {
|
||
...plotConfig.layout?.yaxis,
|
||
gridcolor: '#334155',
|
||
zerolinecolor: '#475569',
|
||
color: '#cbd5e1',
|
||
fixedrange: false
|
||
}
|
||
};
|
||
|
||
const configOverrides = {
|
||
toImageButtonOptions: {
|
||
format: 'png',
|
||
filename: `calibration-plot-${Date.now()}`,
|
||
height: 600,
|
||
width: 800,
|
||
scale: 1
|
||
}
|
||
};
|
||
|
||
createPlotlyPlot(container, plotConfig.data, layoutOverrides, configOverrides);
|
||
}
|
||
|
||
async toggleFullscreen(card) {
|
||
const plot = card.querySelector('.chart-card__plot');
|
||
await togglePlotlyFullscreen(card, plot);
|
||
}
|
||
|
||
setupModalCloseHandlers(modal) {
|
||
modal.querySelectorAll('[data-modal-close]').forEach(el => {
|
||
el.addEventListener('click', () => this.closePlotsModal());
|
||
});
|
||
|
||
const downloadAllBtn = modal.querySelector('#downloadAllBtn');
|
||
if (downloadAllBtn) {
|
||
downloadAllBtn.addEventListener('click', () =>
|
||
this.debouncer.debounce('download-all', () => this.downloadAllCalibrationData(), TIMING.DEBOUNCE_DOWNLOAD)
|
||
);
|
||
}
|
||
|
||
const escHandler = (e) => {
|
||
if (e.key === 'Escape') {
|
||
this.closePlotsModal();
|
||
document.removeEventListener('keydown', escHandler);
|
||
}
|
||
};
|
||
document.addEventListener('keydown', escHandler);
|
||
}
|
||
|
||
closePlotsModal() {
|
||
const modal = this.elements.plotsModal;
|
||
if (!modal) return;
|
||
|
||
modal.classList.remove('modal--active');
|
||
document.body.style.overflow = '';
|
||
|
||
const containers = modal.querySelectorAll('[id^="calibration-plot-"]');
|
||
containers.forEach(c => cleanupPlotly(c));
|
||
|
||
this.currentPlotsData = null;
|
||
}
|
||
|
||
async downloadReferencePlot(plotsData, plotContainer) {
|
||
try {
|
||
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const base = `${plotsData.reference_name}_${ts}`;
|
||
|
||
if (plotContainer) {
|
||
await downloadPlotlyImage(plotContainer, `${base}_plot`);
|
||
}
|
||
|
||
const data = {
|
||
reference_info: {
|
||
name: plotsData.reference_name,
|
||
timestamp: plotsData.timestamp,
|
||
description: plotsData.description,
|
||
preset: plotsData.preset
|
||
},
|
||
plot_data: plotsData.plot
|
||
};
|
||
downloadJSON(data, `${base}_data.json`);
|
||
} catch (e) {
|
||
console.error('Download reference failed:', e);
|
||
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать данные эталона');
|
||
}
|
||
}
|
||
|
||
async downloadCalibrationStandard(standardName, plotContainer) {
|
||
try {
|
||
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const calibrationName = this.currentPlotsData?.calibration_name || 'unknown';
|
||
const base = `${calibrationName}_${standardName}_${ts}`;
|
||
|
||
if (plotContainer) {
|
||
await downloadPlotlyImage(plotContainer, `${base}_plot`);
|
||
}
|
||
|
||
const data = this.prepareCalibrationDownloadData(standardName);
|
||
downloadJSON(data, `${base}_data.json`);
|
||
} catch (e) {
|
||
console.error('Download standard failed:', e);
|
||
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать данные калибровки');
|
||
}
|
||
}
|
||
|
||
async downloadAllCalibrationData() {
|
||
if (!this.currentPlotsData) return;
|
||
|
||
try {
|
||
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const calibrationName = this.currentPlotsData.calibration_name || 'unknown';
|
||
const base = `${calibrationName}_complete_${ts}`;
|
||
|
||
const btn = this.elements.downloadAllBtn;
|
||
if (btn) ButtonState.set(btn, { state: 'loading', icon: 'loader', text: 'Скачивание...' });
|
||
|
||
const complete = {
|
||
export_info: {
|
||
export_timestamp: new Date().toISOString(),
|
||
calibration_name: calibrationName,
|
||
preset: this.currentPlotsData.preset
|
||
},
|
||
standards_data: this.currentPlotsData.individual_plots
|
||
};
|
||
|
||
downloadJSON(complete, `${base}.json`);
|
||
|
||
this.notify(SUCCESS, 'Полное скачивание завершено', `Полный набор данных калибровки сохранён для ${calibrationName}`);
|
||
} catch (e) {
|
||
console.error('Download all failed:', e);
|
||
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать полный набор данных калибровки');
|
||
} finally {
|
||
const btn = this.elements.downloadAllBtn;
|
||
if (btn) ButtonState.set(btn, { state: 'normal', icon: 'download-cloud', text: 'Скачать всё' });
|
||
}
|
||
}
|
||
|
||
prepareCalibrationDownloadData(standardName) {
|
||
if (!this.currentPlotsData) return null;
|
||
const plot = this.currentPlotsData.individual_plots[standardName];
|
||
return {
|
||
calibration_info: {
|
||
calibration_name: this.currentPlotsData.calibration_name,
|
||
preset: this.currentPlotsData.preset,
|
||
standard_name: standardName,
|
||
download_timestamp: new Date().toISOString()
|
||
},
|
||
plot_data: plot
|
||
};
|
||
}
|
||
|
||
async refresh() {
|
||
if (!this.isInitialized) return;
|
||
await this.loadInitialData();
|
||
}
|
||
|
||
destroy() {
|
||
this.presetManager.destroy();
|
||
this.calibrationManager.destroy();
|
||
this.referenceManager.destroy();
|
||
|
||
this.elements.viewPlotsBtn?.removeEventListener('click', this.handleViewPlots);
|
||
this.elements.viewCurrentPlotsBtn?.removeEventListener('click', this.handleViewCurrentPlots);
|
||
|
||
this.isInitialized = false;
|
||
console.log('Settings Manager destroyed');
|
||
}
|
||
|
||
formatFrequency(value) {
|
||
if (value === null || value === undefined) return null;
|
||
const numeric = Number(value);
|
||
if (!Number.isFinite(numeric)) return null;
|
||
const abs = Math.abs(numeric);
|
||
const units = [
|
||
{ divider: 1e9, suffix: 'ГГц' },
|
||
{ divider: 1e6, suffix: 'МГц' },
|
||
{ divider: 1e3, suffix: 'кГц' }
|
||
];
|
||
|
||
for (const unit of units) {
|
||
if (abs >= unit.divider) {
|
||
const scaled = numeric / unit.divider;
|
||
const formatted = scaled >= 10 ? scaled.toFixed(0) : scaled.toFixed(2);
|
||
return `${formatted} ${unit.suffix}`;
|
||
}
|
||
}
|
||
|
||
return `${numeric.toFixed(0)} Гц`;
|
||
}
|
||
|
||
formatPresetSummary(preset) {
|
||
if (!preset) return 'Не выбран';
|
||
|
||
const parts = [];
|
||
if (preset.mode) {
|
||
parts.push(preset.mode.toUpperCase());
|
||
}
|
||
|
||
const start = this.formatFrequency(preset.start_freq);
|
||
const stop = this.formatFrequency(preset.stop_freq);
|
||
if (start && stop) {
|
||
parts.push(`${start} – ${stop}`);
|
||
} else if (start || stop) {
|
||
parts.push(start || stop);
|
||
}
|
||
|
||
if (preset.points) {
|
||
parts.push(`${preset.points} точек`);
|
||
}
|
||
|
||
if (preset.bandwidth) {
|
||
const bw = this.formatFrequency(preset.bandwidth);
|
||
if (bw) parts.push(`ПП ${bw}`);
|
||
}
|
||
|
||
return parts.join(' • ') || 'Пресет настроен';
|
||
}
|
||
|
||
formatCalibrationSummary(status) {
|
||
const active = status?.current_calibration;
|
||
if (active?.calibration_name) {
|
||
return `Активна • ${active.calibration_name}`;
|
||
}
|
||
|
||
const working = status?.working_calibration;
|
||
if (working?.progress) {
|
||
const missingCount = working.missing_standards?.length || 0;
|
||
const missingText = missingCount ? ` • не хватает ${missingCount} стандартов` : '';
|
||
return `В процессе • ${working.progress}${missingText}`;
|
||
}
|
||
|
||
return 'Не задано';
|
||
}
|
||
|
||
updateHeaderSummary(status) {
|
||
if (!this.headerElements) return;
|
||
const { preset, calibration } = this.headerElements;
|
||
if (preset) {
|
||
preset.textContent = this.formatPresetSummary(status?.current_preset);
|
||
}
|
||
if (calibration) {
|
||
calibration.textContent = this.formatCalibrationSummary(status);
|
||
}
|
||
}
|
||
|
||
updateReferenceSummary(reference) {
|
||
const target = this.headerElements?.reference;
|
||
if (!target) return;
|
||
|
||
if (!reference) {
|
||
target.textContent = 'Не снят';
|
||
return;
|
||
}
|
||
|
||
const name = reference.name || 'Эталон';
|
||
let timestampText = '';
|
||
if (reference.timestamp) {
|
||
const timestamp = new Date(reference.timestamp);
|
||
if (!Number.isNaN(timestamp.getTime())) {
|
||
timestampText = timestamp.toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' });
|
||
}
|
||
}
|
||
|
||
const summaryParts = ['Активен', name];
|
||
if (timestampText) {
|
||
summaryParts.push(timestampText);
|
||
}
|
||
target.textContent = summaryParts.join(' • ');
|
||
}
|
||
|
||
notify(type, title, message) {
|
||
this.notifications?.show?.({ type, title, message });
|
||
}
|
||
}
|