#!/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 '); 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);