mirror of https://github.com/ANL-CEEESA/RELOG.git
parent
56b673fb9e
commit
01452441dc
@ -1,22 +1,22 @@
|
||||
import styles from './Button.module.css';
|
||||
import styles from "./Button.module.css";
|
||||
|
||||
const Button = (props) => {
|
||||
let className = styles.Button;
|
||||
if (props.kind === "inline") {
|
||||
className += " " + styles.inline;
|
||||
}
|
||||
let className = styles.Button;
|
||||
if (props.kind === "inline") {
|
||||
className += " " + styles.inline;
|
||||
}
|
||||
|
||||
let tooltip = "";
|
||||
if (props.tooltip != undefined) {
|
||||
tooltip = <span className={styles.tooltip}>{props.tooltip}</span>;
|
||||
}
|
||||
let tooltip = "";
|
||||
if (props.tooltip !== undefined) {
|
||||
tooltip = <span className={styles.tooltip}>{props.tooltip}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={className} onClick={props.onClick}>
|
||||
{tooltip}
|
||||
{props.label}
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<button className={className} onClick={props.onClick}>
|
||||
{tooltip}
|
||||
{props.label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
@ -1,7 +0,0 @@
|
||||
import styles from './ButtonRow.module.css';
|
||||
|
||||
const ButtonRow = (props) => {
|
||||
return <div className={styles.ButtonRow}>{props.children}</div>;
|
||||
};
|
||||
|
||||
export default ButtonRow;
|
@ -1,3 +0,0 @@
|
||||
.ButtonRow {
|
||||
text-align: center;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import styles from './Card.module.css';
|
||||
import styles from "./Card.module.css";
|
||||
|
||||
const Card = (props) => {
|
||||
return (<div className={styles.Card}>{props.children}</div>);
|
||||
return <div className={styles.Card}>{props.children}</div>;
|
||||
};
|
||||
|
||||
export default Card;
|
@ -1,22 +1,22 @@
|
||||
.Card {
|
||||
border: var(--box-border);
|
||||
box-shadow: var(--box-shadow);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: white;
|
||||
padding: 12px;
|
||||
min-height: 24px;
|
||||
border: var(--box-border);
|
||||
box-shadow: var(--box-shadow);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: white;
|
||||
padding: 12px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.Card h1 {
|
||||
margin: 12px -12px 0px -12px;
|
||||
padding: 6px 12px 0px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 35px;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 12px -12px 0px -12px;
|
||||
padding: 6px 12px 0px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 35px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.Card h1:first-child {
|
||||
margin: -12px -12px 0px -12px;
|
||||
border-top: none;
|
||||
background: none;
|
||||
margin: -12px -12px 0px -12px;
|
||||
border-top: none;
|
||||
background: none;
|
||||
}
|
@ -1,87 +1,91 @@
|
||||
import form_styles from './Form.module.css';
|
||||
import Button from './Button';
|
||||
import { validate } from './Form';
|
||||
import form_styles from "./Form.module.css";
|
||||
import Button from "./Button";
|
||||
import { validate } from "./Form";
|
||||
|
||||
const DictInputRow = (props) => {
|
||||
const dict = { ...props.value };
|
||||
if (!props.disableKeys) {
|
||||
dict[""] = "0";
|
||||
}
|
||||
const dict = { ...props.value };
|
||||
if (!props.disableKeys) {
|
||||
dict[""] = "0";
|
||||
}
|
||||
|
||||
let unit = "";
|
||||
if (props.unit) {
|
||||
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
|
||||
}
|
||||
let unit = "";
|
||||
if (props.unit) {
|
||||
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
|
||||
}
|
||||
|
||||
let tooltip = "";
|
||||
if (props.tooltip != undefined) {
|
||||
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
||||
}
|
||||
let tooltip = "";
|
||||
if (props.tooltip !== undefined) {
|
||||
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
||||
}
|
||||
|
||||
const onChangeValue = (key, v) => {
|
||||
const newDict = { ...dict };
|
||||
newDict[key] = v;
|
||||
props.onChange(newDict);
|
||||
};
|
||||
const onChangeValue = (key, v) => {
|
||||
const newDict = { ...dict };
|
||||
newDict[key] = v;
|
||||
props.onChange(newDict);
|
||||
};
|
||||
|
||||
const onChangeKey = (prevKey, newKey) => {
|
||||
const newDict = renameKey(dict, prevKey, newKey);
|
||||
if (!("" in newDict)) newDict[""] = "";
|
||||
props.onChange(newDict);
|
||||
};
|
||||
const onChangeKey = (prevKey, newKey) => {
|
||||
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>;
|
||||
if (index > 0) {
|
||||
label = "";
|
||||
}
|
||||
const form = [];
|
||||
Object.keys(dict).forEach((key, index) => {
|
||||
let label = (
|
||||
<span>
|
||||
{props.label} {unit}
|
||||
</span>
|
||||
);
|
||||
if (index > 0) {
|
||||
label = "";
|
||||
}
|
||||
|
||||
let isValid = true;
|
||||
if (props.validate !== undefined) {
|
||||
isValid = validate(props.validate, dict[key]);
|
||||
}
|
||||
let isValid = true;
|
||||
if (props.validate !== undefined) {
|
||||
isValid = validate(props.validate, dict[key]);
|
||||
}
|
||||
|
||||
let className = "";
|
||||
if (!isValid) className = form_styles.invalid;
|
||||
let className = "";
|
||||
if (!isValid) className = form_styles.invalid;
|
||||
|
||||
form.push(
|
||||
<div className={form_styles.FormRow} key={index}>
|
||||
<label>{label}</label>
|
||||
<input
|
||||
type="text"
|
||||
data-index={index}
|
||||
value={key}
|
||||
placeholder={props.keyPlaceholder}
|
||||
disabled={props.disableKeys}
|
||||
onChange={e => onChangeKey(key, e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
data-index={index}
|
||||
value={dict[key]}
|
||||
placeholder={props.valuePlaceholder}
|
||||
className={className}
|
||||
onChange={e => onChangeValue(key, e.target.value)}
|
||||
/>
|
||||
{tooltip}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
form.push(
|
||||
<div className={form_styles.FormRow} key={index}>
|
||||
<label>{label}</label>
|
||||
<input
|
||||
type="text"
|
||||
data-index={index}
|
||||
value={key}
|
||||
placeholder={props.keyPlaceholder}
|
||||
disabled={props.disableKeys}
|
||||
onChange={(e) => onChangeKey(key, e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
data-index={index}
|
||||
value={dict[key]}
|
||||
placeholder={props.valuePlaceholder}
|
||||
className={className}
|
||||
onChange={(e) => onChangeValue(key, e.target.value)}
|
||||
/>
|
||||
{tooltip}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return <>{form}</>;
|
||||
return <>{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 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;
|
@ -1,64 +1,49 @@
|
||||
import form_styles from './Form.module.css';
|
||||
import Button from './Button';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import form_styles from "./Form.module.css";
|
||||
import Button from "./Button";
|
||||
import { useRef } from "react";
|
||||
|
||||
const FileInputRow = (props) => {
|
||||
let tooltip = "";
|
||||
if (props.tooltip != undefined) {
|
||||
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
||||
}
|
||||
let tooltip = "";
|
||||
if (props.tooltip !== undefined) {
|
||||
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
||||
}
|
||||
|
||||
const fileElem = useRef();
|
||||
const fileElem = useRef();
|
||||
|
||||
const onClickUpload = () => {
|
||||
fileElem.current.click();
|
||||
};
|
||||
const onClickUpload = () => {
|
||||
fileElem.current.click();
|
||||
};
|
||||
|
||||
const onFileSelected = () => {
|
||||
const file = fileElem.current.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", () => {
|
||||
props.onFile(reader.result);
|
||||
});
|
||||
reader.readAsText(file);
|
||||
}
|
||||
fileElem.current.value = "";
|
||||
};
|
||||
const onFileSelected = () => {
|
||||
const file = fileElem.current.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", () => {
|
||||
props.onFile(reader.result);
|
||||
});
|
||||
reader.readAsText(file);
|
||||
}
|
||||
fileElem.current.value = "";
|
||||
};
|
||||
|
||||
return <div className={form_styles.FormRow}>
|
||||
<label>{props.label}</label>
|
||||
<input type="text" value={props.value} disabled="disabled" />
|
||||
<Button
|
||||
label="Upload"
|
||||
kind="inline"
|
||||
onClick={onClickUpload}
|
||||
/>
|
||||
<Button
|
||||
label="Download"
|
||||
kind="inline"
|
||||
onClick={props.onDownload}
|
||||
/>
|
||||
<Button
|
||||
label="Clear"
|
||||
kind="inline"
|
||||
onClick={props.onClear}
|
||||
/>
|
||||
<Button
|
||||
label="Template"
|
||||
kind="inline"
|
||||
onClick={props.onTemplate}
|
||||
/>
|
||||
{tooltip}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileElem}
|
||||
accept={props.accept}
|
||||
style={{ display: "none" }}
|
||||
onChange={onFileSelected}
|
||||
/>
|
||||
</div>;
|
||||
return (
|
||||
<div className={form_styles.FormRow}>
|
||||
<label>{props.label}</label>
|
||||
<input type="text" value={props.value} disabled="disabled" />
|
||||
<Button label="Upload" kind="inline" onClick={onClickUpload} />
|
||||
<Button label="Download" kind="inline" onClick={props.onDownload} />
|
||||
<Button label="Clear" kind="inline" onClick={props.onClear} />
|
||||
<Button label="Template" kind="inline" onClick={props.onTemplate} />
|
||||
{tooltip}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileElem}
|
||||
accept={props.accept}
|
||||
style={{ display: "none" }}
|
||||
onChange={onFileSelected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileInputRow;
|
@ -1,10 +1,15 @@
|
||||
import styles from './Footer.module.css';
|
||||
import styles from "./Footer.module.css";
|
||||
|
||||
const Footer = () => {
|
||||
return <div className={styles.Footer}>
|
||||
<p>RELOG: Reverse Logistics Optimization</p>
|
||||
<p>Copyright © 2020—2022, UChicago Argonne, LLC. All Rights Reserved.</p>
|
||||
</div>;
|
||||
return (
|
||||
<div className={styles.Footer}>
|
||||
<p>RELOG: Reverse Logistics Optimization</p>
|
||||
<p>
|
||||
Copyright © 2020—2022, UChicago Argonne, LLC. All Rights
|
||||
Reserved.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
@ -1,11 +1,10 @@
|
||||
.Footer {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
padding: 24px;
|
||||
margin-top: 24px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 8px;
|
||||
min-width: 900px;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
padding: 24px;
|
||||
margin-top: 24px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 8px;
|
||||
min-width: 900px;
|
||||
}
|
@ -1,19 +1,19 @@
|
||||
const VALIDATION_REGEX = {
|
||||
int: new RegExp('^[0-9]+$'),
|
||||
intList: new RegExp('^\[[0-9]+(,[0-9]+)*\]$'),
|
||||
float: new RegExp('^[0-9]*\\.?[0-9]*$'),
|
||||
int: new RegExp("^[0-9]+$"),
|
||||
intList: new RegExp("[[0-9]*]$"),
|
||||
float: new RegExp("^[0-9]*\\.?[0-9]*$"),
|
||||
floatList: new RegExp("^[?[0-9,.]*]?$"),
|
||||
};
|
||||
|
||||
export const validate = (kind, value) => {
|
||||
if (value.length == 0) return false;
|
||||
if (!VALIDATION_REGEX[kind].test(value)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
if (!VALIDATION_REGEX[kind].test(value)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const Form = (props) => {
|
||||
return <>{props.children}</>;
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
export default Form;
|
@ -1,28 +1,28 @@
|
||||
.FormRow {
|
||||
display: flex;
|
||||
line-height: 24px;
|
||||
display: flex;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.FormRow label {
|
||||
width: 350px;
|
||||
padding: 6px 12px;
|
||||
text-align: right;
|
||||
width: 350px;
|
||||
padding: 6px 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.FormRow input {
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
border: var(--box-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 4px;
|
||||
margin: 2px 3px;
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
border: var(--box-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 4px;
|
||||
margin: 2px 3px;
|
||||
}
|
||||
|
||||
.FormRow_unit {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.invalid {
|
||||
border: 2px solid #faa !important;
|
||||
background-color: rgba(255, 0, 0, 0.05);
|
||||
border: 2px solid #faa !important;
|
||||
background-color: rgba(255, 0, 0, 0.05);
|
||||
}
|
@ -1,13 +1,17 @@
|
||||
import styles from './Header.module.css';
|
||||
import styles from "./Header.module.css";
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<div className={styles.HeaderBox}>
|
||||
<div className={styles.HeaderContent}>
|
||||
<h1>RELOG</h1>
|
||||
</div>
|
||||
const Header = (props) => {
|
||||
return (
|
||||
<div className={styles.HeaderBox}>
|
||||
<div className={styles.HeaderContent}>
|
||||
<h1>RELOG</h1>
|
||||
<h2>{props.title}</h2>
|
||||
<div style={{ float: "right", paddingTop: "5px" }}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
@ -1,19 +1,28 @@
|
||||
.HeaderBox {
|
||||
background-color: white;
|
||||
border-bottom: var(--box-border);
|
||||
box-shadow: var(--box-shadow);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: white;
|
||||
border-bottom: var(--box-border);
|
||||
box-shadow: var(--box-shadow);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.HeaderContent {
|
||||
margin: 0 auto;
|
||||
max-width: var(--site-width);
|
||||
margin: 0 auto;
|
||||
max-width: var(--site-width);
|
||||
}
|
||||
|
||||
.HeaderContent h1 {
|
||||
line-height: 48px;
|
||||
font-size: 28px;
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
.HeaderContent h1,
|
||||
.HeaderContent h2 {
|
||||
line-height: 48px;
|
||||
font-size: 28px;
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.HeaderContent h2 {
|
||||
font-size: 22px;
|
||||
font-weight: normal;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
@ -1,254 +1,332 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import './index.css';
|
||||
import PipelineBlock from './PipelineBlock';
|
||||
import ParametersBlock from './ParametersBlock';
|
||||
import ProductBlock from './ProductBlock';
|
||||
import PlantBlock from './PlantBlock';
|
||||
import ButtonRow from './ButtonRow';
|
||||
import Button from './Button';
|
||||
|
||||
const defaultData = {
|
||||
parameters: {
|
||||
"time horizon (years)": "1",
|
||||
"building period (years)": "[1]",
|
||||
"annual inflation rate (%)": "0",
|
||||
},
|
||||
products: {
|
||||
},
|
||||
plants: {
|
||||
import React, { useState, useRef } from "react";
|
||||
|
||||
import "./index.css";
|
||||
import PipelineBlock from "./PipelineBlock";
|
||||
import ParametersBlock from "./ParametersBlock";
|
||||
import ProductBlock from "./ProductBlock";
|
||||
import PlantBlock from "./PlantBlock";
|
||||
import Button from "./Button";
|
||||
import Header from "./Header";
|
||||
import Footer from "./Footer";
|
||||
import { defaultData, defaultPlant, defaultProduct } from "./defaults";
|
||||
import { randomPosition } from "./PipelineBlock";
|
||||
import { exportData, importData } from "./export";
|
||||
import { generateFile } from "./csv";
|
||||
|
||||
const setDefaults = (actualDict, defaultDict) => {
|
||||
for (const [key, defaultValue] of Object.entries(defaultDict)) {
|
||||
if (!(key in actualDict)) {
|
||||
if (typeof defaultValue === "object") {
|
||||
actualDict[key] = { ...defaultValue };
|
||||
} else {
|
||||
actualDict[key] = defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const defaultProduct = {
|
||||
"initial amounts": {},
|
||||
"acquisition cost ($/tonne)": "0",
|
||||
"disposal cost ($/tonne)": "0",
|
||||
"disposal limit (tonne)": "0",
|
||||
"transportation cost ($/km/tonne)": "0",
|
||||
"transportation energy (J/km/tonne)": "0",
|
||||
"transportation emissions (J/km/tonne)": {}
|
||||
const cleanDict = (dict, defaultDict) => {
|
||||
for (const key of Object.keys(dict)) {
|
||||
if (!(key in defaultDict)) {
|
||||
delete dict[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const randomPosition = () => {
|
||||
return Math.round(Math.random() * 30) * 15;
|
||||
const fixLists = (dict, blacklist, stringify) => {
|
||||
for (const [key, val] of Object.entries(dict)) {
|
||||
if (blacklist.includes(key)) continue;
|
||||
if (Array.isArray(val)) {
|
||||
// Replace constant lists by a single number
|
||||
let isConstant = true;
|
||||
for (let i = 1; i < val.length; i++) {
|
||||
if (val[i - 1] !== val[i]) {
|
||||
isConstant = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isConstant) dict[key] = val[0];
|
||||
|
||||
// Convert lists to JSON strings
|
||||
if (stringify) dict[key] = JSON.stringify(dict[key]);
|
||||
}
|
||||
if (typeof val === "object") {
|
||||
fixLists(val, blacklist, stringify);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const InputPage = () => {
|
||||
let [data, setData] = useState(defaultData);
|
||||
|
||||
// onAdd
|
||||
// ------------------------------------------------------------------------
|
||||
const promptName = (prevData) => {
|
||||
const name = prompt("Name");
|
||||
if (!name || name.length == 0) return;
|
||||
if (name in prevData.products || name in prevData.plants) return;
|
||||
return name;
|
||||
};
|
||||
|
||||
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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const fileElem = useRef();
|
||||
|
||||
let savedData = JSON.parse(localStorage.getItem("data"));
|
||||
if (!savedData) savedData = defaultData;
|
||||
|
||||
let [data, setData] = useState(savedData);
|
||||
|
||||
const save = (data) => {
|
||||
localStorage.setItem("data", JSON.stringify(data));
|
||||
};
|
||||
|
||||
const promptName = (prevData) => {
|
||||
const name = prompt("Name");
|
||||
if (!name || name.length === 0) return;
|
||||
if (name in prevData.products || name in prevData.plants) return;
|
||||
return name;
|
||||
};
|
||||
|
||||
const onAddPlant = () => {
|
||||
setData((prevData) => {
|
||||
const name = promptName(prevData);
|
||||
if (name === undefined) return prevData;
|
||||
const newData = { ...prevData };
|
||||
const [x, y] = randomPosition();
|
||||
newData.plants[name] = {
|
||||
...defaultPlant,
|
||||
x: x,
|
||||
y: y,
|
||||
};
|
||||
save(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const onAddProduct = () => {
|
||||
setData((prevData) => {
|
||||
const name = promptName(prevData);
|
||||
if (name === undefined) return prevData;
|
||||
const newData = { ...prevData };
|
||||
const [x, y] = randomPosition();
|
||||
console.log(x, y);
|
||||
newData.products[name] = {
|
||||
...defaultProduct,
|
||||
x: x,
|
||||
y: y,
|
||||
};
|
||||
save(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const onRenamePlant = (prevName, newName) => {
|
||||
setData((prevData) => {
|
||||
const newData = { ...prevData };
|
||||
newData.plants[newName] = newData.plants[prevName];
|
||||
delete newData.plants[prevName];
|
||||
save(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
let plantComps = [];
|
||||
for (const [plantName, plant] of Object.entries(data.plants)) {
|
||||
plantComps.push(
|
||||
<PlantBlock key={plantName} name={plantName} />
|
||||
);
|
||||
const onRenameProduct = (prevName, newName) => {
|
||||
setData((prevData) => {
|
||||
const newData = { ...prevData };
|
||||
newData.products[newName] = newData.products[prevName];
|
||||
delete newData.products[prevName];
|
||||
for (const [, plant] of Object.entries(newData.plants)) {
|
||||
if (plant.input === prevName) {
|
||||
plant.input = newName;
|
||||
}
|
||||
let outputFound = false;
|
||||
for (const [outputName] of Object.entries(
|
||||
plant["outputs (tonne/tonne)"]
|
||||
)) {
|
||||
if (outputName === prevName) outputFound = true;
|
||||
}
|
||||
if (outputFound) {
|
||||
plant["outputs (tonne/tonne)"][newName] =
|
||||
plant["outputs (tonne/tonne)"][prevName];
|
||||
delete plant["outputs (tonne/tonne)"][prevName];
|
||||
}
|
||||
}
|
||||
save(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const onMovePlant = (plantName, x, y) => {
|
||||
setData((prevData) => {
|
||||
const newData = { ...prevData };
|
||||
newData.plants[plantName].x = x;
|
||||
newData.plants[plantName].y = y;
|
||||
save(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const onMoveProduct = (productName, x, y) => {
|
||||
setData((prevData) => {
|
||||
const newData = { ...prevData };
|
||||
newData.products[productName].x = x;
|
||||
newData.products[productName].y = y;
|
||||
save(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const onRemovePlant = (plantName) => {
|
||||
setData((prevData) => {
|
||||
const newData = { ...prevData };
|
||||
delete newData.plants[plantName];
|
||||
save(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const onRemoveProduct = (productName) => {
|
||||
setData((prevData) => {
|
||||
const newData = { ...prevData };
|
||||
delete newData.products[productName];
|
||||
for (const [, plant] of Object.entries(newData.plants)) {
|
||||
if (plant.input === productName) {
|
||||
delete plant.input;
|
||||
}
|
||||
let outputFound = false;
|
||||
for (const [outputName] of Object.entries(
|
||||
plant["outputs (tonne/tonne)"]
|
||||
)) {
|
||||
if (outputName === productName) outputFound = true;
|
||||
}
|
||||
if (outputFound) {
|
||||
delete plant["outputs (tonne/tonne)"][productName];
|
||||
}
|
||||
}
|
||||
save(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const onSetPlantInput = (plantName, productName) => {
|
||||
setData((prevData) => {
|
||||
const newData = { ...prevData };
|
||||
newData.plants[plantName].input = productName;
|
||||
save(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const onAddPlantOutput = (plantName, productName) => {
|
||||
setData((prevData) => {
|
||||
if (productName in prevData.plants[plantName]["outputs (tonne/tonne)"]) {
|
||||
return prevData;
|
||||
}
|
||||
const newData = { ...prevData };
|
||||
[
|
||||
"outputs (tonne/tonne)",
|
||||
"disposal cost ($/tonne)",
|
||||
"disposal limit (tonne)",
|
||||
].forEach((key) => {
|
||||
newData.plants[plantName][key] = { ...newData.plants[plantName][key] };
|
||||
newData.plants[plantName][key][productName] = 0;
|
||||
});
|
||||
save(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
generateFile("case.json", JSON.stringify(exportData(data), null, 2));
|
||||
};
|
||||
|
||||
const onClear = () => {
|
||||
const newData = JSON.parse(JSON.stringify(defaultData));
|
||||
setData(newData);
|
||||
save(newData);
|
||||
};
|
||||
|
||||
const onLoad = (contents) => {
|
||||
const newData = importData(JSON.parse(contents));
|
||||
setData(newData);
|
||||
save(newData);
|
||||
};
|
||||
|
||||
const onChange = (val, field1, field2) => {
|
||||
setData((prevData) => {
|
||||
const newData = { ...prevData };
|
||||
if (field2 !== undefined) {
|
||||
newData[field1][field2] = val;
|
||||
} else {
|
||||
newData[field1] = val;
|
||||
}
|
||||
save(newData);
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
let productComps = [];
|
||||
for (const [prodName, prod] of Object.entries(data.products)) {
|
||||
productComps.push(
|
||||
<ProductBlock
|
||||
key={prodName}
|
||||
name={prodName}
|
||||
value={prod}
|
||||
onChange={(v) => onChange(v, "products", prodName, v)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let plantComps = [];
|
||||
for (const [plantName, plant] of Object.entries(data.plants)) {
|
||||
plantComps.push(
|
||||
<PlantBlock
|
||||
key={plantName}
|
||||
name={plantName}
|
||||
value={plant}
|
||||
onChange={(v) => onChange(v, "plants", plantName)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const onFileSelected = () => {
|
||||
const file = fileElem.current.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", () => {
|
||||
onLoad(reader.result);
|
||||
});
|
||||
reader.readAsText(file);
|
||||
}
|
||||
fileElem.current.value = "";
|
||||
};
|
||||
|
||||
return <>
|
||||
return (
|
||||
<>
|
||||
<Header title="Case Builder">
|
||||
<Button label="Clear" onClick={onClear} />
|
||||
<Button label="Load" onClick={(e) => fileElem.current.click()} />
|
||||
<Button label="Save" onClick={onSave} />
|
||||
<input
|
||||
type="file"
|
||||
ref={fileElem}
|
||||
accept=".json"
|
||||
style={{ display: "none" }}
|
||||
onChange={onFileSelected}
|
||||
/>
|
||||
</Header>
|
||||
<div id="content">
|
||||
<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}
|
||||
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}
|
||||
value={data.parameters}
|
||||
onChange={(v) => onChange(v, "parameters")}
|
||||
/>
|
||||
{productComps}
|
||||
{plantComps}
|
||||
<ButtonRow>
|
||||
<Button label="Load" />
|
||||
<Button label="Save" onClick={onSave} />
|
||||
</ButtonRow>
|
||||
</>;
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputPage;
|
@ -1,46 +1,46 @@
|
||||
import Section from './Section';
|
||||
import Card from './Card';
|
||||
import Form from './Form';
|
||||
import TextInputRow from './TextInputRow';
|
||||
import Section from "./Section";
|
||||
import Card from "./Card";
|
||||
import Form from "./Form";
|
||||
import TextInputRow from "./TextInputRow";
|
||||
|
||||
const ParametersBlock = (props) => {
|
||||
const onChangeField = (field, val) => {
|
||||
props.value[field] = val;
|
||||
props.onChange(props.value);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Section title="Parameters" />
|
||||
<Card>
|
||||
<Form>
|
||||
<TextInputRow
|
||||
label="Time horizon"
|
||||
unit="years"
|
||||
tooltip="Number of years in the simulation."
|
||||
value={props.value["time horizon (years)"]}
|
||||
onChange={v => onChangeField("time horizon (years)", v)}
|
||||
validate="int"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Building period"
|
||||
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."
|
||||
value={props.value["building period (years)"]}
|
||||
onChange={v => onChangeField("building period (years)", v)}
|
||||
validate="intList"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Annual inflation rate"
|
||||
unit="%"
|
||||
tooltip="Rate of inflation applied to all costs."
|
||||
value={props.value["annual inflation rate (%)"]}
|
||||
onChange={v => onChangeField("annual inflation rate (%)", v)}
|
||||
validate="float"
|
||||
/>
|
||||
</Form>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
const onChangeField = (field, val) => {
|
||||
props.value[field] = val;
|
||||
props.onChange(props.value);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Section title="Parameters" />
|
||||
<Card>
|
||||
<Form>
|
||||
<TextInputRow
|
||||
label="Time horizon"
|
||||
unit="years"
|
||||
tooltip="Number of years in the simulation."
|
||||
value={props.value["time horizon (years)"]}
|
||||
onChange={(v) => onChangeField("time horizon (years)", v)}
|
||||
validate="int"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Building period"
|
||||
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."
|
||||
value={props.value["building period (years)"]}
|
||||
onChange={(v) => onChangeField("building period (years)", v)}
|
||||
validate="intList"
|
||||
/>
|
||||
{/* <TextInputRow
|
||||
label="Annual inflation rate"
|
||||
unit="%"
|
||||
tooltip="Rate of inflation applied to all costs."
|
||||
value={props.value["annual inflation rate (%)"]}
|
||||
onChange={(v) => onChangeField("annual inflation rate (%)", v)}
|
||||
validate="float"
|
||||
/> */}
|
||||
</Form>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParametersBlock;
|
@ -1,142 +1,191 @@
|
||||
import React from 'react';
|
||||
import ReactFlow, { Background } from 'react-flow-renderer';
|
||||
import Section from './Section';
|
||||
import Card from './Card';
|
||||
import Button from './Button';
|
||||
import styles from './PipelineBlock.module.css';
|
||||
import React from "react";
|
||||
import ReactFlow, { Background, isNode } from "react-flow-renderer";
|
||||
import Section from "./Section";
|
||||
import Card from "./Card";
|
||||
import Button from "./Button";
|
||||
import styles from "./PipelineBlock.module.css";
|
||||
import dagre from "dagre";
|
||||
|
||||
window.nextX = 15;
|
||||
window.nextY = 15;
|
||||
|
||||
const PipelineBlock = (props) => {
|
||||
let elements = [];
|
||||
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',
|
||||
targetPosition: 'left',
|
||||
className: styles.ProductNode,
|
||||
});
|
||||
export const randomPosition = () => {
|
||||
window.nextY += 60;
|
||||
if (window.nextY >= 500) {
|
||||
window.nextY = 15;
|
||||
window.nextX += 150;
|
||||
}
|
||||
return [window.nextX, window.nextY];
|
||||
};
|
||||
|
||||
const getLayoutedElements = (elements) => {
|
||||
const nodeWidth = 125;
|
||||
const nodeHeight = 45;
|
||||
const dagreGraph = new dagre.graphlib.Graph();
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||
dagreGraph.setGraph({ rankdir: "LR" });
|
||||
elements.forEach((el) => {
|
||||
if (isNode(el)) {
|
||||
dagreGraph.setNode(el.id, { width: nodeWidth, height: nodeHeight });
|
||||
} else {
|
||||
dagreGraph.setEdge(el.source, el.target);
|
||||
}
|
||||
});
|
||||
dagre.layout(dagreGraph);
|
||||
return elements.map((el) => {
|
||||
if (isNode(el)) {
|
||||
const n = dagreGraph.node(el.id);
|
||||
el.position = {
|
||||
x: 15 + n.x - nodeWidth / 2,
|
||||
y: 15 + n.y - nodeHeight / 2,
|
||||
};
|
||||
}
|
||||
return el;
|
||||
});
|
||||
};
|
||||
|
||||
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',
|
||||
targetPosition: 'left',
|
||||
className: styles.PlantNode,
|
||||
});
|
||||
const PipelineBlock = (props) => {
|
||||
let elements = [];
|
||||
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",
|
||||
targetPosition: "left",
|
||||
className: styles.ProductNode,
|
||||
});
|
||||
}
|
||||
|
||||
if (plant.input != undefined) {
|
||||
elements.push({
|
||||
id: `${plant.input}-${plantName}`,
|
||||
source: plant.input,
|
||||
target: plantName,
|
||||
animated: true,
|
||||
style: { stroke: "black" },
|
||||
selectable: false,
|
||||
});
|
||||
}
|
||||
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",
|
||||
targetPosition: "left",
|
||||
className: styles.PlantNode,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
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] of Object.entries(
|
||||
plant["outputs (tonne/tonne)"]
|
||||
)) {
|
||||
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 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 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 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 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 onLayout = () => {
|
||||
const layoutedElements = getLayoutedElements(elements);
|
||||
layoutedElements.forEach((el) => {
|
||||
if (isNode(el)) {
|
||||
if (el.data.type === "plant") {
|
||||
props.onMovePlant(el.data.label, el.position.x, el.position.y);
|
||||
} else {
|
||||
props.onMoveProduct(el.data.label, el.position.x, el.position.y);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section title="Pipeline" />
|
||||
<Card>
|
||||
<div className={styles.PipelineBlock}>
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onConnect={onConnect}
|
||||
onElementsRemove={onElementsRemove}
|
||||
deleteKeyCode={46}
|
||||
maxZoom={1}
|
||||
minZoom={1}
|
||||
snapToGrid={true}
|
||||
preventScrolling={false}
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Button
|
||||
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>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Section title="Pipeline" />
|
||||
<Card>
|
||||
<div className={styles.PipelineBlock}>
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onConnect={onConnect}
|
||||
onElementsRemove={onElementsRemove}
|
||||
deleteKeyCode={46}
|
||||
maxZoom={1}
|
||||
minZoom={1}
|
||||
snapToGrid={true}
|
||||
preventScrolling={false}
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Button
|
||||
label="Add product"
|
||||
kind="inline"
|
||||
onClick={props.onAddProduct}
|
||||
/>
|
||||
<Button label="Add plant" kind="inline" onClick={props.onAddPlant} />
|
||||
<Button label="Auto-Layout" kind="inline" onClick={onLayout} />
|
||||
<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>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PipelineBlock;
|
@ -1,24 +1,25 @@
|
||||
.PipelineBlock {
|
||||
height: 600px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 12px;
|
||||
height: 600px !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1) !important;
|
||||
border-radius: var(--border-radius) !important;
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
.PlantNode, .ProductNode {
|
||||
border-color: rgba(0, 0, 0, 0.8);
|
||||
color: black;
|
||||
font-size: 13px;
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0px 2px 4px -3px black;
|
||||
width: 100px;
|
||||
.PlantNode,
|
||||
.ProductNode {
|
||||
border-color: rgba(0, 0, 0, 0.8) !important;
|
||||
color: black !important;
|
||||
font-size: 13px !important;
|
||||
border-width: 1px !important;
|
||||
border-radius: 6px !important;
|
||||
box-shadow: 0px 2px 4px -3px black !important;
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
.PlantNode {
|
||||
background-color: #0df;
|
||||
background-color: #8d8 !important;
|
||||
}
|
||||
|
||||
.ProductNode {
|
||||
background-color: #f6f6f6;
|
||||
background-color: #e6e6e6 !important;
|
||||
}
|
@ -1,142 +1,266 @@
|
||||
import Section from './Section';
|
||||
import Card from './Card';
|
||||
import Form from './Form';
|
||||
import TextInputRow from './TextInputRow';
|
||||
import FileInputRow from './FileInputRow';
|
||||
import DictInputRow from './DictInputRow';
|
||||
import Section from "./Section";
|
||||
import Card from "./Card";
|
||||
import Form from "./Form";
|
||||
import TextInputRow from "./TextInputRow";
|
||||
import FileInputRow from "./FileInputRow";
|
||||
import DictInputRow from "./DictInputRow";
|
||||
import { csvFormat, csvParse, generateFile } from "./csv";
|
||||
|
||||
const PlantBlock = (props) => {
|
||||
const emissions = {
|
||||
"CO2": "0.05",
|
||||
"CH4": "0.01",
|
||||
"N2O": "0.04",
|
||||
};
|
||||
const output = {
|
||||
"Nickel": "0.5",
|
||||
"Metal casing": "0.35",
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Section title={props.name} />
|
||||
<Card>
|
||||
<Form>
|
||||
<h1>General information</h1>
|
||||
<FileInputRow
|
||||
label="Candidate locations"
|
||||
tooltip="A dictionary mapping the name of the location to a dictionary which describes the site characteristics."
|
||||
/>
|
||||
const onChange = (val, field1, field2, field3) => {
|
||||
const newPlant = { ...props.value };
|
||||
if (field3 !== undefined) {
|
||||
newPlant[field1][field2][field3] = val;
|
||||
} else if (field2 !== undefined) {
|
||||
newPlant[field1][field2] = val;
|
||||
} else {
|
||||
newPlant[field1] = val;
|
||||
}
|
||||
props.onChange(newPlant);
|
||||
};
|
||||
|
||||
const onCandidateLocationsTemplate = () => {
|
||||
generateFile(
|
||||
"Candidate locations - Template.csv",
|
||||
csvFormat([
|
||||
{
|
||||
name: "Washakie County",
|
||||
"latitude (deg)": "43.8356",
|
||||
"longitude (deg)": "-107.6602",
|
||||
"area cost factor": "0.88",
|
||||
},
|
||||
{
|
||||
name: "Platte County",
|
||||
"latitude (deg)": "42.1314",
|
||||
"longitude (deg)": "-104.9676",
|
||||
"area cost factor": "1.29",
|
||||
},
|
||||
{
|
||||
name: "Park County",
|
||||
"latitude (deg)": "44.4063",
|
||||
"longitude (deg)": "-109.4153",
|
||||
"area cost factor": "0.99",
|
||||
},
|
||||
{
|
||||
name: "Goshen County",
|
||||
"latitude (deg)": "42.0853",
|
||||
"longitude (deg)": "-104.3534",
|
||||
"area cost factor": "1",
|
||||
},
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
<h1>Inputs & Outputs</h1>
|
||||
<TextInputRow
|
||||
label="Input"
|
||||
tooltip="The name of the product that this plant takes as input. Only one input is accepted per plant."
|
||||
disabled="disabled"
|
||||
value="Battery"
|
||||
/>
|
||||
<DictInputRow
|
||||
label="Outputs"
|
||||
unit="tonne/tonne"
|
||||
tooltip="A dictionary specifying how many tonnes of each product is produced for each tonnes of input. If the plant does not output anything, this key may be omitted."
|
||||
value={output}
|
||||
disableKeys={true}
|
||||
default="0"
|
||||
/>
|
||||
const onCandidateLocationsFile = (contents) => {
|
||||
const data = csvParse({
|
||||
contents: contents,
|
||||
requiredCols: [
|
||||
"name",
|
||||
"latitude (deg)",
|
||||
"longitude (deg)",
|
||||
"area cost factor",
|
||||
],
|
||||
});
|
||||
const result = {};
|
||||
data.forEach((el) => {
|
||||
result[el["name"]] = {
|
||||
"latitude (deg)": el["latitude (deg)"],
|
||||
"longitude (deg)": el["longitude (deg)"],
|
||||
"area cost factor": el["area cost factor"],
|
||||
};
|
||||
});
|
||||
onChange(result, "locations");
|
||||
};
|
||||
|
||||
<h1>Capacity & costs</h1>
|
||||
<TextInputRow
|
||||
label="Minimum capacity"
|
||||
unit="tonne"
|
||||
tooltip="The minimum size of the plant."
|
||||
default="0"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Opening cost (min capacity)"
|
||||
unit="$"
|
||||
tooltip="The cost to open the plant at minimum capacity."
|
||||
default="0.00"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Fixed operating cost (min capacity)"
|
||||
unit="$"
|
||||
tooltip="The cost to keep the plant open, even if the plant doesn't process anything."
|
||||
default="0.00"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Maximum capacity"
|
||||
unit="tonne"
|
||||
tooltip="The maximum size of the plant."
|
||||
default="0"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Opening cost (max capacity)"
|
||||
unit="$"
|
||||
tooltip="The cost to open a plant of this size."
|
||||
default="0.00"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Fixed operating cost (max capacity)"
|
||||
unit="$"
|
||||
tooltip="The cost to keep the plant open, even if the plant doesn't process anything."
|
||||
default="0.00"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Variable operating cost"
|
||||
unit="$"
|
||||
tooltip="The cost that the plant incurs to process each tonne of input."
|
||||
default="0.00"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Energy expenditure"
|
||||
unit="GJ/tonne"
|
||||
tooltip="The energy required to process 1 tonne of the input."
|
||||
default="0"
|
||||
/>
|
||||
const onCandidateLocationsDownload = () => {
|
||||
const result = [];
|
||||
for (const [locationName, locationDict] of Object.entries(
|
||||
props.value["locations"]
|
||||
)) {
|
||||
result.push({
|
||||
name: locationName,
|
||||
"latitude (deg)": locationDict["latitude (deg)"],
|
||||
"longitude (deg)": locationDict["longitude (deg)"],
|
||||
"area cost factor": locationDict["area cost factor"],
|
||||
});
|
||||
}
|
||||
generateFile(`Candidate locations - ${props.name}.csv`, csvFormat(result));
|
||||
};
|
||||
|
||||
<h1>Storage</h1>
|
||||
<TextInputRow
|
||||
label="Storage cost"
|
||||
unit="$/tonne"
|
||||
tooltip="The cost to store a tonne of input product for one time period."
|
||||
default="0.00"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Storage limit"
|
||||
unit="tonne"
|
||||
tooltip="The maximum amount of input product this plant can have in storage at any given time."
|
||||
default="0"
|
||||
/>
|
||||
const onCandidateLocationsClear = () => {
|
||||
onChange({}, "locations");
|
||||
};
|
||||
|
||||
<h1>Disposal</h1>
|
||||
<DictInputRow
|
||||
label="Disposal cost"
|
||||
unit="$/tonne"
|
||||
tooltip="The cost to dispose of the product."
|
||||
value={output}
|
||||
disableKeys={true}
|
||||
/>
|
||||
<DictInputRow
|
||||
label="Disposal limit"
|
||||
unit="tonne"
|
||||
tooltip="The maximum amount that can be disposed of. If an unlimited amount can be disposed, this key may be omitted."
|
||||
value={output}
|
||||
disableKeys={true}
|
||||
/>
|
||||
let description = "No locations set";
|
||||
const nCenters = Object.keys(props.value["locations"]).length;
|
||||
if (nCenters > 0) description = `${nCenters} locations`;
|
||||
|
||||
<h1>Emissions</h1>
|
||||
<DictInputRow
|
||||
label="Emissions"
|
||||
unit="tonne/tonne"
|
||||
tooltip="A dictionary mapping the name of each greenhouse gas, produced to process each tonne of input, to the amount of gas produced (in tonne)."
|
||||
value={emissions}
|
||||
keyPlaceholder="Emission name"
|
||||
valuePlaceholder="0"
|
||||
/>
|
||||
const shouldDisableMaxCap =
|
||||
props.value["minimum capacity (tonne)"] ===
|
||||
props.value["maximum capacity (tonne)"];
|
||||
|
||||
</Form>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Section title={props.name} />
|
||||
<Card>
|
||||
<Form>
|
||||
<h1>General information</h1>
|
||||
<FileInputRow
|
||||
label="Candidate locations"
|
||||
tooltip="A table describing potential locations where plants can be built and their characteristics."
|
||||
onTemplate={onCandidateLocationsTemplate}
|
||||
onFile={onCandidateLocationsFile}
|
||||
onDownload={onCandidateLocationsDownload}
|
||||
onClear={onCandidateLocationsClear}
|
||||
value={description}
|
||||
/>
|
||||
|
||||
<h1>Inputs & Outputs</h1>
|
||||
<TextInputRow
|
||||
label="Input"
|
||||
tooltip="The name of the product that this plant takes as input."
|
||||
disabled="disabled"
|
||||
value="Battery"
|
||||
/>
|
||||
<DictInputRow
|
||||
label="Outputs"
|
||||
unit="tonne/tonne"
|
||||
tooltip="A dictionary specifying how many tonnes of each product is produced for each tonne of input."
|
||||
value={props.value["outputs (tonne/tonne)"]}
|
||||
onChange={(v) => onChange(v, "outputs (tonne/tonne)")}
|
||||
disableKeys={true}
|
||||
validate="float"
|
||||
/>
|
||||
|
||||
<h1>Capacity & Costs</h1>
|
||||
<TextInputRow
|
||||
label="Minimum capacity"
|
||||
unit="tonne"
|
||||
tooltip="The minimum size of the plant."
|
||||
value={props.value["minimum capacity (tonne)"]}
|
||||
onChange={(v) => onChange(v, "minimum capacity (tonne)")}
|
||||
validate="float"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Opening cost (min capacity)"
|
||||
unit="$"
|
||||
tooltip="The cost to open the plant at minimum capacity."
|
||||
value={props.value["opening cost (min capacity) ($)"]}
|
||||
onChange={(v) => onChange(v, "opening cost (min capacity) ($)")}
|
||||
validate="float"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Fixed operating cost (min capacity)"
|
||||
unit="$"
|
||||
tooltip="The cost to keep the plant open, even if the plant doesn't process anything."
|
||||
value={props.value["fixed operating cost (min capacity) ($)"]}
|
||||
onChange={(v) =>
|
||||
onChange(v, "fixed operating cost (min capacity) ($)")
|
||||
}
|
||||
validate="float"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Maximum capacity"
|
||||
unit="tonne"
|
||||
tooltip="The maximum size of the plant."
|
||||
value={props.value["maximum capacity (tonne)"]}
|
||||
onChange={(v) => onChange(v, "maximum capacity (tonne)")}
|
||||
validate="float"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Opening cost (max capacity)"
|
||||
unit="$"
|
||||
tooltip="The cost to open a plant of this size."
|
||||
value={
|
||||
shouldDisableMaxCap
|
||||
? ""
|
||||
: props.value["opening cost (max capacity) ($)"]
|
||||
}
|
||||
onChange={(v) => onChange(v, "opening cost (max capacity) ($)")}
|
||||
validate="float"
|
||||
disabled={shouldDisableMaxCap}
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Fixed operating cost (max capacity)"
|
||||
unit="$"
|
||||
tooltip="The cost to keep the plant open, even if the plant doesn't process anything."
|
||||
value={
|
||||
shouldDisableMaxCap
|
||||
? ""
|
||||
: props.value["fixed operating cost (max capacity) ($)"]
|
||||
}
|
||||
onChange={(v) =>
|
||||
onChange(v, "fixed operating cost (max capacity) ($)")
|
||||
}
|
||||
validate="float"
|
||||
disabled={shouldDisableMaxCap}
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Variable operating cost"
|
||||
unit="$"
|
||||
tooltip="The cost that the plant incurs to process each tonne of input."
|
||||
value={props.value["variable operating cost ($/tonne)"]}
|
||||
onChange={(v) => onChange(v, "variable operating cost ($/tonne)")}
|
||||
validate="float"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Energy expenditure"
|
||||
unit="GJ/tonne"
|
||||
tooltip="The energy required to process one tonne of the input."
|
||||
value={props.value["energy (GJ/tonne)"]}
|
||||
onChange={(v) => onChange(v, "energy (GJ/tonne)")}
|
||||
validate="float"
|
||||
/>
|
||||
|
||||
<h1>Storage</h1>
|
||||
<TextInputRow
|
||||
label="Storage cost"
|
||||
unit="$/tonne"
|
||||
tooltip="The cost to store a tonne of input product for one time period."
|
||||
value={props.value["storage"]["cost ($/tonne)"]}
|
||||
onChange={(v) => onChange(v, "storage", "cost ($/tonne)")}
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Storage limit"
|
||||
unit="tonne"
|
||||
tooltip="The maximum amount of input product this plant can have in storage at any given time."
|
||||
value={props.value["storage"]["limit (tonne)"]}
|
||||
onChange={(v) => onChange(v, "storage", "limit (tonne)")}
|
||||
/>
|
||||
|
||||
<h1>Disposal</h1>
|
||||
<DictInputRow
|
||||
label="Disposal cost"
|
||||
unit="$/tonne"
|
||||
tooltip="The cost to dispose of the product."
|
||||
value={props.value["disposal cost ($/tonne)"]}
|
||||
onChange={(v) => onChange(v, "disposal cost ($/tonne)")}
|
||||
disableKeys={true}
|
||||
/>
|
||||
<DictInputRow
|
||||
label="Disposal limit"
|
||||
unit="tonne"
|
||||
tooltip="The maximum amount that can be disposed of. If an unlimited amount can be disposed, leave blank."
|
||||
value={props.value["disposal limit (tonne)"]}
|
||||
onChange={(v) => onChange(v, "disposal limit (tonne)")}
|
||||
disableKeys={true}
|
||||
valuePlaceholder="Unlimited"
|
||||
/>
|
||||
|
||||
<h1>Emissions</h1>
|
||||
<DictInputRow
|
||||
label="Emissions"
|
||||
unit="tonne/tonne"
|
||||
tooltip="A dictionary mapping the name of each greenhouse gas, produced to process each tonne of input, to the amount of gas produced (in tonne)."
|
||||
value={props.value["emissions (tonne/tonne)"]}
|
||||
onChange={(v) => onChange(v, "emissions (tonne/tonne)")}
|
||||
keyPlaceholder="Emission name"
|
||||
valuePlaceholder="0"
|
||||
/>
|
||||
</Form>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlantBlock;
|
@ -1,212 +1,177 @@
|
||||
import { useState } from 'react';
|
||||
import Section from './Section';
|
||||
import Card from './Card';
|
||||
import Form from './Form';
|
||||
import TextInputRow from './TextInputRow';
|
||||
import FileInputRow from './FileInputRow';
|
||||
import DictInputRow from './DictInputRow';
|
||||
import * as d3 from 'd3';
|
||||
import Section from "./Section";
|
||||
import Card from "./Card";
|
||||
import Form from "./Form";
|
||||
import TextInputRow from "./TextInputRow";
|
||||
import FileInputRow from "./FileInputRow";
|
||||
import DictInputRow from "./DictInputRow";
|
||||
import { csvParse, extractNumericColumns, generateFile } from "./csv";
|
||||
import { csvFormat } from "d3";
|
||||
|
||||
const ProductBlock = (props) => {
|
||||
const onChange = (field, val) => {
|
||||
const newProduct = { ...props.value };
|
||||
newProduct[field] = val;
|
||||
props.onChange(newProduct);
|
||||
};
|
||||
|
||||
const onInitialAmountsFile = (contents) => {
|
||||
const data = d3.csvParse(contents);
|
||||
const T = data.columns.length - 3;
|
||||
|
||||
// Construct list of required columns
|
||||
let isValid = true;
|
||||
const requiredCols = ["latitude (deg)", "longitude (deg)", "name"];
|
||||
for (let t = 0; t < T; t++) {
|
||||
requiredCols.push(t + 1);
|
||||
}
|
||||
|
||||
// Check required columns
|
||||
requiredCols.forEach(col => {
|
||||
if (!(col in data[0])) {
|
||||
console.log(`Column "${col}" not found in CSV file.`);
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
if (!isValid) return;
|
||||
|
||||
// Construct initial amounts dict
|
||||
const result = {};
|
||||
data.forEach(el => {
|
||||
let amounts = [];
|
||||
for (let t = 0; t < T; t++) {
|
||||
amounts.push(el[t + 1]);
|
||||
}
|
||||
result[el["name"]] = {
|
||||
"latitude (deg)": el["latitude (deg)"],
|
||||
"longitude (deg)": el["longitude (deg)"],
|
||||
"amount (tonne)": amounts,
|
||||
};
|
||||
});
|
||||
|
||||
onChange("initial amounts", result);
|
||||
};
|
||||
|
||||
const onInitialAmountsClear = () => {
|
||||
onChange("initial amounts", {});
|
||||
};
|
||||
|
||||
const onInitialAmountsTemplate = () => {
|
||||
exportToCsv(
|
||||
"Initial amounts - Template.csv", [
|
||||
["name", "latitude (deg)", "longitude (deg)", "1", "2", "3", "4", "5"],
|
||||
["Washakie County", "43.8356", "-107.6602", "21902", "6160", "2721", "12917", "18048"],
|
||||
["Platte County", "42.1314", "-104.9676", "16723", "8709", "22584", "12278", "7196"],
|
||||
["Park County", "44.4063", "-109.4153", "14731", "11729", "15562", "7703", "23349"],
|
||||
["Goshen County", "42.0853", "-104.3534", "23266", "16299", "11470", "20107", "21592"],
|
||||
]);
|
||||
};
|
||||
|
||||
const onInitialAmountsDownload = () => {
|
||||
const result = [];
|
||||
for (const [locationName, locationDict] of Object.entries(props.value["initial amounts"])) {
|
||||
// Add header
|
||||
if (result.length == 0) {
|
||||
const T = locationDict["amount (tonne)"].length;
|
||||
const row = ["name", "latitude (deg)", "longitude (deg)"];
|
||||
for (let t = 0; t < T; t++) {
|
||||
row.push(t + 1);
|
||||
}
|
||||
result.push(row);
|
||||
}
|
||||
|
||||
// Add content row
|
||||
const row = [locationName, locationDict["latitude (deg)"], locationDict["longitude (deg)"]];
|
||||
locationDict["amount (tonne)"].forEach(el => {
|
||||
row.push(el);
|
||||
});
|
||||
result.push(row);
|
||||
}
|
||||
exportToCsv(`Initial amounts - ${props.name}`, result);
|
||||
};
|
||||
|
||||
let description = "Not initially available";
|
||||
const nCenters = Object.keys(props.value["initial amounts"]).length;
|
||||
if (nCenters > 0) {
|
||||
description = `${nCenters} collection centers`;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section title={props.name} />
|
||||
<Card>
|
||||
<Form>
|
||||
<h1>General information</h1>
|
||||
<FileInputRow
|
||||
value={description}
|
||||
label="Initial amounts"
|
||||
tooltip="A dictionary mapping the name of each location to its description (see below). If this product is not initially available, this key may be omitted."
|
||||
accept=".csv"
|
||||
onFile={onInitialAmountsFile}
|
||||
onDownload={onInitialAmountsDownload}
|
||||
onClear={onInitialAmountsClear}
|
||||
onTemplate={onInitialAmountsTemplate}
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Acquisition cost"
|
||||
unit="$/tonne"
|
||||
tooltip="The cost to acquire one tonne of this product from collection centers. Does not apply to plant outputs."
|
||||
value={props.value["acquisition cost ($/tonne)"]}
|
||||
onChange={v => onChange("acquisition cost ($/tonne)", v)}
|
||||
validate="float"
|
||||
/>
|
||||
|
||||
<h1>Disposal</h1>
|
||||
<TextInputRow
|
||||
label="Disposal cost"
|
||||
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."
|
||||
value={props.value["disposal cost ($/tonne)"]}
|
||||
onChange={v => onChange("disposal cost ($/tonne)", v)}
|
||||
validate="float"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Disposal limit"
|
||||
unit="tonne"
|
||||
tooltip="The maximum amount of this product that can be disposed of across all collection centers, without further processing."
|
||||
value={props.value["disposal limit (tonne)"]}
|
||||
onChange={v => onChange("disposal limit (tonne)", v)}
|
||||
validate="float"
|
||||
/>
|
||||
|
||||
<h1>Transportation</h1>
|
||||
<TextInputRow
|
||||
label="Transportation cost"
|
||||
unit="$/km/tonne"
|
||||
tooltip="The cost to transport this product."
|
||||
value={props.value["transportation cost ($/km/tonne)"]}
|
||||
onChange={v => onChange("transportation cost ($/km/tonne)", v)}
|
||||
validate="float"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Transportation energy"
|
||||
unit="J/km/tonne"
|
||||
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)}
|
||||
validate="float"
|
||||
/>
|
||||
<DictInputRow
|
||||
label="Transportation emissions"
|
||||
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)."
|
||||
keyPlaceholder="Emission name"
|
||||
value={props.value["transportation emissions (J/km/tonne)"]}
|
||||
onChange={v => onChange("transportation emissions (J/km/tonne)", v)}
|
||||
validate="float"
|
||||
/>
|
||||
</Form>
|
||||
</Card>
|
||||
</>
|
||||
const onChange = (field, val) => {
|
||||
const newProduct = { ...props.value };
|
||||
newProduct[field] = val;
|
||||
props.onChange(newProduct);
|
||||
};
|
||||
|
||||
const onInitialAmountsFile = (contents) => {
|
||||
const data = csvParse({
|
||||
contents: contents,
|
||||
requiredCols: ["latitude (deg)", "longitude (deg)", "name"],
|
||||
});
|
||||
const result = {};
|
||||
data.forEach((el) => {
|
||||
result[el["name"]] = {
|
||||
"latitude (deg)": el["latitude (deg)"],
|
||||
"longitude (deg)": el["longitude (deg)"],
|
||||
"amount (tonne)": extractNumericColumns(el, "amount"),
|
||||
};
|
||||
});
|
||||
onChange("initial amounts", result);
|
||||
};
|
||||
|
||||
const onInitialAmountsClear = () => {
|
||||
onChange("initial amounts", {});
|
||||
};
|
||||
|
||||
const onInitialAmountsTemplate = () => {
|
||||
generateFile(
|
||||
"Initial amounts - Template.csv",
|
||||
csvFormat([
|
||||
{
|
||||
name: "Washakie County",
|
||||
"latitude (deg)": "43.8356",
|
||||
"longitude (deg)": "-107.6602",
|
||||
"amount 1": "21902",
|
||||
"amount 2": "6160",
|
||||
"amount 3": "2721",
|
||||
"amount 4": "12917",
|
||||
"amount 5": "18048",
|
||||
},
|
||||
{
|
||||
name: "Platte County",
|
||||
"latitude (deg)": "42.1314",
|
||||
"longitude (deg)": "-104.9676",
|
||||
"amount 1": "16723",
|
||||
"amount 2": "8709",
|
||||
"amount 3": "22584",
|
||||
"amount 4": "12278",
|
||||
"amount 5": "7196",
|
||||
},
|
||||
{
|
||||
name: "Park County",
|
||||
"latitude (deg)": "44.4063",
|
||||
"longitude (deg)": "-109.4153",
|
||||
"amount 1": "14731",
|
||||
"amount 2": "11729",
|
||||
"amount 3": "15562",
|
||||
"amount 4": "7703",
|
||||
"amount 5": "23349",
|
||||
},
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
function exportToCsv(filename, rows) {
|
||||
var processRow = function (row) {
|
||||
var finalVal = "";
|
||||
for (var j = 0; j < row.length; j++) {
|
||||
var innerValue = row[j] === null ? "" : row[j].toString();
|
||||
if (row[j] instanceof Date) {
|
||||
innerValue = row[j].toLocaleString();
|
||||
}
|
||||
var result = innerValue.replace(/"/g, '""');
|
||||
if (result.search(/("|,|\n)/g) >= 0) result = '"' + result + '"';
|
||||
if (j > 0) finalVal += ",";
|
||||
finalVal += result;
|
||||
}
|
||||
return finalVal + "\n";
|
||||
};
|
||||
|
||||
var csvFile = "";
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
csvFile += processRow(rows[i]);
|
||||
}
|
||||
|
||||
var blob = new Blob([csvFile], { type: "text/csv;charset=utf-8;" });
|
||||
if (navigator.msSaveBlob) {
|
||||
// IE 10+
|
||||
navigator.msSaveBlob(blob, filename);
|
||||
} else {
|
||||
var link = document.createElement("a");
|
||||
if (link.download !== undefined) {
|
||||
var url = URL.createObjectURL(blob);
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", filename);
|
||||
link.style.visibility = "hidden";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
};
|
||||
|
||||
const onInitialAmountsDownload = () => {
|
||||
const results = [];
|
||||
for (const [locationName, locationDict] of Object.entries(
|
||||
props.value["initial amounts"]
|
||||
)) {
|
||||
const row = {
|
||||
name: locationName,
|
||||
"latitude (deg)": locationDict["latitude (deg)"],
|
||||
"longitude (deg)": locationDict["longitude (deg)"],
|
||||
};
|
||||
locationDict["amount (tonne)"].forEach((el, idx) => {
|
||||
row[`amount ${idx + 1}`] = el;
|
||||
});
|
||||
results.push(row);
|
||||
}
|
||||
}
|
||||
generateFile(`Initial amounts - ${props.name}.csv`, csvFormat(results));
|
||||
};
|
||||
|
||||
let description = "Not initially available";
|
||||
const nCenters = Object.keys(props.value["initial amounts"]).length;
|
||||
if (nCenters > 0) description = `${nCenters} collection centers`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section title={props.name} />
|
||||
<Card>
|
||||
<Form>
|
||||
<h1>General Information</h1>
|
||||
<FileInputRow
|
||||
value={description}
|
||||
label="Initial amounts"
|
||||
tooltip="A table indicating the amount of this product initially available at each collection center."
|
||||
accept=".csv"
|
||||
onFile={onInitialAmountsFile}
|
||||
onDownload={onInitialAmountsDownload}
|
||||
onClear={onInitialAmountsClear}
|
||||
onTemplate={onInitialAmountsTemplate}
|
||||
/>
|
||||
|
||||
<h1>Disposal</h1>
|
||||
<TextInputRow
|
||||
label="Disposal cost"
|
||||
unit="$/tonne"
|
||||
tooltip="The cost to dispose of one tonne of this product at a collection center, without further processing."
|
||||
value={props.value["disposal cost ($/tonne)"]}
|
||||
onChange={(v) => onChange("disposal cost ($/tonne)", v)}
|
||||
validate="floatList"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Disposal limit"
|
||||
unit="tonne"
|
||||
tooltip="The maximum amount (in tonnes) of this product that can be disposed of across all collection centers, without further processing."
|
||||
value={props.value["disposal limit (tonne)"]}
|
||||
onChange={(v) => onChange("disposal limit (tonne)", v)}
|
||||
validate="floatList"
|
||||
disabled={String(props.value["disposal limit (%)"]).length > 0}
|
||||
/>
|
||||
|
||||
<TextInputRow
|
||||
label="Disposal limit"
|
||||
unit="%"
|
||||
tooltip="The maximum amount of this product that can be disposed of across all collection centers, without further processing, as a percentage of the total amount available."
|
||||
value={props.value["disposal limit (%)"]}
|
||||
onChange={(v) => onChange("disposal limit (%)", v)}
|
||||
validate="floatList"
|
||||
disabled={props.value["disposal limit (tonne)"].length > 0}
|
||||
/>
|
||||
|
||||
<h1>Transportation</h1>
|
||||
<TextInputRow
|
||||
label="Transportation cost"
|
||||
unit="$/km/tonne"
|
||||
tooltip="The cost to transport this product."
|
||||
value={props.value["transportation cost ($/km/tonne)"]}
|
||||
onChange={(v) => onChange("transportation cost ($/km/tonne)", v)}
|
||||
validate="floatList"
|
||||
/>
|
||||
<TextInputRow
|
||||
label="Transportation energy"
|
||||
unit="J/km/tonne"
|
||||
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)}
|
||||
validate="floatList"
|
||||
/>
|
||||
<DictInputRow
|
||||
label="Transportation emissions"
|
||||
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."
|
||||
keyPlaceholder="Emission name"
|
||||
value={props.value["transportation emissions (tonne/km/tonne)"]}
|
||||
onChange={(v) =>
|
||||
onChange("transportation emissions (tonne/km/tonne)", v)
|
||||
}
|
||||
validate="floatList"
|
||||
/>
|
||||
</Form>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductBlock;
|
@ -1,7 +1,7 @@
|
||||
import styles from './Section.module.css';
|
||||
import styles from "./Section.module.css";
|
||||
|
||||
const Section = (props) => {
|
||||
return <h2 className={styles.Section}>{props.title}</h2>;
|
||||
return <h2 className={styles.Section}>{props.title}</h2>;
|
||||
};
|
||||
|
||||
export default Section;
|
@ -1,6 +1,6 @@
|
||||
.Section {
|
||||
line-height: 36px;
|
||||
margin: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 36px;
|
||||
margin: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
@ -1,40 +1,44 @@
|
||||
import form_styles from './Form.module.css';
|
||||
import Button from './Button';
|
||||
import { validate } from './Form';
|
||||
import form_styles from "./Form.module.css";
|
||||
import Button from "./Button";
|
||||
import { validate } from "./Form";
|
||||
import React from "react";
|
||||
|
||||
const TextInputRow = (props) => {
|
||||
let unit = "";
|
||||
if (props.unit) {
|
||||
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
|
||||
}
|
||||
const TextInputRow = React.forwardRef((props, ref) => {
|
||||
let unit = "";
|
||||
if (props.unit) {
|
||||
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
|
||||
}
|
||||
|
||||
let tooltip = "";
|
||||
if (props.tooltip != undefined) {
|
||||
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
||||
}
|
||||
let tooltip = "";
|
||||
if (props.tooltip !== undefined) {
|
||||
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
||||
}
|
||||
|
||||
let isValid = true;
|
||||
if (props.validate !== undefined) {
|
||||
isValid = validate(props.validate, props.value);
|
||||
}
|
||||
let isValid = true;
|
||||
if (!props.disabled && props.validate !== undefined) {
|
||||
isValid = validate(props.validate, props.value);
|
||||
}
|
||||
|
||||
let className = "";
|
||||
if (!isValid) className = form_styles.invalid;
|
||||
let className = "";
|
||||
if (!isValid) className = form_styles.invalid;
|
||||
|
||||
return <div className={form_styles.FormRow}>
|
||||
<label>
|
||||
{props.label} {unit}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={props.default}
|
||||
disabled={props.disabled}
|
||||
value={props.value}
|
||||
className={className}
|
||||
onChange={e => props.onChange(e.target.value)}
|
||||
/>
|
||||
{tooltip}
|
||||
</div>;
|
||||
};
|
||||
return (
|
||||
<div className={form_styles.FormRow}>
|
||||
<label>
|
||||
{props.label} {unit}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={props.default}
|
||||
disabled={props.disabled}
|
||||
value={props.value}
|
||||
className={className}
|
||||
onChange={(e) => props.onChange(e.target.value)}
|
||||
ref={ref}
|
||||
/>
|
||||
{tooltip}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default TextInputRow;
|
@ -0,0 +1,50 @@
|
||||
import * as d3 from "d3";
|
||||
|
||||
export const csvParse = ({ contents, requiredCols }) => {
|
||||
const data = d3.csvParse(contents, d3.autoType);
|
||||
requiredCols.forEach((col) => {
|
||||
if (!(col in data[0])) {
|
||||
throw Error(`Column "${col}" not found in CSV file.`);
|
||||
}
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const parseCsv = (contents, requiredCols = []) => {
|
||||
const data = d3.csvParse(contents);
|
||||
const T = data.columns.length - requiredCols.length;
|
||||
let isValid = true;
|
||||
for (let t = 0; t < T; t++) {
|
||||
requiredCols.push(t + 1);
|
||||
}
|
||||
requiredCols.forEach((col) => {
|
||||
if (!(col in data[0])) {
|
||||
console.log(`Column "${col}" not found in CSV file.`);
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
if (!isValid) return [undefined, undefined];
|
||||
return [data, T];
|
||||
};
|
||||
|
||||
export const extractNumericColumns = (obj, prefix) => {
|
||||
const result = [];
|
||||
for (let i = 1; `${prefix} ${i}` in obj; i++) {
|
||||
result.push(obj[`${prefix} ${i}`]);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const csvFormat = (data) => {
|
||||
return d3.csvFormat(data);
|
||||
};
|
||||
|
||||
export const generateFile = (filename, contents) => {
|
||||
var link = document.createElement("a");
|
||||
link.setAttribute("href", URL.createObjectURL(new Blob([contents])));
|
||||
link.setAttribute("download", filename);
|
||||
link.style.visibility = "hidden";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
@ -0,0 +1,53 @@
|
||||
import { csvParse, extractNumericColumns, csvFormat } from "./csv";
|
||||
import { exportValue } from "./export";
|
||||
|
||||
test("parse CSV", () => {
|
||||
const contents = "name,location,1,2,3\ntest,illinois,100,200,300";
|
||||
const actual = csvParse({
|
||||
contents: contents,
|
||||
requiredCols: ["name", "location"],
|
||||
});
|
||||
expect(actual.length).toEqual(1);
|
||||
expect(actual[0]).toEqual({
|
||||
name: "test",
|
||||
location: "illinois",
|
||||
1: 100,
|
||||
2: 200,
|
||||
3: 300,
|
||||
});
|
||||
});
|
||||
|
||||
test("parse CSV with missing columns", () => {
|
||||
const contents = "name,location,1,2,3\ntest,illinois,100,200,300";
|
||||
expect(() =>
|
||||
csvParse({
|
||||
contents: contents,
|
||||
requiredCols: ["name", "location", "latitude"],
|
||||
})
|
||||
).toThrow('Column "latitude" not found in CSV file.');
|
||||
});
|
||||
|
||||
test("extract numeric columns from object", () => {
|
||||
const obj1 = {
|
||||
"amount 1": "hello",
|
||||
"amount 2": "world",
|
||||
"amount 4": "ignored",
|
||||
};
|
||||
const obj2 = { hello: "world" };
|
||||
expect(extractNumericColumns(obj1, "amount")).toEqual(["hello", "world"]);
|
||||
expect(extractNumericColumns(obj2, "amount")).toEqual([]);
|
||||
});
|
||||
|
||||
test("generate CSV", () => {
|
||||
const data = [
|
||||
{ name: "alice", age: 20 },
|
||||
{ name: "bob", age: null },
|
||||
];
|
||||
expect(csvFormat(data)).toEqual("name,age\nalice,20\nbob,");
|
||||
});
|
||||
|
||||
test("export value", () => {
|
||||
expect(exportValue("1")).toEqual(1);
|
||||
expect(exportValue("[1,2,3]")).toEqual([1, 2, 3]);
|
||||
expect(exportValue("qwe")).toEqual("qwe");
|
||||
});
|
@ -0,0 +1,49 @@
|
||||
export const defaultProduct = {
|
||||
"initial amounts": {},
|
||||
"disposal cost ($/tonne)": "0",
|
||||
"disposal limit (tonne)": "0",
|
||||
"disposal limit (%)": "",
|
||||
"transportation cost ($/km/tonne)": "0",
|
||||
"transportation energy (J/km/tonne)": "0",
|
||||
"transportation emissions (tonne/km/tonne)": {},
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
export const defaultPlantLocation = {
|
||||
"area cost factor": 1.0,
|
||||
"latitude (deg)": 0,
|
||||
"longitude (deg)": 0,
|
||||
};
|
||||
|
||||
export const defaultPlant = {
|
||||
locations: {},
|
||||
"outputs (tonne/tonne)": {},
|
||||
"disposal cost ($/tonne)": {},
|
||||
"disposal limit (tonne)": {},
|
||||
"emissions (tonne/tonne)": {},
|
||||
storage: {
|
||||
"cost ($/tonne)": 0,
|
||||
"limit (tonne)": 0,
|
||||
},
|
||||
"maximum capacity (tonne)": 0,
|
||||
"minimum capacity (tonne)": 0,
|
||||
"opening cost (max capacity) ($)": 0,
|
||||
"opening cost (min capacity) ($)": 0,
|
||||
"fixed operating cost (max capacity) ($)": 0,
|
||||
"fixed operating cost (min capacity) ($)": 0,
|
||||
"variable operating cost ($/tonne)": 0,
|
||||
"energy (GJ/tonne)": 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
export const defaultData = {
|
||||
parameters: {
|
||||
"time horizon (years)": "1",
|
||||
"building period (years)": "[1]",
|
||||
// "annual inflation rate (%)": "0",
|
||||
},
|
||||
products: {},
|
||||
plants: {},
|
||||
};
|
@ -0,0 +1,464 @@
|
||||
const isNumeric = (val) => {
|
||||
return String(val).length > 0 && !isNaN(val);
|
||||
};
|
||||
|
||||
const keysToList = (obj) => {
|
||||
const result = [];
|
||||
for (const key of Object.keys(obj)) {
|
||||
result.push(key);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const exportValue = (original, T) => {
|
||||
if (isNumeric(original)) {
|
||||
if (T) {
|
||||
const v = parseFloat(original);
|
||||
const result = [];
|
||||
for (let i = 0; i < T; i++) result.push(v);
|
||||
return result;
|
||||
} else {
|
||||
return parseFloat(original);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(original);
|
||||
return parsed;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return original;
|
||||
};
|
||||
|
||||
const exportValueDict = (original, T) => {
|
||||
const result = {};
|
||||
for (const [key, val] of Object.entries(original)) {
|
||||
if (key.length === 0) continue;
|
||||
result[key] = exportValue(val, T);
|
||||
}
|
||||
if (Object.keys(result).length > 0) {
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const computeTotalInitialAmount = (prod) => {
|
||||
let total = null;
|
||||
for (const locDict of Object.values(prod["initial amounts"])) {
|
||||
const locAmount = locDict["amount (tonne)"];
|
||||
if (!total) total = [...locAmount];
|
||||
else {
|
||||
for (let i = 0; i < locAmount.length; i++) {
|
||||
total[i] += locAmount[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
export const importList = (args) => {
|
||||
if (!args) return "";
|
||||
if (Array.isArray(args) && args.length > 0) {
|
||||
let isConstant = true;
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
if (args[i - 1] !== args[i]) {
|
||||
isConstant = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isConstant) {
|
||||
return String(args[0]);
|
||||
} else {
|
||||
return JSON.stringify(args);
|
||||
}
|
||||
} else {
|
||||
return args;
|
||||
}
|
||||
};
|
||||
|
||||
export const importDict = (args) => {
|
||||
if (!args) return {};
|
||||
const result = {};
|
||||
for (const [key, val] of Object.entries(args)) {
|
||||
result[key] = importList(val);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const computeAbsDisposal = (prod) => {
|
||||
const disposalPerc = prod["disposal limit (%)"];
|
||||
const total = computeTotalInitialAmount(prod);
|
||||
const disposalAbs = [];
|
||||
for (let i = 0; i < total.length; i++) {
|
||||
disposalAbs[i] = (total[i] * disposalPerc) / 100;
|
||||
}
|
||||
return disposalAbs;
|
||||
};
|
||||
|
||||
export const exportProduct = (original, T) => {
|
||||
const result = {};
|
||||
|
||||
// Copy time series values
|
||||
result["initial amounts"] = original["initial amounts"];
|
||||
[
|
||||
"disposal cost ($/tonne)",
|
||||
"disposal limit (tonne)",
|
||||
"transportation cost ($/km/tonne)",
|
||||
"transportation energy (J/km/tonne)",
|
||||
].forEach((key) => {
|
||||
const v = exportValue(original[key], T);
|
||||
if (v.length > 0) result[key] = v;
|
||||
});
|
||||
|
||||
// Copy dictionaries
|
||||
["transportation emissions (tonne/km/tonne)"].forEach((key) => {
|
||||
const v = exportValueDict(original[key], T);
|
||||
if (v) result[key] = v;
|
||||
});
|
||||
|
||||
// Transform percentage disposal limits into absolute
|
||||
if (isNumeric(original["disposal limit (%)"])) {
|
||||
result["disposal limit (tonne)"] = computeAbsDisposal(original);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const exportPlant = (original, T) => {
|
||||
const result = {};
|
||||
|
||||
// Copy scalar values
|
||||
["input"].forEach((key) => {
|
||||
result[key] = original[key];
|
||||
});
|
||||
|
||||
// Copy time series values
|
||||
["energy (GJ/tonne)"].forEach((key) => {
|
||||
result[key] = exportValue(original[key], T);
|
||||
});
|
||||
|
||||
// Copy scalar dicts
|
||||
["outputs (tonne/tonne)"].forEach((key) => {
|
||||
const v = exportValueDict(original[key]);
|
||||
if (v) result[key] = v;
|
||||
});
|
||||
|
||||
// Copy time series dicts
|
||||
["emissions (tonne/tonne)"].forEach((key) => {
|
||||
const v = exportValueDict(original[key], T);
|
||||
if (v) result[key] = v;
|
||||
});
|
||||
|
||||
const minCap = original["minimum capacity (tonne)"];
|
||||
const maxCap = original["maximum capacity (tonne)"];
|
||||
|
||||
result.locations = {};
|
||||
for (const [locName, origDict] of Object.entries(original["locations"])) {
|
||||
const resDict = (result.locations[locName] = {});
|
||||
const capDict = (resDict["capacities (tonne)"] = {});
|
||||
|
||||
const acf = origDict["area cost factor"];
|
||||
|
||||
const exportValueAcf = (obj, T) => {
|
||||
const v = exportValue(obj, T);
|
||||
if (Array.isArray(v)) {
|
||||
return v.map((v) => v * acf);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// Copy scalar values
|
||||
["latitude (deg)", "longitude (deg)"].forEach((key) => {
|
||||
resDict[key] = origDict[key];
|
||||
});
|
||||
|
||||
// Copy minimum capacity dict
|
||||
capDict[minCap] = {};
|
||||
for (const [resKeyName, origKeyName] of Object.entries({
|
||||
"opening cost ($)": "opening cost (min capacity) ($)",
|
||||
"fixed operating cost ($)": "fixed operating cost (min capacity) ($)",
|
||||
"variable operating cost ($/tonne)": "variable operating cost ($/tonne)",
|
||||
})) {
|
||||
capDict[minCap][resKeyName] = exportValueAcf(original[origKeyName], T);
|
||||
}
|
||||
|
||||
if (maxCap !== minCap) {
|
||||
// Copy maximum capacity dict
|
||||
capDict[maxCap] = {};
|
||||
for (const [resKeyName, origKeyName] of Object.entries({
|
||||
"opening cost ($)": "opening cost (max capacity) ($)",
|
||||
"fixed operating cost ($)": "fixed operating cost (max capacity) ($)",
|
||||
"variable operating cost ($/tonne)":
|
||||
"variable operating cost ($/tonne)",
|
||||
})) {
|
||||
capDict[maxCap][resKeyName] = exportValueAcf(original[origKeyName], T);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy disposal
|
||||
resDict.disposal = {};
|
||||
for (const [dispName, dispCost] of Object.entries(
|
||||
original["disposal cost ($/tonne)"]
|
||||
)) {
|
||||
if (dispName.length === 0) continue;
|
||||
const v = exportValueAcf(dispCost, T);
|
||||
if (v) {
|
||||
resDict.disposal[dispName] = { "cost ($/tonne)": v };
|
||||
const limit = original["disposal limit (tonne)"][dispName];
|
||||
if (isNumeric(limit)) {
|
||||
resDict.disposal[dispName]["limit (tonne)"] = exportValue(limit, T);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy storage
|
||||
resDict.storage = {
|
||||
"cost ($/tonne)": exportValueAcf(
|
||||
original["storage"]["cost ($/tonne)"],
|
||||
T
|
||||
),
|
||||
};
|
||||
const storLimit = original["storage"]["limit (tonne)"];
|
||||
if (isNumeric(storLimit)) {
|
||||
resDict.storage["limit (tonne)"] = exportValue(storLimit);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const exportData = (original) => {
|
||||
const result = {
|
||||
parameters: {},
|
||||
products: {},
|
||||
plants: {},
|
||||
};
|
||||
|
||||
// Export parameters
|
||||
["time horizon (years)", "building period (years)"].forEach((key) => {
|
||||
result.parameters[key] = exportValue(original.parameters[key]);
|
||||
});
|
||||
|
||||
// Read time horizon
|
||||
let T = result.parameters["time horizon (years)"];
|
||||
if (!isNumeric(T)) T = 1;
|
||||
|
||||
// Export products
|
||||
for (const [prodName, prodDict] of Object.entries(original.products)) {
|
||||
result.products[prodName] = exportProduct(prodDict, T);
|
||||
}
|
||||
|
||||
// Export plants
|
||||
for (const [plantName, plantDict] of Object.entries(original.plants)) {
|
||||
result.plants[plantName] = exportPlant(plantDict, T);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const compressDisposalLimits = (original, result) => {
|
||||
if (!("disposal limit (tonne)" in original)) {
|
||||
return;
|
||||
}
|
||||
const total = computeTotalInitialAmount(original);
|
||||
const limit = original["disposal limit (tonne)"];
|
||||
let perc = Math.round((limit[0] / total[0]) * 1e6) / 1e6;
|
||||
for (let i = 1; i < limit.length; i++) {
|
||||
if (Math.abs(limit[i] / total[i] - perc) > 1e-5) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
result["disposal limit (tonne)"] = "";
|
||||
result["disposal limit (%)"] = String(perc * 100);
|
||||
};
|
||||
|
||||
export const importProduct = (original) => {
|
||||
const result = {};
|
||||
|
||||
result["initial amounts"] = { ...original["initial amounts"] };
|
||||
|
||||
// Initialize null values
|
||||
["x", "y"].forEach((key) => {
|
||||
result[key] = null;
|
||||
});
|
||||
|
||||
// Initialize empty values
|
||||
["disposal limit (%)"].forEach((key) => {
|
||||
result[key] = "";
|
||||
});
|
||||
|
||||
// Import lists
|
||||
[
|
||||
"transportation energy (J/km/tonne)",
|
||||
"transportation cost ($/km/tonne)",
|
||||
"disposal cost ($/tonne)",
|
||||
"disposal limit (tonne)",
|
||||
].forEach((key) => {
|
||||
result[key] = importList(original[key]);
|
||||
});
|
||||
|
||||
// Import dicts
|
||||
["transportation emissions (tonne/km/tonne)"].forEach((key) => {
|
||||
result[key] = importDict(original[key]);
|
||||
});
|
||||
|
||||
// Attempt to convert absolute disposal limits to relative
|
||||
compressDisposalLimits(original, result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const importPlant = (original) => {
|
||||
const result = {};
|
||||
|
||||
// Initialize null values
|
||||
["x", "y"].forEach((key) => {
|
||||
result[key] = null;
|
||||
});
|
||||
|
||||
// Import scalar values
|
||||
["input"].forEach((key) => {
|
||||
result[key] = original[key];
|
||||
});
|
||||
|
||||
// Import timeseries values
|
||||
["energy (GJ/tonne)"].forEach((key) => {
|
||||
result[key] = importList(original[key]);
|
||||
});
|
||||
|
||||
// Import dicts
|
||||
["outputs (tonne/tonne)", "emissions (tonne/tonne)"].forEach((key) => {
|
||||
result[key] = importDict(original[key]);
|
||||
});
|
||||
|
||||
// Read locations
|
||||
let costsInitialized = false;
|
||||
const resLocDict = (result.locations = {});
|
||||
for (const [locName, origLocDict] of Object.entries(original["locations"])) {
|
||||
resLocDict[locName] = {};
|
||||
|
||||
// Import latitude and longitude
|
||||
["latitude (deg)", "longitude (deg)"].forEach((key) => {
|
||||
resLocDict[locName][key] = origLocDict[key];
|
||||
});
|
||||
|
||||
const capacities = keysToList(origLocDict["capacities (tonne)"]);
|
||||
const last = capacities.length - 1;
|
||||
const minCap = capacities[0];
|
||||
const maxCap = capacities[last];
|
||||
const minCapDict = origLocDict["capacities (tonne)"][minCap];
|
||||
const maxCapDict = origLocDict["capacities (tonne)"][maxCap];
|
||||
|
||||
// Import min/max capacity
|
||||
if ("minimum capacity (tonne)" in result) {
|
||||
if (
|
||||
result["minimum capacity (tonne)"] !== minCap ||
|
||||
result["maximum capacity (tonne)"] !== maxCap
|
||||
) {
|
||||
throw "Data loss";
|
||||
}
|
||||
} else {
|
||||
result["minimum capacity (tonne)"] = minCap;
|
||||
result["maximum capacity (tonne)"] = maxCap;
|
||||
}
|
||||
|
||||
// Compute area cost factor
|
||||
let acf = 1;
|
||||
if (costsInitialized) {
|
||||
acf = result["opening cost (min capacity) ($)"];
|
||||
if (Array.isArray(acf)) acf = acf[0];
|
||||
acf = minCapDict["opening cost ($)"][0] / acf;
|
||||
}
|
||||
resLocDict[locName]["area cost factor"] = acf;
|
||||
|
||||
// Read adjusted costs
|
||||
const importListAcf = (obj) => importList(obj.map((v) => v / acf));
|
||||
const openCostMax = importListAcf(maxCapDict["opening cost ($)"]);
|
||||
const openCostMin = importListAcf(minCapDict["opening cost ($)"]);
|
||||
const fixCostMax = importListAcf(maxCapDict["fixed operating cost ($)"]);
|
||||
const fixCostMin = importListAcf(minCapDict["fixed operating cost ($)"]);
|
||||
const storCost = importListAcf(origLocDict.storage["cost ($/tonne)"]);
|
||||
const storLimit = String(origLocDict.storage["limit (tonne)"]);
|
||||
const varCost = importListAcf(
|
||||
minCapDict["variable operating cost ($/tonne)"]
|
||||
);
|
||||
|
||||
const dispCost = {};
|
||||
const dispLimit = {};
|
||||
for (const prodName of Object.keys(original["outputs (tonne/tonne)"])) {
|
||||
dispCost[prodName] = "";
|
||||
dispLimit[prodName] = "";
|
||||
|
||||
if (prodName in origLocDict["disposal"]) {
|
||||
const prodDict = origLocDict["disposal"][prodName];
|
||||
dispCost[prodName] = importListAcf(prodDict["cost ($/tonne)"]);
|
||||
if ("limit (tonne)" in prodDict)
|
||||
dispLimit[prodName] = importList(prodDict["limit (tonne)"]);
|
||||
}
|
||||
}
|
||||
|
||||
const check = (left, right) => {
|
||||
let valid = true;
|
||||
if (isNumeric(left) && isNumeric(right)) {
|
||||
valid = Math.abs(left - right) < 1.0;
|
||||
} else {
|
||||
valid = left === right;
|
||||
}
|
||||
if (!valid)
|
||||
console.warn(`Data loss detected: ${locName}, ${left} != ${right}`);
|
||||
};
|
||||
|
||||
if (costsInitialized) {
|
||||
// Verify that location costs match the previously initialized ones
|
||||
check(result["opening cost (max capacity) ($)"], openCostMax);
|
||||
check(result["opening cost (min capacity) ($)"], openCostMin);
|
||||
check(result["fixed operating cost (max capacity) ($)"], fixCostMax);
|
||||
check(result["fixed operating cost (min capacity) ($)"], fixCostMin);
|
||||
check(result["variable operating cost ($/tonne)"], varCost);
|
||||
check(result["storage"]["cost ($/tonne)"], storCost);
|
||||
check(result["storage"]["limit (tonne)"], storLimit);
|
||||
check(String(result["disposal cost ($/tonne)"]), String(dispCost));
|
||||
check(String(result["disposal limit (tonne)"]), String(dispLimit));
|
||||
} else {
|
||||
// Initialize plant costs
|
||||
costsInitialized = true;
|
||||
result["opening cost (max capacity) ($)"] = openCostMax;
|
||||
result["opening cost (min capacity) ($)"] = openCostMin;
|
||||
result["fixed operating cost (max capacity) ($)"] = fixCostMax;
|
||||
result["fixed operating cost (min capacity) ($)"] = fixCostMin;
|
||||
result["variable operating cost ($/tonne)"] = varCost;
|
||||
result["storage"] = {};
|
||||
result["storage"]["cost ($/tonne)"] = storCost;
|
||||
result["storage"]["limit (tonne)"] = storLimit;
|
||||
result["disposal cost ($/tonne)"] = dispCost;
|
||||
result["disposal limit (tonne)"] = dispLimit;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const importData = (original) => {
|
||||
["parameters", "plants", "products"].forEach((key) => {
|
||||
if (!(key in original)) {
|
||||
throw "File not recognized.";
|
||||
}
|
||||
});
|
||||
|
||||
const result = {};
|
||||
result.parameters = importDict(original.parameters);
|
||||
|
||||
// Import products
|
||||
result.products = {};
|
||||
for (const [prodName, origProdDict] of Object.entries(original.products)) {
|
||||
result.products[prodName] = importProduct(origProdDict);
|
||||
}
|
||||
|
||||
// Import plants
|
||||
result.plants = {};
|
||||
for (const [plantName, origPlantDict] of Object.entries(original.plants)) {
|
||||
result.plants[plantName] = importPlant(origPlantDict);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
@ -0,0 +1,564 @@
|
||||
import {
|
||||
exportProduct,
|
||||
exportPlant,
|
||||
importProduct,
|
||||
importList,
|
||||
importDict,
|
||||
importPlant,
|
||||
} from "./export";
|
||||
|
||||
const sampleProductsOriginal = [
|
||||
// basic product
|
||||
{
|
||||
"initial amounts": {
|
||||
"Washakie County": {
|
||||
"latitude (deg)": 43.8356,
|
||||
"longitude (deg)": -107.6602,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
"Platte County": {
|
||||
"latitude (deg)": 42.1314,
|
||||
"longitude (deg)": -104.9676,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
"Park County": {
|
||||
"latitude (deg)": 44.4063,
|
||||
"longitude (deg)": -109.4153,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
},
|
||||
"disposal cost ($/tonne)": "50",
|
||||
"disposal limit (tonne)": "30",
|
||||
"disposal limit (%)": "",
|
||||
"transportation cost ($/km/tonne)": "5",
|
||||
"transportation energy (J/km/tonne)": "10",
|
||||
"transportation emissions (tonne/km/tonne)": {
|
||||
CO2: "0.5",
|
||||
},
|
||||
x: null,
|
||||
y: null,
|
||||
},
|
||||
// product with percentage disposal limit
|
||||
{
|
||||
"initial amounts": {
|
||||
"Washakie County": {
|
||||
"latitude (deg)": 43.8356,
|
||||
"longitude (deg)": -107.6602,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
"Platte County": {
|
||||
"latitude (deg)": 42.1314,
|
||||
"longitude (deg)": -104.9676,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
"Park County": {
|
||||
"latitude (deg)": 44.4063,
|
||||
"longitude (deg)": -109.4153,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
},
|
||||
"disposal cost ($/tonne)": "50",
|
||||
"disposal limit (tonne)": "",
|
||||
"disposal limit (%)": "10",
|
||||
"transportation cost ($/km/tonne)": "5",
|
||||
"transportation energy (J/km/tonne)": "10",
|
||||
"transportation emissions (tonne/km/tonne)": {
|
||||
CO2: "0.5",
|
||||
},
|
||||
x: null,
|
||||
y: null,
|
||||
},
|
||||
// product using defaults
|
||||
{
|
||||
"initial amounts": {
|
||||
"Washakie County": {
|
||||
"latitude (deg)": 43.8356,
|
||||
"longitude (deg)": -107.6602,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
"Platte County": {
|
||||
"latitude (deg)": 42.1314,
|
||||
"longitude (deg)": -104.9676,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
"Park County": {
|
||||
"latitude (deg)": 44.4063,
|
||||
"longitude (deg)": -109.4153,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
},
|
||||
"disposal cost ($/tonne)": "50",
|
||||
"disposal limit (tonne)": "",
|
||||
"disposal limit (%)": "",
|
||||
"transportation cost ($/km/tonne)": "5",
|
||||
"transportation energy (J/km/tonne)": "",
|
||||
"transportation emissions (tonne/km/tonne)": {},
|
||||
x: null,
|
||||
y: null,
|
||||
},
|
||||
];
|
||||
|
||||
const sampleProductsExported = [
|
||||
// basic product
|
||||
{
|
||||
"initial amounts": {
|
||||
"Washakie County": {
|
||||
"latitude (deg)": 43.8356,
|
||||
"longitude (deg)": -107.6602,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
"Platte County": {
|
||||
"latitude (deg)": 42.1314,
|
||||
"longitude (deg)": -104.9676,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
"Park County": {
|
||||
"latitude (deg)": 44.4063,
|
||||
"longitude (deg)": -109.4153,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
},
|
||||
"disposal cost ($/tonne)": [50, 50, 50],
|
||||
"disposal limit (tonne)": [30, 30, 30],
|
||||
"transportation cost ($/km/tonne)": [5, 5, 5],
|
||||
"transportation energy (J/km/tonne)": [10, 10, 10],
|
||||
"transportation emissions (tonne/km/tonne)": {
|
||||
CO2: [0.5, 0.5, 0.5],
|
||||
},
|
||||
},
|
||||
// product with percentage disposal limit
|
||||
{
|
||||
"initial amounts": {
|
||||
"Washakie County": {
|
||||
"latitude (deg)": 43.8356,
|
||||
"longitude (deg)": -107.6602,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
"Platte County": {
|
||||
"latitude (deg)": 42.1314,
|
||||
"longitude (deg)": -104.9676,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
"Park County": {
|
||||
"latitude (deg)": 44.4063,
|
||||
"longitude (deg)": -109.4153,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
},
|
||||
"disposal cost ($/tonne)": [50, 50, 50],
|
||||
"disposal limit (tonne)": [30, 60, 90],
|
||||
"transportation cost ($/km/tonne)": [5, 5, 5],
|
||||
"transportation energy (J/km/tonne)": [10, 10, 10],
|
||||
"transportation emissions (tonne/km/tonne)": {
|
||||
CO2: [0.5, 0.5, 0.5],
|
||||
},
|
||||
},
|
||||
// product using defaults
|
||||
{
|
||||
"initial amounts": {
|
||||
"Washakie County": {
|
||||
"latitude (deg)": 43.8356,
|
||||
"longitude (deg)": -107.6602,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
"Platte County": {
|
||||
"latitude (deg)": 42.1314,
|
||||
"longitude (deg)": -104.9676,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
"Park County": {
|
||||
"latitude (deg)": 44.4063,
|
||||
"longitude (deg)": -109.4153,
|
||||
"amount (tonne)": [100, 200, 300],
|
||||
},
|
||||
},
|
||||
"disposal cost ($/tonne)": [50, 50, 50],
|
||||
"transportation cost ($/km/tonne)": [5, 5, 5],
|
||||
},
|
||||
];
|
||||
|
||||
const samplePlantsOriginal = [
|
||||
// basic plant
|
||||
{
|
||||
input: "Baled agricultural biomass",
|
||||
"outputs (tonne/tonne)": {
|
||||
"Hydrogen gas": 0.095,
|
||||
"Carbon dioxide": 1.164,
|
||||
Tar: 0.06,
|
||||
},
|
||||
"energy (GJ/tonne)": "50",
|
||||
locations: {
|
||||
"Washakie County": {
|
||||
"latitude (deg)": 43.8356,
|
||||
"longitude (deg)": -107.6602,
|
||||
"area cost factor": 1.0,
|
||||
},
|
||||
"Platte County": {
|
||||
"latitude (deg)": 42.1314,
|
||||
"longitude (deg)": -104.9676,
|
||||
"area cost factor": 0.5,
|
||||
},
|
||||
},
|
||||
"disposal cost ($/tonne)": {
|
||||
"Hydrogen gas": "0",
|
||||
"Carbon dioxide": "0",
|
||||
Tar: "200",
|
||||
},
|
||||
"disposal limit (tonne)": {
|
||||
"Hydrogen gas": "10",
|
||||
"Carbon dioxide": "",
|
||||
Tar: "",
|
||||
},
|
||||
"emissions (tonne/tonne)": {
|
||||
CO2: "100",
|
||||
},
|
||||
storage: {
|
||||
"cost ($/tonne)": "5",
|
||||
"limit (tonne)": "10000",
|
||||
},
|
||||
"maximum capacity (tonne)": "730000",
|
||||
"minimum capacity (tonne)": "182500",
|
||||
"opening cost (max capacity) ($)": "300000",
|
||||
"opening cost (min capacity) ($)": "200000",
|
||||
"fixed operating cost (max capacity) ($)": "7000",
|
||||
"fixed operating cost (min capacity) ($)": "5000",
|
||||
"variable operating cost ($/tonne)": "10",
|
||||
x: null,
|
||||
y: null,
|
||||
},
|
||||
// plant with fixed capacity
|
||||
{
|
||||
input: "Baled agricultural biomass",
|
||||
"outputs (tonne/tonne)": {
|
||||
"Hydrogen gas": 0.095,
|
||||
"Carbon dioxide": 1.164,
|
||||
Tar: 0.06,
|
||||
},
|
||||
"energy (GJ/tonne)": "50",
|
||||
locations: {
|
||||
"Washakie County": {
|
||||
"latitude (deg)": 43.8356,
|
||||
"longitude (deg)": -107.6602,
|
||||
"area cost factor": 1.0,
|
||||
},
|
||||
"Platte County": {
|
||||
"latitude (deg)": 42.1314,
|
||||
"longitude (deg)": -104.9676,
|
||||
"area cost factor": 0.5,
|
||||
},
|
||||
},
|
||||
"disposal cost ($/tonne)": {
|
||||
"Hydrogen gas": "0",
|
||||
"Carbon dioxide": "0",
|
||||
Tar: "200",
|
||||
},
|
||||
"disposal limit (tonne)": {
|
||||
"Hydrogen gas": "10",
|
||||
"Carbon dioxide": "",
|
||||
Tar: "",
|
||||
},
|
||||
"emissions (tonne/tonne)": {
|
||||
CO2: "100",
|
||||
},
|
||||
storage: {
|
||||
"cost ($/tonne)": "5",
|
||||
"limit (tonne)": "10000",
|
||||
},
|
||||
"maximum capacity (tonne)": "182500",
|
||||
"minimum capacity (tonne)": "182500",
|
||||
"opening cost (max capacity) ($)": "200000",
|
||||
"opening cost (min capacity) ($)": "200000",
|
||||
"fixed operating cost (max capacity) ($)": "5000",
|
||||
"fixed operating cost (min capacity) ($)": "5000",
|
||||
"variable operating cost ($/tonne)": "10",
|
||||
x: null,
|
||||
y: null,
|
||||
},
|
||||
// plant with defaults
|
||||
{
|
||||
input: "Baled agricultural biomass",
|
||||
"outputs (tonne/tonne)": {
|
||||
"Hydrogen gas": 0.095,
|
||||
"Carbon dioxide": 1.164,
|
||||
Tar: 0.06,
|
||||
},
|
||||
"energy (GJ/tonne)": "50",
|
||||
locations: {
|
||||
"Washakie County": {
|
||||
"latitude (deg)": 43.8356,
|
||||
"longitude (deg)": -107.6602,
|
||||
"area cost factor": 1.0,
|
||||
},
|
||||
"Platte County": {
|
||||
"latitude (deg)": 42.1314,
|
||||
"longitude (deg)": -104.9676,
|
||||
"area cost factor": 0.5,
|
||||
},
|
||||
},
|
||||
"disposal cost ($/tonne)": {
|
||||
"Hydrogen gas": "",
|
||||
"Carbon dioxide": "",
|
||||
Tar: "",
|
||||
},
|
||||
"disposal limit (tonne)": {
|
||||
"Hydrogen gas": "",
|
||||
"Carbon dioxide": "",
|
||||
Tar: "",
|
||||
},
|
||||
"emissions (tonne/tonne)": {
|
||||
CO2: "100",
|
||||
},
|
||||
storage: {
|
||||
"cost ($/tonne)": "5",
|
||||
"limit (tonne)": "10000",
|
||||
},
|
||||
"maximum capacity (tonne)": "730000",
|
||||
"minimum capacity (tonne)": "182500",
|
||||
"opening cost (max capacity) ($)": "300000",
|
||||
"opening cost (min capacity) ($)": "200000",
|
||||
"fixed operating cost (max capacity) ($)": "7000",
|
||||
"fixed operating cost (min capacity) ($)": "5000",
|
||||
"variable operating cost ($/tonne)": "10",
|
||||
x: null,
|
||||
y: null,
|
||||
},
|
||||
];
|
||||
|
||||
const samplePlantsExported = [
|
||||
//basic plant
|
||||
{
|
||||
input: "Baled agricultural biomass",
|
||||
"outputs (tonne/tonne)": {
|
||||
"Hydrogen gas": 0.095,
|
||||
"Carbon dioxide": 1.164,
|
||||
Tar: 0.06,
|
||||
},
|
||||
"energy (GJ/tonne)": [50, 50, 50],
|
||||
locations: {
|
||||
"Washakie County": {
|
||||
"latitude (deg)": 43.8356,
|
||||
"longitude (deg)": -107.6602,
|
||||
disposal: {
|
||||
"Hydrogen gas": {
|
||||
"cost ($/tonne)": [0, 0, 0],
|
||||
"limit (tonne)": [10, 10, 10],
|
||||
},
|
||||
"Carbon dioxide": {
|
||||
"cost ($/tonne)": [0, 0, 0],
|
||||
},
|
||||
Tar: {
|
||||
"cost ($/tonne)": [200.0, 200.0, 200.0],
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
"cost ($/tonne)": [5, 5, 5],
|
||||
"limit (tonne)": 10000,
|
||||
},
|
||||
"capacities (tonne)": {
|
||||
182500: {
|
||||
"opening cost ($)": [200000, 200000, 200000],
|
||||
"fixed operating cost ($)": [5000, 5000, 5000],
|
||||
"variable operating cost ($/tonne)": [10, 10, 10],
|
||||
},
|
||||
730000: {
|
||||
"opening cost ($)": [300000, 300000, 300000],
|
||||
"fixed operating cost ($)": [7000, 7000, 7000],
|
||||
"variable operating cost ($/tonne)": [10, 10, 10],
|
||||
},
|
||||
},
|
||||
},
|
||||
"Platte County": {
|
||||
"latitude (deg)": 42.1314,
|
||||
"longitude (deg)": -104.9676,
|
||||
disposal: {
|
||||
"Hydrogen gas": {
|
||||
"cost ($/tonne)": [0, 0, 0],
|
||||
"limit (tonne)": [10, 10, 10],
|
||||
},
|
||||
"Carbon dioxide": {
|
||||
"cost ($/tonne)": [0, 0, 0],
|
||||
},
|
||||
Tar: {
|
||||
"cost ($/tonne)": [100.0, 100.0, 100.0],
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
"cost ($/tonne)": [2.5, 2.5, 2.5],
|
||||
"limit (tonne)": 10000,
|
||||
},
|
||||
"capacities (tonne)": {
|
||||
182500: {
|
||||
"opening cost ($)": [100000, 100000, 100000],
|
||||
"fixed operating cost ($)": [2500, 2500, 2500],
|
||||
"variable operating cost ($/tonne)": [5, 5, 5],
|
||||
},
|
||||
730000: {
|
||||
"opening cost ($)": [150000, 150000, 150000],
|
||||
"fixed operating cost ($)": [3500, 3500, 3500],
|
||||
"variable operating cost ($/tonne)": [5, 5, 5],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"emissions (tonne/tonne)": {
|
||||
CO2: [100, 100, 100],
|
||||
},
|
||||
},
|
||||
// plant with fixed capacity
|
||||
{
|
||||
input: "Baled agricultural biomass",
|
||||
"outputs (tonne/tonne)": {
|
||||
"Hydrogen gas": 0.095,
|
||||
"Carbon dioxide": 1.164,
|
||||
Tar: 0.06,
|
||||
},
|
||||
"energy (GJ/tonne)": [50, 50, 50],
|
||||
locations: {
|
||||
"Washakie County": {
|
||||
"latitude (deg)": 43.8356,
|
||||
"longitude (deg)": -107.6602,
|
||||
disposal: {
|
||||
"Hydrogen gas": {
|
||||
"cost ($/tonne)": [0, 0, 0],
|
||||
"limit (tonne)": [10, 10, 10],
|
||||
},
|
||||
"Carbon dioxide": {
|
||||
"cost ($/tonne)": [0, 0, 0],
|
||||
},
|
||||
Tar: {
|
||||
"cost ($/tonne)": [200.0, 200.0, 200.0],
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
"cost ($/tonne)": [5, 5, 5],
|
||||
"limit (tonne)": 10000,
|
||||
},
|
||||
"capacities (tonne)": {
|
||||
182500: {
|
||||
"opening cost ($)": [200000, 200000, 200000],
|
||||
"fixed operating cost ($)": [5000, 5000, 5000],
|
||||
"variable operating cost ($/tonne)": [10, 10, 10],
|
||||
},
|
||||
},
|
||||
},
|
||||
"Platte County": {
|
||||
"latitude (deg)": 42.1314,
|
||||
"longitude (deg)": -104.9676,
|
||||
disposal: {
|
||||
"Hydrogen gas": {
|
||||
"cost ($/tonne)": [0, 0, 0],
|
||||
"limit (tonne)": [10, 10, 10],
|
||||
},
|
||||
"Carbon dioxide": {
|
||||
"cost ($/tonne)": [0, 0, 0],
|
||||
},
|
||||
Tar: {
|
||||
"cost ($/tonne)": [100.0, 100.0, 100.0],
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
"cost ($/tonne)": [2.5, 2.5, 2.5],
|
||||
"limit (tonne)": 10000,
|
||||
},
|
||||
"capacities (tonne)": {
|
||||
182500: {
|
||||
"opening cost ($)": [100000, 100000, 100000],
|
||||
"fixed operating cost ($)": [2500, 2500, 2500],
|
||||
"variable operating cost ($/tonne)": [5, 5, 5],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"emissions (tonne/tonne)": {
|
||||
CO2: [100, 100, 100],
|
||||
},
|
||||
},
|
||||
// plant with defaults
|
||||
{
|
||||
input: "Baled agricultural biomass",
|
||||
"outputs (tonne/tonne)": {
|
||||
"Hydrogen gas": 0.095,
|
||||
"Carbon dioxide": 1.164,
|
||||
Tar: 0.06,
|
||||
},
|
||||
"energy (GJ/tonne)": [50, 50, 50],
|
||||
locations: {
|
||||
"Washakie County": {
|
||||
"latitude (deg)": 43.8356,
|
||||
"longitude (deg)": -107.6602,
|
||||
disposal: {},
|
||||
storage: {
|
||||
"cost ($/tonne)": [5, 5, 5],
|
||||
"limit (tonne)": 10000,
|
||||
},
|
||||
"capacities (tonne)": {
|
||||
182500: {
|
||||
"opening cost ($)": [200000, 200000, 200000],
|
||||
"fixed operating cost ($)": [5000, 5000, 5000],
|
||||
"variable operating cost ($/tonne)": [10, 10, 10],
|
||||
},
|
||||
730000: {
|
||||
"opening cost ($)": [300000, 300000, 300000],
|
||||
"fixed operating cost ($)": [7000, 7000, 7000],
|
||||
"variable operating cost ($/tonne)": [10, 10, 10],
|
||||
},
|
||||
},
|
||||
},
|
||||
"Platte County": {
|
||||
"latitude (deg)": 42.1314,
|
||||
"longitude (deg)": -104.9676,
|
||||
disposal: {},
|
||||
storage: {
|
||||
"cost ($/tonne)": [2.5, 2.5, 2.5],
|
||||
"limit (tonne)": 10000,
|
||||
},
|
||||
"capacities (tonne)": {
|
||||
182500: {
|
||||
"opening cost ($)": [100000, 100000, 100000],
|
||||
"fixed operating cost ($)": [2500, 2500, 2500],
|
||||
"variable operating cost ($/tonne)": [5, 5, 5],
|
||||
},
|
||||
730000: {
|
||||
"opening cost ($)": [150000, 150000, 150000],
|
||||
"fixed operating cost ($)": [3500, 3500, 3500],
|
||||
"variable operating cost ($/tonne)": [5, 5, 5],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"emissions (tonne/tonne)": {
|
||||
CO2: [100, 100, 100],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test("export products", () => {
|
||||
for (let i = 0; i < sampleProductsOriginal.length; i++) {
|
||||
const original = sampleProductsOriginal[i];
|
||||
const exported = sampleProductsExported[i];
|
||||
expect(exportProduct(original, 3)).toEqual(exported);
|
||||
expect(importProduct(exported)).toEqual(original);
|
||||
}
|
||||
});
|
||||
|
||||
test("export plants", () => {
|
||||
for (let i = 0; i < samplePlantsOriginal.length; i++) {
|
||||
const original = samplePlantsOriginal[i];
|
||||
const exported = samplePlantsExported[i];
|
||||
expect(exportPlant(original, 3)).toEqual(exported);
|
||||
expect(importPlant(exported)).toEqual(original);
|
||||
}
|
||||
});
|
||||
|
||||
test("importList", () => {
|
||||
expect(importList("invalid")).toEqual("invalid");
|
||||
expect(importList([1, 1, 1])).toEqual("1");
|
||||
expect(importList([1, 2, 3])).toEqual("[1,2,3]");
|
||||
expect(importList(["A", "A", "A"])).toEqual("A");
|
||||
});
|
||||
|
||||
test("importDict", () => {
|
||||
expect(importDict({ a: [5, 5, 5] })).toEqual({ a: "5" });
|
||||
expect(importDict({ a: [1, 2, 3] })).toEqual({ a: "[1,2,3]" });
|
||||
expect(importDict({ a: "invalid" })).toEqual({ a: "invalid" });
|
||||
});
|
@ -1,52 +1,52 @@
|
||||
:root {
|
||||
--site-width: 1200px;
|
||||
--box-border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
--box-shadow: 0px 2px 4px -3px rgba(0, 0, 0, 0.2);
|
||||
--border-radius: 4px;
|
||||
--site-width: 1200px;
|
||||
--box-border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
--box-shadow: 0px 2px 4px -3px rgba(0, 0, 0, 0.2);
|
||||
--border-radius: 4px;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
color: rgba(0, 0, 0, 0.95);
|
||||
background-color: #f6f6f6;
|
||||
color: rgba(0, 0, 0, 0.95);
|
||||
}
|
||||
|
||||
#content {
|
||||
max-width: var(--site-width);
|
||||
min-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 0 6px;
|
||||
max-width: var(--site-width);
|
||||
min-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.react-flow__node.selected {
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2) !important;
|
||||
border-width: 2px !important;
|
||||
margin-top: -1px !important;
|
||||
margin-left: -1px !important;
|
||||
border-radius: 8px !important;
|
||||
font-weight: bold;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2) !important;
|
||||
border-width: 2px !important;
|
||||
margin-top: -1px !important;
|
||||
margin-left: -1px !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.react-flow__handle {
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
background-color: white !important;
|
||||
border: 1px solid black !important;
|
||||
width: 6px !important;
|
||||
height: 6px !important;
|
||||
background-color: white !important;
|
||||
border: 1px solid black !important;
|
||||
}
|
||||
|
||||
.react-flow__handle:hover {
|
||||
background-color: black !important;
|
||||
background-color: black !important;
|
||||
}
|
||||
|
||||
.react-flow__handle-right {
|
||||
right: -5px !important;
|
||||
right: -4px !important;
|
||||
}
|
||||
|
||||
.react-flow__handle-left {
|
||||
left: -5px !important;
|
||||
left: -4px !important;
|
||||
}
|
@ -1,17 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import Header from './Header';
|
||||
import InputPage from './InputPage';
|
||||
import Footer from './Footer';
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./index.css";
|
||||
import InputPage from "./InputPage";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Header />
|
||||
<div id="content">
|
||||
<InputPage />
|
||||
</div>
|
||||
<Footer />
|
||||
<InputPage />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
Loading…
Reference in new issue