401 lines
17 KiB
JavaScript
401 lines
17 KiB
JavaScript
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));
|