@rfkit/spectrum-analyzer
Version:
A high-performance spectrum analyzer library for RF signal processing, supporting real-time spectrum analysis, waterfall display, and multi-segment frequency scanning
1 lines • 41.7 kB
JavaScript
const LEVEL_STREAM={DEFAULT_CACHE_TIME:15e3,DEFAULT_GRANULARITY:10,DEFAULT_RANGE:[-20,100]};const DEFAULT_LEVEL_STREAM_CONFIG={cacheTime:LEVEL_STREAM.DEFAULT_CACHE_TIME,granularity:LEVEL_STREAM.DEFAULT_GRANULARITY,range:LEVEL_STREAM.DEFAULT_RANGE,maxPoints:void 0,onLevelStreamUpdate:data=>{}};class LevelStreamAnalyzer{config;spectrumData=[];probabilityData=new Map;count=0;constructor(config){this.config={...DEFAULT_LEVEL_STREAM_CONFIG,...config}}reset(){this.spectrumData=[];this.probabilityData=new Map;this.count=0}setConfig(config){this.config={...DEFAULT_LEVEL_STREAM_CONFIG,...this.config,...config};if(config.cacheTime&&this.config.cacheTime!==config.cacheTime||config.granularity&&this.config.granularity!==config.granularity||void 0!==config.maxPoints&&this.config.maxPoints!==config.maxPoints)this.reset();if(config.range)this.outputData()}process(level,timestamp){this.removeExpiredData();this.addNewData(level,timestamp);this.updateProbability(level);this.outputData()}updateProbability(level){const{granularity}=this.config;this.count++;const bin=Math.round(level/granularity)*granularity;let binCount=this.probabilityData.get(bin)||0;binCount++;this.probabilityData.set(bin,binCount)}getAll(){return this.probabilityData}removeExpiredData(){const{cacheTime,maxPoints}=this.config;if(0===this.spectrumData.length)return;if(void 0!==maxPoints&&maxPoints>0){if(this.spectrumData.length>maxPoints)this.spectrumData=this.spectrumData.slice(-maxPoints);return}const now=Date.now();if(cacheTime<=0){this.spectrumData=[];return}let left=0;let right=this.spectrumData.length-1;while(left<=right){const mid=Math.floor((left+right)/2);if(now-this.spectrumData[mid].timestamp>=cacheTime)left=mid+1;else right=mid-1}this.spectrumData=this.spectrumData.slice(left)}addNewData(level,timestamp){this.spectrumData.push({value:level,timestamp})}outputData(){const{range,granularity,onLevelStreamUpdate}=this.config;const rangeMin=range[0];const rangeMax=range[1];const probabilityRangeData=new Array(Math.round((rangeMax-rangeMin)/granularity)+1).fill(0);for(const[bin,count]of this.probabilityData)if(bin>=rangeMin&&bin<=rangeMax){const index=Math.round((rangeMax-bin)/granularity);if(this.count>0)probabilityRangeData[index]=count/this.count*100}onLevelStreamUpdate?.({probabilityRangeData:new Float32Array(probabilityRangeData),spectrumData:new Float32Array(this.spectrumData.map(item=>item.value)),timestampData:new Float64Array(this.spectrumData.map(item=>item.timestamp))})}}const SPECTRUM={INITIAL_VALUE:Number.NaN,WATERFALL_MAX_FRAMES:100,OUTPUT_POINTS:1001};const PEAK_DETECTION={MAX_PEAKS:2};const RF_UNIT=null;const FLUORESCENCE={UPDATE_INTERVAL:1,LEVEL_MIN:-20,LEVEL_MAX:140,LEVEL_RANGE:161};const DEFAULT_SPECTRUM_CONFIG={maxPoints:SPECTRUM.OUTPUT_POINTS,waterfallMaxFrames:SPECTRUM.WATERFALL_MAX_FRAMES,initialValue:SPECTRUM.INITIAL_VALUE,processing:{enableWaterfall:false,enableMetrics:false,enableFluorescence:false,enablePeakStats:false,fluorescenceUpdateInterval:FLUORESCENCE.UPDATE_INTERVAL},peakDetection:{maxPeaks:PEAK_DETECTION.MAX_PEAKS},outputPoints:SPECTRUM.OUTPUT_POINTS,outputRange:{start:0,end:SPECTRUM.OUTPUT_POINTS},templateTolerance:0};const ERROR_MESSAGES={EMPTY_SEGMENTS:"频段配置不能为空",INVALID_CONFIG:"必须且只能配置 segments 或 bandwidthConfig 其中之一",EMPTY_BANDWIDTH:"bandwidthConfig 不能为空",INVALID_SEGMENT:index=>`无效的段索引: ${index}`,INDEX_OUT_OF_BOUNDS:index=>`索引超出范围: ${index}`,INVALID_ANTENNA_FACTOR_LENGTH:points=>`天线因子数据长度必须等于实时数据长度 (${points})`,INVALID_ANTENNA_FACTOR:"天线因子数据必须是有效的正数",INVALID_SAMPLING_RANGE:"采样范围无效",INVALID_MAX_POINTS:"点数必须大于0",INVALID_LENGTH:expected=>`频率占用度数据长度不匹配,期望长度为 ${expected}`};class SpectrumError extends Error{code;details;constructor(message,code,details){super(message),this.code=code,this.details=details;this.name="SpectrumError"}}class DataValidationError extends SpectrumError{constructor(message,details){super(message,"DATA_VALIDATION_ERROR",details)}}class IndexOutOfBoundsError extends SpectrumError{index;constructor(message,index,details){super(message,"INDEX_OUT_OF_BOUNDS_ERROR",{index,...details||{}}),this.index=index}}const arrayKeepAttribute=(source,target)=>{if(void 0!==source.max)target.max=source.max;if(void 0!==source.maxIndex)target.maxIndex=source.maxIndex;if(void 0!==source.timestamp)target.timestamp=source.timestamp;if(void 0!==source.progress)target.progress=source.progress};const resample=({realData,antennaFactorData,antennaFactorSwitch=false,outputPoints})=>{const realDataLength=realData.length;const isLessThanDataLength=realDataLength<=outputPoints;const outputLength=isLessThanDataLength?realDataLength:outputPoints;const srcIndexCache=new Uint32Array(outputLength);const realOutputData=new Float32Array(outputLength);realOutputData.timestamp=realData.timestamp;if(isLessThanDataLength){if(!antennaFactorSwitch){for(let i=0;i<realDataLength;i++)srcIndexCache[i]=i;realOutputData.set(realData);return{realOutputData,srcIndexCache}}for(let i=0;i<realDataLength;i++){srcIndexCache[i]=i;realOutputData[i]=realData[i]+antennaFactorData[i]}return{realOutputData,srcIndexCache}}const ratio=realDataLength/outputPoints;let pos=ratio/2;for(let i=0;i<outputLength;i++){const start=Math.floor(pos);const end=Math.min(Math.floor(pos+ratio),realDataLength);let maxValue=realData[start];let maxIndex=start;for(let j=start+1;j<end;j++)if(realData[j]>maxValue){maxValue=realData[j];maxIndex=j}realOutputData[i]=antennaFactorSwitch?maxValue+antennaFactorData[maxIndex]:maxValue;srcIndexCache[i]=maxIndex;pos+=ratio}return{realOutputData,srcIndexCache}};const resampleMultiple=({antennaFactorData,antennaFactorSwitch=false,outputPoints,realData,maxData,minData,avgData,templateData,backgroundNoiseData,fluorescenceData,occupancyData,enablePeakStats=false,maxPeaks=2,outputRangeStart=0})=>{const realDataLength=realData.length;const isLessThanDataLength=realDataLength<=outputPoints;const outputLength=isLessThanDataLength?realDataLength:outputPoints;const hasMaxData=maxData&&maxData.length>0;const hasMinData=minData&&minData.length>0;const hasAvgData=avgData&&avgData.length>0;const hasTemplateData=templateData&&templateData.length>0;const hasOccupancyData=occupancyData&&occupancyData.length>0;const hasBackgroundNoiseData=backgroundNoiseData&&backgroundNoiseData.length>0;const hasFluorescenceStats=fluorescenceData&&fluorescenceData.length>0;const candidatePeaks=[];const applyAntennaFactor=antennaFactorSwitch?(value,index)=>value+antennaFactorData[index]:value=>value;const realOutputData=new Float32Array(outputLength);realOutputData.timestamp=realData.timestamp;const maxOutputData=new Float32Array(hasMaxData?outputLength:0);const minOutputData=new Float32Array(hasMinData?outputLength:0);const avgOutputData=new Float32Array(hasAvgData?outputLength:0);const templateOutputData=new Float32Array(hasTemplateData?outputLength:0);const occupancyOutputData=new Float32Array(hasOccupancyData?outputLength:0);const backgroundNoiseOutputData=new Float32Array(hasBackgroundNoiseData?outputLength:0);let fluorescenceOutputData=new Array(hasFluorescenceStats?outputLength:0).fill(null);const srcIndexCache=new Uint32Array(outputLength);if(isLessThanDataLength){for(let i=0;i<realDataLength;i++)srcIndexCache[i]=i;if(antennaFactorSwitch)for(let i=0;i<realDataLength;i++){const antennaFactor=antennaFactorData[i];realOutputData[i]=realData[i]+antennaFactor;if(hasMaxData)maxOutputData[i]=maxData[i]+antennaFactor;if(hasMinData)minOutputData[i]=minData[i]+antennaFactor;if(hasAvgData)avgOutputData[i]=avgData[i]+antennaFactor;if(hasTemplateData)templateOutputData[i]=templateData[i]+antennaFactor;if(hasOccupancyData)occupancyOutputData[i]=occupancyData[i];if(hasBackgroundNoiseData)backgroundNoiseOutputData[i]=backgroundNoiseData[i]+antennaFactor;if(hasFluorescenceStats){const fluorescence=fluorescenceData[i];const newFluorescence=new Map;for(const[key,value]of fluorescence)newFluorescence.set(key+antennaFactor,value);fluorescenceOutputData[i]=newFluorescence}}else{realOutputData.set(realData);if(hasMaxData)maxOutputData.set(maxData);if(hasMinData)minOutputData.set(minData);if(hasAvgData)avgOutputData.set(avgData);if(hasTemplateData)templateOutputData.set(templateData);if(hasOccupancyData)occupancyOutputData.set(occupancyData);if(hasBackgroundNoiseData)backgroundNoiseOutputData.set(backgroundNoiseData);if(hasFluorescenceStats)fluorescenceOutputData=fluorescenceData}if(enablePeakStats)for(let i=0;i<realDataLength;i++){const value=realOutputData[i];if(Number.isFinite(value))candidatePeaks.push({index:i,srcIndex:i+outputRangeStart,value})}}else{const ratio=realDataLength/outputPoints;let pos=ratio/2;for(let i=0;i<outputLength;i++){const start=Math.floor(pos);const end=Math.min(Math.floor(pos+ratio),realDataLength);let realMaxVal=realData[start];let realMaxIdx=start;let maxVal=hasMaxData?maxData[start]:0;let maxIdx=start;let minVal=hasMinData?minData[start]:0;let minIdx=start;let occupancyMaxVal=hasOccupancyData?occupancyData[start]:0;for(let j=start+1;j<end;j++){if(realData[j]>realMaxVal){realMaxVal=realData[j];realMaxIdx=j}if(hasMaxData&&maxData[j]>maxVal){maxVal=maxData[j];maxIdx=j}if(hasMinData&&minData[j]<minVal){minVal=minData[j];minIdx=j}if(hasOccupancyData&&occupancyData[j]>occupancyMaxVal)occupancyMaxVal=occupancyData[j]}const finalValue=applyAntennaFactor(realMaxVal,realMaxIdx);realOutputData[i]=finalValue;srcIndexCache[i]=realMaxIdx;if(enablePeakStats&&Number.isFinite(finalValue))candidatePeaks.push({index:i,srcIndex:realMaxIdx+outputRangeStart,value:finalValue});if(hasMaxData)maxOutputData[i]=applyAntennaFactor(maxVal,maxIdx);if(hasMinData)minOutputData[i]=applyAntennaFactor(minVal,minIdx);if(hasAvgData){const avgValue=avgData[realMaxIdx];avgOutputData[i]=applyAntennaFactor(avgValue,realMaxIdx)}if(hasTemplateData){const templateValue=templateData[realMaxIdx];templateOutputData[i]=applyAntennaFactor(templateValue,realMaxIdx)}if(hasOccupancyData)occupancyOutputData[i]=occupancyMaxVal;if(hasBackgroundNoiseData){const backgroundNoiseValue=backgroundNoiseData[realMaxIdx];backgroundNoiseOutputData[i]=applyAntennaFactor(backgroundNoiseValue,realMaxIdx)}if(hasFluorescenceStats){const fluorescence=fluorescenceData[realMaxIdx];const newFluorescence=new Map;for(const[key,value]of fluorescence)newFluorescence.set(applyAntennaFactor(key,realMaxIdx),value);fluorescenceOutputData[i]=newFluorescence}pos+=ratio}}if(enablePeakStats&&candidatePeaks.length>0){candidatePeaks.sort((a,b)=>b.value-a.value);const finalPeaks=candidatePeaks.slice(0,maxPeaks);realOutputData.peaks={peaks:finalPeaks,timestamp:realData.timestamp}}return{realOutputData,maxOutputData,minOutputData,avgOutputData,templateOutputData,occupancyOutputData,backgroundNoiseOutputData,srcIndexCache,fluorescenceOutputData}};const resampleExtraData=(extraData,antennaFactorData,antennaFactorSwitch,srcIndexCache,getOutputData)=>{if(!extraData||0===Object.keys(extraData).length)return{};const extraOutputData={};const extraDataKeys=Object.keys(extraData);const outputLength=srcIndexCache.length;for(let i=0;i<extraDataKeys.length;i++)extraOutputData[extraDataKeys[i]]=new Float32Array(outputLength);for(let k=0;k<extraDataKeys.length;k++){const key=extraDataKeys[k];const sourceArray=getOutputData(extraData[key]);const outputArray=extraOutputData[key];if(antennaFactorSwitch)for(let i=0;i<outputLength;i++){const srcIndex=srcIndexCache[i];outputArray[i]=sourceArray[srcIndex]+antennaFactorData[srcIndex]}else for(let i=0;i<outputLength;i++){const srcIndex=srcIndexCache[i];outputArray[i]=sourceArray[srcIndex]}}return extraOutputData};const findExceedingDatasCore=({realData,maxData,minData,templateData,tolerance=0,startIndex=0,endIndex,usePreallocation=false})=>{const{timestamp}=realData;const actualEndIndex=endIndex??realData.length;const segments=[];let start=-1;let sum=0;let count=0;if(usePreallocation&&0===startIndex&&actualEndIndex===realData.length){const exceedingFlags=new Uint8Array(realData.length);for(let i=0;i<realData.length;i++)exceedingFlags[i]=realData[i]>templateData[i]+tolerance?1:0;for(let i=0;i<realData.length;i++)if(exceedingFlags[i]){if(-1===start){start=i;sum=realData[i];count=1}else{sum+=realData[i];count++}}else if(-1!==start){const end=i-1;if(end-start+1>=2){const avgValue=sum/count;const middleIndex=start+(end-start>>1);segments.push({maxValue:maxData[middleIndex],minValue:minData[middleIndex],avgValue,timestamp,startIndex:start,endIndex:end})}start=-1}}else for(let i=startIndex;i<Math.min(actualEndIndex,realData.length);i++){const isExceeding=realData[i]>templateData[i]+tolerance;if(isExceeding){if(-1===start){start=i;sum=realData[i];count=1}else{sum+=realData[i];count++}}else if(-1!==start){const end=i-1;if(end-start+1>=2){const avgValue=sum/count;const middleIndex=start+(end-start>>1);segments.push({maxValue:maxData[middleIndex],minValue:minData[middleIndex],avgValue,timestamp,startIndex:start,endIndex:end})}start=-1}}if(-1!==start){const end=actualEndIndex-1;if(end-start+1>=2){const avgValue=sum/count;const middleIndex=start+(end-start>>1);segments.push({maxValue:maxData[middleIndex],minValue:minData[middleIndex],avgValue,timestamp,startIndex:start,endIndex:end})}}return segments};const findExceedingDatas=({realData,maxData,minData,templateData,tolerance=0})=>{if(!templateData||0===templateData.length)return[];return findExceedingDatasCore({realData,maxData,minData,templateData,tolerance,usePreallocation:true})};const findExceedingDatasIncremental=({realData,maxData,minData,templateData,tolerance=0,startIndex,endIndex,previousSegments=[]})=>{if(!templateData||0===templateData.length||startIndex>=endIndex)return previousSegments;const newSegments=findExceedingDatasCore({realData,maxData,minData,templateData,tolerance,startIndex,endIndex});return[...previousSegments,...newSegments]};function isAllValid(arr){for(let i=0;i<arr.length;i++)if(!Number.isFinite(arr[i]))return false;return true}var types_ExtraDataMode=/*#__PURE__*/function(ExtraDataMode){ExtraDataMode["REPLACE"]="replace";ExtraDataMode["MERGE"]="merge";ExtraDataMode["CLEAR"]="clear";return ExtraDataMode}({});var SpectrumAnalyzer_PendingDataType=/*#__PURE__*/null;class SpectrumAnalyzer{config=DEFAULT_SPECTRUM_CONFIG;segments=[];antennaFactorData;antennaFactorSwitch;realData;maxData;minData;avgData;templateData;occupancyData;backgroundNoiseData;waterfallData;srcIndexCache;realOutputData;maxOutputData;minOutputData;avgOutputData;templateOutputData;occupancyOutputData;backgroundNoiseOutputData;waterfallOutputData;scanProgress;lastIndex;processTimes;lastProcessTime;cachedExceedingDatas=[];fluorescenceData;fluorescenceMaxCount=0;fluorescenceUpdateCounter=0;extraData={};extraOutputData={};pendingDataFlags={["occupancy"]:false,["template"]:false,["backgroundNoise"]:false,["extra"]:false};hasPendingData=false;hasProcessedData=false;isProcessing=false;onSpectrumUpdate;constructor(config){const{onSpectrumUpdate,maxPoints}=config;this.onSpectrumUpdate=onSpectrumUpdate;this.config={...DEFAULT_SPECTRUM_CONFIG,...config,processing:{...DEFAULT_SPECTRUM_CONFIG.processing,...config.processing},peakDetection:{...DEFAULT_SPECTRUM_CONFIG.peakDetection,...config.peakDetection},outputRange:{start:config.outputRange?.start??0,end:config.outputRange?.end??maxPoints??SPECTRUM.OUTPUT_POINTS}};this.reset()}process({data,timestamp,segmentOffset=0,offset=0,fluorescenceData,fluorescenceMaxCount}){const processStartTime=performance.now();try{if(!data?.length)return;this.isProcessing=true;this.hasProcessedData=true;let index=offset;if(this.segments.length){const segment=this.segments[segmentOffset];if(!segment)throw new DataValidationError(ERROR_MESSAGES.INVALID_SEGMENT(segmentOffset));index=segment.startIndex+offset;this.scanProgress=(index+data.length)/this.config.maxPoints}else if(data.length!==this.config.maxPoints)this.updateMaxPoints(data.length);if(fluorescenceData&&void 0!==fluorescenceMaxCount){this.fluorescenceData=fluorescenceData;this.fluorescenceMaxCount=fluorescenceMaxCount}this.validateInput(data,index);const{maxPoints}=this.config;const endIndex=index+data.length;this.lastIndex=endIndex;const isOver=endIndex>=maxPoints||endIndex<this.lastIndex;this.realData.set(data,index);this.realData.timestamp=timestamp;this.processDataPoints(data,index,isOver);const processedData=this.resampleDataSeries();if(isOver&&processedData.realData)this.updateWaterfallData(this.realData,processedData.realData);const templateOverData=this.updateTemplateOverData(isOver,isOver?void 0:{startIndex:index,endIndex:index+data.length});const pendingData=this.processPendingData();this.notifySpectrumUpdate({...processedData,...pendingData||{},extraData:this.extraOutputData,waterfallData:this.waterfallOutputData,scanProgress:this.scanProgress,processTimes:this.processTimes,...templateOverData&&{templateOverData}})}catch(error){throw error instanceof Error?error:new Error(String(error))}finally{this.isProcessing=false;this.lastProcessTime=performance.now()-processStartTime}}initializeSegments(segments){if(!segments?.length)throw new DataValidationError(ERROR_MESSAGES.EMPTY_SEGMENTS);let totalPoints=0;this.segments=segments.map(segment=>{const{startFrequency,stopFrequency,stepFrequency}=segment;const frequencyRange=stopFrequency-startFrequency;const stepCount=1e3*frequencyRange/stepFrequency;const pointCount=Math.round(stepCount)+1;const startIndex=totalPoints;totalPoints+=pointCount;return{startFrequency,stopFrequency,stepFrequency,pointCount,startIndex}});this.updateMaxPoints(totalPoints)}setAntennaFactor(d){const{antennaFactorData,config:{maxPoints}}=this;let data=new Float32Array(d);if(data.length!==maxPoints){data=new Float32Array(data).subarray(0,maxPoints);console.warn(ERROR_MESSAGES.INVALID_ANTENNA_FACTOR_LENGTH(maxPoints))}let hasInvalid=false;for(let i=0;i<data.length;i++){const value=data[i];const isNiceFinite=Number.isFinite(value)&&value>0;if(!isNiceFinite)hasInvalid=true;antennaFactorData[i]=isNiceFinite?value:0}if(hasInvalid)console.warn(ERROR_MESSAGES.INVALID_ANTENNA_FACTOR);if(this.antennaFactorSwitch)this.setAntennaFactorSwitch(this.antennaFactorSwitch)}setAntennaFactorSwitch(newAntennaFactorSwitch){const isChange=this.antennaFactorSwitch!==newAntennaFactorSwitch;this.antennaFactorSwitch=newAntennaFactorSwitch;if(isChange){const processedData=this.resampleDataSeries();this.resampleWaterfallOutputData();this.notifySpectrumUpdate({...processedData,waterfallData:this.waterfallOutputData})}}updateProcessing(next){const{processing:prev}=this.config;const newProcessing={...prev,...void 0!==next.enableMetrics&&{enableMetrics:next.enableMetrics},...void 0!==next.enableFluorescence&&{enableFluorescence:next.enableFluorescence},...void 0!==next.enablePeakStats&&{enablePeakStats:next.enablePeakStats},...void 0!==next.fluorescenceUpdateInterval&&{fluorescenceUpdateInterval:next.fluorescenceUpdateInterval}};if(newProcessing.enableMetrics===prev.enableMetrics&&newProcessing.enableFluorescence===prev.enableFluorescence&&newProcessing.enablePeakStats===prev.enablePeakStats&&newProcessing.fluorescenceUpdateInterval===prev.fluorescenceUpdateInterval)return;this.config={...this.config,processing:newProcessing};if(newProcessing.enableMetrics&&!prev.enableMetrics){const needInit=0===this.maxData.length||0===this.minData.length||0===this.avgData.length;if(needInit){this.allocateMetricsBuffers();this.clearMetricsBuffers()}}if(newProcessing.enableFluorescence&&!prev.enableFluorescence){if(0===this.fluorescenceData.length)this.fluorescenceData=new Uint32Array(this.config.maxPoints*FLUORESCENCE.LEVEL_RANGE)}const processedData=this.resampleDataSeries();this.extraOutputData=resampleExtraData(this.extraData,this.antennaFactorData,this.antennaFactorSwitch,this.srcIndexCache,this.getOutputData.bind(this));this.notifySpectrumUpdate({...processedData,extraData:this.extraOutputData})}clearMetricsData(){const needAllocate=0===this.maxData.length||0===this.minData.length||0===this.avgData.length||0===this.maxOutputData.length||0===this.minOutputData.length||0===this.avgOutputData.length;if(this.config.processing.enableMetrics&&needAllocate){const outputLength=this.realOutputData?.length??this.config.outputPoints;this.allocateMetricsBuffers(outputLength)}this.clearMetricsBuffers();this.processTimes=0;this.notifySpectrumUpdate({maxData:this.maxOutputData,minData:this.minOutputData,avgData:this.avgOutputData,processTimes:0})}allocateMetricsBuffers(outputLengthOverride){const{maxPoints,outputPoints,processing:{enableMetrics}}=this.config;const rawLength=enableMetrics?maxPoints:0;let outputLength=0;if(enableMetrics){const desired=outputLengthOverride??outputPoints;outputLength=Math.max(0,Math.floor(desired));outputLength=Math.min(outputLength,outputPoints)}this.maxData=new Float32Array(rawLength);this.minData=new Float32Array(rawLength);this.avgData=new Float32Array(rawLength);this.maxOutputData=new Float32Array(outputLength);this.minOutputData=new Float32Array(outputLength);this.avgOutputData=new Float32Array(outputLength)}clearMetricsBuffers(){this.maxData.fill(SPECTRUM.INITIAL_VALUE);this.minData.fill(SPECTRUM.INITIAL_VALUE);this.avgData.fill(SPECTRUM.INITIAL_VALUE);this.maxData.allValid=false;this.minData.allValid=false;this.avgData.allValid=false;this.maxOutputData.fill(SPECTRUM.INITIAL_VALUE);this.minOutputData.fill(SPECTRUM.INITIAL_VALUE);this.avgOutputData.fill(SPECTRUM.INITIAL_VALUE)}setWaterfallData(newWaterfallData){if(!(newWaterfallData?.[0]?.length>0))return;const{processing}=this.config;let{waterfallMaxFrames}=this.config;if(!processing.enableWaterfall)return;waterfallMaxFrames=newWaterfallData.length;this.config={...this.config,waterfallMaxFrames};this.waterfallData=newWaterfallData;this.resampleWaterfallOutputData();this.notifySpectrumUpdate({waterfallData:this.waterfallOutputData})}setFluorescenceData(fluorescenceData,fluorescenceMaxCount){if(!this.config.processing.enableFluorescence)return;if(!fluorescenceData||0===fluorescenceData.length)return;const expectedLength=this.config.maxPoints*FLUORESCENCE.LEVEL_RANGE;if(fluorescenceData.length!==expectedLength)return;this.fluorescenceData=fluorescenceData;this.fluorescenceMaxCount=fluorescenceMaxCount;const outputLength=this.config.outputRange.end-this.config.outputRange.start;const fluorescenceOutput=this.getFluorescenceOutputData(outputLength);this.notifySpectrumUpdate({fluorescenceData:fluorescenceOutput,fluorescenceMaxCount:this.fluorescenceMaxCount})}setRealData(data){if(!(data?.length>0)||data.length!==this.config.maxPoints)return;const newData=new Float32Array(data.length);newData.set(data);newData.timestamp=data.timestamp;this.realData=newData;const processedData=this.resampleDataSeries();this.notifySpectrumUpdate(processedData)}setMaxData(data){if(!(data?.length>0)||data.length!==this.config.maxPoints)return;const newData=new Float32Array(data.length);newData.set(data);this.maxData=newData;this.maxData.allValid=isAllValid(this.maxData);const processedData=this.resampleDataSeries();this.notifySpectrumUpdate(processedData)}setMinData(data){if(!(data?.length>0)||data.length!==this.config.maxPoints)return;const newData=new Float32Array(data.length);newData.set(data);this.minData=newData;this.minData.allValid=isAllValid(this.minData);const processedData=this.resampleDataSeries();this.notifySpectrumUpdate(processedData)}setAvgData(data){if(!(data?.length>0)||data.length!==this.config.maxPoints)return;const newData=new Float32Array(data.length);newData.set(data);this.avgData=newData;this.avgData.allValid=isAllValid(this.avgData);const processedData=this.resampleDataSeries();this.notifySpectrumUpdate(processedData)}setOccupancyData(data){if(!data||0===data.length){this.occupancyData=new Float32Array(0);this.setPendingFlag("occupancy",false);return}this.occupancyData=new Float32Array(data);if(!this.hasProcessedData||this.isProcessing){this.setPendingFlag("occupancy",true);return}const processedData=this.resampleDataSeries();this.notifySpectrumUpdate({occupancyData:processedData.occupancyData});this.setPendingFlag("occupancy",false)}setExtraData(data,mode=types_ExtraDataMode.MERGE){if(mode===types_ExtraDataMode.CLEAR||mode===types_ExtraDataMode.REPLACE){this.extraData={};this.extraOutputData={};this.setPendingFlag("extra",false)}if(mode===types_ExtraDataMode.CLEAR)return;if(!data)return;for(const[key,value]of Object.entries(data)){if(!value||0===value.length){delete this.extraData[key];continue}this.extraData[key]=new Float32Array(value)}if(!this.hasProcessedData||this.isProcessing){this.setPendingFlag("extra",true);return}this.extraOutputData=resampleExtraData(this.extraData,this.antennaFactorData,this.antennaFactorSwitch,this.srcIndexCache,this.getOutputData.bind(this));this.notifySpectrumUpdate({extraData:this.extraOutputData});this.setPendingFlag("extra",false)}reset(preserveProcessedFlag=false){const{maxPoints,waterfallMaxFrames,processing:{enableFluorescence}}=this.config;this.antennaFactorData=new Float32Array(maxPoints);const realData=new Float32Array(maxPoints);realData.timestamp="";realData.fill(SPECTRUM.INITIAL_VALUE);this.realData=realData;this.allocateMetricsBuffers();this.clearMetricsBuffers();this.templateData=new Float32Array;this.backgroundNoiseData=new Float32Array;this.waterfallData=Array.from({length:waterfallMaxFrames},()=>{const frame=new Float32Array;frame.timestamp="";frame.fill(SPECTRUM.INITIAL_VALUE);return frame});this.waterfallOutputData=Array.from({length:waterfallMaxFrames},()=>{const frame=new Float32Array;frame.timestamp="";frame.fill(SPECTRUM.INITIAL_VALUE);return frame});const outputPoints=this.config.outputPoints;this.realOutputData=new Float32Array(outputPoints).fill(SPECTRUM.INITIAL_VALUE);this.templateOutputData=new Float32Array;this.occupancyOutputData=new Float32Array;this.backgroundNoiseOutputData=new Float32Array;this.fluorescenceData=enableFluorescence?new Uint32Array(maxPoints*FLUORESCENCE.LEVEL_RANGE):new Uint32Array(0);this.fluorescenceMaxCount=0;this.fluorescenceUpdateCounter=0;this.extraData={};this.extraOutputData={};this.srcIndexCache=new Uint32Array(maxPoints);this.scanProgress=0;this.lastIndex=0;this.processTimes=0;this.lastProcessTime=0;this.cachedExceedingDatas=[];this.pendingDataFlags={["occupancy"]:false,["template"]:false,["backgroundNoise"]:false,["extra"]:false};this.hasPendingData=false;if(!preserveProcessedFlag)this.hasProcessedData=false;this.notifySpectrumUpdate({realData:this.realData,maxData:this.maxData,minData:this.minData,avgData:this.avgData,templateData:this.templateData,backgroundNoiseData:this.backgroundNoiseData,extraData:this.extraData,srcIndexCache:this.srcIndexCache,processTimes:0,scanProgress:0})}getPerformanceMetrics(){return{lastProcessTime:this.lastProcessTime,dataPoints:this.config.maxPoints,waterfallFrames:this.waterfallData.length,isInitialized:this.realData.length>0,memoryUsage:this.calculateMemoryUsage()}}getPeakStats(){if(!this.config.processing.enablePeakStats)return;const{realData}=this.resampleDataSeries();if(!realData)return;return realData.peaks}updateSamplingRange(start,end){if(start<0||start>=end)throw new DataValidationError(ERROR_MESSAGES.INVALID_SAMPLING_RANGE);this.config={...this.config,outputRange:{start:Math.floor(start),end:Math.ceil(end)}};const processedData=this.resampleDataSeries();this.resampleWaterfallOutputData();this.extraOutputData=resampleExtraData(this.extraData,this.antennaFactorData,this.antennaFactorSwitch,this.srcIndexCache,this.getOutputData.bind(this));this.notifySpectrumUpdate({...processedData,extraData:this.extraOutputData,waterfallData:this.waterfallOutputData});return this.srcIndexCache}setTemplateData(data,templateTolerance){if(!data||0===data.length){this.templateData=new Float32Array(0);this.setPendingFlag("template",false);return}if(void 0!==templateTolerance)this.config.templateTolerance=templateTolerance;this.templateData=new Float32Array(data);if(!this.hasProcessedData||this.isProcessing){this.setPendingFlag("template",true);return}this.notifySpectrumUpdate({templateData:this.resampleSingleData(this.templateData),templateOverData:this.updateTemplateOverData(true)});this.setPendingFlag("template",false)}setBackgroundNoiseData(data){if(!data||0===data.length){this.backgroundNoiseData=new Float32Array(0);this.setPendingFlag("backgroundNoise",false);return}this.backgroundNoiseData=new Float32Array(data);if(!this.hasProcessedData||this.isProcessing){this.setPendingFlag("backgroundNoise",true);return}this.notifySpectrumUpdate({backgroundNoiseData:this.resampleSingleData(this.backgroundNoiseData)});this.setPendingFlag("backgroundNoise",false)}updateTemplateOverData(forceFullCalculation=false,incrementalRange){if(!this.templateData||0===this.templateData.length){this.cachedExceedingDatas=[];return[]}let templateOverData;if(forceFullCalculation||!incrementalRange){templateOverData=findExceedingDatas({realData:this.realData,maxData:this.maxData,minData:this.minData,templateData:this.templateData,tolerance:this.config.templateTolerance});this.cachedExceedingDatas=templateOverData}else{this.cachedExceedingDatas=findExceedingDatasIncremental({realData:this.realData,maxData:this.maxData,minData:this.minData,templateData:this.templateData,tolerance:this.config.templateTolerance,startIndex:incrementalRange.startIndex,endIndex:incrementalRange.endIndex,previousSegments:this.cachedExceedingDatas});templateOverData=this.cachedExceedingDatas}return templateOverData}resampleDataSeries(){const{antennaFactorData,antennaFactorSwitch,maxData,minData,avgData,templateData,occupancyData,backgroundNoiseData,config:{maxPoints,outputPoints}}=this;const activeAntennaFactorData=antennaFactorSwitch?antennaFactorData:new Float32Array(maxPoints);const{realOutputData,srcIndexCache,maxOutputData,minOutputData,avgOutputData,templateOutputData,occupancyOutputData,backgroundNoiseOutputData}=resampleMultiple({antennaFactorData:activeAntennaFactorData,antennaFactorSwitch,outputPoints,realData:this.getOutputData(),maxData:maxData?.length>0?this.getOutputData(maxData):void 0,minData:minData?.length>0?this.getOutputData(minData):void 0,avgData:avgData&&avgData.length>0?this.getOutputData(avgData):void 0,templateData:templateData&&templateData.length>0?this.getOutputData(templateData):void 0,occupancyData:occupancyData&&occupancyData.length>0?this.getOutputData(occupancyData):void 0,backgroundNoiseData:backgroundNoiseData&&backgroundNoiseData.length>0?this.getOutputData(backgroundNoiseData):void 0,enablePeakStats:this.config.processing.enablePeakStats,maxPeaks:this.config.peakDetection.maxPeaks,outputRangeStart:this.config.outputRange.start});this.srcIndexCache=srcIndexCache;this.realOutputData=realOutputData;this.maxOutputData=maxOutputData||new Float32Array;this.minOutputData=minOutputData||new Float32Array;this.avgOutputData=avgOutputData||new Float32Array;this.templateOutputData=templateOutputData||new Float32Array;this.occupancyOutputData=occupancyOutputData||new Float32Array;this.backgroundNoiseOutputData=backgroundNoiseOutputData||new Float32Array;const fluorescenceOutput=this.getFluorescenceOutputData(realOutputData.length);return{realData:realOutputData,maxData:maxOutputData,minData:minOutputData,avgData:avgOutputData,templateData:templateOutputData,occupancyData:occupancyOutputData,backgroundNoiseData:backgroundNoiseOutputData,fluorescenceData:fluorescenceOutput,fluorescenceMaxCount:this.fluorescenceMaxCount,extraData:this.extraOutputData,srcIndexCache}}resampleWaterfallOutputData(){const{antennaFactorData,antennaFactorSwitch,waterfallData,config:{maxPoints,processing:{enableWaterfall},outputPoints}}=this;if(!enableWaterfall)return;const activeAntennaFactorData=antennaFactorSwitch?antennaFactorData:new Float32Array(maxPoints);this.waterfallOutputData=waterfallData.map(frame=>{const realData=this.getOutputData(frame);const{realOutputData}=resampleMultiple({realData,antennaFactorData:activeAntennaFactorData,antennaFactorSwitch,outputPoints});return realOutputData})}updateWaterfallData(data,outputData){const{waterfallMaxFrames,processing}=this.config;if(!processing.enableWaterfall)return;if(this.waterfallData.length>=waterfallMaxFrames){this.waterfallData.shift();this.waterfallOutputData.shift()}const newData=new Float32Array(data.length);newData.set(data);newData.timestamp=data.timestamp;this.waterfallData.push(newData);this.waterfallOutputData.push(outputData)}updateMaxPoints(maxPoints){if(this.config.maxPoints===maxPoints)return;this.config={...this.config,maxPoints,outputRange:{start:0,end:maxPoints}};this.reset(true)}validateInput(data,index){if(index<0||index+data.length>this.config.maxPoints)throw new IndexOutOfBoundsError(ERROR_MESSAGES.INDEX_OUT_OF_BOUNDS(index),index)}processDataPoints(data,index,isOver){const{maxData,minData,avgData,config:{processing:{enableMetrics}}}=this;if(enableMetrics&&isOver){this.processTimes+=1;if(!maxData.allValid)maxData.allValid=isAllValid(maxData);if(!minData.allValid)minData.allValid=isAllValid(minData);if(!avgData.allValid)avgData.allValid=isAllValid(avgData)}const length=data.length;if(this.config.processing.enableFluorescence)for(let i=0;i<length;i++)this.updateFluorescenceStats(index+i,data[i]);if(!enableMetrics)return;let allValid=true;for(let i=0;i<length;i++)if(!Number.isFinite(data[i])){allValid=false;break}for(let i=0;i<length;i++){const dataIndex=index+i;const value=data[i];if(!allValid&&!Number.isFinite(value))continue;const oldMax=maxData[dataIndex];if(value>oldMax||!maxData.allValid&&Number.isNaN(oldMax))maxData[dataIndex]=value;const oldMin=minData[dataIndex];if(value<oldMin||!minData.allValid&&Number.isNaN(oldMin))minData[dataIndex]=value;const oldAvg=avgData[dataIndex];if(0===this.processTimes||Number.isNaN(oldAvg))avgData[dataIndex]=value;else avgData[dataIndex]=oldAvg+(value-oldAvg)/(this.processTimes+1)}}updateFluorescenceStats(dataIndex,value){if(!this.config.processing.enableFluorescence||!this.fluorescenceData)return;if(!Number.isFinite(value))return;const level=Math.round(value);if(level<FLUORESCENCE.LEVEL_MIN||level>FLUORESCENCE.LEVEL_MAX)return;const index=dataIndex*FLUORESCENCE.LEVEL_RANGE+(level-FLUORESCENCE.LEVEL_MIN);const newCount=++this.fluorescenceData[index];if(newCount>this.fluorescenceMaxCount)this.fluorescenceMaxCount=newCount}setPendingFlag(type,value){this.pendingDataFlags[type]=value;this.hasPendingData=this.pendingDataFlags["occupancy"]||this.pendingDataFlags["template"]||this.pendingDataFlags["backgroundNoise"]||this.pendingDataFlags["extra"]}resampleSingleData(sourceData){const{srcIndexCache}=this;const{outputRange:{start,end}}=this.config;const slicedSourceData=sourceData.subarray(start,end);const outputLength=srcIndexCache.length;const outputData=new Float32Array(outputLength);for(let i=0;i<outputLength;i++)outputData[i]=slicedSourceData[srcIndexCache[i]];return outputData}processPendingData(){if(!this.hasPendingData)return null;const pendingOutput={};if(this.pendingDataFlags["occupancy"]&&this.occupancyData.length>0){const processedData=this.resampleDataSeries();pendingOutput.occupancyData=processedData.occupancyData;this.setPendingFlag("occupancy",false)}if(this.pendingDataFlags["template"]&&this.templateData.length>0){pendingOutput.templateData=this.resampleSingleData(this.templateData);pendingOutput.templateOverData=this.updateTemplateOverData(true);this.setPendingFlag("template",false)}if(this.pendingDataFlags["backgroundNoise"]&&this.backgroundNoiseData.length>0){pendingOutput.backgroundNoiseData=this.resampleSingleData(this.backgroundNoiseData);this.setPendingFlag("backgroundNoise",false)}if(this.pendingDataFlags["extra"]&&Object.keys(this.extraData).length>0){this.extraOutputData=resampleExtraData(this.extraData,this.antennaFactorData,this.antennaFactorSwitch,this.srcIndexCache,this.getOutputData.bind(this));pendingOutput.extraData=this.extraOutputData;this.setPendingFlag("extra",false)}return pendingOutput}notifySpectrumUpdate(processedData){this.onSpectrumUpdate?.(processedData)}calculateMemoryUsage(){const arraySize=this.config.maxPoints*Float32Array.BYTES_PER_ELEMENT;const baseArrayMemory=4*arraySize;const rawWaterfallMemory=this.waterfallData.reduce((sum,frame)=>sum+frame.length*Float32Array.BYTES_PER_ELEMENT,0);const outputWaterfallMemory=this.waterfallOutputData.reduce((sum,frame)=>sum+frame.length*Float32Array.BYTES_PER_ELEMENT,0);return baseArrayMemory+rawWaterfallMemory+outputWaterfallMemory}getOutputData(data){const{outputRange:{start,end}}=this.config;const sourceData=data??this.realData;const outputData=sourceData.subarray(start,end);outputData.timestamp=sourceData.timestamp;return outputData}getFluorescenceOutputData(outputLength){const{processing:{enableFluorescence},outputRange:{start:outputRangeStart,end:outputRangeEnd}}=this.config;if(!enableFluorescence||0===this.fluorescenceData.length)return;if(outputLength<=0)return;const outputData=new Uint32Array(outputLength*FLUORESCENCE.LEVEL_RANGE);const srcLength=outputRangeEnd-outputRangeStart;const ratio=srcLength/outputLength;for(let i=0;i<outputLength;i++){const srcIndex=Math.floor(outputRangeStart+(i+.5)*ratio);const srcBase=srcIndex*FLUORESCENCE.LEVEL_RANGE;const dstBase=i*FLUORESCENCE.LEVEL_RANGE;for(let level=0;level<FLUORESCENCE.LEVEL_RANGE;level++)outputData[dstBase+level]=this.fluorescenceData[srcBase+level]}return outputData}getAllRawData(){const{realOutputData,maxOutputData,minOutputData,avgOutputData,templateOutputData,occupancyOutputData,backgroundNoiseOutputData,waterfallOutputData,extraOutputData,srcIndexCache}=this;realOutputData.timestamp=this.realData.timestamp;return{realData:realOutputData,maxData:maxOutputData,minData:minOutputData,avgData:avgOutputData,templateData:templateOutputData,occupancyData:occupancyOutputData,backgroundNoiseData:backgroundNoiseOutputData,fluorescenceData:this.getFluorescenceOutputData(realOutputData.length),waterfallData:waterfallOutputData,extraData:extraOutputData,srcIndexCache}}}var types_OrientationType=/*#__PURE__*/function(OrientationType){OrientationType["Horizontal"]="horizontal";OrientationType["Vertical"]="vertical";return OrientationType}({});var types_GraphicType=/*#__PURE__*/function(GraphicType){GraphicType["Circle"]="circle";GraphicType["Rect"]="rect";GraphicType["Line"]="line";GraphicType["Stepline"]="stepline";GraphicType["Bar"]="bar";GraphicType["Area"]="area";return GraphicType}({});var types_SeriesEventType=/*#__PURE__*/function(SeriesEventType){SeriesEventType["PropertyChanged"]="propertyChanged";SeriesEventType["SeriesAdded"]="seriesAdded";SeriesEventType["SeriesRemoved"]="seriesRemoved";SeriesEventType["SeriesCleared"]="seriesCleared";return SeriesEventType}({});const DEFAULT_SERIES_CONFIG={thickness:1,orientation:types_OrientationType.Horizontal,display:true,type:types_GraphicType.Line,color:"#00000000",label:""};class SeriesManager{seriesMap=new Map;changeCallbacks=[];subscribers=new Map;constructor(initialSeries=[]){this.initializeSeries(initialSeries)}initializeSeries(seriesConfigs){for(const config of seriesConfigs)this.addSeries(config)}addSeries(config){const fullConfig={...DEFAULT_SERIES_CONFIG,...config};this.seriesMap.set(config.name,fullConfig);this.emitEvent({type:types_SeriesEventType.SeriesAdded,name:config.name,series:fullConfig})}getSeries(name){return this.seriesMap.get(name)}getAllSeries(){return Array.from(this.seriesMap.values())}getAllConfigs(){const configs={};for(const[name,config]of this.seriesMap)configs[name]={...config};return configs}setSeriesProperty(name,property,value){const series=this.seriesMap.get(name);if(!series)return false;if("name"===property)return false;const oldValue=series[property];const updatedSeries={...series,[property]:value};this.seriesMap.set(name,updatedSeries);this.notifyChange(name,property,value);this.emitEvent({type:types_SeriesEventType.PropertyChanged,name,property,value,oldValue,series:updatedSeries});return true}removeSeries(name){const series=this.seriesMap.get(name);if(!series)return false;const deleted=this.seriesMap.delete(name);if(deleted)this.emitEvent({type:types_SeriesEventType.SeriesRemoved,name,series});return deleted}hasSeries(name){return this.seriesMap.has(name)}onChange(callback){this.changeCallbacks.push(callback)}offChange(callback){const index=this.changeCallbacks.indexOf(callback);if(index>-1)this.changeCallbacks.splice(index,1)}notifyChange(name,property,value){for(const callback of this.changeCallbacks)callback(name,property,value)}clear(){this.seriesMap.clear();this.emitEvent({type:types_SeriesEventType.SeriesCleared})}size(){return this.seriesMap.size}subscribe(subscriber,options={}){this.subscribers.set(subscriber,options);if(options.immediate){for(const series of this.seriesMap.values())if(this.shouldNotifySubscriber(subscriber,{type:types_SeriesEventType.SeriesAdded,name:series.name,series}))subscriber({type:types_SeriesEventType.SeriesAdded,name:series.name,series})}return()=>{this.subscribers.delete(subscriber)}}unsubscribe(subscriber){this.subscribers.delete(subscriber)}emitEvent(event){for(const[subscriber]of this.subscribers)if(this.shouldNotifySubscriber(subscriber,event))subscriber(event)}shouldNotifySubscriber(subscriber,event){const options=this.subscribers.get(subscriber);if(!options)return false;if(options.eventTypes&&!options.eventTypes.includes(event.type))return false;if(options.seriesNames&&event.name&&!options.seriesNames.includes(event.name))return false;return true}}export{DataValidationError,ERROR_MESSAGES,types_GraphicType as GraphicType,IndexOutOfBoundsError,LevelStreamAnalyzer,types_OrientationType as OrientationType,SPECTRUM,types_SeriesEventType as SeriesEventType,SeriesManager,SpectrumAnalyzer,SpectrumError};