diff --git a/src/dataprocessor.js b/src/dataprocessor.js index da20cf9..e89f6e5 100644 --- a/src/dataprocessor.js +++ b/src/dataprocessor.js @@ -4,6 +4,7 @@ import { Alert } from './alert.js'; import { messenger } from './bus.js'; import { projectManager } from './projectmanager.js'; import { dbManager } from './dbmanager.js'; +import { signalRegistry } from './signalregistry.js'; /** * DataProcessor Module @@ -48,8 +49,6 @@ class DataProcessor { } } - // --- Local File Handling --- - handleLocalFile(event) { const files = Array.from(event.target.files); if (files.length === 0) return; @@ -204,12 +203,26 @@ class DataProcessor { const dictionary = data.signal_dictionary || {}; const series = data.series || {}; + // Pre-compute canonical names from the dictionary to avoid lookups in the loop + const mappedDictionary = {}; + for (const [id, rawLocalizedName] of Object.entries(dictionary)) { + const nameFromId = signalRegistry.getCanonicalByPid(id); + + mappedDictionary[id] = + nameFromId || + signalRegistry.getCanonicalKey(rawLocalizedName) || + rawLocalizedName; + } + + // Iterate through the series for (const [signalId, vectors] of Object.entries(series)) { - const signalName = dictionary[signalId] || signalId; + const signalName = mappedDictionary[signalId] || signalId; + const times = vectors.t || []; const values = vectors.v || []; const length = Math.min(times.length, values.length); + for (let i = 0; i < length; i++) { normalized.push({ s: signalName, diff --git a/src/entry.js b/src/entry.js index 6d68248..697c847 100644 --- a/src/entry.js +++ b/src/entry.js @@ -14,6 +14,7 @@ import { xyAnalysis } from './xyanalysis.js'; import { Histogram } from './histogram.js'; import { mathChannels } from './mathchannels.js'; import { projectManager } from './projectmanager.js'; +import { signalRegistry } from './signalregistry.js'; window.onload = async function () { await dataProcessor.loadConfiguration(); @@ -31,6 +32,7 @@ window.onload = async function () { xyAnalysis.init(); Histogram.init(); projectManager.init(); + signalRegistry.init(); const fileInput = DOM.get('fileInput'); if (fileInput) { diff --git a/src/signalregistry.js b/src/signalregistry.js index 8accccd..220fb47 100644 --- a/src/signalregistry.js +++ b/src/signalregistry.js @@ -3,23 +3,90 @@ import signalConfig from './signals.json'; class SignalRegistry { constructor() { this.mappings = {}; + this.metadata = {}; // Stores units, min, max, etc. + this.pidMap = {}; // Maps PIDs directly to canonical keys this.defaultSignals = []; - this.init(); + + // Synchronous setup of local defaults and aliases + this._initLocal(); } - init() { + _initLocal() { signalConfig.forEach((entry) => { this.mappings[entry.name] = entry.aliases || []; - if (entry.default) { this.defaultSignals.push(entry.name); } + // Initialize base metadata for local signals + this.metadata[entry.name] = { + units: '', + min: null, + max: null, + pid: null, + }; }); } + /** + * Fetches metadata from multiple ObdMetrics definitions and merges them. + */ + async init( + urls = [ + 'https://raw.githubusercontent.com/tzebrowski/ObdMetrics/v11.x/src/main/resources/giulia_2.0_gme.json', + 'https://raw.githubusercontent.com/tzebrowski/ObdMetrics/v11.x/src/main/resources/alfa.json', + ] + ) { + try { + // Ensure we are working with an array + const urlList = Array.isArray(urls) ? urls : [urls]; + + // Fetch all URLs in parallel + const fetchPromises = urlList.map(async (url) => { + try { + const response = await fetch(url); + if (!response.ok) + throw new Error(`HTTP error! status: ${response.status}`); + + const data = await response.json(); + this.#mergeMetadata(data); + + // Log just the filename for cleaner console output + const fileName = url.substring(url.lastIndexOf('/') + 1); + console.log(`SignalRegistry: Loaded metadata from ${fileName}`); + } catch (err) { + console.error(`SignalRegistry: Failed to load from ${url}`, err); + } + }); + + await Promise.all(fetchPromises); + console.log( + `SignalRegistry: All remote metadata loaded successfully. Total PIDs mapped: ${Object.keys(this.pidMap).length}` + ); + } catch (error) { + console.error( + 'SignalRegistry: Critical error fetching remote metadata.', + error + ); + } + } + + /** + * Retrieve metadata for charting (units, min, max limits). + */ + getSignalMetadata(canonicalKey) { + return this.metadata[canonicalKey] || null; + } + + /** + * Retrieve the canonical name based strictly on the PID/ID. + */ + getCanonicalByPid(pid) { + if (!pid) return null; + return this.pidMap[String(pid).toLowerCase()] || null; + } + /** * Returns the list of canonical signal names that should be shown by default. - * @returns {string[]} Array of canonical keys (e.g., ['Engine Speed', 'Gas Pedal Position']) */ getDefaultSignals() { return this.defaultSignals; @@ -32,12 +99,10 @@ class SignalRegistry { findSignal(canonicalKey, availableSignals) { if (!availableSignals || availableSignals.length === 0) return null; - // Direct exact match if (availableSignals.includes(canonicalKey)) return canonicalKey; const aliases = this.mappings[canonicalKey] || []; - // Exact alias match (Case-insensitive) for (const alias of aliases) { const match = availableSignals.find( (s) => s.toLowerCase() === alias.toLowerCase() @@ -45,11 +110,8 @@ class SignalRegistry { if (match) return match; } - // Smart Word-Boundary Match (Replaces .includes()) - // Uses Regex \b to ensure "lat" matches "GPS Lat" but NOT "Calculated" for (const alias of aliases) { try { - // Escape special regex characters to prevent errors if alias has symbols like "+" or "(" const escapedAlias = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(`\\b${escapedAlias}\\b`, 'i'); @@ -64,9 +126,7 @@ class SignalRegistry { } /** - * Checks if a raw signal name (e.g. "RPM") maps to a default canonical signal. - * @param {string} rawSignalName - The signal name from the file - * @returns {boolean} True if this signal should be shown by default + * Checks if a raw signal name maps to a default canonical signal. */ isDefaultSignal(rawSignalName) { const canonical = this.getCanonicalKey(rawSignalName); @@ -80,7 +140,6 @@ class SignalRegistry { for (const [key, aliases] of Object.entries(this.mappings)) { if (key === rawSignalName) return key; - // Also updated to use word boundaries for reverse lookup safety if ( aliases.some((alias) => { if (rawSignalName.toLowerCase() === alias.toLowerCase()) return true; @@ -97,6 +156,78 @@ class SignalRegistry { } return rawSignalName; } + + #mergeMetadata(data) { + let loadedPids = 0; + let metricsArray = []; + + // ObdMetrics JSON files group PIDs into distinct categories (livedata, metadata, capabilities, etc.) + // We need to iterate over all these groups and flatten them into one big array. + if (Array.isArray(data)) { + metricsArray = data; + } else if (data && typeof data === 'object') { + Object.values(data).forEach((value) => { + if (Array.isArray(value)) { + // Merge every array we find (livedata, metadata, etc.) together + metricsArray = metricsArray.concat(value); + } + }); + } + + if (metricsArray.length === 0) { + console.error( + 'SignalRegistry: Failed to parse remote metrics. No arrays found in JSON:', + data + ); + return; + } + + metricsArray.forEach((metric) => { + if (!metric || typeof metric !== 'object') return; + + const rawDesc = metric.description || ''; + if (!rawDesc) return; + + const cleanName = rawDesc.split('\n')[0].trim(); + const canonicalKey = this.getCanonicalKey(cleanName) || cleanName; + + this.metadata[canonicalKey] = { + ...this.metadata[canonicalKey], + units: metric.units || '', + min: metric.min !== undefined ? parseFloat(metric.min) : null, + max: metric.max !== undefined ? parseFloat(metric.max) : null, + }; + + let mappedSomething = false; + + // ObdMetrics definitions use both an internal "id" (e.g., "7040") and an OBD "pid" (e.g., "1001"). + // We map BOTH so that the log file can match against either identifier perfectly. + [metric.id, metric.pid, metric.command].forEach((identifier) => { + if (identifier) { + const cleanId = String(identifier) + .replace(/^(pid_|0x)/i, '') + .toLowerCase(); + this.pidMap[cleanId] = canonicalKey; + mappedSomething = true; + } + }); + + if (mappedSomething) loadedPids++; + + if (!this.mappings[canonicalKey]) { + this.mappings[canonicalKey] = [cleanName]; + } + + const rawAlias = rawDesc.replace(/\n/g, ' ').trim(); + if (!this.mappings[canonicalKey].includes(rawAlias)) { + this.mappings[canonicalKey].push(rawAlias); + } + }); + + console.log( + `SignalRegistry: Successfully mapped ${loadedPids} remote metrics to canonical names.` + ); + } } export const signalRegistry = new SignalRegistry(); diff --git a/tests/dataprocessor.test.js b/tests/dataprocessor.test.js index 3cab16d..12b1398 100644 --- a/tests/dataprocessor.test.js +++ b/tests/dataprocessor.test.js @@ -868,11 +868,10 @@ describe('DataProcessor: Columnar JSON Support', () => { expect(file.metadata['trip.duration']).toBe('3600'); // Available signals check (should map IDs to human-readable names) - expect(file.availableSignals).toContain('Boost Pressure'); - expect(file.availableSignals).toContain('Engine RPM'); + expect(file.availableSignals).toContain('Boost'); // Series data check (un-pivoted successfully) - const boostData = file.signals['Boost Pressure']; + const boostData = file.signals['Boost']; expect(boostData).toHaveLength(2); expect(boostData[0].x).toBe(1000); // timestamp expect(boostData[0].y).toBe(14.1); // value