const fs = require('fs'); const mapping = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); const rows = JSON.parse(fs.readFileSync(process.argv[3], 'utf8')); const templateRules = JSON.parse(fs.readFileSync(process.argv[4], 'utf8')); const isTelemetryTemplate = (rule) => { const filters = (rule.filters || []).map((f) => f.name); const fields = (rule.fields || []).map((f) => f.name); return ( filters.includes('.VideoInput') && !filters.includes('.Position') && !filters.includes('.VideoOutput') && fields.includes('VideoInput') && !fields.includes('Position') ); }; const deriveActionName = (inputCaption) => { if (!inputCaption) return ''; let name = inputCaption.trim(); name = name.replace(/^gevi[\s_]+/i, ''); name = name.replace(/[_\s]\d+$/i, ''); return name.trim(); }; const normalize = (value) => (value || '').trim().toLowerCase(); const normalizeInputName = (value) => (value || '').trim().toLowerCase().replace(/\s+/g, ' '); const findTemplate = (kind) => { if (kind === 'telemetry') { const preferred = mapping.rules.find((rule) => { if (!isTelemetryTemplate(rule)) return false; const name = String(rule.input?.name || ''); return /^GeVi\s+(Pan|Tilt|Zoom|Focus)/i.test(name); }); if (preferred) return preferred; return ( mapping.rules.find((rule) => isTelemetryTemplate(rule)) || (templateRules.telemetry || []).find((rule) => isTelemetryTemplate(rule)) || null ); } return ( mapping.rules.find((rule) => { const filters = (rule.filters || []).map((f) => f.name); const fields = (rule.fields || []).map((f) => f.name); return ( filters.includes('.SwitchMode') && filters.includes('.VideoInput') && filters.includes('.VideoOutput') && fields.includes('SwitchMode') && fields.includes('VideoOutput') ); }) || (templateRules.crossswitch || []).find((rule) => { const filters = (rule.filters || []).map((f) => f.name); const fields = (rule.fields || []).map((f) => f.name); return ( filters.includes('.SwitchMode') && filters.includes('.VideoInput') && filters.includes('.VideoOutput') && fields.includes('SwitchMode') && fields.includes('VideoOutput') ); }) || null ); }; const defaultPosFallback = (templateRules.defaultpos || []).find((rule) => { const filters = (rule.filters || []).map((f) => f.name); const fields = (rule.fields || []).map((f) => f.name); return filters.includes('.Position') || fields.includes('Position'); }) || null; const fallbackTemplate = mapping.rules[0]; const telemetryTemplate = findTemplate('telemetry') || fallbackTemplate; const crossSwitchTemplate = findTemplate('crossswitch') || fallbackTemplate; const defaultPosTemplate = mapping.rules.find((rule) => { const filters = (rule.filters || []).map((f) => f.name); const fields = (rule.fields || []).map((f) => f.name); return filters.includes('.Position') || fields.includes('Position'); }) || defaultPosFallback || fallbackTemplate; const findTelemetryActionTemplate = (actionName) => { if (!actionName) return null; const needle = actionName.toLowerCase(); return ( mapping.rules.find((rule) => { const name = String(rule.input?.name || '').toLowerCase(); if (!name.startsWith('gevi ')) return false; if (!name.includes(needle)) return false; if (name.includes('prepos')) return false; return isTelemetryTemplate(rule); }) || (templateRules.telemetry || []).find((rule) => { const name = String(rule.input?.name || '').toLowerCase(); if (!name.startsWith('gevi ')) return false; if (!name.includes(needle)) return false; if (name.includes('prepos')) return false; return isTelemetryTemplate(rule); }) || null ); }; const findDefaultPosTemplate = (actionName) => { if (!actionName) return null; const needle = actionName.toLowerCase(); return ( mapping.rules.find((rule) => { const name = String(rule.input?.name || '').toLowerCase(); const filters = (rule.filters || []).map((f) => f.name); const fields = (rule.fields || []).map((f) => f.name); if (!name.startsWith('gevi ')) return false; if (!name.includes(needle)) return false; return filters.includes('.Position') || fields.includes('Position'); }) || (templateRules.defaultpos || []).find((rule) => { const name = String(rule.input?.name || '').toLowerCase(); const filters = (rule.filters || []).map((f) => f.name); const fields = (rule.fields || []).map((f) => f.name); if (!name.startsWith('gevi ')) return false; if (!name.includes(needle)) return false; return filters.includes('.Position') || fields.includes('Position'); }) || null ); }; const outputKey = (inputName, o) => [normalize(inputName), normalize(o.actionField), normalize(o.action), normalize(o.server)].join('|'); const ruleIndexByName = new Map(); const ruleIndexByCamera = new Map(); const ruleIndexByAction = new Map(); const existingKeys = new Set(); const flagTemplates = new Map(); const fieldFlagCounts = new Map(); mapping.rules.forEach((rule, idx) => { const normalized = normalizeInputName(rule.input.name); ruleIndexByName.set(normalized, idx); const match = normalized.match(/(\d+)$/); if (match) { const cam = match[1]; if (!ruleIndexByCamera.has(cam)) ruleIndexByCamera.set(cam, []); ruleIndexByCamera.get(cam).push(idx); } for (const out of rule.outputs || []) { existingKeys.add(outputKey(rule.input.name, out)); const key = [normalize(out.actionField), normalize(out.action)].join('|'); if (!flagTemplates.has(key)) { flagTemplates.set(key, { ...out.flags }); } const fieldKey = normalize(out.actionField); const flagPair = `${out.flags.primary}|${out.flags.secondary}`; if (!fieldFlagCounts.has(fieldKey)) fieldFlagCounts.set(fieldKey, new Map()); const counts = fieldFlagCounts.get(fieldKey); counts.set(flagPair, (counts.get(flagPair) || 0) + 1); } }); const fallbackFlags = (actionField) => { const counts = fieldFlagCounts.get(normalize(actionField)); if (!counts) return { primary: 0, secondary: 0 }; let best = ''; let bestCount = -1; counts.forEach((count, key) => { if (count > bestCount) { bestCount = count; best = key; } }); const [primary, secondary] = best.split('|').map((val) => Number(val)); return { primary: primary || 0, secondary: secondary || 0 }; }; let maxRuleId = Math.max(0, ...mapping.rules.map((r) => Number(r.id || 0))); const isCrossSwitchAction = (actionName, category) => /crossswitch/i.test(actionName) || /crossbar/i.test(category); const DEFAULTPOS_ACTIONS = { MoveToDefaultPosition: 'DefaultPosCallUp', ClearDefaultPosition: 'DefaultPosClear', SaveDefaultPosition: 'DefaultPosSave', }; const normalizeOutputAction = (actionName) => DEFAULTPOS_ACTIONS[actionName] || actionName; const isDefaultPosAction = (actionName) => Object.prototype.hasOwnProperty.call(DEFAULTPOS_ACTIONS, actionName); const isFocusSpeedAction = (actionName) => /focusnear|focusfar/i.test(actionName); const isFocusInputAction = (actionName) => /focusnear|focusfar|focusstop|focus/i.test(actionName); const parseSpeed = (value) => { if (!value) return undefined; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : undefined; }; const buildAction = (actionName, cameraId, speed) => { if (!actionName) return ''; const params = []; if (cameraId) params.push('Comment: ""'); if (cameraId) params.push(`Camera: ${cameraId}`); if (speed !== undefined) params.push(`Speed: ${speed}`); if (params.length) return `@ ${actionName} (${params.join(', ')})`; return `@ ${actionName} ()`; }; for (const row of rows) { const output1 = row.outputs.find((o) => o.kind === 'output1'); const inputName = row.caption || `Camera ${row.cameraId}`; const inputCategoryRaw = String(output1?.category ?? ''); const inputActionRaw = String(output1?.action ?? '').trim(); const inputActionName = inputActionRaw || deriveActionName(inputName); const actionName = inputActionName; const inputCategory = inputCategoryRaw.toLowerCase(); const cameraId = String(row.cameraId ?? '').trim(); const outputs = []; const seenOutputs = new Set(); const serverType = String(row.serverType ?? '').toLowerCase(); const isGscTarget = serverType.includes('geviscope') || serverType.includes('gsc'); const isGcoreTarget = serverType.includes('g-core') || serverType.includes('gcore'); const prefer = isGscTarget ? 'gsc' : isGcoreTarget ? 'gcore' : 'both'; const gscOut = prefer !== 'gcore' ? row.outputs.find((o) => o.kind === 'gsc') : null; if (gscOut) { const gscActionRaw = String(gscOut.action ?? actionName).trim(); const gscActionName = normalizeOutputAction(gscActionRaw); const gscCamera = String(gscOut.ptzHead ?? '').trim(); const gscSpeed = parseSpeed(gscOut.speed); const gscSpeedValue = isFocusSpeedAction(gscActionName) ? gscSpeed : undefined; const gscCategory = String(gscOut.category ?? '').toLowerCase(); const crossSwitch = isCrossSwitchAction(gscActionName, inputCategory) || /viewer/.test(gscCategory); const gscCaption = gscOut.caption || `GSC ${gscActionName}${cameraId ? `_${cameraId}` : ''}`; const gscServer = gscOut.server || (String(row.serverType ?? '').toLowerCase().includes('geviscope') ? row.server : ''); if (actionName || gscOut.caption || gscOut.category || gscOut.server || String(row.serverType ?? '').toLowerCase().includes('geviscope')) { const gscAction = crossSwitch ? '@ ViewerConnectLive ()' : buildAction(gscActionName, gscCamera, gscSpeedValue); const gscFlags = flagTemplates.get([normalize('GscAction'), normalize(gscAction)].join('|')) || fallbackFlags('GscAction'); const out = { id: '1', name: gscCaption, flags: gscFlags, actionField: 'GscAction', serverField: 'GscServer', action: gscAction, server: gscServer || '', }; const key = outputKey(inputName, out); if (!seenOutputs.has(key) && !existingKeys.has(key)) { outputs.push(out); seenOutputs.add(key); existingKeys.add(key); } } } const gcoreOut = prefer !== 'gsc' ? row.outputs.find((o) => o.kind === 'gcore') : null; if (gcoreOut) { const gcoreActionRaw = String(gcoreOut.action ?? actionName).trim(); const gcoreActionName = normalizeOutputAction(gcoreActionRaw); const gcoreCamera = String(gcoreOut.ptzHead ?? '').trim(); const gcoreSpeed = parseSpeed(gcoreOut.speed); const gcoreSpeedValue = isFocusSpeedAction(gcoreActionName) ? gcoreSpeed : undefined; const gcoreCategory = String(gcoreOut.category ?? '').toLowerCase(); const crossSwitch = isCrossSwitchAction(gcoreActionName, inputCategory) || /viewer/.test(gcoreCategory); const gcoreCaption = gcoreOut.caption || `GNG ${gcoreActionName}${cameraId ? `_${cameraId}` : ''}`; const gcoreServer = gcoreOut.server || (String(row.serverType ?? '').toLowerCase().includes('g-core') ? row.server : ''); if (actionName || gcoreOut.caption || gcoreOut.category || gcoreOut.server || String(row.serverType ?? '').toLowerCase().includes('g-core')) { const gcoreAction = crossSwitch ? '@ ViewerConnectLive (Comment: "")' : buildAction(gcoreActionName, gcoreCamera, gcoreSpeedValue); const gcoreFlags = flagTemplates.get([normalize('GCoreAction'), normalize(gcoreAction)].join('|')) || fallbackFlags('GCoreAction'); const out = { id: '1', name: gcoreCaption, flags: gcoreFlags, actionField: 'GCoreAction', serverField: 'GCoreServer', action: gcoreAction, server: gcoreServer || '', }; const key = outputKey(inputName, out); if (!seenOutputs.has(key) && !existingKeys.has(key)) { outputs.push(out); seenOutputs.add(key); existingKeys.add(key); } } } const kind = isCrossSwitchAction(actionName, inputCategory) ? 'crossswitch' : isDefaultPosAction(actionName) ? 'defaultpos' : 'telemetry'; let chosenTemplate = null; if (kind === 'crossswitch') { chosenTemplate = crossSwitchTemplate; } else if (kind === 'defaultpos') { chosenTemplate = findDefaultPosTemplate(normalizeOutputAction(actionName) || actionName) || defaultPosTemplate; } else { chosenTemplate = findTelemetryActionTemplate(actionName) || telemetryTemplate; } const nextFilters = chosenTemplate?.filters ? chosenTemplate.filters.map((f) => ({ ...f })) : []; const baseFilters = nextFilters; const isTelemetry = kind === 'telemetry'; const nextFilterSet = isTelemetry ? baseFilters.map((f) => (f.name === '.VideoInput' ? { ...f, value: true } : f)) : baseFilters; const baseFields = chosenTemplate?.fields ? [...chosenTemplate.fields] : []; const nextFields = isTelemetry ? [...baseFields.filter((f) => f.name !== 'Position' && f.name !== 'VideoOutput'), ...(baseFields.some((f) => f.name === 'VideoInput') ? [] : [{ name: 'VideoInput', type: 'int32', value: 0 }])] : baseFields; const cleanedNextFields = isFocusInputAction(actionName) ? nextFields.filter((f) => f.name !== 'Temp') : nextFields; const actionKey = actionName && cameraId ? `action:${normalize(actionName)}|cam:${cameraId}` : null; let existingIdx = (actionKey ? ruleIndexByAction.get(actionKey) : undefined) ?? ruleIndexByName.get(normalizeInputName(inputName)); if (existingIdx === undefined && actionName && cameraId) { const candidates = ruleIndexByCamera.get(cameraId) || []; const actionNeedle = normalize(actionName); for (const idx of candidates) { const ruleName = normalizeInputName(mapping.rules[idx].input.name); if (ruleName.includes(actionNeedle) && ruleName.endsWith(cameraId)) { existingIdx = idx; break; } } } if (existingIdx !== undefined) { const existingRule = mapping.rules[existingIdx]; const existingOutputs = existingRule.outputs || []; let nextId = Math.max(0, ...existingOutputs.map((o) => Number(o.id || 0))) + 1; const mergedOutputs = [...existingOutputs]; for (const out of outputs) { const key = outputKey(existingRule.input.name, out); if (!existingKeys.has(key)) { mergedOutputs.push({ ...out, id: String(nextId) }); nextId += 1; existingKeys.add(key); } } const existingFilters = isTelemetry ? (existingRule.filters || []).filter((f) => f.name !== '.Position' && f.name !== '.VideoOutput').map((f) => (f.name === '.VideoInput' ? { ...f, value: true } : f)) : (existingRule.filters || []).map((f) => (f.name === '.VideoInput' ? { ...f, value: true } : f)); if (isTelemetry && !existingFilters.some((f) => f.name === '.VideoInput')) { existingFilters.push({ name: '.VideoInput', type: 'bool', value: true }); } const existingFields = isTelemetry ? (existingRule.fields || []).filter((f) => f.name !== 'Position' && f.name !== 'VideoOutput') : (existingRule.fields || []); if (isFocusInputAction(actionName)) { for (let i = existingFields.length - 1; i >= 0; i -= 1) { if (existingFields[i]?.name === 'Temp') existingFields.splice(i, 1); } } if (isTelemetry && !existingFields.some((f) => f.name === 'VideoInput')) { existingFields.push({ name: 'VideoInput', type: 'int32', value: 0 }); } mapping.rules[existingIdx] = { ...existingRule, input: { ...existingRule.input, videoInputId: existingRule.input.videoInputId ?? (cameraId ? Number(cameraId) : undefined), ptz: typeof row.ptz === 'string' ? row.ptz.toUpperCase() === 'O' : existingRule.input.ptz, temp: isFocusInputAction(actionName) ? undefined : existingRule.input.temp, }, outputs: mergedOutputs, filters: existingFilters, fields: existingFields, }; } else { maxRuleId += 1; const ruleId = String(maxRuleId); mapping.rules.push({ id: ruleId, input: { name: inputName, flags: chosenTemplate?.input?.flags || { primary: 0, secondary: 0 }, videoInputId: cameraId ? Number(cameraId) : undefined, ptz: typeof row.ptz === 'string' ? row.ptz.toUpperCase() === 'O' : undefined, temp: isFocusInputAction(actionName) ? undefined : chosenTemplate?.input?.temp, }, outputs, _order: chosenTemplate?._order, filters: nextFilterSet, fields: cleanedNextFields, }); ruleIndexByName.set(normalizeInputName(inputName), mapping.rules.length - 1); if (actionKey) ruleIndexByAction.set(actionKey, mapping.rules.length - 1); } } fs.writeFileSync(process.argv[5], JSON.stringify(mapping));