ConceptPortal-public/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx

164 lines
5.3 KiB
TypeScript
Raw Normal View History

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
import Button from '../../components/Common/Button';
import Checkbox from '../../components/Common/Checkbox';
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';
import { useConceptTheme } from '../../context/ThemeContext';
2023-07-30 00:47:07 +03:00
import useLocalStorage from '../../hooks/useLocalStorage';
import { resources } from '../../utils/constants';
2023-08-03 16:42:49 +03:00
import { Graph } from '../../utils/Graph';
import { GraphLayoutSelector,mapLayoutLabels } from '../../utils/staticUI';
2023-07-29 21:23:18 +03:00
function EditorTermGraph() {
const { schema } = useRSForm();
2023-08-04 13:26:51 +03:00
const { darkMode, noNavigation } = useConceptTheme();
2023-08-08 18:44:30 +03:00
const [ layout, setLayout ] = useLocalStorage<LayoutTypes>('graph_layout', 'treeTd2d');
2023-08-03 16:42:49 +03:00
const [ filtered, setFiltered ] = useState<Graph>(new Graph());
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 => {
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
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,
type: 'multi', // 'single' | 'multi' | 'multiModifier'
2023-07-30 00:47:07 +03:00
pathSelectionType: 'all',
focusOnSelect: false
2023-07-30 00:47:07 +03:00
});
2023-08-04 13:26:51 +03:00
const canvasSize = !noNavigation ?
'w-[1240px] h-[736px] 2xl:w-[1880px] 2xl:h-[746px]'
: 'w-[1240px] h-[800px] 2xl:w-[1880px] 2xl:h-[810px]';
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
2023-08-14 00:36:25 +03:00
className='min-w-[9.5rem]'
2023-08-03 16:42:49 +03:00
options={GraphLayoutSelector}
placeholder='Выберите тип'
values={layout ? [{ value: layout, label: mapLayoutLabels.get(layout) }] : []}
onChange={data => { setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }}
/>
</div>
<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>
<div className='flex-wrap w-full h-full overflow-auto'>
2023-08-04 13:26:51 +03:00
<div className={`relative border-t border-r ${canvasSize}`}>
<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-05 01:14:41 +03:00
layoutOverrides={ layout.includes('tree') ? { nodeLevelRatio: schema && schema?.items.length < 50 ? 3 : 1 } : undefined }
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>
</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;