2023-08-03 16:42:49 +03:00
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, lightTheme, Sphere, useSelection } from 'reagraph';
|
2023-07-29 23:00:03 +03:00
|
|
|
|
2023-07-31 22:38:58 +03:00
|
|
|
import Button from '../../components/Common/Button';
|
|
|
|
import Checkbox from '../../components/Common/Checkbox';
|
2023-07-30 15:49:30 +03:00
|
|
|
import ConceptSelect from '../../components/Common/ConceptSelect';
|
2023-08-03 16:42:49 +03:00
|
|
|
import { ArrowsRotateIcon } from '../../components/Icons';
|
2023-07-29 21:23:18 +03:00
|
|
|
import { useRSForm } from '../../context/RSFormContext';
|
2023-07-31 22:38:58 +03:00
|
|
|
import { useConceptTheme } from '../../context/ThemeContext';
|
2023-07-30 00:47:07 +03:00
|
|
|
import useLocalStorage from '../../hooks/useLocalStorage';
|
2023-07-31 22:38:58 +03:00
|
|
|
import { resources } from '../../utils/constants';
|
2023-08-03 16:42:49 +03:00
|
|
|
import { Graph } from '../../utils/Graph';
|
2023-07-31 22:38:58 +03:00
|
|
|
import { GraphLayoutSelector,mapLayoutLabels } from '../../utils/staticUI';
|
2023-07-29 21:23:18 +03:00
|
|
|
|
|
|
|
function EditorTermGraph() {
|
|
|
|
const { schema } = useRSForm();
|
2023-07-31 22:38:58 +03:00
|
|
|
const { darkMode } = useConceptTheme();
|
2023-07-30 00:47:07 +03:00
|
|
|
const [ layout, setLayout ] = useLocalStorage<LayoutTypes>('graph_layout', 'forceatlas2');
|
2023-08-03 16:42:49 +03:00
|
|
|
|
|
|
|
const [ filtered, setFiltered ] = useState<Graph>(new Graph());
|
2023-07-31 22:38:58 +03:00
|
|
|
const [ orbit, setOrbit ] = useState(false);
|
2023-08-03 16:42:49 +03:00
|
|
|
const [ noHermits, setNoHermits ] = useLocalStorage('graph_no_hermits', true);
|
|
|
|
const [ noTransitive, setNoTransitive ] = useLocalStorage('graph_no_transitive', false);
|
2023-07-30 00:47:07 +03:00
|
|
|
const graphRef = useRef<GraphCanvasRef | null>(null);
|
2023-07-29 23:00:03 +03:00
|
|
|
|
2023-08-03 16:42:49 +03:00
|
|
|
useEffect(() => {
|
|
|
|
if (!schema) {
|
|
|
|
setFiltered(new Graph());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const graph = schema.graph.clone();
|
|
|
|
if (noHermits) {
|
|
|
|
graph.removeIsolated();
|
|
|
|
}
|
|
|
|
if (noTransitive) {
|
|
|
|
graph.transitiveReduction();
|
|
|
|
}
|
|
|
|
setFiltered(graph);
|
|
|
|
}, [schema, noHermits, noTransitive]);
|
|
|
|
|
2023-07-29 23:00:03 +03:00
|
|
|
const nodes: GraphNode[] = useMemo(() => {
|
2023-08-03 16:42:49 +03:00
|
|
|
const result: GraphNode[] = [];
|
|
|
|
if (!schema) {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
filtered.nodes.forEach(node => {
|
|
|
|
const cst = schema.items.find(cst => cst.id === node.id);
|
|
|
|
if (cst) {
|
|
|
|
result.push({
|
|
|
|
id: String(node.id),
|
|
|
|
label: cst.term.resolved ? `${cst.alias}: ${cst.term.resolved}` : cst.alias
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}, [schema, filtered.nodes]);
|
2023-07-29 21:23:18 +03:00
|
|
|
|
2023-07-30 00:47:07 +03:00
|
|
|
const edges: GraphEdge[] = useMemo(() => {
|
2023-07-29 23:00:03 +03:00
|
|
|
const result: GraphEdge[] = [];
|
|
|
|
let edgeID = 1;
|
2023-08-03 16:42:49 +03:00
|
|
|
filtered.nodes.forEach(source => {
|
2023-08-02 18:24:17 +03:00
|
|
|
source.outputs.forEach(target => {
|
2023-07-29 23:00:03 +03:00
|
|
|
result.push({
|
|
|
|
id: String(edgeID),
|
|
|
|
source: String(source.id),
|
|
|
|
target: String(target)
|
|
|
|
});
|
|
|
|
edgeID += 1;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return result;
|
2023-08-03 16:42:49 +03:00
|
|
|
}, [filtered.nodes]);
|
2023-07-29 21:23:18 +03:00
|
|
|
|
2023-07-31 22:38:58 +03:00
|
|
|
const handleCenter = useCallback(() => {
|
|
|
|
graphRef.current?.resetControls();
|
|
|
|
graphRef.current?.centerGraph();
|
|
|
|
}, []);
|
|
|
|
|
2023-07-30 00:47:07 +03:00
|
|
|
const {
|
|
|
|
selections, actives,
|
|
|
|
onNodeClick,
|
|
|
|
onCanvasClick,
|
|
|
|
onNodePointerOver,
|
|
|
|
onNodePointerOut
|
|
|
|
} = useSelection({
|
|
|
|
ref: graphRef,
|
|
|
|
nodes,
|
|
|
|
edges,
|
2023-07-31 22:38:58 +03:00
|
|
|
type: 'multi', // 'single' | 'multi' | 'multiModifier'
|
2023-07-30 00:47:07 +03:00
|
|
|
pathSelectionType: 'all',
|
2023-07-31 22:38:58 +03:00
|
|
|
focusOnSelect: false
|
2023-07-30 00:47:07 +03:00
|
|
|
});
|
|
|
|
|
2023-07-31 22:38:58 +03:00
|
|
|
return (<>
|
2023-07-30 00:47:07 +03:00
|
|
|
<div className='relative w-full'>
|
2023-08-03 16:42:49 +03:00
|
|
|
<div className='absolute top-0 left-0 z-20 py-2 w-[12rem] flex flex-col'>
|
|
|
|
<div className='flex items-center gap-1 w-[15rem]'>
|
|
|
|
<Button
|
|
|
|
icon={<ArrowsRotateIcon size={8} />}
|
|
|
|
dense
|
|
|
|
tooltip='Центрировать изображение'
|
|
|
|
widthClass='h-full'
|
|
|
|
onClick={handleCenter}
|
|
|
|
/>
|
|
|
|
<ConceptSelect
|
|
|
|
options={GraphLayoutSelector}
|
|
|
|
placeholder='Выберите тип'
|
|
|
|
values={layout ? [{ value: layout, label: mapLayoutLabels.get(layout) }] : []}
|
|
|
|
onChange={data => { setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }}
|
|
|
|
/>
|
|
|
|
</div>
|
2023-07-31 22:38:58 +03:00
|
|
|
<Checkbox
|
|
|
|
label='Анимация вращения'
|
|
|
|
value={orbit}
|
2023-08-03 16:42:49 +03:00
|
|
|
onChange={ event => setOrbit(event.target.checked) }
|
|
|
|
/>
|
|
|
|
<Checkbox
|
|
|
|
label='Удалить несвязанные'
|
|
|
|
value={noHermits}
|
|
|
|
onChange={ event => setNoHermits(event.target.checked) }
|
|
|
|
/>
|
|
|
|
<Checkbox
|
|
|
|
label='Транзитивная редукция'
|
|
|
|
value={noTransitive}
|
|
|
|
onChange={ event => setNoTransitive(event.target.checked) }
|
2023-07-30 00:47:07 +03:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
2023-07-31 22:38:58 +03:00
|
|
|
<div className='flex-wrap w-full h-full overflow-auto'>
|
2023-08-03 17:57:58 +03:00
|
|
|
<div className='relative w-[1260px] h-[730px] 2xl:w-[1900px] 2xl:h-[750px]'>
|
2023-07-31 22:38:58 +03:00
|
|
|
<GraphCanvas
|
|
|
|
draggable
|
|
|
|
ref={graphRef}
|
|
|
|
nodes={nodes}
|
|
|
|
edges={edges}
|
|
|
|
layoutType={layout}
|
|
|
|
selections={selections}
|
|
|
|
actives={actives}
|
|
|
|
onNodeClick={onNodeClick}
|
|
|
|
onCanvasClick={onCanvasClick}
|
|
|
|
defaultNodeSize={5}
|
|
|
|
onNodePointerOver={onNodePointerOver}
|
|
|
|
onNodePointerOut={onNodePointerOut}
|
|
|
|
cameraMode={ orbit ? 'orbit' : layout.includes('3d') ? 'rotate' : 'pan'}
|
2023-08-03 16:42:49 +03:00
|
|
|
layoutOverrides={ layout.includes('tree') ? { nodeLevelRatio: 3 } : undefined }
|
2023-07-31 22:38:58 +03:00
|
|
|
labelFontUrl={resources.graph_font}
|
|
|
|
theme={darkMode ? darkTheme : lightTheme}
|
2023-08-03 16:42:49 +03:00
|
|
|
renderNode={({ node, ...rest }) => (
|
|
|
|
<Sphere {...rest} node={node} />
|
|
|
|
)}
|
2023-07-29 23:00:03 +03:00
|
|
|
/>
|
2023-07-29 21:23:18 +03:00
|
|
|
</div>
|
2023-07-31 22:38:58 +03:00
|
|
|
</div>
|
|
|
|
</>);
|
2023-07-29 21:23:18 +03:00
|
|
|
}
|
|
|
|
|
2023-07-29 23:00:03 +03:00
|
|
|
|
2023-07-29 21:23:18 +03:00
|
|
|
export default EditorTermGraph;
|