diff --git a/index.html b/index.html index ab9494e3..2724ecc6 100644 --- a/index.html +++ b/index.html @@ -477,10 +477,10 @@

Workspace

- - + + - +
diff --git a/src/graph_imported_curves.js b/src/graph_imported_curves.js index df507285..3c27dd30 100644 --- a/src/graph_imported_curves.js +++ b/src/graph_imported_curves.js @@ -1,75 +1,158 @@ export function ImportedCurves(curvesChanged) { - const maxImportCount = 5; - this._curvesData = []; + const MAX_IMPORT_COUNT = 6; // This value is limited by legends size and curves colors visibility. May be increased if needed by users + const _curvesData = []; const _that = this; this.minX = Number.MAX_VALUE; this.maxX = -Number.MAX_VALUE; this.minY = Number.MAX_VALUE; this.maxY = -Number.MAX_VALUE; - + this.curvesCount = function() { - return this._curvesData.length; + return _curvesData.length; + }; + + this.getCurve = function(index) { + if (index < _curvesData.length) { + return _curvesData[index]; + } else { + throw new RangeError(`The imported curves index (${index}) exceeds the maximum allowed value (${_curvesData.length - 1})`); + } }; this.importCurvesFromCSV = function(files) { - let importsLeft = maxImportCount - this._curvesData.length; + let importsLeft = MAX_IMPORT_COUNT - _curvesData.length; - for (const file of files) { - if (importsLeft-- == 0) { - break; - } - const reader = new FileReader(); - reader.onload = function (e) { - try { - const stringRows = e.target.result.split("\n"); - - const header = stringRows[0].split(","); - if (header.length != 2 || header[0] != "x" || header[1] != "y") { - throw new SyntaxError("Wrong curves CSV data format"); - } - - stringRows.shift(); - //remove bad last row - if (stringRows.at(-1) == "") { - stringRows.pop(); - } - - const curvesData = stringRows.map( function(row) { - const data = row.split(","), - x = parseFloat(data[0]), - y = parseFloat(data[1]); - _that.minX = Math.min(x, _that.minX); - _that.maxX = Math.max(x, _that.maxX); - _that.minY = Math.min(y, _that.minY); - _that.maxY = Math.max(y, _that.maxY); - return { - x: x, - y: y, - }; - }); - - const curve = { - name: file.name.split('.')[0], - points: curvesData, - }; - _that._curvesData.push(curve); - curvesChanged(); - } catch (e) { - alert('Curves data import error: ' + e.message); - return; + for (const file of files) { + if (importsLeft-- == 0) { + break; + } + const reader = new FileReader(); + reader.onload = function (e) { + try { + const stringRows = e.target.result.split("\n"); + + const header = stringRows[0].split(","); + if (header.length !== 2 || header[0].trim() !== "x" || header[1].trim() !== "y") { + throw new SyntaxError("Wrong curves CSV data format"); } - }; - reader.readAsText(file); - } + stringRows.shift(); + //remove bad last row + if (stringRows.at(-1) == "") { + stringRows.pop(); + } + + const curvesData = stringRows.map( function(row) { + const data = row.split(","), + x = parseFloat(data[0].trim()), + y = parseFloat(data[1].trim()); + _that.minX = Math.min(x, _that.minX); + _that.maxX = Math.max(x, _that.maxX); + _that.minY = Math.min(y, _that.minY); + _that.maxY = Math.max(y, _that.maxY); + return { + x: x, + y: y, + }; + }); + + const curve = { + name: file.name.split('.')[0], + points: curvesData, + }; + _curvesData.push(curve); + curvesChanged(); + } catch (e) { + alert('Curves data import error: ' + e.message); + return; + } + }; + + reader.readAsText(file); + } + }; + + const getCurveRange = function(points) { + let minX = Number.MAX_VALUE, + maxX = -Number.MAX_VALUE, + minY = Number.MAX_VALUE, + maxY = -Number.MAX_VALUE; + for (const point of points) { + minX = Math.min(point.x, minX); + maxX = Math.max(point.x, maxX); + minY = Math.min(point.y, minY); + maxY = Math.max(point.y, maxY); + } + return { + minX: minX, + maxX: maxX, + minY: minY, + maxY: maxY, }; + }; - this.removeCurves = function() { - this._curvesData.length = 0; - this.minX = Number.MAX_VALUE; - this.maxX = -Number.MAX_VALUE; - this.minY = Number.MAX_VALUE; - this.maxY = -Number.MAX_VALUE; + const computeGlobalCurvesRange = function () { + _that.minX = Number.MAX_VALUE; + _that.maxX = -Number.MAX_VALUE; + _that.minY = Number.MAX_VALUE; + _that.maxY = -Number.MAX_VALUE; + for (const curve of _curvesData) { + _that.minX = Math.min(curve.range.minX, _that.minX); + _that.maxX = Math.max(curve.range.maxX, _that.maxX); + _that.minY = Math.min(curve.range.minY, _that.minY); + _that.maxY = Math.max(curve.range.maxY, _that.maxY); + } + }; + + this.addCurve = function(points, name) { + if (this.curvesCount() < MAX_IMPORT_COUNT) { + const range = getCurveRange(points); + _curvesData.push({ + name: name, + points: points, + range: range, + }); + + this.minX = Math.min(range.minX, this.minX); + this.maxX = Math.max(range.maxX, this.maxX); + this.minY = Math.min(range.minY, this.minY); + this.maxY = Math.max(range.maxY, this.maxY); + + curvesChanged(); + } + }; + + this.isNewCurve = function(name) { + for (const curve of _curvesData) { + if (curve.name === name) { + return false; + } + } + return true; + }; + + this.removeAllCurves = function() { + _curvesData.length = 0; + computeGlobalCurvesRange(); curvesChanged(); }; + + this.removeCurve = function(name) { + for (let index = 0; index < _curvesData.length; index++) { + if (_curvesData[index].name === name) { + _curvesData.splice(index, 1); + computeGlobalCurvesRange(); + curvesChanged(); + break; + } + } + }; + + this.isFull = function() { + return this.curvesCount() === MAX_IMPORT_COUNT; + }; + + this.isEmpty = function() { + return this.curvesCount() === 0; + }; } diff --git a/src/graph_legend.js b/src/graph_legend.js index b1f95aea..36af61f4 100644 --- a/src/graph_legend.js +++ b/src/graph_legend.js @@ -101,10 +101,10 @@ export function GraphLegend( config.selectedGraphIndex = selectedGraphIndex; config.selectedFieldIndex = selectedFieldIndex; if (onNewSelectionChange) { - onNewSelectionChange(); + onNewSelectionChange(false, e.ctrlKey); } } else { - onNewSelectionChange(true); + onNewSelectionChange(true, e.ctrlKey); } } e.preventDefault(); diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index 8b82f11a..99355af8 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -21,7 +21,8 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { let analyserZoomX = 1.0 /* 100% */, analyserZoomY = 1.0 /* 100% */, dataReload = false, - fftData = null; + fftData = null, + addSpectrumForComparison = false; try { let isFullscreen = false; @@ -47,6 +48,12 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { that.resize(); }; + this.prepareSpectrumForComparison = function () { + if (userSettings.spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY) { + addSpectrumForComparison = true; + } + }; + this.setInTime = function (time) { dataReload = true; return GraphSpectrumCalc.setInTime(time); @@ -156,17 +163,38 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { } }; + this.shouldAddCurrentSpectrumBeforeReload = function () { + return addSpectrumForComparison && fftData !== null && !this.isMultiSpectrum() && !dataReload; + }; + /* This function is called from the canvas drawing routines within grapher.js It is only used to record the current curve positions, collect the data and draw the analyser on screen*/ this.plotSpectrum = function (fieldIndex, curve, fieldName) { // Detect change of selected field.... reload and redraw required. - if (fftData == null || fieldIndex != fftData.fieldIndex || dataReload) { + const isMaxCountOfImportedPSD = GraphSpectrumPlot.isImportedCurvesMaxCount() && userSettings.spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY; + let shouldReload = fftData == null || + fieldIndex != fftData.fieldIndex && !isMaxCountOfImportedPSD || // Lock spectrum data reload while PSD curves import is full + dataReload; + + if (addSpectrumForComparison && !GraphSpectrumPlot.isNewComparedCurve(fieldName)) { + GraphSpectrumPlot.removeComparedCurve(fieldName); + addSpectrumForComparison = false; + shouldReload = false; // Do not load if spectrum was deleted + } + + if (shouldReload) { + if (this.shouldAddCurrentSpectrumBeforeReload()) { + GraphSpectrumPlot.addCurrentSpectrumIntoImport(); // The main curve is added into imported list when the second curve is selected for comparison + } dataReload = false; dataLoad(fieldIndex, curve, fieldName); GraphSpectrumPlot.setData(fftData, userSettings.spectrumType); } - + if (addSpectrumForComparison) { + GraphSpectrumPlot.addCurrentSpectrumIntoImport(); + addSpectrumForComparison = false; + } that.draw(); // draw the analyser on the canvas.... }; @@ -354,6 +382,9 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { const showSpectrumsComparisonPanel = optionSelected === SPECTRUM_TYPE.FREQUENCY || optionSelected === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY; $("#spectrumComparison").css("visibility", (showSpectrumsComparisonPanel ? "visible" : "hidden")); + + const showAddSpectrumButton = optionSelected === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY; + $("#btn-spectrum-add").toggle(showAddSpectrumButton); }) .change(); @@ -438,6 +469,10 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { return fileName; }; + this.isMultiSpectrum = function() { + return GraphSpectrumPlot.isMultiSpectrum(); + }; + } catch (e) { console.error(`Failed to create analyser... error: ${e}`); } diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index c1402485..beeae463 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -60,6 +60,7 @@ export const GraphSpectrumPlot = window.GraphSpectrumPlot || { _importedSpectrums: null, _importedPSD: null, curvesColors : [ + "White", // The first imported curve duplicates existing main curve, therefore it must have same white color "Blue", "Purple", "DeepPink", @@ -217,6 +218,10 @@ GraphSpectrumPlot._drawGraph = function (canvasCtx) { } }; +GraphSpectrumPlot.getCurveColor = function (index) { + return this.curvesColors[index % this.curvesColors.length]; +}; + GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { const HEIGHT = canvasCtx.canvas.height - MARGIN; const WIDTH = canvasCtx.canvas.width; @@ -264,12 +269,12 @@ GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { const scaleX = WIDTH / MAXIMAL_PLOTTED_FREQUENCY; const spectrumCount = this._importedSpectrums.curvesCount(); for (let spectrumNum = 0; spectrumNum < spectrumCount; spectrumNum++) { - const curvesPonts = this._importedSpectrums._curvesData[spectrumNum].points; - const pointsCount = curvesPonts.length; + const curvesPoints = this._importedSpectrums.getCurve(spectrumNum).points; + const pointsCount = curvesPoints.length; canvasCtx.beginPath(); canvasCtx.lineWidth = 1; - canvasCtx.strokeStyle = this.curvesColors[spectrumNum]; + canvasCtx.strokeStyle = this.getCurveColor(spectrumNum); canvasCtx.moveTo(0, HEIGHT); const filterPointsCount = 200; for (let pointNum = 0; pointNum < pointsCount; pointNum++) { @@ -288,18 +293,18 @@ GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { } let middleValue = 0; for (let i = filterStartPoint; i < filterStopPoint; i++) { - middleValue += curvesPonts[i].y; + middleValue += curvesPoints[i].y; } middleValue /= filterPointsCount; - canvasCtx.lineTo(curvesPonts[pointNum].x * scaleX, HEIGHT - middleValue * fftScale); + canvasCtx.lineTo(curvesPoints[pointNum].x * scaleX, HEIGHT - middleValue * fftScale); } canvasCtx.stroke(); } //Legend draw if (this._isFullScreen && spectrumCount > 0) { - this._drawLegend(canvasCtx, WIDTH, HEIGHT, this._importedSpectrums._curvesData); + this._drawLegend(canvasCtx, WIDTH, HEIGHT, this._importedSpectrums); } this._drawAxisLabel( @@ -344,6 +349,7 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { max: maxY, }; + const mainCurveColor = this.getCurveColor(0); // The main curve must have color like the first imported curves - White. const ticksCount = (maxY - minY) / dbStep; const pointsCount = this._fftData.fftLength; const scaleX = WIDTH / MAXIMAL_PLOTTED_FREQUENCY; @@ -352,40 +358,52 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { canvasCtx.translate(LEFT, TOP); this._drawGradientBackground(canvasCtx, WIDTH, HEIGHT); - canvasCtx.beginPath(); - canvasCtx.lineWidth = 1; - canvasCtx.strokeStyle = "white"; - canvasCtx.moveTo(0, 0); - for (let pointNum = 0; pointNum < pointsCount; pointNum++) { - const freq = this._fftData.blackBoxRate / 2 * pointNum / pointsCount; - if(freq > MAXIMAL_PLOTTED_FREQUENCY) { - break; + const comparedSpectrumCount = this._importedPSD.curvesCount(); + + if (comparedSpectrumCount === 0) { // Draw main spectrum curve when there are no spectrums comparison only + canvasCtx.beginPath(); + canvasCtx.lineWidth = 1; + canvasCtx.strokeStyle = mainCurveColor; + for (let pointNum = 0; pointNum < pointsCount; pointNum++) { + const freq = this._fftData.blackBoxRate / 2 * pointNum / pointsCount; + if(freq > MAXIMAL_PLOTTED_FREQUENCY) { + break; + } + const y = HEIGHT - (this._fftData.fftOutput[pointNum] - minY) * scaleY; + if (pointNum === 0) { + canvasCtx.moveTo(freq * scaleX, y); + } else { + canvasCtx.lineTo(freq * scaleX, y); + } } - const y = HEIGHT - (this._fftData.fftOutput[pointNum] - minY) * scaleY; - canvasCtx.lineTo(freq * scaleX, y); + canvasCtx.stroke(); } - canvasCtx.stroke(); - const spectrumCount = this._importedPSD.curvesCount(); - for (let spectrumNum = 0; spectrumNum < spectrumCount; spectrumNum++) { - const curvesPonts = this._importedPSD._curvesData[spectrumNum].points; + + for (let spectrumNum = 0; spectrumNum < comparedSpectrumCount; spectrumNum++) { + const curvesPoints = this._importedPSD.getCurve(spectrumNum).points; canvasCtx.beginPath(); canvasCtx.lineWidth = 1; - canvasCtx.strokeStyle = this.curvesColors[spectrumNum]; - canvasCtx.moveTo(0, HEIGHT); - for (const point of curvesPonts) { - if(point.x > MAXIMAL_PLOTTED_FREQUENCY) { + canvasCtx.strokeStyle = this.getCurveColor(spectrumNum); + let isFirstPoint = true; + for (const point of curvesPoints) { + if (point.x > MAXIMAL_PLOTTED_FREQUENCY) { break; } - canvasCtx.lineTo(point.x * scaleX, HEIGHT - (point.y - minY) * scaleY); + if (isFirstPoint) { + canvasCtx.moveTo(point.x * scaleX, HEIGHT - (point.y - minY) * scaleY); + isFirstPoint = false; + } else { + canvasCtx.lineTo(point.x * scaleX, HEIGHT - (point.y - minY) * scaleY); + } } canvasCtx.stroke(); } //Legend draw - if (this._isFullScreen && spectrumCount > 0) { - this._drawLegend(canvasCtx, WIDTH, HEIGHT, this._importedPSD._curvesData); + if (this._isFullScreen && comparedSpectrumCount > 0) { + this._drawLegend(canvasCtx, WIDTH, HEIGHT, this._importedPSD); } this._drawAxisLabel( @@ -431,16 +449,19 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { }; GraphSpectrumPlot._drawLegend = function (canvasCtx, WIDTH, HEIGHT, importedCurves) { - if (!userSettings?.analyser_legend) { + const left = parseFloat(userSettings?.analyser_legend?.left); + const top = parseFloat(userSettings?.analyser_legend?.top); + const width = parseFloat(userSettings?.analyser_legend?.width); + if (!Number.isFinite(left) || !Number.isFinite(top) || !Number.isFinite(width)) { return; } - const spectrumCount = importedCurves.length, - legendPosX = parseInt(userSettings.analyser_legend.left) / 100 * WIDTH, - legendPosY = parseInt(userSettings.analyser_legend.top) / 100 * HEIGHT, + const spectrumCount = importedCurves.curvesCount(), + legendPosX = left / 100 * WIDTH, + legendPosY = top / 100 * HEIGHT, rowHeight = 16, padding = 4, - legendWidth = parseInt(userSettings.analyser_legend.width) / 100 * WIDTH, - legendHeight = spectrumCount * rowHeight + 3 * padding, + legendWidth = width / 100 * WIDTH, + legendHeight = spectrumCount * rowHeight + padding, legendArea = new Path2D(); canvasCtx.save(); @@ -450,10 +471,11 @@ GraphSpectrumPlot._drawLegend = function (canvasCtx, WIDTH, HEIGHT, importedCurv canvasCtx.strokeRect(legendPosX, legendPosY, legendWidth, legendHeight); canvasCtx.font = `${this._drawingParams.fontSizeFrameLabelFullscreen}pt ${DEFAULT_FONT_FACE}`; canvasCtx.textAlign = "left"; + canvasCtx.textBaseline = "middle"; // Ensure proper vertical centering for (let row = 0; row < spectrumCount; row++) { - const curvesName = importedCurves[row].name; - const Y = legendPosY + padding + rowHeight * (row + 1); - canvasCtx.strokeStyle = this.curvesColors[row]; + const curvesName = importedCurves.getCurve(row).name; + const Y = legendPosY + padding + rowHeight * row + rowHeight / 2; // Center text vertically + canvasCtx.strokeStyle = this.getCurveColor(row); canvasCtx.strokeText(curvesName, legendPosX + padding, Y); } canvasCtx.restore(); @@ -1781,6 +1803,9 @@ GraphSpectrumPlot.importCurvesFromCSV = function(files) { this._importedSpectrums.importCurvesFromCSV(files); break; case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: + if (this._importedPSD.curvesCount() === 0) { + this.addCurrentSpectrumIntoImport(); // Add current main spectrum to import for the first imported file to have same behavior like first Ctrl+Mouse click selection. + } this._importedPSD.importCurvesFromCSV(files); break; default: @@ -1792,10 +1817,43 @@ GraphSpectrumPlot.importCurvesFromCSV = function(files) { GraphSpectrumPlot.removeImportedCurves = function() { switch (this._spectrumType) { case SPECTRUM_TYPE.FREQUENCY: - this._importedSpectrums.removeCurves(); + this._importedSpectrums.removeAllCurves(); break; case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: - this._importedPSD.removeCurves(); + this._importedPSD.removeAllCurves(); break; } }; + +GraphSpectrumPlot.isNewComparedCurve = function(name) { + return this._importedPSD.isNewCurve(name); +}; + +GraphSpectrumPlot.addCurrentSpectrumIntoImport = function() { + if (this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY && + this.isNewComparedCurve(this._fftData.fieldName)) { + const fftLength = this._fftData.fftLength; + const frequencyStep = 0.5 * this._fftData.blackBoxRate / fftLength; + const points = []; + for (let index = 0; index < fftLength; index++) { + const frequency = frequencyStep * index; + points.push({ + x: frequency, + y: this._fftData.fftOutput[index], + }); + } + this._importedPSD.addCurve(points, this._fftData.fieldName); + } +}; + +GraphSpectrumPlot.removeComparedCurve = function(name) { + this._importedPSD.removeCurve(name); +}; + +GraphSpectrumPlot.isMultiSpectrum = function() { + return !this._importedPSD.isEmpty(); +}; + +GraphSpectrumPlot.isImportedCurvesMaxCount = function() { + return this._importedPSD.isFull(); +}; diff --git a/src/grapher.js b/src/grapher.js index ca177a0b..4207fc2c 100644 --- a/src/grapher.js +++ b/src/grapher.js @@ -1240,10 +1240,23 @@ export function FlightLogGrapher( }; // Add option toggling - this.setDrawAnalyser = function (state) { + this.setDrawAnalyser = function (state, ctrlKey = false) { + if (state) { + if (ctrlKey) { + analyser.prepareSpectrumForComparison(); + } else if (this.hasMultiSpectrumAnalyser()) { + analyser.removeImportedSpectrums(); // Remove imported spectrums by simple mouse click at the any curves legend + } + } + options.drawAnalyser = state; }; + // Add option toggling + this.hasMultiSpectrumAnalyser = function () { + return analyser.isMultiSpectrum(); + }; + // Add analyser zoom toggling this.setAnalyser = function (state) { analyser.setFullscreen(state); diff --git a/src/main.js b/src/main.js index 95a15ec9..0ae00a79 100644 --- a/src/main.js +++ b/src/main.js @@ -961,9 +961,18 @@ function BlackboxLogViewer() { updateCanvasSize(); } - function onLegendSelectionChange(toggleAnalizer) { - hasAnalyser = toggleAnalizer ? !hasAnalyser : true; - graph.setDrawAnalyser(hasAnalyser); + function onLegendSelectionChange(toggleAnalizer, ctrlKey) { + const lockAnalyserHide = ctrlKey || graph.hasMultiSpectrumAnalyser(); + if (toggleAnalizer) { + if (lockAnalyserHide) { + hasAnalyser = true; // Do not hide analyser when ctrlKey is pressed or it has many spectrums + } else { + hasAnalyser = !hasAnalyser; // Toggle the analyser state + } + } else { + hasAnalyser = true; // Default to true when toggleAnalizer is false + } + graph.setDrawAnalyser(hasAnalyser, ctrlKey); html.toggleClass("has-analyser", hasAnalyser); prefs.set("hasAnalyser", hasAnalyser); invalidateGraph(); @@ -1771,10 +1780,10 @@ function BlackboxLogViewer() { const exportDialog = new VideoExportDialog($("#dlgVideoExport"), function(newConfig) { videoConfig = newConfig; - + prefs.set('videoConfig', newConfig); }); - + exportDialog.show( flightLog, {