diff --git a/src-frontend/src/App.tsx b/src-frontend/src/App.tsx index 5fd00517..3c37bd8c 100755 --- a/src-frontend/src/App.tsx +++ b/src-frontend/src/App.tsx @@ -12,7 +12,7 @@ import { MassenstromResponse, NodeElements, NodeType, - OptimizationMetadata, OptimizationResult, + OptimizationMetadata, OutputNode, Pipe } from "./models"; @@ -51,17 +51,24 @@ function App() { const [pipes, setPipes] = useState>([]) const [temperatureKey, setTemperatureKey] = useState(defaultTemperatureKey) const [optimizationMetadata, setOptimizationMetadata] = useState(defaultOptimizationMetadata) + const [activeOptimizationId, setActiveOptimizationId] = useState() - const [costs, setCosts] = useState(undefined) + + const [costs, setCosts] = useState(undefined) const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === KeyboardKey.ENTER || e.key === KeyboardKey.ESC){ + if (e.key === KeyboardKey.ENTER || e.key === KeyboardKey.ESC) { e.preventDefault() } } - useEffect(() => console.log(pipes)) + useEffect(() => { + if(activeOptimizationId !== undefined) { + setTabVal("5") + } + console.log("activeOptimizationId: " + activeOptimizationId) + },[activeOptimizationId]) useEffect(() => { document.addEventListener('keydown', handleKeyDown, false); @@ -118,7 +125,6 @@ function App() { } - const getGrid = () => { return {pipes: (pipes as Pipe[]), ...nodeElements, temperatureSeries: temperatureKey} } @@ -129,24 +135,37 @@ function App() { const isCostsComplete = () => !!costs + const handleSetActiveOptimizationId = (id: string) => { + console.log("Set active optimization id") + console.log(id) + setActiveOptimizationId(id) + // setTabVal("5") + } + + const handleSetTabVal = (val: string) => { + setActiveOptimizationId(undefined) + setTabVal(val) + } + return (
- {// @ts-ignore - } -

{getPipe()}Pipify

- setTabVal(val)} aria-label="simple tabs example"> - } label="Formel Check" value="4" /> - } label="Meta Daten" value="2"/> - } label="Editor" value="1" disabled={!isMetaDataComplete()} /> - } label="Max Massenstrom" value="3" disabled={!isMaxMassenstromComplete()} /> - } label="Node Detail" value="5" disabled={!isCostsComplete()} /> - -
+ +

{getPipe()}Pipify

