Simplify UI and add batch build endpoint
This commit is contained in:
400
scripts/build_mapping.js
Normal file
400
scripts/build_mapping.js
Normal file
@@ -0,0 +1,400 @@
|
||||
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));
|
||||
Reference in New Issue
Block a user