327 lines
12 KiB
JavaScript
Executable File
327 lines
12 KiB
JavaScript
Executable File
#!/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);
|