+ handleSetTabVal(val)} aria-label="simple tabs example"> + } label="Formel Check" value="4"/> + } label="Meta Daten" value="2"/> + } label="Editor" value="1" disabled={!isMetaDataComplete()}/> + } label="Max Massenstrom" value="3" + disabled={!isMaxMassenstromComplete()}/> + } label="Node Detail" value="5" disabled={!isCostsComplete()}/> + +
+ setNodeElements={setNodeElements} temperatureSeries={temperatureKey} + setActiveOptimizationId={handleSetActiveOptimizationId}/> - - + + diff --git a/src-frontend/src/Components/DefaultEdge.tsx b/src-frontend/src/Components/DefaultEdge.tsx index 63ad421f..fc2316fb 100644 --- a/src-frontend/src/Components/DefaultEdge.tsx +++ b/src-frontend/src/Components/DefaultEdge.tsx @@ -1,6 +1,9 @@ import {EdgeText, getBezierPath, getMarkerEnd, Position} from "react-flow-renderer"; import {Tooltip} from "@material-ui/core"; import React from "react"; +import {notify} from "../ReactFlow/Overlays/Notifications"; +import {handleNodeCtrlClick} from "../CustomNodes/InputNode"; +import {Pipe} from "../models"; export interface GetCenterParams { @@ -88,7 +91,7 @@ export const DefaultEdge = ({ return <> <> - + handleNodeCtrlClick({data,id} as Pipe)}/> {/**/} {/* */} diff --git a/src-frontend/src/CustomNodes/InputNode.tsx b/src-frontend/src/CustomNodes/InputNode.tsx index 3e44694b..bb94e01e 100644 --- a/src-frontend/src/CustomNodes/InputNode.tsx +++ b/src-frontend/src/CustomNodes/InputNode.tsx @@ -1,9 +1,9 @@ -import {Handle, Position} from "react-flow-renderer"; +import {Edge, Handle, Position} from "react-flow-renderer"; import {Tooltip} from "@material-ui/core"; import React, {ReactElement} from "react"; -import {BaseNode, InputNode as InputNodeModel} from "../models"; +import {BaseNode, InputNode as InputNodeModel, Pipe} from "../models"; import {showNodeInputDialog} from "../ReactFlow/Overlays/NodeContextOverlay"; -import {verifyBackend} from "../ReactFlow/FlowContainer"; +import {notify} from "../ReactFlow/Overlays/Notifications"; const customNodeStyles = { @@ -33,6 +33,20 @@ export const getOptimizationTooltip = (baseNode: BaseNode): ReactElement => { } +export const handleNodeCtrlClick = (flowElement: (BaseNode | Pipe)) => { + if(isCtrlyKeyPressed()) { + if((flowElement.data.annualEnergyDemand || flowElement.data.diameter)){ + flowElement.data.onCtrlClick(flowElement.id) + } + else if(!flowElement.data.annualEnergyDemand && !flowElement.data.diameter){ + notify("Für dieses Element ist leider keine Optimierung verfügbar. Optimiere zunächst das Netz.") + } + } + +} + +// @ts-ignore +export const isCtrlyKeyPressed = () => window.event?.ctrlKey export const InputNode = (node: InputNodeModel) => { @@ -43,24 +57,27 @@ export const InputNode = (node: InputNodeModel) => { return newNode } - const handleClick = () => { + const handleDoubleClick = () => { showNodeInputDialog("Bearbeiten sie diesen Einspeisepunkt", getInputNode(), handleConfirm, () => {/*Nothing to do here*/ }, () => node.data.onDelete(node.data.id ?? node.id)) } + const handleConfirm = (newNode: InputNodeModel) => { console.log(newNode) node.data.updateNode(newNode) } + + return ( Formel Vorlauftemperatur: {node.data.flowTemperatureTemplate}
Formel Rücklauftemperatur: {node.data.returnTemperatureTemplate}
{node.data.annualEnergyDemand? getOptimizationTooltip(node.data): <>} }> -
+
handleNodeCtrlClick(node)}> { {node.data.annualEnergyDemand ? getOptimizationTooltip(node.data) : <>} }> -
+
handleNodeCtrlClick(node)}>
{node.data.label}
{ {node.data.annualEnergyDemand? getOptimizationTooltip(node.data): <>} }> -
+
handleNodeCtrlClick(node)}>
{node?.data.label}
diff --git a/src-frontend/src/Filemanagement/FileUpload.tsx b/src-frontend/src/Filemanagement/FileUpload.tsx index ec5fb7dd..17864afc 100644 --- a/src-frontend/src/Filemanagement/FileUpload.tsx +++ b/src-frontend/src/Filemanagement/FileUpload.tsx @@ -1,9 +1,10 @@ import React, {useCallback} from "react"; import {useDropzone} from 'react-dropzone'; import "./file-upload.css"; -import {HotWaterGrid, instanceOfHotWaterGrid} from "../models"; +import {BaseNode, HotWaterGrid, instanceOfHotWaterGrid, Pipe} from "../models"; import {notify} from "../ReactFlow/Overlays/Notifications"; import {Cancel} from "@material-ui/icons"; +import IdGenerator from "../utils/IdGenerator"; interface UploadProps { @@ -46,6 +47,7 @@ export const FileUpload = (props: UploadProps) => { const jsonResult = mapToJSON(reader); console.log(instanceOfHotWaterGrid(jsonResult)) if(instanceOfHotWaterGrid(jsonResult)) { + determineHighestId(jsonResult as HotWaterGrid) props.loadGrid(jsonResult) } else { notify("Die Eingabedatei ist leider nicht valide") @@ -62,6 +64,41 @@ export const FileUpload = (props: UploadProps) => { const csvStr = mapCSVToArray(reader) } + const getMaxPipeId = (pipes: Pipe[]) => { + const pipeIdNumbers: number[] = pipes.map(p => { + let idStr + if(p.id.match("##\\d+##") && p.id.match("##\\d+##")![0]) { + const idPart = p.id.match("##\\d+##")![0] + idStr = idPart.match("\\d+")![0] + } + return Number.parseInt(idStr ?? "0") + }) + + return Math.max(...pipeIdNumbers) + } + + const getMaxNodeId = (nodes: BaseNode[]) => { + const pipeIdNumbers: number[] = nodes.map(n => { + let idStr + console.log(n.id) + if(n.id.match("\\d+\+")) { + const idPart = n.id.match("\d+\+")![0] + idStr = idPart.match("\d+")![0] + } + return Number.parseInt(idStr ?? "0") + }) + + return Math.max(...pipeIdNumbers) + } + + const determineHighestId = (grid: HotWaterGrid) => { + const maxPipeId = getMaxPipeId(grid.pipes) + const maxNodeId = 0//getMaxNodeId([...grid.inputNodes, ...grid.intermediateNodes, ...grid.outputNodes]) + const maxId = Math.max(maxPipeId, maxNodeId) + alert(maxId) + IdGenerator.setNextId(maxId + 1) + } + const onDrop = useCallback((acceptedFiles) => { acceptedFiles.forEach((file: File) => { const fileType = file.type diff --git a/src-frontend/src/OptimizationNode/OptimizationNodeDetails.tsx b/src-frontend/src/OptimizationNode/OptimizationNodeDetails.tsx index 02504177..f4105f9a 100644 --- a/src-frontend/src/OptimizationNode/OptimizationNodeDetails.tsx +++ b/src-frontend/src/OptimizationNode/OptimizationNodeDetails.tsx @@ -1,5 +1,5 @@ import {BaseNode, NodeElements, Pipe} from "../models"; -import React from "react"; +import React, {MutableRefObject, useEffect, useRef} from "react"; import {Accordion, AccordionDetails, AccordionSummary} from "@material-ui/core"; // @ts-ignore import Plot from 'react-plotly.js'; @@ -7,26 +7,48 @@ import Plot from 'react-plotly.js'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -export const OptimizationNodeDetails = ({nodeElements, pipes}: { nodeElements: NodeElements, pipes: Pipe[] }) => { +export const OptimizationNodeDetails = ({ + nodeElements, + pipes, + activeId + }: { nodeElements: NodeElements, pipes: Pipe[], activeId?: string }) => { + let myRef = useRef() + + useEffect(() => { + if (activeId) { + const offsetContainer: number = document.getElementById("first-optimization-accordion")?.offsetTop!; + document.getElementById("optimization-panel")?.scrollTo({ + behavior: 'smooth', + top: myRef?.current?.offsetTop! - offsetContainer + }); + } + }, []) return <>

Input Nodes

- +

Intermediate Nodes

- +

Output Nodes

- +

Pipes

- + } -export const OptimizationAccordionNode = ({nodes}: { nodes: BaseNode[] }) => { - const getAccordionNode = (n: BaseNode) => { +export const OptimizationAccordionNode = ({ + nodes, + activeId, + myRef, + id + }: { nodes: BaseNode[], activeId?: string, myRef: MutableRefObject, id?: string }) => { + const getAccordionNode = (n: BaseNode, defaultExpanded: boolean) => { return <> - + } aria-controls="panel1a-content" id="panel1a-header"> {n.data?.label} ({n.id}) @@ -125,15 +147,20 @@ export const OptimizationAccordionNode = ({nodes}: { nodes: BaseNode[] }) => { } return <>{nodes.filter(n => n.optimizedThermalEnergyDemand) - .map(n => getAccordionNode(n)) + .map(n => getAccordionNode(n, n.id === activeId)) } } -export const OptimizationAccordionPipe = ({pipes}: { pipes: Pipe[] }) => { - const getAccordionPipe = (p: Pipe) => { +export const OptimizationAccordionPipe = ({ + pipes, + activeId, + myRef + }: { pipes: Pipe[], activeId?: string, myRef: MutableRefObject }) => { + const getAccordionPipe = (p: Pipe, defaultExpanded: boolean) => { return <> - + } aria-controls="panel1a-content" id="panel1a-header"> {p.data?.label} ({p.id}) @@ -240,7 +267,9 @@ export const OptimizationAccordionPipe = ({pipes}: { pipes: Pipe[] }) => {
} - return <>{pipes.filter((p: Pipe) => p.diameter) - .map(p => getAccordionPipe(p)) - } + return <> + { + pipes.filter((p: Pipe) => p.diameter) + .map(p => getAccordionPipe(p, p.id === activeId)) + } } diff --git a/src-frontend/src/ReactFlow/FlowContainer.tsx b/src-frontend/src/ReactFlow/FlowContainer.tsx index ccaf3828..b8158b4c 100644 --- a/src-frontend/src/ReactFlow/FlowContainer.tsx +++ b/src-frontend/src/ReactFlow/FlowContainer.tsx @@ -32,6 +32,7 @@ import {OutputNode} from "../CustomNodes/OutputNode"; import {baseUrl, createGrid} from "../utils/utility"; import {notify} from "./Overlays/Notifications"; import {DefaultEdge} from "../Components/DefaultEdge"; +import IdGenerator from "../utils/IdGenerator"; const style = getComputedStyle(document.body) const corpColor = style.getPropertyValue('--corp-main-color') @@ -40,7 +41,7 @@ const edgeConfiguration = { animated: true, type: 'DEFAULT_EDGE', arrowHeadType: ArrowHeadType.ArrowClosed, - style: {stroke: `rgb(${corpColor})`, strokeWidth: "3px"} + style: {stroke: `rgb(${corpColor})`, strokeWidth: "6px"} } @@ -65,7 +66,8 @@ interface FlowContainerProperties { setPipes: Dispatch>>, nodeElements: NodeElements, setNodeElements: Dispatch>, - temperatureSeries: string + temperatureSeries: string, + setActiveOptimizationId: (id: string) => void } export enum ResultCode { @@ -97,7 +99,14 @@ export const verifyBackend = (grid: HotWaterGrid): Promise => { return false}); } -export const FlowContainer = ({pipes, setPipes, nodeElements, setNodeElements, temperatureSeries}: FlowContainerProperties) => { +export const FlowContainer = ({ + pipes, + setPipes, + nodeElements, + setNodeElements, + temperatureSeries, + setActiveOptimizationId + }: FlowContainerProperties) => { const [popupTarget, setPopupTarget] = useState(null) @@ -105,22 +114,27 @@ export const FlowContainer = ({pipes, setPipes, nodeElements, setNodeElements, t const onConnect = (params) => { params.animated = true; showEditPipeDialog("Füge ein neues Rohr hinzu", - (id, length1, coverageHeight) => {onConfirmPipe(params, id, length1, coverageHeight)}, + (id, length1, coverageHeight) => { + onConfirmPipe(params, id, length1, coverageHeight) + }, () => console.log("Nothing to do here"), params.id, undefined, undefined) }; const onConfirmPipe = (params: any, id: string, length: number, coverageHeight: number) => { const pipesToVerify = [...pipes] + const {source, target} = params; + id = IdGenerator.getNextPipeId(source, target) + const newPipe = { - source: params.source, - target: params.target, + source: source, + target: target, id, length, coverageHeight } pipesToVerify.push(newPipe) verifyBackend(createGrid(nodeElements, pipesToVerify as Pipe[], temperatureSeries)).then((verified: boolean) => { - if(verified) { - params= {...params, ...edgeConfiguration, id, length, coverageHeight, data: {length, coverageHeight }} + if (verified) { + params = {...params, ...edgeConfiguration, id, length, coverageHeight, data: {length, coverageHeight,}} const newPipes = [...pipes] newPipes.push(params) setPipes(newPipes) @@ -149,6 +163,7 @@ export const FlowContainer = ({pipes, setPipes, nodeElements, setNodeElements, t } const handleEditEdge = (id: string, length: number, coverageHeight: number) => { + const newPipes = pipes.map(p => { if(p.id === id) { return {...p, length, coverageHeight, data: {...p.data, length, coverageHeight}} @@ -190,47 +205,48 @@ export const FlowContainer = ({pipes, setPipes, nodeElements, setNodeElements, t const outputNodes = addTypeToNodes(nodeElements.outputNodes, NodeType.OUTPUT_NODE) // console.log(pipes) const defaultPipes = pipes.map((el) => { - return {...el, ...edgeConfiguration} + return {...el, ...edgeConfiguration, data: {...el.data, onCtrlClick: setActiveOptimizationId}} }) + const getCommonProps = (n: BaseNode) => { + const {annualEnergyDemand, maximalNeededPumpPower, maximalPressureLoss} = n + + return { + updateNode, onDelete: handleDeleteNode, annualEnergyDemand, maximalNeededPumpPower, maximalPressureLoss, + onCtrlClick: setActiveOptimizationId + } + } + inputNodes.forEach((n) => { - const {flowTemperatureTemplate, returnTemperatureTemplate, annualEnergyDemand, - maximalNeededPumpPower, maximalPressureLoss} = (n as InputNodeModel) + const {flowTemperatureTemplate, returnTemperatureTemplate} = (n as InputNodeModel) n.data = { - ...n.data, flowTemperatureTemplate, returnTemperatureTemplate, updateNode, onDelete: handleDeleteNode, - annualEnergyDemand, maximalNeededPumpPower, maximalPressureLoss + ...n.data, flowTemperatureTemplate, returnTemperatureTemplate, ...getCommonProps(n) } }) intermediateNodes.forEach((n) => { - const {annualEnergyDemand, maximalNeededPumpPower, maximalPressureLoss} = n; - n.data = {...n.data, updateNode, onDelete: handleDeleteNode, annualEnergyDemand, maximalNeededPumpPower, - maximalPressureLoss} + n.data = {...n.data, ...getCommonProps(n)} }) outputNodes.forEach((n) => { - const {thermalEnergyDemand, pressureLoss, loadProfileName, replicas, annualEnergyDemand, - maximalNeededPumpPower, maximalPressureLoss} = (n as OutputNodeModel) + const {thermalEnergyDemand, pressureLoss, loadProfileName, replicas} = (n as OutputNodeModel) n.data = { ...n.data, thermalEnergyDemand, pressureLoss, - updateNode, loadProfileName, replicas, - onDelete: handleDeleteNode, - annualEnergyDemand, - maximalNeededPumpPower, - maximalPressureLoss + ...getCommonProps(n) } }) - console.log(defaultPipes) - return [...inputNodes, ...intermediateNodes, ...outputNodes, ...defaultPipes] } const updateNode = (newNode: BaseNode) => { + + //todo modify id and edge ids + let nodeType; switch (newNode.type) { case NodeType.INPUT_NODE: @@ -247,20 +263,41 @@ export const FlowContainer = ({pipes, setPipes, nodeElements, setNodeElements, t return; } - console.log(nodeElements) - console.log(pipes) + const onLabelChange = (newNode: BaseNode, oldNode: BaseNode) => { + const oldLabel = oldNode.data.label + const newLabel = newNode.data.label + if (oldLabel !== newLabel) { + newNode.id = IdGenerator.getNextNodeId(newNode.data.label) + + pipes.map((p: FlowElement) => { + const {source, target} = (p as Pipe) + if (source === oldLabel || target === oldLabel) { + (p as Pipe).source = source === oldLabel ? newLabel : source; + (p as Pipe).target = target === oldLabel ? newLabel : target + const newSource = (p as Pipe).source + const newTarget = (p as Pipe).target + p.id = IdGenerator.getNextPipeId(source, target) + } + return p + }) + + } + return newNode + } const newNodeElements = {...nodeElements} // @ts-ignore - newNodeElements[nodeType] = nodeElements[nodeType].map((n) => { - if (n.id === newNode.id) { + newNodeElements[nodeType] = nodeElements[nodeType].map((oldNode) => { + if (oldNode.id === newNode.id) { + newNode = onLabelChange(newNode, oldNode); return newNode - } return n + } + return oldNode }) verifyBackend({pipes: pipes as Pipe[], ...nodeElements, temperatureSeries}).then(b => { - if(b){ + if (b) { setNodeElements(newNodeElements) } }) diff --git a/src-frontend/src/ReactFlow/OverlayButtons/NodeMenu/NodeMenuSpawnerContainer.tsx b/src-frontend/src/ReactFlow/OverlayButtons/NodeMenu/NodeMenuSpawnerContainer.tsx index a7f42b36..05f55258 100644 --- a/src-frontend/src/ReactFlow/OverlayButtons/NodeMenu/NodeMenuSpawnerContainer.tsx +++ b/src-frontend/src/ReactFlow/OverlayButtons/NodeMenu/NodeMenuSpawnerContainer.tsx @@ -4,16 +4,22 @@ import {InputNodeSpawner} from "./InputNodeSpawner"; import {IntermediateNodeSpawner} from "./IntermediateNodeSpawner"; import {OutputNodeSpawner} from "./OutputNodeSpawner"; import "./node-menu-spawner.css" +import IdGenerator from "../../../utils/IdGenerator"; export interface NodeSpawner { onNewNode: (newNode: BaseNode) => void } export const NodeMenuSpawnerContainer = ({onNewNode}: NodeSpawner) => { + + const handleNewNode = (baseNode: BaseNode ) =>{ + baseNode.id = IdGenerator.getNextNodeId(baseNode.data.label); + onNewNode(baseNode) + } + return
- onNewNode(baseNode)}/> - onNewNode(baseNode)}/> - { - onNewNode(baseNode)}}/> + + +
} diff --git a/src-frontend/src/utils/IdGenerator.ts b/src-frontend/src/utils/IdGenerator.ts new file mode 100644 index 00000000..6ee03188 --- /dev/null +++ b/src-frontend/src/utils/IdGenerator.ts @@ -0,0 +1,24 @@ +let nextId: number = 0; + +const getNextPipeId = (sourceId: string, targetId: string) => { + const result = `${sourceId}##${nextId}##${targetId}` + increaseId() + return result; +} + +const increaseId = () => nextId += 1; + +const getNextNodeId = (label: string) => { + const result = `${nextId}+${label}` + increaseId() + return result +} + +const IdGenerator = { + setNextId: (id: number) => nextId = id, + getNextPipeId, + getNextNodeId +} + +Object.freeze(IdGenerator) +export default IdGenerator;