agmission/Development/server/tests/test_satloc_pattern.js

327 lines
12 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* SatLoc Log Pattern Analyzer
* Analyzes and prints statistics about record types in SatLoc log files
*/
const fs = require('fs').promises;
const path = require('path');
const { SatLocLogParser } = require('./helpers/satloc_log_parser');
// Get file path from command line arguments
const logFilePath = process.argv[2];
if (!logFilePath) {
console.error('Usage: node test_satloc_pattern.js <satloc-log-file-path>');
console.error('Example: node test_satloc_pattern.js /path/to/logfile.BIN');
process.exit(1);
}
async function analyzeSatLocFile(filePath) {
try {
console.log('=== SatLoc Log Pattern Analysis ===');
console.log(`File: ${path.basename(filePath)}`);
console.log(`Path: ${filePath}`);
// Check if file exists
try {
await fs.access(filePath);
} catch (error) {
console.error(`Error: File not found - ${filePath}`);
process.exit(1);
}
// Get file size
const stats = await fs.stat(filePath);
console.log(`Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB (${stats.size.toLocaleString()} bytes)`);
console.log();
// Create parser with statistics tracking
const parser = new SatLocLogParser({
skipUnknownRecords: false, // Include unknown records in statistics
validateChecksums: true,
verbose: false
});
console.log('Analyzing log file...');
const startTime = Date.now();
// Parse the file
const result = await parser.parseFile(filePath);
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
console.log(`\nAnalysis completed in ${duration.toFixed(2)} seconds\n`);
// Display parsing statistics
console.log('=== Parsing Statistics ===');
console.log(`Total Records: ${parser.statistics.totalRecords.toLocaleString()}`);
console.log(`Valid Records: ${parser.statistics.validRecords.toLocaleString()}`);
console.log(`Invalid Records: ${parser.statistics.invalidRecords.toLocaleString()}`);
console.log(`Parse Errors: ${parser.statistics.parseErrors.toLocaleString()}`);
console.log(`Application Details Created: ${result.applicationDetailCount.toLocaleString()}`);
console.log();
// Display record type patterns in horizontal format
console.log('=== Record Type Pattern (Sequential Flow Summary) ===');
if (result.records && result.records.length === 0) {
console.log('No records found or parsed.');
return;
}
// Create a summarized sequential flow by grouping consecutive identical record types
console.log('Data flow pattern (summarized):');
const flowSummary = [];
let currentType = null;
let currentCount = 0;
let currentTypeName = '';
for (let i = 0; i < result.records.length; i++) {
const record = result.records[i];
if (record.recordType === currentType) {
// Same type, increment counter
currentCount++;
} else {
// Different type, save previous group if exists
if (currentType !== null) {
flowSummary.push({
type: currentType,
name: currentTypeName,
count: currentCount
});
}
// Start new group
currentType = record.recordType;
currentTypeName = parser.getRecordTypeName(record.recordType);
currentCount = 1;
}
}
// Don't forget the last group
if (currentType !== null) {
flowSummary.push({
type: currentType,
name: currentTypeName,
count: currentCount
});
}
// Display the summarized flow with => arrows, limiting line length and grouping
console.log('Summarized data collection flow:');
let currentLine = '';
const maxLineLength = 120;
const maxItemsPerLine = 6; // Limit items per line for readability
let itemsInCurrentLine = 0;
for (let i = 0; i < flowSummary.length; i++) {
const { type, name, count } = flowSummary[i];
// Create more concise display for large segments
let item;
if (count === 1) {
item = `${type}(${name})`;
} else if (count < 100) {
item = `${type}(${name})×${count}`;
} else {
// For large segments, use abbreviated names and K notation
const shortName = name.length > 12 ? name.substring(0, 12) + '..' : name;
const countDisplay = count >= 1000 ? `${(count/1000).toFixed(1)}K` : count.toString();
item = `${type}(${shortName})×${countDisplay}`;
}
const connector = i < flowSummary.length - 1 ? ' => ' : '';
if ((currentLine.length + item.length + connector.length > maxLineLength && currentLine.length > 0)
|| itemsInCurrentLine >= maxItemsPerLine) {
console.log(currentLine);
currentLine = item + connector;
itemsInCurrentLine = 1;
} else {
currentLine += item + connector;
itemsInCurrentLine++;
}
}
if (currentLine.length > 0) {
console.log(currentLine);
}
console.log(`\nTotal flow segments: ${flowSummary.length.toLocaleString()}`);
console.log();
// Show detailed flow phases - only show significant segments (> 50 records) to reduce clutter
console.log('=== Flow Phases Breakdown (Significant Segments Only) ===');
const significantSegments = flowSummary.filter(segment => segment.count > 50);
if (significantSegments.length > 0) {
significantSegments.forEach((segment, index) => {
const percentage = ((segment.count / result.records.length) * 100).toFixed(2);
console.log(`${(index + 1).toString().padStart(2)}. ${segment.type.toString().padStart(3)}(${segment.name.padEnd(25)}) | Count: ${segment.count.toString().padStart(8)} (${percentage.padStart(5)}%)`);
});
console.log(`\nShowing ${significantSegments.length} significant segments (>50 records each)`);
console.log(`Total segments in file: ${flowSummary.length.toLocaleString()} (including ${flowSummary.length - significantSegments.length} smaller segments)`);
} else {
console.log('No significant consecutive segments found (all segments ≤ 50 records)');
// Show top 10 segments anyway
const top10 = flowSummary.sort((a, b) => b.count - a.count).slice(0, 10);
top10.forEach((segment, index) => {
const percentage = ((segment.count / result.records.length) * 100).toFixed(2);
console.log(`${(index + 1).toString().padStart(2)}. ${segment.type.toString().padStart(3)}(${segment.name.padEnd(25)}) | Count: ${segment.count.toString().padStart(8)} (${percentage.padStart(5)}%)`);
});
}
console.log();
// Show pattern insights - largest segments and transitions
console.log('=== Major Data Segments ===');
// Find the largest consecutive segments
const majorSegments = flowSummary
.filter(segment => segment.count > 10) // Only show significant segments
.sort((a, b) => b.count - a.count)
.slice(0, 10);
if (majorSegments.length > 0) {
majorSegments.forEach(({ type, name, count }, index) => {
const percentage = ((count / result.records.length) * 100).toFixed(2);
console.log(`${(index + 1).toString().padStart(2)}. ${type}(${name}) consecutive × ${count.toLocaleString()} (${percentage}% of file)`);
});
} else {
console.log('No major consecutive segments found (all segments < 10 records)');
}
console.log();
// Show transitions between different record types
console.log('=== Data Collection Transitions ===');
const transitions = [];
for (let i = 0; i < flowSummary.length - 1; i++) {
const from = flowSummary[i];
const to = flowSummary[i + 1];
if (from.type !== to.type) {
transitions.push(`${from.type}(${from.name}) => ${to.type}(${to.name})`);
}
}
if (transitions.length > 0) {
console.log(`Total transitions: ${transitions.length}`);
// Show first 10 transitions
const displayTransitions = transitions.slice(0, 10);
displayTransitions.forEach((transition, index) => {
console.log(`${(index + 1).toString().padStart(2)}. ${transition}`);
});
if (transitions.length > 10) {
console.log(`... (showing first 10 of ${transitions.length} transitions)`);
}
} else {
console.log('No transitions found - file contains only one record type');
}
console.log();
console.log();
// Display record type statistics
console.log('=== Record Type Statistics ===');
if (Object.keys(parser.statistics.recordTypes).length === 0) {
console.log('No record statistics available.');
} else {
// Sort record types by count (descending)
const sortedRecords = Object.entries(parser.statistics.recordTypes)
.map(([typeStr, count]) => ({
type: parseInt(typeStr),
count: count
}))
.sort((a, b) => b.count - a.count);
// Create horizontal display for statistics
const recordDisplay = sortedRecords.map(({ type, count }) => {
const typeName = parser.getRecordTypeName(type);
const percentage = ((count / parser.statistics.validRecords) * 100).toFixed(1);
return `${type}=>${typeName} (${count.toLocaleString()}, ${percentage}%)`;
});
// Display in rows of 2 records each for better readability
for (let i = 0; i < recordDisplay.length; i += 2) {
const line = recordDisplay.slice(i, i + 2).join(' | ');
console.log(line);
}
}
console.log();
// Show top 10 most frequent records in detail
console.log('=== Top Record Types (Detailed) ===');
if (Object.keys(parser.statistics.recordTypes).length > 0) {
const sortedRecords = Object.entries(parser.statistics.recordTypes)
.map(([typeStr, count]) => ({
type: parseInt(typeStr),
count: count
}))
.sort((a, b) => b.count - a.count);
const top10 = sortedRecords.slice(0, 10);
top10.forEach(({ type, count }, index) => {
const typeName = parser.getRecordTypeName(type);
const percentage = ((count / parser.statistics.validRecords) * 100).toFixed(2);
console.log(`${(index + 1).toString().padStart(2)}. Type ${type.toString().padStart(3)} => ${typeName.padEnd(30)} | Count: ${count.toString().padStart(8)} (${percentage.padStart(5)}%)`);
});
}
console.log();
// Show data distribution
const positionRecords = parser.statistics.recordTypes[1] || 0;
const gpsRecords = parser.statistics.recordTypes[10] || 0;
const flowRecords = parser.statistics.recordTypes[30] || 0;
const windRecords = parser.statistics.recordTypes[50] || 0;
console.log('=== Key Data Types ===');
console.log(`Position Records (Type 1) : ${positionRecords.toLocaleString().padStart(8)} => Application data points`);
console.log(`GPS Records (Type 10) : ${gpsRecords.toLocaleString().padStart(8)} => GPS quality data`);
console.log(`Flow Monitor (Type 30) : ${flowRecords.toLocaleString().padStart(8)} => Flow rate data`);
console.log(`Wind Records (Type 50) : ${windRecords.toLocaleString().padStart(8)} => Environmental data`);
console.log();
// File format analysis
if (result.records && result.records.length > 0) {
const firstRecord = result.records[0];
const lastRecord = result.records[result.records.length - 1];
console.log('=== File Span Analysis ===');
if (firstRecord.timestamp && lastRecord.timestamp) {
const span = (lastRecord.timestamp - firstRecord.timestamp) / 1000 / 60; // minutes
const recordsPerMinute = parser.statistics.validRecords / span;
console.log(`Time Span: ${span.toFixed(1)} minutes`);
console.log(`Recording Rate: ${recordsPerMinute.toFixed(1)} records/minute`);
if (positionRecords > 0) {
const applicationRate = positionRecords / span;
console.log(`Application Rate: ${applicationRate.toFixed(1)} data points/minute`);
}
}
}
console.log('\n=== Analysis Complete ===');
} catch (error) {
console.error('Error analyzing SatLoc file:', error.message);
console.error('Stack trace:', error.stack);
process.exit(1);
}
}
// Run the analysis
analyzeSatLocFile(logFilePath);