80 lines
3.6 KiB
TypeScript
80 lines
3.6 KiB
TypeScript
import type { ObsidianGraphModel } from './obsidian-graph';
|
|
|
|
export const FILTERED_GRAPH_SELECTION_ID = '__filtered-graph-selection__';
|
|
|
|
function oppositeNode(edge: { source: string; target: string }, nodeId: string): string | undefined {
|
|
if (edge.source === nodeId) return edge.target;
|
|
if (edge.target === nodeId) return edge.source;
|
|
return undefined;
|
|
}
|
|
|
|
export function relationBranchNodeIds(graph: ObsidianGraphModel, rootId: string): string[] {
|
|
const kindById = new Map(graph.nodes.map(node => [node.id, node.kind]));
|
|
const rootKind = kindById.get(rootId);
|
|
if (!rootKind || rootKind === 'finding') return [];
|
|
|
|
const selected = new Set<string>([rootId]);
|
|
const findingAnchors = rootKind === 'tactic'
|
|
? [rootId, ...graph.edges.flatMap(edge => {
|
|
const candidate = oppositeNode(edge, rootId);
|
|
return candidate && kindById.get(candidate) === 'technique' ? [candidate] : [];
|
|
})]
|
|
: [rootId];
|
|
findingAnchors.forEach(id => selected.add(id));
|
|
const findingIds = graph.edges.flatMap(edge => findingAnchors.flatMap(anchorId => {
|
|
const candidate = oppositeNode(edge, anchorId);
|
|
return candidate && kindById.get(candidate) === 'finding' ? [candidate] : [];
|
|
}));
|
|
findingIds.forEach(id => selected.add(id));
|
|
|
|
// Alle direkten Beziehungen der zur Wurzel gehoerenden Findings werden
|
|
// aufgenommen, auch weitere Endpunkte derselben Art wie die Wurzel. Das ist
|
|
// insbesondere fuer Findings mit mehreren Kategorien oder D3FEND-Zuordnungen
|
|
// wichtig. Ein gemeinsam genutzter Relationsknoten darf die Auswahl trotzdem
|
|
// nicht rekursiv in weitere Findings ausdehnen.
|
|
graph.edges.forEach(edge => {
|
|
const sourceFinding = findingIds.includes(edge.source);
|
|
const targetFinding = findingIds.includes(edge.target);
|
|
if (!sourceFinding && !targetFinding) return;
|
|
const candidate = sourceFinding ? edge.target : edge.source;
|
|
selected.add(candidate);
|
|
});
|
|
|
|
// Techniques besitzen eine zusaetzliche, fachlich gewollte Ebene zu ihren
|
|
// Tactics. Dort endet die Traversierung bewusst.
|
|
const techniqueIds = [...selected].filter(id => kindById.get(id) === 'technique');
|
|
graph.edges.forEach(edge => {
|
|
const techniqueId = techniqueIds.includes(edge.source) ? edge.source : techniqueIds.includes(edge.target) ? edge.target : undefined;
|
|
if (!techniqueId) return;
|
|
const candidate = oppositeNode(edge, techniqueId);
|
|
if (candidate && kindById.get(candidate) === 'tactic' && (rootKind !== 'tactic' || candidate === rootId)) selected.add(candidate);
|
|
});
|
|
|
|
return graph.nodes.map(node => node.id).filter(id => selected.has(id));
|
|
}
|
|
|
|
export function graphSelectionNodeIds(graph: ObsidianGraphModel, selectionId: string): string[] {
|
|
return selectionId === FILTERED_GRAPH_SELECTION_ID
|
|
? graph.nodes.map(node => node.id)
|
|
: relationBranchNodeIds(graph, selectionId);
|
|
}
|
|
|
|
export function graphSelectionUnionNodeIds(graph: ObsidianGraphModel, selectionIds: string[]): string[] {
|
|
const selected = new Set<string>();
|
|
for (const selectionId of selectionIds) {
|
|
if (selectionId === FILTERED_GRAPH_SELECTION_ID) return graph.nodes.map(node => node.id);
|
|
const branchNodeIds = relationBranchNodeIds(graph, selectionId);
|
|
if (branchNodeIds.length) {
|
|
branchNodeIds.forEach(nodeId => selected.add(nodeId));
|
|
continue;
|
|
}
|
|
if (!graph.nodes.some(node => node.id === selectionId)) continue;
|
|
selected.add(selectionId);
|
|
graph.edges.forEach(edge => {
|
|
if (edge.source === selectionId) selected.add(edge.target);
|
|
if (edge.target === selectionId) selected.add(edge.source);
|
|
});
|
|
}
|
|
return graph.nodes.map(node => node.id).filter(nodeId => selected.has(nodeId));
|
|
}
|