diff --git a/index.html b/index.html index fb31e032..f92185f2 100644 --- a/index.html +++ b/index.html @@ -493,6 +493,8 @@

Workspace

/> + Workspace + @@ -2989,6 +2994,29 @@

%

+ + + + + + + +

%

+ + + + +

%

+ + + + +

%

+ + diff --git a/public/js/webworkers/spectrum-export-worker.js b/public/js/webworkers/spectrum-export-worker.js index 124ba8b7..a2160aad 100644 --- a/public/js/webworkers/spectrum-export-worker.js +++ b/public/js/webworkers/spectrum-export-worker.js @@ -1,11 +1,11 @@ onmessage = function(event) { const columnDelimiter = event.data.opts.columnDelimiter; const fftOutput = event.data.fftOutput; - const spectrumDataLength = fftOutput.length / 2; + const spectrumDataLength = fftOutput.length; const frequencyStep = 0.5 * event.data.blackBoxRate / spectrumDataLength; - let outText = "freq" + columnDelimiter + "value" + "\n"; - for (let index = 0; index < spectrumDataLength; index += 10) { + let outText = "x" + columnDelimiter + "y" + "\n"; + for (let index = 0; index < spectrumDataLength; index++) { const frequency = frequencyStep * index; outText += frequency.toString() + columnDelimiter + fftOutput[index].toString() + "\n"; } diff --git a/src/css/main.css b/src/css/main.css index e3be6f5a..28106486 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -690,7 +690,9 @@ html.has-analyser-fullscreen.has-analyser .analyser input#analyserMaxPSD::-webkit-inner-spin-button, .analyser input#analyserMaxPSD::-webkit-outer-spin-button, .analyser input#analyserLowLevelPSD::-webkit-inner-spin-button, -.analyser input#analyserLowLevelPSD::-webkit-outer-spin-button { +.analyser input#analyserLowLevelPSD::-webkit-outer-spin-button, +.analyser input#analyserSegmentLengthPSD::-webkit-inner-spin-button, +.analyser input#analyserSegmentLengthPSD::-webkit-outer-spin-button { -webkit-appearance: auto !important; -moz-appearance: auto !important; appearance: auto !important; @@ -744,6 +746,22 @@ html.has-analyser-fullscreen.has-analyser font-size: 12px; } +.analyser input#analyserSegmentLengthPSD { + width: 80px; + height: 20px; + left: 0px; + top: 50px; +} + +.analyser label#analyserSegmentLengthPSDLabel { + position:absolute; + color:gray; + width: 50px; + height: 20px; + left: 0px; + top: 28px; +} + .analyser input.onlyFullScreen { display: none; padding: 3px; diff --git a/src/graph_imported_curves.js b/src/graph_imported_curves.js new file mode 100644 index 00000000..df507285 --- /dev/null +++ b/src/graph_imported_curves.js @@ -0,0 +1,75 @@ +export function ImportedCurves(curvesChanged) { + const maxImportCount = 5; + this._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; + }; + + this.importCurvesFromCSV = function(files) { + let importsLeft = maxImportCount - this._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; + } + }; + + reader.readAsText(file); + } + }; + + 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; + curvesChanged(); + }; +} diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index 1f5db1fe..5f66e868 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -17,7 +17,8 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { const that = this, prefs = new PrefStorage(), DEFAULT_PSD_HEATMAP_MIN = -40, - DEFAULT_PSD_HEATMAP_MAX = 10; + DEFAULT_PSD_HEATMAP_MAX = 10, + DEFAULT_PSD_SEGMENT_LENGTH = 512; let analyserZoomX = 1.0 /* 100% */, analyserZoomY = 1.0 /* 100% */, dataReload = false, @@ -35,6 +36,7 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { const analyserMinPSD = $("#analyserMinPSD"); const analyserMaxPSD = $("#analyserMaxPSD"); const analyserLowLevelPSD = $("#analyserLowLevelPSD"); + const analyserSegmentLengthPSD = $("#analyserSegmentLengthPSD"); const spectrumToolbarElem = $("#spectrumToolbar"); @@ -117,6 +119,12 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { $("#analyserLowLevelPSDLabel", parentElem).css({ left: `${newSize.width - 155}px`, }); + $("#analyserSegmentLengthPSD", parentElem).css({ + left: `${newSize.width - 120}px`, + }); + $("#analyserSegmentLengthPSDLabel", parentElem).css({ + left: `${newSize.width - 135}px`, + }); }; const dataLoad = function (fieldIndex, curve, fieldName) { @@ -147,6 +155,7 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: fftData = GraphSpectrumCalc.dataLoadPSD(analyserZoomY); + analyserSegmentLengthPSD.prop("max", fftData.maximalSegmentsLength); break; case SPECTRUM_TYPE.FREQUENCY: @@ -213,11 +222,6 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { debounce(100, function () { analyserZoomY = 1 / (analyserZoomYElem.val() / 100); GraphSpectrumPlot.setZoom(analyserZoomX, analyserZoomY); - // Recalculate PSD with updated samples per segment count - if (userSettings.spectrumType == SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY) { - dataLoad(); - GraphSpectrumPlot.setData(fftData, userSettings.spectrumType); - } that.refresh(); }), ) @@ -246,9 +250,7 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { saveOneUserSetting("psdHeatmapMin", min); analyserLowLevelPSD.prop("min", min); analyserMaxPSD.prop("min", min + 5); - if (analyserLowLevelPSD.val() < min) { - analyserLowLevelPSD.val(min).trigger("input"); - } + analyserLowLevelPSD.val(min).trigger("input"); that.refresh(); }), ) @@ -297,6 +299,42 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { }) .val(analyserMinPSD.val()); + let segmentLengthPSD = DEFAULT_PSD_SEGMENT_LENGTH; + GraphSpectrumCalc.setPointsPerSegmentPSD(segmentLengthPSD); + analyserSegmentLengthPSD + .on( + "input", + function () { + const currentValue = parseInt($(this).val()); + if (currentValue > segmentLengthPSD) { + segmentLengthPSD *= 2; + } else if (currentValue < segmentLengthPSD){ + segmentLengthPSD /= 2; + } + $(this).val(segmentLengthPSD); + // Recalculate PSD with updated samples per segment count + GraphSpectrumCalc.setPointsPerSegmentPSD(segmentLengthPSD); + dataLoad(); + GraphSpectrumPlot.setData(fftData, userSettings.spectrumType); + that.refresh(); + }, + ) + .dblclick(function (e) { + if (e.ctrlKey) { + segmentLengthPSD = DEFAULT_PSD_SEGMENT_LENGTH; + $(this).val(DEFAULT_PSD_SEGMENT_LENGTH).trigger("input"); + } + }) + .val(DEFAULT_PSD_SEGMENT_LENGTH); + + analyserSegmentLengthPSD + .on( + "keydown", + function (e) { + e.preventDefault(); + }, + ); + // Spectrum type to show userSettings.spectrumType = userSettings.spectrumType || SPECTRUM_TYPE.FREQUENCY; @@ -321,10 +359,16 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { const psdHeatMapSelected = optionSelected === SPECTRUM_TYPE.PSD_VS_THROTTLE || optionSelected === SPECTRUM_TYPE.PSD_VS_RPM; + const psdCurveSelected = + optionSelected === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY; overdrawSpectrumTypeElem.toggle(!pidErrorVsSetpointSelected); analyserZoomYElem.toggleClass( "onlyFullScreenException", - pidErrorVsSetpointSelected || psdHeatMapSelected, + pidErrorVsSetpointSelected || psdHeatMapSelected || psdCurveSelected, + ); + analyserSegmentLengthPSD.toggleClass( + "onlyFullScreenException", + !psdCurveSelected, ); analyserLowLevelPSD.toggleClass( "onlyFullScreenException", @@ -350,8 +394,14 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { "onlyFullScreenException", !psdHeatMapSelected, ); + $("#analyserSegmentLengthPSDLabel").toggleClass( + "onlyFullScreenException", + !psdCurveSelected, + ); + - $("#spectrumComparison").css("visibility", (optionSelected == 0 ? "visible" : "hidden")); + const showSpectrumsComparisonPanel = optionSelected === SPECTRUM_TYPE.FREQUENCY || optionSelected === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY; + $("#spectrumComparison").css("visibility", (showSpectrumsComparisonPanel ? "visible" : "hidden")); }) .change(); @@ -416,48 +466,27 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { }; this.importSpectrumFromCSV = function(files) { - const maxImportCount = 5; - let importsLeft = maxImportCount - GraphSpectrumPlot.getImportedSpectrumCount(); + GraphSpectrumPlot.importCurvesFromCSV(files); + }; - 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] != "freq" || header[1] != "value") { - throw new SyntaxError("Wrong spectrum CSV data format"); - } - - stringRows.shift(); - const spectrumData = stringRows.map( function(row) { - const data = row.split(","); - return { - freq: parseFloat(data[0]), - value: parseFloat(data[1]), - }; - }); - - GraphSpectrumPlot.addImportedSpectrumData(spectrumData, file.name); - } catch (e) { - alert('Spectrum data import error: ' + e.message); - return; - } - }; + this.removeImportedSpectrums = function() { + GraphSpectrumPlot.removeImportedCurves(); + }; - reader.readAsText(file); + this.getExportedFileName = function() { + let fileName = $(".log-filename").text().split(".")[0]; + switch (userSettings.spectrumType) { + case SPECTRUM_TYPE.FREQUENCY: + fileName = fileName + "_sp"; + break; + case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: + fileName = fileName + "_psd"; + break; } + return fileName; }; } catch (e) { console.error(`Failed to create analyser... error: ${e}`); } - - this.clearImportedSpectrums = function() { - GraphSpectrumPlot.clearImportedSpectrums(); - }; } diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index e5d07190..0e599460 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -35,6 +35,7 @@ export const GraphSpectrumCalc = { _flightLog : null, _sysConfig : null, _motorPoles : null, + _pointsPerSegmentPSD : 0, }; GraphSpectrumCalc.initialize = function(flightLog, sysConfig) { @@ -112,17 +113,19 @@ GraphSpectrumCalc.dataLoadFrequency = function() { return fftData; }; +GraphSpectrumCalc.setPointsPerSegmentPSD = function(pointsCount) { + this._pointsPerSegmentPSD = pointsCount; +}; + GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { const flightSamples = this._getFlightSamplesFreq(false); - const multiplier = Math.floor(1 / analyserZoomY); // 0. ... 10 - let pointsPerSegment = 2 ** (8 + multiplier); //256, 512, 1024 ... - - let overlapCount; - if (pointsPerSegment > flightSamples.samples.length) { - pointsPerSegment = flightSamples.samples.length; // Use actual sample length. It will transform to power at 2 value inside the _psd() - fft_segmented - overlapCount = 0; + let pointsPerSegment, overlapCount; + if (this._pointsPerSegmentPSD > flightSamples.samples.length) { + pointsPerSegment = flightSamples.samples.length; // Use actual sample length. It will transform to power at 2 value inside the _psd() - fft_segmented + overlapCount = 0; } else { - overlapCount = pointsPerSegment / 2; + pointsPerSegment = this._pointsPerSegmentPSD; + overlapCount = pointsPerSegment * 3 / 4; } const psd = this._psd(flightSamples.samples, pointsPerSegment, overlapCount); @@ -130,12 +133,13 @@ GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { const psdData = { fieldIndex : this._dataBuffer.fieldIndex, fieldName : this._dataBuffer.fieldName, - psdLength : psd.psdOutput.length, - psdOutput : psd.psdOutput, + fftLength : psd.psdOutput.length, + fftOutput : psd.psdOutput, blackBoxRate : this._blackBoxRate, minimum: psd.min, maximum: psd.max, maxNoiseFrequency: psd.maxNoiseFrequency, + maximalSegmentsLength: this.getNearPower2Value(flightSamples.samples.length), }; return psdData; }; diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 960ca343..c1402485 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -1,6 +1,7 @@ import { FILTER_TYPE } from "./flightlog_fielddefs"; import { constrain } from "./tools"; import { NUM_VS_BINS } from "./graph_spectrum_calc"; +import { ImportedCurves } from "./graph_imported_curves"; const BLUR_FILTER_PIXEL = 1, DEFAULT_FONT_FACE = "Verdana, Arial, sans-serif", @@ -56,10 +57,20 @@ export const GraphSpectrumPlot = window.GraphSpectrumPlot || { fontSizeFrameLabel: "6", fontSizeFrameLabelFullscreen: "9", }, - _importedSpectrumsData: [], + _importedSpectrums: null, + _importedPSD: null, + curvesColors : [ + "Blue", + "Purple", + "DeepPink", + "DarkCyan", + "Chocolate", + ], }; GraphSpectrumPlot.initialize = function (canvas, sysConfig) { + this._importedSpectrums = new ImportedCurves(() => GraphSpectrumPlot.redraw()); + this._importedPSD = new ImportedCurves(() => GraphSpectrumPlot.redraw()); this._canvasCtx = canvas.getContext("2d"); this._sysConfig = sysConfig; this._invalidateCache(); @@ -126,23 +137,7 @@ GraphSpectrumPlot.setData = function (fftData, spectrumType) { this._invalidateDataCache(); }; -GraphSpectrumPlot.getImportedSpectrumCount = function () { - return this._importedSpectrumsData.length; -}; - -GraphSpectrumPlot.addImportedSpectrumData = function (curvesData, name) { - const curve = { - points: curvesData, - name: name, - }; - this._importedSpectrumsData.push(curve); - this._invalidateCache(); - this._invalidateDataCache(); - GraphSpectrumPlot.draw(); -}; - -GraphSpectrumPlot.clearImportedSpectrums = function (curvesData) { - this._importedSpectrumsData.length = 0; +GraphSpectrumPlot.redraw = function () { this._invalidateCache(); this._invalidateDataCache(); GraphSpectrumPlot.draw(); @@ -227,11 +222,9 @@ GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { const WIDTH = canvasCtx.canvas.width; const LEFT = canvasCtx.canvas.left; const TOP = canvasCtx.canvas.top; - const PLOTTED_BUFFER_LENGTH = this._fftData.fftLength / this._zoomX; - const PLOTTED_BLACKBOX_RATE = this._fftData.blackBoxRate / this._zoomX; + const MAXIMAL_PLOTTED_FREQUENCY = 0.5 * this._fftData.blackBoxRate / this._zoomX; - canvasCtx.save(); canvasCtx.translate(LEFT, TOP); this._drawGradientBackground(canvasCtx, WIDTH, HEIGHT); @@ -268,26 +261,17 @@ GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { x += stepX; } - //Draw imported spectrums - const curvesColors = [ - "Blue", - "Purple", - "DeepPink", - "DarkCyan", - "Chocolate", - ]; - - const spectrumCount = this._importedSpectrumsData.length; + const scaleX = WIDTH / MAXIMAL_PLOTTED_FREQUENCY; + const spectrumCount = this._importedSpectrums.curvesCount(); for (let spectrumNum = 0; spectrumNum < spectrumCount; spectrumNum++) { - const curvesPonts = this._importedSpectrumsData[spectrumNum].points; + const curvesPonts = this._importedSpectrums._curvesData[spectrumNum].points; const pointsCount = curvesPonts.length; - const scaleX = 2 * WIDTH / PLOTTED_BLACKBOX_RATE * this._zoomX; canvasCtx.beginPath(); canvasCtx.lineWidth = 1; - canvasCtx.strokeStyle = curvesColors[spectrumNum]; + canvasCtx.strokeStyle = this.curvesColors[spectrumNum]; canvasCtx.moveTo(0, HEIGHT); - const filterPointsCount = 50; + const filterPointsCount = 200; for (let pointNum = 0; pointNum < pointsCount; pointNum++) { // Apply moving average filter at spectrum points to get visible line let filterStartPoint = pointNum - filterPointsCount / 2; @@ -304,39 +288,19 @@ GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { } let middleValue = 0; for (let i = filterStartPoint; i < filterStopPoint; i++) { - middleValue += curvesPonts[i].value; + middleValue += curvesPonts[i].y; } middleValue /= filterPointsCount; - canvasCtx.lineTo(curvesPonts[pointNum].freq * scaleX, HEIGHT - middleValue * fftScale); + canvasCtx.lineTo(curvesPonts[pointNum].x * scaleX, HEIGHT - middleValue * fftScale); } canvasCtx.stroke(); } //Legend draw if (this._isFullScreen && spectrumCount > 0) { - const legendPosX = 0.84 * WIDTH, - legendPosY = 0.6 * HEIGHT, - rowHeight = 16, - padding = 4, - legendWidth = 0.13 * WIDTH + padding, - legendHeight = spectrumCount * rowHeight + 3 * padding; - - const legendArea = new Path2D(); - legendArea.rect(legendPosX, legendPosY, legendWidth, legendHeight); - canvasCtx.clip(legendArea); - canvasCtx.strokeStyle = "gray"; - canvasCtx.strokeRect(legendPosX, legendPosY, legendWidth, legendHeight); - canvasCtx.font = `${this._drawingParams.fontSizeFrameLabelFullscreen}pt ${DEFAULT_FONT_FACE}`; - canvasCtx.textAlign = "left"; - for (let row = 0; row < spectrumCount; row++) { - const curvesName = this._importedSpectrumsData[row].name.split('.')[0]; - const Y = legendPosY + padding + rowHeight * (row + 1); - canvasCtx.strokeStyle = curvesColors[row]; - canvasCtx.strokeText(curvesName, legendPosX + padding, Y); - } + this._drawLegend(canvasCtx, WIDTH, HEIGHT, this._importedSpectrums._curvesData); } - canvasCtx.restore(); this._drawAxisLabel( canvasCtx, @@ -347,7 +311,7 @@ GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { ); this._drawHorizontalGridLines( canvasCtx, - PLOTTED_BLACKBOX_RATE / 2, + MAXIMAL_PLOTTED_FREQUENCY, LEFT, TOP, WIDTH, @@ -363,41 +327,67 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { const WIDTH = canvasCtx.canvas.width - ACTUAL_MARGIN_LEFT; const LEFT = canvasCtx.canvas.offsetLeft + ACTUAL_MARGIN_LEFT; const TOP = canvasCtx.canvas.offsetTop; - - const PLOTTED_BLACKBOX_RATE = this._fftData.blackBoxRate / this._zoomX; - - canvasCtx.save(); - canvasCtx.translate(LEFT, TOP); - this._drawGradientBackground(canvasCtx, WIDTH, HEIGHT); - - const pointsCount = this._fftData.psdLength; - const scaleX = 2 * WIDTH / PLOTTED_BLACKBOX_RATE * this._zoomX; - canvasCtx.beginPath(); - canvasCtx.lineWidth = 1; - canvasCtx.strokeStyle = "white"; + const MAXIMAL_PLOTTED_FREQUENCY = 0.5 * this._fftData.blackBoxRate / this._zoomX; // Allign y axis range by 10db + const minimum = Math.min(this._fftData.minimum, this._importedPSD.minY), + maximum = Math.max(this._fftData.maximum, this._importedPSD.maxY); const dbStep = 10; - const minY = Math.floor(this._fftData.minimum / dbStep) * dbStep; - let maxY = (Math.floor(this._fftData.maximum / dbStep) + 1) * dbStep; + const minY = Math.floor(minimum / dbStep) * dbStep; + let maxY = (Math.floor(maximum / dbStep) + 1) * dbStep; if (minY == maxY) { maxY = minY + 1; // prevent divide by zero } - const ticksCount = (maxY - minY) / dbStep; - const scaleY = HEIGHT / (maxY - minY); //Store vsRange for _drawMousePosition this._fftData.vsRange = { min: minY, max: maxY, }; + + const ticksCount = (maxY - minY) / dbStep; + const pointsCount = this._fftData.fftLength; + const scaleX = WIDTH / MAXIMAL_PLOTTED_FREQUENCY; + const scaleY = HEIGHT / (maxY - minY); + + 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 = PLOTTED_BLACKBOX_RATE / 2 * pointNum / pointsCount; - const y = HEIGHT - (this._fftData.psdOutput[pointNum] - minY) * scaleY; + const freq = this._fftData.blackBoxRate / 2 * pointNum / pointsCount; + if(freq > MAXIMAL_PLOTTED_FREQUENCY) { + break; + } + const y = HEIGHT - (this._fftData.fftOutput[pointNum] - minY) * scaleY; canvasCtx.lineTo(freq * scaleX, y); } canvasCtx.stroke(); + const spectrumCount = this._importedPSD.curvesCount(); + for (let spectrumNum = 0; spectrumNum < spectrumCount; spectrumNum++) { + const curvesPonts = this._importedPSD._curvesData[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) { + break; + } + 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); + } + this._drawAxisLabel( canvasCtx, this._fftData.fieldName, @@ -407,7 +397,7 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { ); this._drawHorizontalGridLines( canvasCtx, - PLOTTED_BLACKBOX_RATE / 2, + MAXIMAL_PLOTTED_FREQUENCY, LEFT, TOP, WIDTH, @@ -430,7 +420,7 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { this._drawInterestFrequency( canvasCtx, this._fftData.maxNoiseFrequency, - PLOTTED_BLACKBOX_RATE, + MAXIMAL_PLOTTED_FREQUENCY, "Max noise", WIDTH, HEIGHT, @@ -438,20 +428,46 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { "rgba(255,0,0,0.50)", 3, ); +}; + +GraphSpectrumPlot._drawLegend = function (canvasCtx, WIDTH, HEIGHT, importedCurves) { + if (!userSettings?.analyser_legend) { + return; + } + const spectrumCount = importedCurves.length, + legendPosX = parseInt(userSettings.analyser_legend.left) / 100 * WIDTH, + legendPosY = parseInt(userSettings.analyser_legend.top) / 100 * HEIGHT, + rowHeight = 16, + padding = 4, + legendWidth = parseInt(userSettings.analyser_legend.width) / 100 * WIDTH, + legendHeight = spectrumCount * rowHeight + 3 * padding, + legendArea = new Path2D(); + canvasCtx.save(); + legendArea.rect(legendPosX, legendPosY, legendWidth, legendHeight); + canvasCtx.clip(legendArea); + canvasCtx.strokeStyle = "gray"; + canvasCtx.strokeRect(legendPosX, legendPosY, legendWidth, legendHeight); + canvasCtx.font = `${this._drawingParams.fontSizeFrameLabelFullscreen}pt ${DEFAULT_FONT_FACE}`; + canvasCtx.textAlign = "left"; + for (let row = 0; row < spectrumCount; row++) { + const curvesName = importedCurves[row].name; + const Y = legendPosY + padding + rowHeight * (row + 1); + canvasCtx.strokeStyle = this.curvesColors[row]; + canvasCtx.strokeText(curvesName, legendPosX + padding, Y); + } canvasCtx.restore(); }; GraphSpectrumPlot.getPSDbyFreq = function(frequency) { - let freqIndex = Math.round(2 * frequency / this._fftData.blackBoxRate * (this._fftData.psdOutput.length - 1) ); - freqIndex = Math.min(freqIndex, this._fftData.psdOutput.length - 1); + let freqIndex = Math.round(2 * frequency / this._fftData.blackBoxRate * (this._fftData.fftOutput.length - 1) ); + freqIndex = Math.min(freqIndex, this._fftData.fftOutput.length - 1); freqIndex = Math.max(freqIndex, 0); - return this._fftData.psdOutput.length ? this._fftData.psdOutput[freqIndex] : 0; + return this._fftData.fftOutput.length ? this._fftData.fftOutput[freqIndex] : 0; }; GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) { - const PLOTTED_BLACKBOX_RATE = this._fftData.blackBoxRate / this._zoomX; - + const MAXIMAL_PLOTTED_FREQUENCY = 0.5 * this._fftData.blackBoxRate / this._zoomX; const ACTUAL_MARGIN_LEFT = this._getActualMarginLeft(); const WIDTH = canvasCtx.canvas.width - ACTUAL_MARGIN_LEFT; const HEIGHT = canvasCtx.canvas.height - MARGIN_BOTTOM; @@ -487,7 +503,7 @@ GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) ); this._drawHorizontalGridLines( canvasCtx, - PLOTTED_BLACKBOX_RATE / 2, + MAXIMAL_PLOTTED_FREQUENCY, LEFT, TOP, WIDTH, @@ -758,7 +774,7 @@ GraphSpectrumPlot._drawPidErrorVsSetpointGraphGroups = function ( GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { const HEIGHT = this._canvasCtx.canvas.height - MARGIN; const WIDTH = this._canvasCtx.canvas.width - this._getActualMarginLeft(); - const PLOTTED_BLACKBOX_RATE = this._fftData.blackBoxRate / this._zoomX; + const MAXIMAL_PLOTTED_FREQUENCY = 0.5 * this._fftData.blackBoxRate / this._zoomX; let offset = 2; // make some space! Includes the space for the mouse frequency. In this way the other elements don't move in the screen when used @@ -786,7 +802,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { canvasCtx, this._sysConfig.gyro_lowpass_dyn_hz[0], this._sysConfig.gyro_lowpass_dyn_hz[1], - PLOTTED_BLACKBOX_RATE, + MAXIMAL_PLOTTED_FREQUENCY, label, WIDTH, HEIGHT, @@ -807,7 +823,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { this._drawLowpassFilter( canvasCtx, this._sysConfig.gyro_lowpass_hz, - PLOTTED_BLACKBOX_RATE, + MAXIMAL_PLOTTED_FREQUENCY, label, WIDTH, HEIGHT, @@ -829,7 +845,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { this._drawLowpassFilter( canvasCtx, this._sysConfig.gyro_lowpass2_hz, - PLOTTED_BLACKBOX_RATE, + MAXIMAL_PLOTTED_FREQUENCY, label, WIDTH, HEIGHT, @@ -855,7 +871,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { canvasCtx, this._sysConfig.gyro_notch_hz[i], this._sysConfig.gyro_notch_cutoff[i], - PLOTTED_BLACKBOX_RATE, + MAXIMAL_PLOTTED_FREQUENCY, "GYRO Notch", WIDTH, HEIGHT, @@ -875,7 +891,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { canvasCtx, this._sysConfig.gyro_notch_hz, this._sysConfig.gyro_notch_cutoff, - PLOTTED_BLACKBOX_RATE, + MAXIMAL_PLOTTED_FREQUENCY, "GYRO Notch", WIDTH, HEIGHT, @@ -900,7 +916,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { this._drawLowpassFilter( canvasCtx, this._sysConfig.yaw_lpf_hz, - PLOTTED_BLACKBOX_RATE, + MAXIMAL_PLOTTED_FREQUENCY, "YAW LPF cutoff", WIDTH, HEIGHT, @@ -935,7 +951,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { canvasCtx, this._sysConfig.dterm_lpf_dyn_hz[0], this._sysConfig.dterm_lpf_dyn_hz[1], - PLOTTED_BLACKBOX_RATE, + MAXIMAL_PLOTTED_FREQUENCY, label, WIDTH, HEIGHT, @@ -958,7 +974,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { this._drawLowpassFilter( canvasCtx, this._sysConfig.dterm_lpf_hz, - PLOTTED_BLACKBOX_RATE, + MAXIMAL_PLOTTED_FREQUENCY, label, WIDTH, HEIGHT, @@ -982,7 +998,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { this._drawLowpassFilter( canvasCtx, this._sysConfig.dterm_lpf2_hz, - PLOTTED_BLACKBOX_RATE, + MAXIMAL_PLOTTED_FREQUENCY, label, WIDTH, HEIGHT, @@ -1005,7 +1021,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { canvasCtx, this._sysConfig.dterm_notch_hz, this._sysConfig.dterm_notch_cutoff, - PLOTTED_BLACKBOX_RATE, + MAXIMAL_PLOTTED_FREQUENCY, "D-TERM Notch", WIDTH, HEIGHT, @@ -1025,7 +1041,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { this._drawInterestFrequency( canvasCtx, this._fftData.maxNoiseFrequency, - PLOTTED_BLACKBOX_RATE, + MAXIMAL_PLOTTED_FREQUENCY, "Max noise", WIDTH, HEIGHT, @@ -1252,7 +1268,8 @@ GraphSpectrumPlot._drawHorizontalMarkerLine = function ( let realLineWidth = lineWidth || DEFAULT_MARK_LINE_WIDTH; if (realLineWidth > 5) { // is the linewidth specified as a frequency band - realLineWidth = (WIDTH * (2 * realLineWidth)) / (sampleRate / 2); + const maximalFrequency = 0.5 * this._fftData.blackBoxRate / this._zoomX; + realLineWidth = (WIDTH * (2 * realLineWidth)) / maximalFrequency; } if (realLineWidth < 1) { realLineWidth = 1; @@ -1307,7 +1324,7 @@ GraphSpectrumPlot._drawGradientBackground = function ( GraphSpectrumPlot._drawInterestFrequency = function ( canvasCtx, frequency, - sampleRate, + maximalFrequency, label, WIDTH, HEIGHT, @@ -1326,7 +1343,7 @@ GraphSpectrumPlot._drawInterestFrequency = function ( return this._drawVerticalMarkerLine( canvasCtx, frequency, - sampleRate / 2, + maximalFrequency, interestLabel, WIDTH, HEIGHT, @@ -1339,7 +1356,7 @@ GraphSpectrumPlot._drawInterestFrequency = function ( GraphSpectrumPlot._drawLowpassFilter = function ( canvasCtx, frequency, - sampleRate, + maximalFrequency, label, WIDTH, HEIGHT, @@ -1351,7 +1368,7 @@ GraphSpectrumPlot._drawLowpassFilter = function ( return this._drawVerticalMarkerLine( canvasCtx, frequency, - sampleRate / 2, + maximalFrequency, lpfLabel, WIDTH, HEIGHT, @@ -1365,7 +1382,7 @@ GraphSpectrumPlot._drawLowpassDynFilter = function ( canvasCtx, frequency1, frequency2, - sampleRate, + maximalFrequency, label, WIDTH, HEIGHT, @@ -1380,7 +1397,7 @@ GraphSpectrumPlot._drawLowpassDynFilter = function ( const x1 = this._drawVerticalMarkerLine( canvasCtx, frequency1, - sampleRate / 2, + maximalFrequency, dynFilterLabel, WIDTH, HEIGHT, @@ -1400,7 +1417,7 @@ GraphSpectrumPlot._drawLowpassDynFilter = function ( const x2 = this._drawVerticalMarkerLine( canvasCtx, frequency2, - sampleRate / 2, + maximalFrequency, null, WIDTH, HEIGHT, @@ -1425,7 +1442,7 @@ GraphSpectrumPlot._drawLowpassDynFilter = function ( * frequency = (throttle - (throttle * throttle * throttle) / 3.0f) * 1.5f; * but need to scale the 1.5f using the max value of the dyn filter */ - const scale = frequency2 / (sampleRate / 2); + const scale = frequency2 / (maximalFrequency); const NUMBER_OF_POINTS = this._isFullScreen ? 30 : 10; let startPlot = false; @@ -1467,7 +1484,7 @@ GraphSpectrumPlot._drawNotchFilter = function ( canvasCtx, center, cutoff, - sampleRate, + maximalFrequency, label, WIDTH, HEIGHT, @@ -1475,8 +1492,8 @@ GraphSpectrumPlot._drawNotchFilter = function ( stroke, lineWidth ) { - const cutoffX = (WIDTH * cutoff) / (sampleRate / 2); - const centerX = (WIDTH * center) / (sampleRate / 2); + const cutoffX = (WIDTH * cutoff) / (maximalFrequency); + const centerX = (WIDTH * center) / (maximalFrequency); canvasCtx.beginPath(); canvasCtx.lineWidth = lineWidth || DEFAULT_MARK_LINE_WIDTH; @@ -1512,7 +1529,7 @@ GraphSpectrumPlot._drawNotchFilter = function ( this._drawVerticalMarkerLine( canvasCtx, center, - sampleRate / 2, + maximalFrequency, labelNotch, WIDTH, HEIGHT, @@ -1543,17 +1560,15 @@ GraphSpectrumPlot._drawMousePosition = function ( this._spectrumType === SPECTRUM_TYPE.PSD_VS_RPM ) { // Calculate frequency at mouse - const sampleRate = this._fftData.blackBoxRate / this._zoomX; + const maximalFrequency = 0.5 * this._fftData.blackBoxRate / this._zoomX; const marginLeft = this._getActualMarginLeft(); - mouseFrequency = - ((mouseX - marginLeft) / WIDTH) * - (this._fftData.blackBoxRate / this._zoomX / 2); - if (mouseFrequency >= 0 && mouseFrequency <= sampleRate) { + mouseFrequency = ((mouseX - marginLeft) / WIDTH) * maximalFrequency; + if (mouseFrequency >= 0 && mouseFrequency <= maximalFrequency) { this._drawInterestFrequency( canvasCtx, mouseFrequency, - sampleRate, + maximalFrequency, "", WIDTH, HEIGHT, @@ -1563,17 +1578,6 @@ GraphSpectrumPlot._drawMousePosition = function ( ); } - if (this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY) { - const psdLabel = Math.round(this.getPSDbyFreq(mouseFrequency)).toString() + "dBm/Hz"; - this._drawAxisLabel( - canvasCtx, - psdLabel, - mouseX - 30, - mouseY - 4, - "left", - ); - } - // Y axis let unitLabel; switch (this._spectrumType) { @@ -1596,7 +1600,23 @@ GraphSpectrumPlot._drawMousePosition = function ( const val_min = this._fftData.vsRange.min; const val_max = this._fftData.vsRange.max; const vsArgValue = (1 - mouseY / HEIGHT) * (val_max - val_min) + val_min; - if (vsArgValue >= val_min && vsArgValue <= val_max) { + + if (this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY && this._importedPSD.curvesCount() == 0) { // single PSD spectrum + const currentPSD = this.getPSDbyFreq(mouseFrequency); + const psdLabel = Math.round(currentPSD).toString() + unitLabel; + this._drawHorizontalMarkerLine( + canvasCtx, + currentPSD, + val_min, + val_max, + psdLabel, + WIDTH, + HEIGHT, + OFFSET, + stroke, + lineWidth, + ); + } else if (vsArgValue >= val_min && vsArgValue <= val_max) { const valueLabel = `${vsArgValue.toFixed(0)}${unitLabel}`; this._drawHorizontalMarkerLine( canvasCtx, @@ -1610,18 +1630,17 @@ GraphSpectrumPlot._drawMousePosition = function ( stroke, lineWidth ); - - if (this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE || - this._spectrumType === SPECTRUM_TYPE.PSD_VS_RPM) { - const label = Math.round(this.getValueFromMatrixFFT(mouseFrequency, vsArgValue)).toString() + "dBm/Hz"; - this._drawAxisLabel( - canvasCtx, - label, - mouseX - 30, - mouseY - 4, - "left", - ); - } + } + if (this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_RPM) { + const label = Math.round(this.getValueFromMatrixFFT(mouseFrequency, vsArgValue)).toString() + "dBm/Hz"; + this._drawAxisLabel( + canvasCtx, + label, + mouseX - 30, + mouseY - 4, + "left", + ); } } } else if (this._spectrumType === SPECTRUM_TYPE.PIDERROR_VS_SETPOINT) { @@ -1755,3 +1774,28 @@ GraphSpectrumPlot._drawRateWarning = function (canvasCtx) { canvasCtx.restore(); } }; + +GraphSpectrumPlot.importCurvesFromCSV = function(files) { + switch (this._spectrumType) { + case SPECTRUM_TYPE.FREQUENCY: + this._importedSpectrums.importCurvesFromCSV(files); + break; + case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: + this._importedPSD.importCurvesFromCSV(files); + break; + default: + console.warn(`Import not supported for spectrum type: ${this._spectrumType}`); + break; + } +}; + +GraphSpectrumPlot.removeImportedCurves = function() { + switch (this._spectrumType) { + case SPECTRUM_TYPE.FREQUENCY: + this._importedSpectrums.removeCurves(); + break; + case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: + this._importedPSD.removeCurves(); + break; + } +}; diff --git a/src/main.js b/src/main.js index 83d2088d..95a15ec9 100644 --- a/src/main.js +++ b/src/main.js @@ -1149,11 +1149,17 @@ function BlackboxLogViewer() { CsvExporter(flightLog, options).dump(onSuccess); } - function exportSpectrumToCsv(file, options = {}) { + function exportSpectrumToCsv(options = {}) { + const fileName = graph.getAnalyser().getExportedFileName(); + if (fileName == null) { + console.warn("The export is not supported for this spectrum type"); + return; + } + const onSuccess = createExportCallback( "csv", "text/csv", - file, + fileName, performance.now(), ); graph.getAnalyser().exportSpectrumToCSV(onSuccess, options); @@ -1732,7 +1738,7 @@ function BlackboxLogViewer() { }); $("#btn-spectrum-export").click(function (e) { - exportSpectrumToCsv("bf_spectrum"); + exportSpectrumToCsv(); e.preventDefault(); }); @@ -1741,12 +1747,12 @@ function BlackboxLogViewer() { e.preventDefault(); e.target.value = ""; }); - + $("#btn-spectrum-clear").click(function (e) { - graph.getAnalyser().clearImportedSpectrums(); + graph.getAnalyser().removeImportedSpectrums(); e.preventDefault(); }); - + $(".btn-gpx-export").click(function (e) { setGraphState(GRAPH_STATE_PAUSED); exportGpx(); diff --git a/src/user_settings_dialog.js b/src/user_settings_dialog.js index c525d1f1..0fc7928b 100644 --- a/src/user_settings_dialog.js +++ b/src/user_settings_dialog.js @@ -231,6 +231,11 @@ export function UserSettingsDialog(dialog, onLoad, onSave) { top: "60%", // position from top (as a percentage of height) size: "35%", // size (as a percentage of width) }, + analyser_legend: { + left: "88%", // position from left (as a percentage of width) + top: "7%", // position from top (as a percentage of height) + width: "10%", // legend width + }, map: { left: "2%", // position from left (as a percentage of width) top: "5%", // position from top (as a percentage of height) @@ -301,6 +306,11 @@ export function UserSettingsDialog(dialog, onLoad, onSave) { left: `${$('.analyser-settings input[name="analyser-left"]').val()}%`, size: `${$('.analyser-settings input[name="analyser-size"]').val()}%`, }, + analyser_legend: { + top: `${$('.analyser-settings input[name="analyser-legend-top"]').val()}%`, + left: `${$('.analyser-settings input[name="analyser-legend-left"]').val()}%`, + width: `${$('.analyser-settings input[name="analyser-legend-width"]').val()}%`, + }, map: { top: `${$('.map-settings input[name="map-top"]').val()}%`, left: `${$('.map-settings input[name="map-left"]').val()}%`, @@ -649,6 +659,16 @@ export function UserSettingsDialog(dialog, onLoad, onSave) { $('.analyser-settings input[name="analyser-size"]').val( parseInt(currentSettings.analyser.size) ); + + $('.analyser-settings input[name="analyser-legend-top"]').val( + parseInt(currentSettings.analyser_legend.top), + ); + $('.analyser-settings input[name="analyser-legend-left"]').val( + parseInt(currentSettings.analyser_legend.left), + ); + $('.analyser-settings input[name="analyser-legend-width"]').val( + parseInt(currentSettings.analyser_legend.width), + ); $('.map-settings input[name="map-top"]').val( parseInt(currentSettings.map.top) );