Skip to content
This repository has been archived by the owner on Dec 1, 2022. It is now read-only.

Commit

Permalink
GeoBlock visual component (#977)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoanphungt authored Oct 28, 2021
1 parent 9721310 commit a670ec8
Show file tree
Hide file tree
Showing 29 changed files with 2,267 additions and 1,444 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lizard-management-client",
"version": "0.2.83",
"version": "0.2.85",
"private": true,
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions src/data_management/geoblocks/GeoBlockForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ const GeoBlockForm: React.FC<Props & DispatchProps & RouteComponentProps> = (pro
) : null}
{buildModal ? (
<GeoBlockBuildModal
uuid={currentRecord ? currentRecord.uuid : null}
source={values.source}
onChange={value => handleValueChange('source', value)}
handleClose={() => setBuildModal(false)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Elements } from 'react-flow-renderer';
import { GeoBlockJsonComponent } from './GeoBlockJsonComponent';
import { GeoBlockVisualComponent } from './GeoBlockVisualComponent';
import { SubmitButton } from '../../../form/SubmitButton';
import { addNotification } from '../../../actions';
import { jsonValidator } from '../../../form/validators';
import { createGraphLayout } from '../../../utils/createGraphLayout';
import { fetchGeoBlock } from '../../../utils/geoblockValidators';
import {
convertElementsToGeoBlockSource,
convertGeoblockSourceToFlowElements
} from '../../../utils/geoblockUtils';
import ModalBackground from '../../../components/ModalBackground';
import styles from './GeoBlockBuildModal.module.css';
import formStyles from './../../../styles/Forms.module.css';
import buttonStyles from './../../../styles/Buttons.module.css';

interface MyProps {
uuid: string | null,
source: Object | null,
onChange: (value: Object) => void,
handleClose: () => void
Expand All @@ -20,6 +29,23 @@ function GeoBlockBuildModal (props: MyProps & DispatchProps) {
const [jsonString, setJsonString] = useState<string>(sourceString);
const [geoBlockView, setGeoBlockView] = useState<'json' | 'visual'>('json');

// Block elements of a geoblock for React-Flow are kept in here
// to make use of the SAVE, SWITCH and VALIDATE buttons
const [elements, setElements] = useState<Elements>([]);

// useEffect to create geoblock elements and build the graph layout using dagre library
// useEffect only gets called when the visual component is open
// use the useEffect here instead of the GeoBlockVisualComponent to avoid Maximum Depth Exceeded error
useEffect(() => {
if (geoBlockView === 'visual') {
const geoblockElements = convertGeoblockSourceToFlowElements(jsonString, setElements);
const layoutedElements = createGraphLayout(jsonString, geoblockElements);
setElements(layoutedElements);
};
// setElements back to [] when component unmounted
return () => setElements([]);
}, [jsonString, setElements, geoBlockView]);

return (
<ModalBackground
title={'Geo Block Builder'}
Expand All @@ -35,6 +61,8 @@ function GeoBlockBuildModal (props: MyProps & DispatchProps) {
/>
) : (
<GeoBlockVisualComponent
elements={elements}
setElements={setElements}
/>
)}
<div className={`${formStyles.ButtonContainer} ${formStyles.FixedButtonContainer}`}>
Expand All @@ -46,19 +74,49 @@ function GeoBlockBuildModal (props: MyProps & DispatchProps) {
</button>
<button
onClick={() => {
if (geoBlockView === 'json') {
setGeoBlockView('visual');
if (jsonValidator(jsonString)) {
return alert(jsonValidator(jsonString));
};
if (geoBlockView === 'visual') {
const geoBlockSource = convertElementsToGeoBlockSource(elements, jsonString, setJsonString);
if (geoBlockSource) setGeoBlockView('json');
} else {
setGeoBlockView('json')
setGeoBlockView('visual');
};
}}
>
Switch
</button>
<button
onClick={() => {
if (geoBlockView === 'visual') {
const geoBlockSource = convertElementsToGeoBlockSource(elements, jsonString, setJsonString);
if (geoBlockSource) fetchGeoBlock(props.uuid, geoBlockSource);
} else {
if (jsonValidator(jsonString)) {
return alert(jsonValidator(jsonString));
};
fetchGeoBlock(props.uuid, JSON.parse(jsonString))
};
}}
>
Validate
</button>
<SubmitButton
onClick={() => {
props.onChange(JSON.parse(jsonString));
props.handleClose();
if (jsonValidator(jsonString)) {
return alert(jsonValidator(jsonString));
};
if (geoBlockView === 'visual') {
const geoBlockSource = convertElementsToGeoBlockSource(elements, jsonString, setJsonString);
if (geoBlockSource) {
props.onChange(geoBlockSource);
props.handleClose();
};
} else {
props.onChange(JSON.parse(jsonString));
props.handleClose();
};
}}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ interface MyProps {
setJsonString: (e: string) => void
}

const prettyJsonFormat = (e: Object) => {
return JSON.stringify(e, null, 4);
};

export const GeoBlockJsonComponent = (props: MyProps) => {
const { jsonString, setJsonString } = props;
const [jsonTextView, setJsonTextView] = useState<boolean>(true);

// Helper function to convert json to pretty format
const setJsonStringInPrettyFormat = (e: Object) => {
setJsonString(JSON.stringify(e, undefined, 4));
};

return (
<>
<button
Expand Down Expand Up @@ -45,7 +44,7 @@ export const GeoBlockJsonComponent = (props: MyProps) => {
return alert(jsonValidator(jsonString));
};
const object = JSON.parse(jsonString);
setJsonStringInPrettyFormat(object);
setJsonString(prettyJsonFormat(object));
}}
style={{
position: 'absolute',
Expand Down Expand Up @@ -73,9 +72,9 @@ export const GeoBlockJsonComponent = (props: MyProps) => {
src={JSON.parse(jsonString)}
name="source"
theme="shapeshifter:inverted"
onEdit={e => setJsonStringInPrettyFormat(e.updated_src)}
onAdd={e => setJsonStringInPrettyFormat(e.updated_src)}
onDelete={e => setJsonStringInPrettyFormat(e.updated_src)}
onEdit={e => setJsonString(prettyJsonFormat(e.updated_src))}
onAdd={e => setJsonString(prettyJsonFormat(e.updated_src))}
onDelete={e => setJsonString(prettyJsonFormat(e.updated_src))}
displayDataTypes={false}
displayObjectSize={false}
quotesOnKeys={false}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,181 @@
import React from 'react';
import React, { useRef, useState } from 'react';
import ReactFlow, {
addEdge,
Connection,
ConnectionLineType,
Controls,
Edge,
Elements,
isNode,
MiniMap,
Position,
ReactFlowProvider,
removeElements,
updateEdge,
useUpdateNodeInternals
} from 'react-flow-renderer';
import { SideBar } from './blockComponents/SideBar';
import { Block } from './blockComponents/Block';
import { BooleanBlock } from './blockComponents/BooleanBlock';
import { GroupBlock } from './blockComponents/GroupBlock';
import { NumberBlock } from './blockComponents/NumberBlock';
import { RasterBlock } from './blockComponents/RasterBlock';
import { StringBlock } from './blockComponents/StringBlock';
import { ArrayBlock } from './blockComponents/ArrayBlock';
import { geoblockType } from '../../../types/geoBlockType';
import { getBlockData } from '../../../utils/geoblockUtils';
import { targetHandleValidator } from '../../../utils/geoblockValidators';
import edgeStyle from './blockComponents/Edge.module.css';

interface MyProps {
elements: Elements,
setElements: React.Dispatch<React.SetStateAction<Elements<any>>>
}

const GeoBlockVisualFlow = (props: MyProps) => {
const { elements, setElements } = props;
const reactFlowWrapper = useRef<any>(null);
const updateNodeInternals = useUpdateNodeInternals();
const [reactFlowInstance, setReactFlowInstance] = useState<any>(null);

// gets called after end of edge gets dragged to another source or target
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => {
setElements((els) => updateEdge(oldEdge, newConnection, els));
newConnection.target && updateNodeInternals(newConnection.target); // update node internals
};

const onConnect = (params: Edge | Connection) => {
const connectError = targetHandleValidator(elements, params);
if (connectError) return console.error(connectError.errorMessage);

setElements((els) => {
const source = els.find(el => el.id === params.source)!;
return addEdge({
...params,
type: ConnectionLineType.SmoothStep,
animated: true,
className: (
source.type === 'NumberBlock' ? edgeStyle.NumberEdge :
source.type === 'StringBlock' ? edgeStyle.StringEdge :
source.type === 'BooleanBlock' ? edgeStyle.BooleanEdge :
edgeStyle.BlockEdge
)
}, els);
});
params.target && updateNodeInternals(params.target); // update node internals
};

const onElementsRemove = (elementsToRemove: Elements) => {
setElements((els) => removeElements(elementsToRemove, els))
};

const onLoad = (_reactFlowInstance: any) => {
setReactFlowInstance(_reactFlowInstance);
};

// Drag and drop actions
const onDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
};

const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
if (reactFlowWrapper && reactFlowWrapper.current) {
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const blockName = event.dataTransfer.getData('application/reactflow');
const position = reactFlowInstance.project({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
});
const sourcePosition = Position.Right;
const targetPosition = Position.Left;

// Keep track of number of block elements in the graph to create block id
const numberOfBlocks = elements.filter(elm => {
// @ts-ignore
return isNode(elm) && elm.data && elm.data.classOfBlock === geoblockType[blockName].class;
}).length;
const idOfNewBlock = blockName + numberOfBlocks;

const newBlock = {
id: idOfNewBlock,
type: (
blockName === 'RasterBlock' || blockName === 'NumberBlock' || blockName === 'BooleanBlock' || blockName === 'StringBlock' ? blockName :
blockName === 'Group' || blockName === 'FillNoData' ? 'GroupBlock' : 'Block'
),
position,
sourcePosition,
targetPosition,
data: getBlockData(blockName, numberOfBlocks, idOfNewBlock, setElements)
};
console.log('newBlock', newBlock);
setElements((es) => es.concat(newBlock));
};
};

export const GeoBlockVisualComponent = () => {
return (
<div>
GeoBlock Visual Component
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 150px',
columnGap: 10,
margin: '20px 0',
height: 550
}}
>
<ReactFlow
ref={reactFlowWrapper}
elements={elements}
onElementsRemove={onElementsRemove}
style={{
position: 'relative',
border: '1px solid lightgrey',
borderRadius: 10
}}
snapToGrid
onEdgeUpdate={onEdgeUpdate}
onConnect={onConnect}
onLoad={onLoad}
onDragOver={onDragOver}
onDrop={onDrop}
nodeTypes={{
Block: Block,
BooleanBlock: BooleanBlock,
GroupBlock: GroupBlock,
RasterBlock: RasterBlock,
NumberBlock: NumberBlock,
StringBlock: StringBlock,
ArrayBlock: ArrayBlock
}}
>
{elements.length > 100 ? (
<MiniMap
nodeColor={(node) => {
if (node.type === 'RasterBlock') return 'orange';
if (node.type === 'Block' && node.data.outputBlock) return 'red';
if (node.type === 'Block' || node.type === 'GroupBlock') return 'green';
return 'lightgrey';
}}
nodeStrokeWidth={3}
/>
) : null}
<Controls />
</ReactFlow>
<SideBar />
</div>
)
}
}

export const GeoBlockVisualComponent = (props: MyProps) => (
// Wrap the GeoBlockVisualFlow component inside the ReactFlowProvider
// to have access to the useUpdateNodeInternals hook of react-flow.
// the useUpdateNodeInternals hook is used to update the node manually
// when there are changes that cannot be automatically updated to the node.
<ReactFlowProvider>
<GeoBlockVisualFlow
elements={props.elements}
setElements={props.setElements}
/>
</ReactFlowProvider>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { Handle, Node, Position } from 'react-flow-renderer';
import formStyles from './../../../../styles/Forms.module.css';
// import styles from './Block.module.css';

interface ArrayBlockInput {
value: string,
onChange: (value: string) => void,
}

export const ArrayBlock = (props: Node<ArrayBlockInput>) => {
const { value } = props.data!;
return (
<div>
<input
type="text"
title={JSON.stringify(value)}
className={formStyles.FormControl}
// onChange={e => onChange(e.target.value)}
value={JSON.stringify(value)}
readOnly
style={{
width: 140,
fontSize: 12
}}
/>
<Handle
type="source"
position={Position.Right}
/>
</div>
)
}
Loading

0 comments on commit a670ec8

Please sign in to comment.