mirror of
https://github.com/ANL-CEEESA/RELOG.git
synced 2025-12-06 07:48:50 -06:00
Make pipeline, parameters & product interactive
This commit is contained in:
@@ -1,22 +1,22 @@
|
|||||||
import styles from './Button.module.css'
|
import styles from './Button.module.css';
|
||||||
|
|
||||||
const Button = (props) => {
|
const Button = (props) => {
|
||||||
let className = styles.Button
|
let className = styles.Button;
|
||||||
if (props.kind === "inline") {
|
if (props.kind === "inline") {
|
||||||
className += " " + styles.inline
|
className += " " + styles.inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tooltip = "";
|
let tooltip = "";
|
||||||
if (props.tooltip != undefined) {
|
if (props.tooltip != undefined) {
|
||||||
tooltip = <span className={styles.tooltip}>{props.tooltip}</span>
|
tooltip = <span className={styles.tooltip}>{props.tooltip}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={className}>
|
<button className={className} onClick={props.onClick}>
|
||||||
{tooltip}
|
{tooltip}
|
||||||
{props.label}
|
{props.label}
|
||||||
</button>
|
</button>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Button;
|
export default Button;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import styles from './ButtonRow.module.css'
|
import styles from './ButtonRow.module.css';
|
||||||
|
|
||||||
const ButtonRow = (props) => {
|
const ButtonRow = (props) => {
|
||||||
return <div className={styles.ButtonRow}>{props.children}</div>
|
return <div className={styles.ButtonRow}>{props.children}</div>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ButtonRow;
|
export default ButtonRow;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import styles from './Card.module.css'
|
import styles from './Card.module.css';
|
||||||
|
|
||||||
const Card = (props) => {
|
const Card = (props) => {
|
||||||
return (<div className={styles.Card}>{props.children}</div>)
|
return (<div className={styles.Card}>{props.children}</div>);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Card;
|
export default Card;
|
||||||
@@ -1,27 +1,37 @@
|
|||||||
import form_styles from './Form.module.css'
|
import { useState } from 'react';
|
||||||
import Button from './Button'
|
import form_styles from './Form.module.css';
|
||||||
|
import Button from './Button';
|
||||||
|
|
||||||
const DictInputRow = (props) => {
|
const DictInputRow = (props) => {
|
||||||
|
const dict = { ...props.value };
|
||||||
|
if (!props.disableKeys) {
|
||||||
|
dict[""] = "";
|
||||||
|
}
|
||||||
|
|
||||||
let unit = "";
|
let unit = "";
|
||||||
if (props.unit) {
|
if (props.unit) {
|
||||||
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>
|
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tooltip = "";
|
let tooltip = "";
|
||||||
if (props.tooltip != undefined) {
|
if (props.tooltip != undefined) {
|
||||||
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />
|
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = {}
|
const onChangeValue = (key, v) => {
|
||||||
if (props.value != undefined) {
|
const newDict = { ...dict };
|
||||||
value = props.value;
|
newDict[key] = v;
|
||||||
}
|
props.onChange(newDict);
|
||||||
if (props.disableKeys === undefined) {
|
};
|
||||||
value[""] = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = []
|
const onChangeKey = (prevKey, newKey) => {
|
||||||
Object.keys(value).forEach((key, index) => {
|
const newDict = renameKey(dict, prevKey, newKey);
|
||||||
|
if (!("" in newDict)) newDict[""] = "";
|
||||||
|
props.onChange(newDict);
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = [];
|
||||||
|
Object.keys(dict).forEach((key, index) => {
|
||||||
let label = <span>{props.label} {unit}</span>;
|
let label = <span>{props.label} {unit}</span>;
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
label = "";
|
label = "";
|
||||||
@@ -35,21 +45,33 @@ const DictInputRow = (props) => {
|
|||||||
value={key}
|
value={key}
|
||||||
placeholder={props.keyPlaceholder}
|
placeholder={props.keyPlaceholder}
|
||||||
disabled={props.disableKeys}
|
disabled={props.disableKeys}
|
||||||
|
onChange={e => onChangeKey(key, e.target.value)}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
data-index={index}
|
data-index={index}
|
||||||
value={value[key]}
|
value={dict[key]}
|
||||||
placeholder={props.valuePlaceholder}
|
placeholder={props.valuePlaceholder}
|
||||||
|
onChange={e => onChangeValue(key, e.target.value)}
|
||||||
/>
|
/>
|
||||||
{tooltip}
|
{tooltip}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return <>
|
return <>{form}</>;
|
||||||
{form}
|
};
|
||||||
</>;
|
|
||||||
|
function renameKey(obj, prevKey, newKey) {
|
||||||
|
const keys = Object.keys(obj);
|
||||||
|
return keys.reduce((acc, val) => {
|
||||||
|
if (val === prevKey) {
|
||||||
|
acc[newKey] = obj[prevKey];
|
||||||
|
} else {
|
||||||
|
acc[val] = obj[val];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DictInputRow;
|
export default DictInputRow;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import form_styles from './Form.module.css'
|
import form_styles from './Form.module.css';
|
||||||
import Button from './Button'
|
import Button from './Button';
|
||||||
|
|
||||||
const FileInputRow = (props) => {
|
const FileInputRow = (props) => {
|
||||||
|
|
||||||
let tooltip = "";
|
let tooltip = "";
|
||||||
if (props.tooltip != undefined) {
|
if (props.tooltip != undefined) {
|
||||||
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />
|
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={form_styles.FormRow}>
|
return <div className={form_styles.FormRow}>
|
||||||
@@ -16,6 +16,6 @@ const FileInputRow = (props) => {
|
|||||||
<Button label="Template" kind="inline" />
|
<Button label="Template" kind="inline" />
|
||||||
{tooltip}
|
{tooltip}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FileInputRow;
|
export default FileInputRow;
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import styles from './Footer.module.css'
|
import styles from './Footer.module.css';
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
return <div className={styles.Footer}>
|
return <div className={styles.Footer}>
|
||||||
<p>RELOG: Reverse Logistics Optimization</p>
|
<p>RELOG: Reverse Logistics Optimization</p>
|
||||||
<p>Copyright © 2020—2022, UChicago Argonne, LLC. All Rights Reserved.</p>
|
<p>Copyright © 2020—2022, UChicago Argonne, LLC. All Rights Reserved.</p>
|
||||||
</div>
|
</div>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
const Form = (props) => {
|
const Form = (props) => {
|
||||||
return <>{props.children}</>;
|
return <>{props.children}</>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Form;
|
export default Form;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import styles from './Header.module.css'
|
import styles from './Header.module.css';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
return (
|
return (
|
||||||
@@ -7,7 +7,7 @@ const Header = () => {
|
|||||||
<h1>RELOG</h1>
|
<h1>RELOG</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import PipelineBlock from './PipelineBlock';
|
import PipelineBlock from './PipelineBlock';
|
||||||
import ParametersBlock from './ParametersBlock';
|
import ParametersBlock from './ParametersBlock';
|
||||||
@@ -7,20 +8,249 @@ import PlantBlock from './PlantBlock';
|
|||||||
import ButtonRow from './ButtonRow';
|
import ButtonRow from './ButtonRow';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
|
|
||||||
|
const defaultData = {
|
||||||
|
parameters: {
|
||||||
|
"time horizon (years)": "1",
|
||||||
|
"building period (years)": "[1]",
|
||||||
|
"annual inflation rate (%)": "0.0",
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
},
|
||||||
|
plants: {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProduct = {
|
||||||
|
"acquisition cost ($/tonne)": "0.00",
|
||||||
|
"disposal cost ($/tonne)": "0.00",
|
||||||
|
"disposal limit (tonne)": "0",
|
||||||
|
"transportation cost ($/km/tonne)": "0.00",
|
||||||
|
"transportation energy (J/km/tonne)": "0",
|
||||||
|
"transportation emissions (J/km/tonne)": {
|
||||||
|
"CO2": 0,
|
||||||
|
"NH2": 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const randomPosition = () => {
|
||||||
|
return Math.round(Math.random() * 30) * 15;
|
||||||
|
};
|
||||||
|
|
||||||
const InputPage = () => {
|
const InputPage = () => {
|
||||||
return <>
|
let [data, setData] = useState(defaultData);
|
||||||
<PipelineBlock />
|
|
||||||
<ParametersBlock />
|
// onAdd
|
||||||
<ProductBlock name="Battery" />
|
// ------------------------------------------------------------------------
|
||||||
<ProductBlock name="Nickel" />
|
const promptName = (prevData) => {
|
||||||
<ProductBlock name="Metal casing" />
|
const name = prompt("Name");
|
||||||
<PlantBlock name="Battery Recycling Plant" />
|
if (!name || name.length == 0) return;
|
||||||
<ButtonRow>
|
if (name in prevData.products || name in prevData.plants) return;
|
||||||
<Button label="Load" />
|
return name;
|
||||||
<Button label="Save" />
|
};
|
||||||
</ButtonRow>
|
|
||||||
</>
|
const onAddPlant = () => {
|
||||||
|
setData((prevData) => {
|
||||||
|
const name = promptName(prevData);
|
||||||
|
if (name === undefined) return prevData;
|
||||||
|
const newData = { ...prevData };
|
||||||
|
newData.plants[name] = {
|
||||||
|
x: randomPosition(),
|
||||||
|
y: randomPosition(),
|
||||||
|
outputs: {},
|
||||||
|
};
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddProduct = () => {
|
||||||
|
setData((prevData) => {
|
||||||
|
const name = promptName(prevData);
|
||||||
|
if (name === undefined) return prevData;
|
||||||
|
const newData = { ...prevData };
|
||||||
|
newData.products[name] = {
|
||||||
|
...defaultProduct,
|
||||||
|
x: randomPosition(),
|
||||||
|
y: randomPosition(),
|
||||||
|
};
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// onRename
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
const onRenamePlant = (prevName, newName) => {
|
||||||
|
setData((prevData) => {
|
||||||
|
const newData = { ...prevData };
|
||||||
|
newData.plants[newName] = newData.plants[prevName];
|
||||||
|
delete newData.plants[prevName];
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRenameProduct = (prevName, newName) => {
|
||||||
|
setData((prevData) => {
|
||||||
|
const newData = { ...prevData };
|
||||||
|
newData.products[newName] = newData.products[prevName];
|
||||||
|
delete newData.products[prevName];
|
||||||
|
for (const [plantName, plant] of Object.entries(newData.plants)) {
|
||||||
|
if (plant.input == prevName) {
|
||||||
|
plant.input = newName;
|
||||||
|
}
|
||||||
|
let outputFound = false;
|
||||||
|
for (const [outputName, outputValue] of Object.entries(plant.outputs)) {
|
||||||
|
if (outputName == prevName) outputFound = true;
|
||||||
|
}
|
||||||
|
if (outputFound) {
|
||||||
|
plant.outputs[newName] = plant.outputs[prevName];
|
||||||
|
delete plant.outputs[prevName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// onMove
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
const onMovePlant = (plantName, x, y) => {
|
||||||
|
setData((prevData) => {
|
||||||
|
const newData = { ...prevData };
|
||||||
|
newData.plants[plantName].x = x;
|
||||||
|
newData.plants[plantName].y = y;
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMoveProduct = (productName, x, y) => {
|
||||||
|
setData((prevData) => {
|
||||||
|
const newData = { ...prevData };
|
||||||
|
newData.products[productName].x = x;
|
||||||
|
newData.products[productName].y = y;
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// onRemove
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
const onRemovePlant = (plantName) => {
|
||||||
|
setData((prevData) => {
|
||||||
|
const newData = { ...prevData };
|
||||||
|
delete newData.plants[plantName];
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveProduct = (productName) => {
|
||||||
|
setData((prevData) => {
|
||||||
|
const newData = { ...prevData };
|
||||||
|
delete newData.products[productName];
|
||||||
|
for (const [plantName, plant] of Object.entries(newData.plants)) {
|
||||||
|
if (plant.input == productName) {
|
||||||
|
delete plant.input;
|
||||||
|
}
|
||||||
|
let outputFound = false;
|
||||||
|
for (const [outputName, outputValue] of Object.entries(plant.outputs)) {
|
||||||
|
if (outputName == productName) outputFound = true;
|
||||||
|
}
|
||||||
|
if (outputFound) {
|
||||||
|
delete plant.outputs[productName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inputs & Outputs
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
const onSetPlantInput = (plantName, productName) => {
|
||||||
|
setData((prevData) => {
|
||||||
|
const newData = { ...prevData };
|
||||||
|
newData.plants[plantName].input = productName;
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddPlantOutput = (plantName, productName) => {
|
||||||
|
setData((prevData) => {
|
||||||
|
if (productName in prevData.plants[plantName].outputs) {
|
||||||
|
return prevData;
|
||||||
|
}
|
||||||
|
const newData = { ...prevData };
|
||||||
|
newData.plants[plantName].outputs[productName] = 0;
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// onSave
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
const onSave = () => {
|
||||||
|
console.log(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// onChange
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
const onChangeParameters = (val) => {
|
||||||
|
setData(prevData => {
|
||||||
|
const newData = { ...prevData };
|
||||||
|
newData.parameters = val;
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeProduct = (prodName, val) => {
|
||||||
|
setData(prevData => {
|
||||||
|
const newData = { ...prevData };
|
||||||
|
newData.products[prodName] = val;
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
let productComps = [];
|
||||||
|
for (const [prodName, prod] of Object.entries(data.products)) {
|
||||||
|
productComps.push(
|
||||||
|
<ProductBlock
|
||||||
|
key={prodName}
|
||||||
|
name={prodName}
|
||||||
|
value={prod}
|
||||||
|
onChange={v => onChangeProduct(prodName, v)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let plantComps = [];
|
||||||
|
for (const [plantName, plant] of Object.entries(data.plants)) {
|
||||||
|
plantComps.push(
|
||||||
|
<PlantBlock key={plantName} name={plantName} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<PipelineBlock
|
||||||
|
onAddPlant={onAddPlant}
|
||||||
|
onAddPlantOutput={onAddPlantOutput}
|
||||||
|
onAddProduct={onAddProduct}
|
||||||
|
onMovePlant={onMovePlant}
|
||||||
|
onMoveProduct={onMoveProduct}
|
||||||
|
onRenamePlant={onRenamePlant}
|
||||||
|
onRenameProduct={onRenameProduct}
|
||||||
|
onSetPlantInput={onSetPlantInput}
|
||||||
|
onRemovePlant={onRemovePlant}
|
||||||
|
onRemoveProduct={onRemoveProduct}
|
||||||
|
plants={data.plants}
|
||||||
|
products={data.products}
|
||||||
|
/>
|
||||||
|
<ParametersBlock
|
||||||
|
value={data.parameters}
|
||||||
|
onChange={onChangeParameters}
|
||||||
|
/>
|
||||||
|
{productComps}
|
||||||
|
{plantComps}
|
||||||
|
<ButtonRow>
|
||||||
|
<Button label="Load" />
|
||||||
|
<Button label="Save" onClick={onSave} />
|
||||||
|
</ButtonRow>
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
export default InputPage;
|
export default InputPage;
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import Section from './Section'
|
import Section from './Section';
|
||||||
import Card from './Card'
|
import Card from './Card';
|
||||||
import Form from './Form'
|
import Form from './Form';
|
||||||
import TextInputRow from './TextInputRow'
|
import TextInputRow from './TextInputRow';
|
||||||
|
|
||||||
const ParametersBlock = () => {
|
const ParametersBlock = (props) => {
|
||||||
|
const onChangeField = (field, val) => {
|
||||||
|
props.value[field] = val;
|
||||||
|
props.onChange(props.value);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Section title="Parameters" />
|
<Section title="Parameters" />
|
||||||
@@ -13,24 +17,27 @@ const ParametersBlock = () => {
|
|||||||
label="Time horizon"
|
label="Time horizon"
|
||||||
unit="years"
|
unit="years"
|
||||||
tooltip="Number of years in the simulation."
|
tooltip="Number of years in the simulation."
|
||||||
default="1"
|
value={props.value["time horizon (years)"]}
|
||||||
|
onChange={v => onChangeField("time horizon (years)", v)}
|
||||||
/>
|
/>
|
||||||
<TextInputRow
|
<TextInputRow
|
||||||
label="Building period"
|
label="Building period"
|
||||||
unit="years"
|
unit="years"
|
||||||
tooltip="List of years in which we are allowed to open new plants. For example, if this parameter is set to [1,2,3], we can only open plants during the first three years. By default, this equals [1]; that is, plants can only be opened during the first year."
|
tooltip="List of years in which we are allowed to open new plants. For example, if this parameter is set to [1,2,3], we can only open plants during the first three years. By default, this equals [1]; that is, plants can only be opened during the first year."
|
||||||
default="[1]"
|
value={props.value["building period (years)"]}
|
||||||
|
onChange={v => onChangeField("building period (years)", v)}
|
||||||
/>
|
/>
|
||||||
<TextInputRow
|
<TextInputRow
|
||||||
label="Annual inflation rate"
|
label="Annual inflation rate"
|
||||||
unit="%"
|
unit="%"
|
||||||
tooltip="Rate of inflation applied to all costs."
|
tooltip="Rate of inflation applied to all costs."
|
||||||
default="0"
|
value={props.value["annual inflation rate (%)"]}
|
||||||
|
onChange={v => onChangeField("annual inflation rate (%)", v)}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ParametersBlock;
|
export default ParametersBlock;
|
||||||
@@ -5,83 +5,138 @@ import Card from './Card';
|
|||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import styles from './PipelineBlock.module.css';
|
import styles from './PipelineBlock.module.css';
|
||||||
|
|
||||||
const elements = [
|
|
||||||
{
|
const PipelineBlock = (props) => {
|
||||||
id: '1',
|
let elements = [];
|
||||||
data: { label: 'Battery' },
|
let mapNameToType = {};
|
||||||
|
for (const [productName, product] of Object.entries(props.products)) {
|
||||||
|
mapNameToType[productName] = "product";
|
||||||
|
elements.push({
|
||||||
|
id: productName,
|
||||||
|
data: { label: productName, type: 'product' },
|
||||||
|
position: { x: product.x, y: product.y },
|
||||||
sourcePosition: 'right',
|
sourcePosition: 'right',
|
||||||
targetPosition: 'left',
|
targetPosition: 'left',
|
||||||
position: { x: 100, y: 200 },
|
|
||||||
className: styles.ProductNode,
|
className: styles.ProductNode,
|
||||||
},
|
});
|
||||||
{
|
}
|
||||||
id: '2',
|
|
||||||
data: { label: "Battery Recycling Plant" },
|
for (const [plantName, plant] of Object.entries(props.plants)) {
|
||||||
|
mapNameToType[plantName] = "plant";
|
||||||
|
elements.push({
|
||||||
|
id: plantName,
|
||||||
|
data: { label: plantName, type: 'plant' },
|
||||||
|
position: { x: plant.x, y: plant.y },
|
||||||
sourcePosition: 'right',
|
sourcePosition: 'right',
|
||||||
targetPosition: 'left',
|
targetPosition: 'left',
|
||||||
position: { x: 500, y: 150 },
|
|
||||||
className: styles.PlantNode,
|
className: styles.PlantNode,
|
||||||
},
|
});
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
data: { label: 'Nickel' },
|
|
||||||
sourcePosition: 'right',
|
|
||||||
targetPosition: 'left',
|
|
||||||
position: { x: 900, y: 100 },
|
|
||||||
className: styles.ProductNode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
data: { label: 'Metal casing' },
|
|
||||||
sourcePosition: 'right',
|
|
||||||
targetPosition: 'left',
|
|
||||||
position: { x: 900, y: 300 },
|
|
||||||
className: styles.ProductNode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'e1-2',
|
|
||||||
source: '1',
|
|
||||||
target: '2',
|
|
||||||
animated: true,
|
|
||||||
selectable: false,
|
|
||||||
style: { stroke: "black" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'e2-3',
|
|
||||||
source: '2',
|
|
||||||
target: '3',
|
|
||||||
animated: true,
|
|
||||||
selectable: false,
|
|
||||||
style: { stroke: "black" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'e2-4',
|
|
||||||
source: '2',
|
|
||||||
target: '4',
|
|
||||||
animated: true,
|
|
||||||
selectable: false,
|
|
||||||
style: { stroke: "black" },
|
|
||||||
},
|
|
||||||
|
|
||||||
];
|
if (plant.input != undefined) {
|
||||||
|
elements.push({
|
||||||
|
id: `${plant.input}-${plantName}`,
|
||||||
|
source: plant.input,
|
||||||
|
target: plantName,
|
||||||
|
animated: true,
|
||||||
|
style: { stroke: "black" },
|
||||||
|
selectable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [productName, amount] of Object.entries(plant.outputs)) {
|
||||||
|
elements.push({
|
||||||
|
id: `${plantName}-${productName}`,
|
||||||
|
source: plantName,
|
||||||
|
target: productName,
|
||||||
|
animated: true,
|
||||||
|
style: { stroke: "black" },
|
||||||
|
selectable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const onNodeDoubleClick = (ev, node) => {
|
||||||
|
const oldName = node.data.label;
|
||||||
|
const newName = window.prompt("Enter new name", oldName);
|
||||||
|
if (newName == undefined || newName.length == 0) return;
|
||||||
|
if (newName in mapNameToType) return;
|
||||||
|
if (node.data.type == "plant") {
|
||||||
|
props.onRenamePlant(oldName, newName);
|
||||||
|
} else {
|
||||||
|
props.onRenameProduct(oldName, newName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onElementsRemove = (elements) => {
|
||||||
|
elements.forEach(el => {
|
||||||
|
if (!(el.id in mapNameToType)) return;
|
||||||
|
if (el.data.type == "plant") {
|
||||||
|
props.onRemovePlant(el.data.label);
|
||||||
|
} else {
|
||||||
|
props.onRemoveProduct(el.data.label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNodeDragStop = (ev, node) => {
|
||||||
|
if (node.data.type == "plant") {
|
||||||
|
props.onMovePlant(node.data.label, node.position.x, node.position.y);
|
||||||
|
} else {
|
||||||
|
props.onMoveProduct(node.data.label, node.position.x, node.position.y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConnect = (args) => {
|
||||||
|
const sourceType = mapNameToType[args.source];
|
||||||
|
const targetType = mapNameToType[args.target];
|
||||||
|
if (sourceType === "product" && targetType === "plant") {
|
||||||
|
props.onSetPlantInput(args.target, args.source);
|
||||||
|
} else if (sourceType === "plant" && targetType === "product") {
|
||||||
|
props.onAddPlantOutput(args.source, args.target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const PipelineBlock = () => {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Section title="Pipeline" />
|
<Section title="Pipeline" />
|
||||||
<Card>
|
<Card>
|
||||||
<div className={styles.PipelineBlock}>
|
<div className={styles.PipelineBlock}>
|
||||||
<ReactFlow elements={elements}>
|
<ReactFlow
|
||||||
|
elements={elements}
|
||||||
|
onNodeDoubleClick={onNodeDoubleClick}
|
||||||
|
onNodeDragStop={onNodeDragStop}
|
||||||
|
onConnect={onConnect}
|
||||||
|
onElementsRemove={onElementsRemove}
|
||||||
|
deleteKeyCode={46}
|
||||||
|
maxZoom={1}
|
||||||
|
minZoom={1}
|
||||||
|
snapToGrid={true}
|
||||||
|
preventScrolling={false}
|
||||||
|
>
|
||||||
<Background />
|
<Background />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<Button label="Add product" kind="inline" />
|
<Button
|
||||||
<Button label="Add plant" kind="inline" />
|
label="Add product"
|
||||||
|
kind="inline"
|
||||||
|
onClick={props.onAddProduct}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Add plant"
|
||||||
|
kind="inline"
|
||||||
|
onClick={props.onAddPlant}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="?"
|
||||||
|
kind="inline"
|
||||||
|
tooltip="Drag from one connector to another to create links between products and plants. Double click to rename an element. Press [Delete] to remove an element."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PipelineBlock;
|
export default PipelineBlock;
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
.PipelineBlock {
|
.PipelineBlock {
|
||||||
height: 600px;
|
height: 600px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlantNode, .ProductNode {
|
.PlantNode, .ProductNode {
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import Section from './Section'
|
import Section from './Section';
|
||||||
import Card from './Card'
|
import Card from './Card';
|
||||||
import Form from './Form'
|
import Form from './Form';
|
||||||
import TextInputRow from './TextInputRow'
|
import TextInputRow from './TextInputRow';
|
||||||
import FileInputRow from './FileInputRow'
|
import FileInputRow from './FileInputRow';
|
||||||
import DictInputRow from './DictInputRow'
|
import DictInputRow from './DictInputRow';
|
||||||
|
|
||||||
const PlantBlock = (props) => {
|
const PlantBlock = (props) => {
|
||||||
const emissions = {
|
const emissions = {
|
||||||
"CO2": "0.05",
|
"CO2": "0.05",
|
||||||
"CH4": "0.01",
|
"CH4": "0.01",
|
||||||
"N2O": "0.04",
|
"N2O": "0.04",
|
||||||
}
|
};
|
||||||
const output = {
|
const output = {
|
||||||
"Nickel": "0.5",
|
"Nickel": "0.5",
|
||||||
"Metal casing": "0.35",
|
"Metal casing": "0.35",
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Section title={props.name} />
|
<Section title={props.name} />
|
||||||
@@ -136,7 +136,7 @@ const PlantBlock = (props) => {
|
|||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PlantBlock;
|
export default PlantBlock;
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
import Section from './Section'
|
import { useState } from 'react';
|
||||||
import Card from './Card'
|
import Section from './Section';
|
||||||
import Form from './Form'
|
import Card from './Card';
|
||||||
import TextInputRow from './TextInputRow'
|
import Form from './Form';
|
||||||
import FileInputRow from './FileInputRow'
|
import TextInputRow from './TextInputRow';
|
||||||
|
import FileInputRow from './FileInputRow';
|
||||||
|
import DictInputRow from './DictInputRow';
|
||||||
|
|
||||||
const ProductBlock = (props) => {
|
const ProductBlock = (props) => {
|
||||||
|
const onChange = (field, val) => {
|
||||||
|
const newProduct = { ...props.value };
|
||||||
|
newProduct[field] = val;
|
||||||
|
props.onChange(newProduct);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Section title={props.name} />
|
<Section title={props.name} />
|
||||||
@@ -19,7 +27,8 @@ const ProductBlock = (props) => {
|
|||||||
label="Acquisition cost"
|
label="Acquisition cost"
|
||||||
unit="$/tonne"
|
unit="$/tonne"
|
||||||
tooltip="The cost to acquire one tonne of this product from collection centers. Does not apply to plant outputs."
|
tooltip="The cost to acquire one tonne of this product from collection centers. Does not apply to plant outputs."
|
||||||
default="0.00"
|
value={props.value["acquisition cost ($/tonne)"]}
|
||||||
|
onChange={v => onChange("acquisition cost ($/tonne)", v)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h1>Disposal</h1>
|
<h1>Disposal</h1>
|
||||||
@@ -27,13 +36,15 @@ const ProductBlock = (props) => {
|
|||||||
label="Disposal cost"
|
label="Disposal cost"
|
||||||
unit="$/tonne"
|
unit="$/tonne"
|
||||||
tooltip="The cost to dispose of one tonne of this product at a collection center, without further processing. Does not apply to plant outputs."
|
tooltip="The cost to dispose of one tonne of this product at a collection center, without further processing. Does not apply to plant outputs."
|
||||||
default="0"
|
value={props.value["disposal cost ($/tonne)"]}
|
||||||
|
onChange={v => onChange("disposal cost ($/tonne)", v)}
|
||||||
/>
|
/>
|
||||||
<TextInputRow
|
<TextInputRow
|
||||||
label="Disposal limit"
|
label="Disposal limit"
|
||||||
unit="tonne"
|
unit="tonne"
|
||||||
tooltip="The maximum amount of this product that can be disposed of across all collection centers, without further processing."
|
tooltip="The maximum amount of this product that can be disposed of across all collection centers, without further processing."
|
||||||
default="0"
|
value={props.value["disposal limit (tonne)"]}
|
||||||
|
onChange={v => onChange("disposal limit (tonne)", v)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h1>Transportation</h1>
|
<h1>Transportation</h1>
|
||||||
@@ -41,24 +52,29 @@ const ProductBlock = (props) => {
|
|||||||
label="Transportation cost"
|
label="Transportation cost"
|
||||||
unit="$/km/tonne"
|
unit="$/km/tonne"
|
||||||
tooltip="The cost to transport this product."
|
tooltip="The cost to transport this product."
|
||||||
default="0.00"
|
value={props.value["transportation cost ($/km/tonne)"]}
|
||||||
|
onChange={v => onChange("transportation cost ($/km/tonne)", v)}
|
||||||
/>
|
/>
|
||||||
<TextInputRow
|
<TextInputRow
|
||||||
label="Transportation energy"
|
label="Transportation energy"
|
||||||
unit="J/km/tonne"
|
unit="J/km/tonne"
|
||||||
default="0"
|
|
||||||
tooltip="The energy required to transport this product."
|
tooltip="The energy required to transport this product."
|
||||||
|
value={props.value["transportation energy (J/km/tonne)"]}
|
||||||
|
onChange={v => onChange("transportation energy (J/km/tonne)", v)}
|
||||||
/>
|
/>
|
||||||
<TextInputRow
|
<DictInputRow
|
||||||
label="Transportation emissions"
|
label="Transportation emissions"
|
||||||
unit="J/km/tonne"
|
unit="J/km/tonne"
|
||||||
tooltip="A dictionary mapping the name of each greenhouse gas, produced to transport one tonne of this product along one kilometer, to the amount of gas produced (in tonnes)."
|
tooltip="A dictionary mapping the name of each greenhouse gas, produced to transport one tonne of this product along one kilometer, to the amount of gas produced (in tonnes)."
|
||||||
default="0"
|
keyPlaceholder="Emission name"
|
||||||
|
valuePlaceholder="0"
|
||||||
|
value={props.value["transportation emissions (J/km/tonne)"]}
|
||||||
|
onChange={v => onChange("transportation emissions (J/km/tonne)", v)}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ProductBlock;
|
export default ProductBlock;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import styles from './Section.module.css'
|
import styles from './Section.module.css';
|
||||||
|
|
||||||
const Section = (props) => {
|
const Section = (props) => {
|
||||||
return <h2 className={styles.Section}>{props.title}</h2>
|
return <h2 className={styles.Section}>{props.title}</h2>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Section;
|
export default Section;
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import form_styles from './Form.module.css'
|
import form_styles from './Form.module.css';
|
||||||
import Button from './Button'
|
import Button from './Button';
|
||||||
|
|
||||||
const TextInputRow = (props) => {
|
const TextInputRow = (props) => {
|
||||||
let unit = "";
|
let unit = "";
|
||||||
if (props.unit) {
|
if (props.unit) {
|
||||||
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>
|
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tooltip = "";
|
let tooltip = "";
|
||||||
if (props.tooltip != undefined) {
|
if (props.tooltip != undefined) {
|
||||||
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />
|
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={form_styles.FormRow}>
|
return <div className={form_styles.FormRow}>
|
||||||
@@ -21,9 +21,10 @@ const TextInputRow = (props) => {
|
|||||||
placeholder={props.default}
|
placeholder={props.default}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
|
onChange={e => props.onChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
{tooltip}
|
{tooltip}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default TextInputRow;
|
export default TextInputRow;
|
||||||
@@ -39,6 +39,10 @@ body {
|
|||||||
border: 1px solid black !important;
|
border: 1px solid black !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-flow__handle:hover {
|
||||||
|
background-color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
.react-flow__handle-right {
|
.react-flow__handle-right {
|
||||||
right: -5px !important;
|
right: -5px !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import InputPage from './InputPage';
|
import InputPage from './InputPage';
|
||||||
import Footer from './Footer'
|
import Footer from './Footer';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|||||||
Reference in New Issue
Block a user