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) => {
|
const Button = (props) => {
|
||||||
let className = styles.Button;
|
let className = styles.Button;
|
||||||
if (props.kind === "inline") {
|
if (props.kind === "inline") {
|
||||||
className += " " + styles.inline;
|
className += " " + styles.inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tooltip = "";
|
let tooltip = "";
|
||||||
if (props.tooltip != undefined) {
|
if (props.tooltip !== undefined) {
|
||||||
tooltip = <span className={styles.tooltip}>{props.tooltip}</span>;
|
tooltip = <span className={styles.tooltip}>{props.tooltip}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={className} onClick={props.onClick}>
|
<button className={className} onClick={props.onClick}>
|
||||||
{tooltip}
|
{tooltip}
|
||||||
{props.label}
|
{props.label}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default 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) => {
|
const Card = (props) => {
|
||||||
return (<div className={styles.Card}>{props.children}</div>);
|
return <div className={styles.Card}>{props.children}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Card;
|
export default Card;
|
@ -1,22 +1,22 @@
|
|||||||
.Card {
|
.Card {
|
||||||
border: var(--box-border);
|
border: var(--box-border);
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Card h1 {
|
.Card h1 {
|
||||||
margin: 12px -12px 0px -12px;
|
margin: 12px -12px 0px -12px;
|
||||||
padding: 6px 12px 0px 12px;
|
padding: 6px 12px 0px 12px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 35px;
|
line-height: 35px;
|
||||||
border-top: 1px solid #ddd;
|
border-top: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Card h1:first-child {
|
.Card h1:first-child {
|
||||||
margin: -12px -12px 0px -12px;
|
margin: -12px -12px 0px -12px;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
@ -1,87 +1,91 @@
|
|||||||
import form_styles from './Form.module.css';
|
import form_styles from "./Form.module.css";
|
||||||
import Button from './Button';
|
import Button from "./Button";
|
||||||
import { validate } from './Form';
|
import { validate } from "./Form";
|
||||||
|
|
||||||
const DictInputRow = (props) => {
|
const DictInputRow = (props) => {
|
||||||
const dict = { ...props.value };
|
const dict = { ...props.value };
|
||||||
if (!props.disableKeys) {
|
if (!props.disableKeys) {
|
||||||
dict[""] = "0";
|
dict[""] = "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
let unit = "";
|
let unit = "";
|
||||||
if (props.unit) {
|
if (props.unit) {
|
||||||
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
|
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tooltip = "";
|
let tooltip = "";
|
||||||
if (props.tooltip != undefined) {
|
if (props.tooltip !== undefined) {
|
||||||
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeValue = (key, v) => {
|
const onChangeValue = (key, v) => {
|
||||||
const newDict = { ...dict };
|
const newDict = { ...dict };
|
||||||
newDict[key] = v;
|
newDict[key] = v;
|
||||||
props.onChange(newDict);
|
props.onChange(newDict);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeKey = (prevKey, newKey) => {
|
const onChangeKey = (prevKey, newKey) => {
|
||||||
const newDict = renameKey(dict, prevKey, newKey);
|
const newDict = renameKey(dict, prevKey, newKey);
|
||||||
if (!("" in newDict)) newDict[""] = "";
|
if (!("" in newDict)) newDict[""] = "";
|
||||||
props.onChange(newDict);
|
props.onChange(newDict);
|
||||||
};
|
};
|
||||||
|
|
||||||
const form = [];
|
const form = [];
|
||||||
Object.keys(dict).forEach((key, index) => {
|
Object.keys(dict).forEach((key, index) => {
|
||||||
let label = <span>{props.label} {unit}</span>;
|
let label = (
|
||||||
if (index > 0) {
|
<span>
|
||||||
label = "";
|
{props.label} {unit}
|
||||||
}
|
</span>
|
||||||
|
);
|
||||||
|
if (index > 0) {
|
||||||
|
label = "";
|
||||||
|
}
|
||||||
|
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
if (props.validate !== undefined) {
|
if (props.validate !== undefined) {
|
||||||
isValid = validate(props.validate, dict[key]);
|
isValid = validate(props.validate, dict[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let className = "";
|
let className = "";
|
||||||
if (!isValid) className = form_styles.invalid;
|
if (!isValid) className = form_styles.invalid;
|
||||||
|
|
||||||
form.push(
|
form.push(
|
||||||
<div className={form_styles.FormRow} key={index}>
|
<div className={form_styles.FormRow} key={index}>
|
||||||
<label>{label}</label>
|
<label>{label}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
data-index={index}
|
data-index={index}
|
||||||
value={key}
|
value={key}
|
||||||
placeholder={props.keyPlaceholder}
|
placeholder={props.keyPlaceholder}
|
||||||
disabled={props.disableKeys}
|
disabled={props.disableKeys}
|
||||||
onChange={e => onChangeKey(key, e.target.value)}
|
onChange={(e) => onChangeKey(key, e.target.value)}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
data-index={index}
|
data-index={index}
|
||||||
value={dict[key]}
|
value={dict[key]}
|
||||||
placeholder={props.valuePlaceholder}
|
placeholder={props.valuePlaceholder}
|
||||||
className={className}
|
className={className}
|
||||||
onChange={e => onChangeValue(key, e.target.value)}
|
onChange={(e) => onChangeValue(key, e.target.value)}
|
||||||
/>
|
/>
|
||||||
{tooltip}
|
{tooltip}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return <>{form}</>;
|
return <>{form}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function renameKey(obj, prevKey, newKey) {
|
export function renameKey(obj, prevKey, newKey) {
|
||||||
const keys = Object.keys(obj);
|
const keys = Object.keys(obj);
|
||||||
return keys.reduce((acc, val) => {
|
return keys.reduce((acc, val) => {
|
||||||
if (val === prevKey) {
|
if (val === prevKey) {
|
||||||
acc[newKey] = obj[prevKey];
|
acc[newKey] = obj[prevKey];
|
||||||
} else {
|
} else {
|
||||||
acc[val] = obj[val];
|
acc[val] = obj[val];
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DictInputRow;
|
export default DictInputRow;
|
@ -1,64 +1,49 @@
|
|||||||
import form_styles from './Form.module.css';
|
import form_styles from "./Form.module.css";
|
||||||
import Button from './Button';
|
import Button from "./Button";
|
||||||
import { useRef } from 'react';
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
|
||||||
const FileInputRow = (props) => {
|
const FileInputRow = (props) => {
|
||||||
let tooltip = "";
|
let tooltip = "";
|
||||||
if (props.tooltip != undefined) {
|
if (props.tooltip !== undefined) {
|
||||||
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileElem = useRef();
|
const fileElem = useRef();
|
||||||
|
|
||||||
const onClickUpload = () => {
|
const onClickUpload = () => {
|
||||||
fileElem.current.click();
|
fileElem.current.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFileSelected = () => {
|
const onFileSelected = () => {
|
||||||
const file = fileElem.current.files[0];
|
const file = fileElem.current.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.addEventListener("load", () => {
|
reader.addEventListener("load", () => {
|
||||||
props.onFile(reader.result);
|
props.onFile(reader.result);
|
||||||
});
|
});
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}
|
}
|
||||||
fileElem.current.value = "";
|
fileElem.current.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className={form_styles.FormRow}>
|
return (
|
||||||
<label>{props.label}</label>
|
<div className={form_styles.FormRow}>
|
||||||
<input type="text" value={props.value} disabled="disabled" />
|
<label>{props.label}</label>
|
||||||
<Button
|
<input type="text" value={props.value} disabled="disabled" />
|
||||||
label="Upload"
|
<Button label="Upload" kind="inline" onClick={onClickUpload} />
|
||||||
kind="inline"
|
<Button label="Download" kind="inline" onClick={props.onDownload} />
|
||||||
onClick={onClickUpload}
|
<Button label="Clear" kind="inline" onClick={props.onClear} />
|
||||||
/>
|
<Button label="Template" kind="inline" onClick={props.onTemplate} />
|
||||||
<Button
|
{tooltip}
|
||||||
label="Download"
|
<input
|
||||||
kind="inline"
|
type="file"
|
||||||
onClick={props.onDownload}
|
ref={fileElem}
|
||||||
/>
|
accept={props.accept}
|
||||||
<Button
|
style={{ display: "none" }}
|
||||||
label="Clear"
|
onChange={onFileSelected}
|
||||||
kind="inline"
|
/>
|
||||||
onClick={props.onClear}
|
</div>
|
||||||
/>
|
);
|
||||||
<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;
|
export default FileInputRow;
|
@ -1,10 +1,15 @@
|
|||||||
import styles from './Footer.module.css';
|
import styles from "./Footer.module.css";
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
return <div className={styles.Footer}>
|
return (
|
||||||
<p>RELOG: Reverse Logistics Optimization</p>
|
<div className={styles.Footer}>
|
||||||
<p>Copyright © 2020—2022, UChicago Argonne, LLC. All Rights Reserved.</p>
|
<p>RELOG: Reverse Logistics Optimization</p>
|
||||||
</div>;
|
<p>
|
||||||
|
Copyright © 2020—2022, UChicago Argonne, LLC. All Rights
|
||||||
|
Reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
@ -1,11 +1,10 @@
|
|||||||
.Footer {
|
.Footer {
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 8px;
|
line-height: 8px;
|
||||||
min-width: 900px;
|
min-width: 900px;
|
||||||
|
|
||||||
}
|
}
|
@ -1,19 +1,19 @@
|
|||||||
const VALIDATION_REGEX = {
|
const VALIDATION_REGEX = {
|
||||||
int: new RegExp('^[0-9]+$'),
|
int: new RegExp("^[0-9]+$"),
|
||||||
intList: new RegExp('^\[[0-9]+(,[0-9]+)*\]$'),
|
intList: new RegExp("[[0-9]*]$"),
|
||||||
float: new RegExp('^[0-9]*\\.?[0-9]*$'),
|
float: new RegExp("^[0-9]*\\.?[0-9]*$"),
|
||||||
|
floatList: new RegExp("^[?[0-9,.]*]?$"),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validate = (kind, value) => {
|
export const validate = (kind, value) => {
|
||||||
if (value.length == 0) return false;
|
if (!VALIDATION_REGEX[kind].test(value)) {
|
||||||
if (!VALIDATION_REGEX[kind].test(value)) {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
return true;
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Form = (props) => {
|
const Form = (props) => {
|
||||||
return <>{props.children}</>;
|
return <>{props.children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Form;
|
export default Form;
|
@ -1,28 +1,28 @@
|
|||||||
.FormRow {
|
.FormRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.FormRow label {
|
.FormRow label {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.FormRow input {
|
.FormRow input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
border: var(--box-border);
|
border: var(--box-border);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
margin: 2px 3px;
|
margin: 2px 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.FormRow_unit {
|
.FormRow_unit {
|
||||||
color: rgba(0, 0, 0, 0.4);
|
color: rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.invalid {
|
.invalid {
|
||||||
border: 2px solid #faa !important;
|
border: 2px solid #faa !important;
|
||||||
background-color: rgba(255, 0, 0, 0.05);
|
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 = () => {
|
const Header = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.HeaderBox}>
|
<div className={styles.HeaderBox}>
|
||||||
<div className={styles.HeaderContent}>
|
<div className={styles.HeaderContent}>
|
||||||
<h1>RELOG</h1>
|
<h1>RELOG</h1>
|
||||||
</div>
|
<h2>{props.title}</h2>
|
||||||
|
<div style={{ float: "right", paddingTop: "5px" }}>
|
||||||
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
@ -1,19 +1,28 @@
|
|||||||
.HeaderBox {
|
.HeaderBox {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-bottom: var(--box-border);
|
border-bottom: var(--box-border);
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.HeaderContent {
|
.HeaderContent {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: var(--site-width);
|
max-width: var(--site-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.HeaderContent h1 {
|
.HeaderContent h1,
|
||||||
line-height: 48px;
|
.HeaderContent h2 {
|
||||||
font-size: 28px;
|
line-height: 48px;
|
||||||
padding: 12px;
|
font-size: 28px;
|
||||||
margin: 0;
|
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 React, { useState, useRef } from "react";
|
||||||
|
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
import PipelineBlock from './PipelineBlock';
|
import PipelineBlock from "./PipelineBlock";
|
||||||
import ParametersBlock from './ParametersBlock';
|
import ParametersBlock from "./ParametersBlock";
|
||||||
import ProductBlock from './ProductBlock';
|
import ProductBlock from "./ProductBlock";
|
||||||
import PlantBlock from './PlantBlock';
|
import PlantBlock from "./PlantBlock";
|
||||||
import ButtonRow from './ButtonRow';
|
import Button from "./Button";
|
||||||
import Button from './Button';
|
import Header from "./Header";
|
||||||
|
import Footer from "./Footer";
|
||||||
const defaultData = {
|
import { defaultData, defaultPlant, defaultProduct } from "./defaults";
|
||||||
parameters: {
|
import { randomPosition } from "./PipelineBlock";
|
||||||
"time horizon (years)": "1",
|
import { exportData, importData } from "./export";
|
||||||
"building period (years)": "[1]",
|
import { generateFile } from "./csv";
|
||||||
"annual inflation rate (%)": "0",
|
|
||||||
},
|
const setDefaults = (actualDict, defaultDict) => {
|
||||||
products: {
|
for (const [key, defaultValue] of Object.entries(defaultDict)) {
|
||||||
},
|
if (!(key in actualDict)) {
|
||||||
plants: {
|
if (typeof defaultValue === "object") {
|
||||||
|
actualDict[key] = { ...defaultValue };
|
||||||
|
} else {
|
||||||
|
actualDict[key] = defaultValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProduct = {
|
const cleanDict = (dict, defaultDict) => {
|
||||||
"initial amounts": {},
|
for (const key of Object.keys(dict)) {
|
||||||
"acquisition cost ($/tonne)": "0",
|
if (!(key in defaultDict)) {
|
||||||
"disposal cost ($/tonne)": "0",
|
delete dict[key];
|
||||||
"disposal limit (tonne)": "0",
|
}
|
||||||
"transportation cost ($/km/tonne)": "0",
|
}
|
||||||
"transportation energy (J/km/tonne)": "0",
|
|
||||||
"transportation emissions (J/km/tonne)": {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const randomPosition = () => {
|
const fixLists = (dict, blacklist, stringify) => {
|
||||||
return Math.round(Math.random() * 30) * 15;
|
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 = () => {
|
const InputPage = () => {
|
||||||
let [data, setData] = useState(defaultData);
|
const fileElem = useRef();
|
||||||
|
|
||||||
// onAdd
|
let savedData = JSON.parse(localStorage.getItem("data"));
|
||||||
// ------------------------------------------------------------------------
|
if (!savedData) savedData = defaultData;
|
||||||
const promptName = (prevData) => {
|
|
||||||
const name = prompt("Name");
|
let [data, setData] = useState(savedData);
|
||||||
if (!name || name.length == 0) return;
|
|
||||||
if (name in prevData.products || name in prevData.plants) return;
|
const save = (data) => {
|
||||||
return name;
|
localStorage.setItem("data", JSON.stringify(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddPlant = () => {
|
const promptName = (prevData) => {
|
||||||
setData((prevData) => {
|
const name = prompt("Name");
|
||||||
const name = promptName(prevData);
|
if (!name || name.length === 0) return;
|
||||||
if (name === undefined) return prevData;
|
if (name in prevData.products || name in prevData.plants) return;
|
||||||
const newData = { ...prevData };
|
return name;
|
||||||
newData.plants[name] = {
|
};
|
||||||
x: randomPosition(),
|
|
||||||
y: randomPosition(),
|
const onAddPlant = () => {
|
||||||
outputs: {},
|
setData((prevData) => {
|
||||||
};
|
const name = promptName(prevData);
|
||||||
return newData;
|
if (name === undefined) return prevData;
|
||||||
});
|
const newData = { ...prevData };
|
||||||
};
|
const [x, y] = randomPosition();
|
||||||
|
newData.plants[name] = {
|
||||||
const onAddProduct = () => {
|
...defaultPlant,
|
||||||
setData((prevData) => {
|
x: x,
|
||||||
const name = promptName(prevData);
|
y: y,
|
||||||
if (name === undefined) return prevData;
|
};
|
||||||
const newData = { ...prevData };
|
save(newData);
|
||||||
newData.products[name] = {
|
return newData;
|
||||||
...defaultProduct,
|
});
|
||||||
x: randomPosition(),
|
};
|
||||||
y: randomPosition(),
|
|
||||||
};
|
const onAddProduct = () => {
|
||||||
return newData;
|
setData((prevData) => {
|
||||||
});
|
const name = promptName(prevData);
|
||||||
};
|
if (name === undefined) return prevData;
|
||||||
|
const newData = { ...prevData };
|
||||||
// onRename
|
const [x, y] = randomPosition();
|
||||||
// ------------------------------------------------------------------------
|
console.log(x, y);
|
||||||
const onRenamePlant = (prevName, newName) => {
|
newData.products[name] = {
|
||||||
setData((prevData) => {
|
...defaultProduct,
|
||||||
const newData = { ...prevData };
|
x: x,
|
||||||
newData.plants[newName] = newData.plants[prevName];
|
y: y,
|
||||||
delete newData.plants[prevName];
|
};
|
||||||
return newData;
|
save(newData);
|
||||||
});
|
return newData;
|
||||||
};
|
});
|
||||||
|
};
|
||||||
const onRenameProduct = (prevName, newName) => {
|
|
||||||
setData((prevData) => {
|
const onRenamePlant = (prevName, newName) => {
|
||||||
const newData = { ...prevData };
|
setData((prevData) => {
|
||||||
newData.products[newName] = newData.products[prevName];
|
const newData = { ...prevData };
|
||||||
delete newData.products[prevName];
|
newData.plants[newName] = newData.plants[prevName];
|
||||||
for (const [plantName, plant] of Object.entries(newData.plants)) {
|
delete newData.plants[prevName];
|
||||||
if (plant.input == prevName) {
|
save(newData);
|
||||||
plant.input = newName;
|
return newData;
|
||||||
}
|
});
|
||||||
let outputFound = false;
|
};
|
||||||
for (const [outputName, outputValue] of Object.entries(plant.outputs)) {
|
|
||||||
if (outputName == prevName) outputFound = true;
|
|
||||||
}
|
|
||||||
if (outputFound) {
|
|
||||||
plant.outputs[newName] = plant.outputs[prevName];
|
|
||||||
delete plant.outputs[prevName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// onMove
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
const onMovePlant = (plantName, x, y) => {
|
|
||||||
setData((prevData) => {
|
|
||||||
const newData = { ...prevData };
|
|
||||||
newData.plants[plantName].x = x;
|
|
||||||
newData.plants[plantName].y = y;
|
|
||||||
return newData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMoveProduct = (productName, x, y) => {
|
|
||||||
setData((prevData) => {
|
|
||||||
const newData = { ...prevData };
|
|
||||||
newData.products[productName].x = x;
|
|
||||||
newData.products[productName].y = y;
|
|
||||||
return newData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// onRemove
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
const onRemovePlant = (plantName) => {
|
|
||||||
setData((prevData) => {
|
|
||||||
const newData = { ...prevData };
|
|
||||||
delete newData.plants[plantName];
|
|
||||||
return newData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemoveProduct = (productName) => {
|
|
||||||
setData((prevData) => {
|
|
||||||
const newData = { ...prevData };
|
|
||||||
delete newData.products[productName];
|
|
||||||
for (const [plantName, plant] of Object.entries(newData.plants)) {
|
|
||||||
if (plant.input == productName) {
|
|
||||||
delete plant.input;
|
|
||||||
}
|
|
||||||
let outputFound = false;
|
|
||||||
for (const [outputName, outputValue] of Object.entries(plant.outputs)) {
|
|
||||||
if (outputName == productName) outputFound = true;
|
|
||||||
}
|
|
||||||
if (outputFound) {
|
|
||||||
delete plant.outputs[productName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Inputs & Outputs
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
const onSetPlantInput = (plantName, productName) => {
|
|
||||||
setData((prevData) => {
|
|
||||||
const newData = { ...prevData };
|
|
||||||
newData.plants[plantName].input = productName;
|
|
||||||
return newData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAddPlantOutput = (plantName, productName) => {
|
|
||||||
setData((prevData) => {
|
|
||||||
if (productName in prevData.plants[plantName].outputs) {
|
|
||||||
return prevData;
|
|
||||||
}
|
|
||||||
const newData = { ...prevData };
|
|
||||||
newData.plants[plantName].outputs[productName] = 0;
|
|
||||||
return newData;
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
// onSave
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
const onSave = () => {
|
|
||||||
console.log(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
// onChange
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
const onChangeParameters = (val) => {
|
|
||||||
setData(prevData => {
|
|
||||||
const newData = { ...prevData };
|
|
||||||
newData.parameters = val;
|
|
||||||
return newData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChangeProduct = (prodName, val) => {
|
|
||||||
setData(prevData => {
|
|
||||||
const newData = { ...prevData };
|
|
||||||
newData.products[prodName] = val;
|
|
||||||
return newData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
let productComps = [];
|
|
||||||
for (const [prodName, prod] of Object.entries(data.products)) {
|
|
||||||
productComps.push(
|
|
||||||
<ProductBlock
|
|
||||||
key={prodName}
|
|
||||||
name={prodName}
|
|
||||||
value={prod}
|
|
||||||
onChange={v => onChangeProduct(prodName, v)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let plantComps = [];
|
const onRenameProduct = (prevName, newName) => {
|
||||||
for (const [plantName, plant] of Object.entries(data.plants)) {
|
setData((prevData) => {
|
||||||
plantComps.push(
|
const newData = { ...prevData };
|
||||||
<PlantBlock key={plantName} name={plantName} />
|
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
|
<PipelineBlock
|
||||||
onAddPlant={onAddPlant}
|
onAddPlant={onAddPlant}
|
||||||
onAddPlantOutput={onAddPlantOutput}
|
onAddPlantOutput={onAddPlantOutput}
|
||||||
onAddProduct={onAddProduct}
|
onAddProduct={onAddProduct}
|
||||||
onMovePlant={onMovePlant}
|
onMovePlant={onMovePlant}
|
||||||
onMoveProduct={onMoveProduct}
|
onMoveProduct={onMoveProduct}
|
||||||
onRenamePlant={onRenamePlant}
|
onRenamePlant={onRenamePlant}
|
||||||
onRenameProduct={onRenameProduct}
|
onRenameProduct={onRenameProduct}
|
||||||
onSetPlantInput={onSetPlantInput}
|
onSetPlantInput={onSetPlantInput}
|
||||||
onRemovePlant={onRemovePlant}
|
onRemovePlant={onRemovePlant}
|
||||||
onRemoveProduct={onRemoveProduct}
|
onRemoveProduct={onRemoveProduct}
|
||||||
plants={data.plants}
|
plants={data.plants}
|
||||||
products={data.products}
|
products={data.products}
|
||||||
/>
|
/>
|
||||||
<ParametersBlock
|
<ParametersBlock
|
||||||
value={data.parameters}
|
value={data.parameters}
|
||||||
onChange={onChangeParameters}
|
onChange={(v) => onChange(v, "parameters")}
|
||||||
/>
|
/>
|
||||||
{productComps}
|
{productComps}
|
||||||
{plantComps}
|
{plantComps}
|
||||||
<ButtonRow>
|
</div>
|
||||||
<Button label="Load" />
|
<Footer />
|
||||||
<Button label="Save" onClick={onSave} />
|
</>
|
||||||
</ButtonRow>
|
);
|
||||||
</>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InputPage;
|
export default InputPage;
|
@ -1,46 +1,46 @@
|
|||||||
import Section from './Section';
|
import Section from "./Section";
|
||||||
import Card from './Card';
|
import Card from "./Card";
|
||||||
import Form from './Form';
|
import Form from "./Form";
|
||||||
import TextInputRow from './TextInputRow';
|
import TextInputRow from "./TextInputRow";
|
||||||
|
|
||||||
const ParametersBlock = (props) => {
|
const ParametersBlock = (props) => {
|
||||||
const onChangeField = (field, val) => {
|
const onChangeField = (field, val) => {
|
||||||
props.value[field] = val;
|
props.value[field] = val;
|
||||||
props.onChange(props.value);
|
props.onChange(props.value);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Section title="Parameters" />
|
<Section title="Parameters" />
|
||||||
<Card>
|
<Card>
|
||||||
<Form>
|
<Form>
|
||||||
<TextInputRow
|
<TextInputRow
|
||||||
label="Time horizon"
|
label="Time horizon"
|
||||||
unit="years"
|
unit="years"
|
||||||
tooltip="Number of years in the simulation."
|
tooltip="Number of years in the simulation."
|
||||||
value={props.value["time horizon (years)"]}
|
value={props.value["time horizon (years)"]}
|
||||||
onChange={v => onChangeField("time horizon (years)", v)}
|
onChange={(v) => onChangeField("time horizon (years)", v)}
|
||||||
validate="int"
|
validate="int"
|
||||||
/>
|
/>
|
||||||
<TextInputRow
|
<TextInputRow
|
||||||
label="Building period"
|
label="Building period"
|
||||||
unit="years"
|
unit="years"
|
||||||
tooltip="List of years in which we are allowed to open new plants. For example, if this parameter is set to [1,2,3], we can only open plants during the first three years. By default, this equals [1]; that is, plants can only be opened during the first year."
|
tooltip="List of years in which we are allowed to open new plants. For example, if this parameter is set to [1,2,3], we can only open plants during the first three years. By default, this equals [1]; that is, plants can only be opened during the first year."
|
||||||
value={props.value["building period (years)"]}
|
value={props.value["building period (years)"]}
|
||||||
onChange={v => onChangeField("building period (years)", v)}
|
onChange={(v) => onChangeField("building period (years)", v)}
|
||||||
validate="intList"
|
validate="intList"
|
||||||
/>
|
/>
|
||||||
<TextInputRow
|
{/* <TextInputRow
|
||||||
label="Annual inflation rate"
|
label="Annual inflation rate"
|
||||||
unit="%"
|
unit="%"
|
||||||
tooltip="Rate of inflation applied to all costs."
|
tooltip="Rate of inflation applied to all costs."
|
||||||
value={props.value["annual inflation rate (%)"]}
|
value={props.value["annual inflation rate (%)"]}
|
||||||
onChange={v => onChangeField("annual inflation rate (%)", v)}
|
onChange={(v) => onChangeField("annual inflation rate (%)", v)}
|
||||||
validate="float"
|
validate="float"
|
||||||
/>
|
/> */}
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ParametersBlock;
|
export default ParametersBlock;
|
@ -1,142 +1,191 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import ReactFlow, { Background } from 'react-flow-renderer';
|
import ReactFlow, { Background, isNode } from "react-flow-renderer";
|
||||||
import Section from './Section';
|
import Section from "./Section";
|
||||||
import Card from './Card';
|
import Card from "./Card";
|
||||||
import Button from './Button';
|
import Button from "./Button";
|
||||||
import styles from './PipelineBlock.module.css';
|
import styles from "./PipelineBlock.module.css";
|
||||||
|
import dagre from "dagre";
|
||||||
|
|
||||||
|
window.nextX = 15;
|
||||||
|
window.nextY = 15;
|
||||||
|
|
||||||
const PipelineBlock = (props) => {
|
export const randomPosition = () => {
|
||||||
let elements = [];
|
window.nextY += 60;
|
||||||
let mapNameToType = {};
|
if (window.nextY >= 500) {
|
||||||
for (const [productName, product] of Object.entries(props.products)) {
|
window.nextY = 15;
|
||||||
mapNameToType[productName] = "product";
|
window.nextX += 150;
|
||||||
elements.push({
|
}
|
||||||
id: productName,
|
return [window.nextX, window.nextY];
|
||||||
data: { label: productName, type: 'product' },
|
};
|
||||||
position: { x: product.x, y: product.y },
|
|
||||||
sourcePosition: 'right',
|
const getLayoutedElements = (elements) => {
|
||||||
targetPosition: 'left',
|
const nodeWidth = 125;
|
||||||
className: styles.ProductNode,
|
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)) {
|
const PipelineBlock = (props) => {
|
||||||
mapNameToType[plantName] = "plant";
|
let elements = [];
|
||||||
elements.push({
|
let mapNameToType = {};
|
||||||
id: plantName,
|
for (const [productName, product] of Object.entries(props.products)) {
|
||||||
data: { label: plantName, type: 'plant' },
|
mapNameToType[productName] = "product";
|
||||||
position: { x: plant.x, y: plant.y },
|
elements.push({
|
||||||
sourcePosition: 'right',
|
id: productName,
|
||||||
targetPosition: 'left',
|
data: { label: productName, type: "product" },
|
||||||
className: styles.PlantNode,
|
position: { x: product.x, y: product.y },
|
||||||
});
|
sourcePosition: "right",
|
||||||
|
targetPosition: "left",
|
||||||
|
className: styles.ProductNode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (plant.input != undefined) {
|
for (const [plantName, plant] of Object.entries(props.plants)) {
|
||||||
elements.push({
|
mapNameToType[plantName] = "plant";
|
||||||
id: `${plant.input}-${plantName}`,
|
elements.push({
|
||||||
source: plant.input,
|
id: plantName,
|
||||||
target: plantName,
|
data: { label: plantName, type: "plant" },
|
||||||
animated: true,
|
position: { x: plant.x, y: plant.y },
|
||||||
style: { stroke: "black" },
|
sourcePosition: "right",
|
||||||
selectable: false,
|
targetPosition: "left",
|
||||||
});
|
className: styles.PlantNode,
|
||||||
}
|
});
|
||||||
|
|
||||||
for (const [productName, amount] of Object.entries(plant.outputs)) {
|
if (plant.input !== undefined) {
|
||||||
elements.push({
|
elements.push({
|
||||||
id: `${plantName}-${productName}`,
|
id: `${plant.input}-${plantName}`,
|
||||||
source: plantName,
|
source: plant.input,
|
||||||
target: productName,
|
target: plantName,
|
||||||
animated: true,
|
animated: true,
|
||||||
style: { stroke: "black" },
|
style: { stroke: "black" },
|
||||||
selectable: false,
|
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 onNodeDoubleClick = (ev, node) => {
|
||||||
const oldName = node.data.label;
|
const oldName = node.data.label;
|
||||||
const newName = window.prompt("Enter new name", oldName);
|
const newName = window.prompt("Enter new name", oldName);
|
||||||
if (newName == undefined || newName.length == 0) return;
|
if (newName === undefined || newName.length === 0) return;
|
||||||
if (newName in mapNameToType) return;
|
if (newName in mapNameToType) return;
|
||||||
if (node.data.type == "plant") {
|
if (node.data.type === "plant") {
|
||||||
props.onRenamePlant(oldName, newName);
|
props.onRenamePlant(oldName, newName);
|
||||||
} else {
|
} else {
|
||||||
props.onRenameProduct(oldName, newName);
|
props.onRenameProduct(oldName, newName);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onElementsRemove = (elements) => {
|
const onElementsRemove = (elements) => {
|
||||||
elements.forEach(el => {
|
elements.forEach((el) => {
|
||||||
if (!(el.id in mapNameToType)) return;
|
if (!(el.id in mapNameToType)) return;
|
||||||
if (el.data.type == "plant") {
|
if (el.data.type === "plant") {
|
||||||
props.onRemovePlant(el.data.label);
|
props.onRemovePlant(el.data.label);
|
||||||
} else {
|
} else {
|
||||||
props.onRemoveProduct(el.data.label);
|
props.onRemoveProduct(el.data.label);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNodeDragStop = (ev, node) => {
|
const onNodeDragStop = (ev, node) => {
|
||||||
if (node.data.type == "plant") {
|
if (node.data.type === "plant") {
|
||||||
props.onMovePlant(node.data.label, node.position.x, node.position.y);
|
props.onMovePlant(node.data.label, node.position.x, node.position.y);
|
||||||
} else {
|
} else {
|
||||||
props.onMoveProduct(node.data.label, node.position.x, node.position.y);
|
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 onLayout = () => {
|
||||||
const sourceType = mapNameToType[args.source];
|
const layoutedElements = getLayoutedElements(elements);
|
||||||
const targetType = mapNameToType[args.target];
|
layoutedElements.forEach((el) => {
|
||||||
if (sourceType === "product" && targetType === "plant") {
|
if (isNode(el)) {
|
||||||
props.onSetPlantInput(args.target, args.source);
|
if (el.data.type === "plant") {
|
||||||
} else if (sourceType === "plant" && targetType === "product") {
|
props.onMovePlant(el.data.label, el.position.x, el.position.y);
|
||||||
props.onAddPlantOutput(args.source, args.target);
|
} else {
|
||||||
|
props.onMoveProduct(el.data.label, el.position.x, el.position.y);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Section title="Pipeline" />
|
<Section title="Pipeline" />
|
||||||
<Card>
|
<Card>
|
||||||
<div className={styles.PipelineBlock}>
|
<div className={styles.PipelineBlock}>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
elements={elements}
|
elements={elements}
|
||||||
onNodeDoubleClick={onNodeDoubleClick}
|
onNodeDoubleClick={onNodeDoubleClick}
|
||||||
onNodeDragStop={onNodeDragStop}
|
onNodeDragStop={onNodeDragStop}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onElementsRemove={onElementsRemove}
|
onElementsRemove={onElementsRemove}
|
||||||
deleteKeyCode={46}
|
deleteKeyCode={46}
|
||||||
maxZoom={1}
|
maxZoom={1}
|
||||||
minZoom={1}
|
minZoom={1}
|
||||||
snapToGrid={true}
|
snapToGrid={true}
|
||||||
preventScrolling={false}
|
preventScrolling={false}
|
||||||
>
|
>
|
||||||
<Background />
|
<Background />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: "center" }}>
|
||||||
<Button
|
<Button
|
||||||
label="Add product"
|
label="Add product"
|
||||||
kind="inline"
|
kind="inline"
|
||||||
onClick={props.onAddProduct}
|
onClick={props.onAddProduct}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button label="Add plant" kind="inline" onClick={props.onAddPlant} />
|
||||||
label="Add plant"
|
<Button label="Auto-Layout" kind="inline" onClick={onLayout} />
|
||||||
kind="inline"
|
<Button
|
||||||
onClick={props.onAddPlant}
|
label="?"
|
||||||
/>
|
kind="inline"
|
||||||
<Button
|
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."
|
||||||
label="?"
|
/>
|
||||||
kind="inline"
|
</div>
|
||||||
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."
|
</Card>
|
||||||
/>
|
</>
|
||||||
</div>
|
);
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PipelineBlock;
|
export default PipelineBlock;
|
@ -1,24 +1,25 @@
|
|||||||
.PipelineBlock {
|
.PipelineBlock {
|
||||||
height: 600px;
|
height: 600px !important;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1) !important;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius) !important;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlantNode, .ProductNode {
|
.PlantNode,
|
||||||
border-color: rgba(0, 0, 0, 0.8);
|
.ProductNode {
|
||||||
color: black;
|
border-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
font-size: 13px;
|
color: black !important;
|
||||||
border-width: 1px;
|
font-size: 13px !important;
|
||||||
border-radius: 6px;
|
border-width: 1px !important;
|
||||||
box-shadow: 0px 2px 4px -3px black;
|
border-radius: 6px !important;
|
||||||
width: 100px;
|
box-shadow: 0px 2px 4px -3px black !important;
|
||||||
|
width: 100px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlantNode {
|
.PlantNode {
|
||||||
background-color: #0df;
|
background-color: #8d8 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProductNode {
|
.ProductNode {
|
||||||
background-color: #f6f6f6;
|
background-color: #e6e6e6 !important;
|
||||||
}
|
}
|
@ -1,142 +1,266 @@
|
|||||||
import Section from './Section';
|
import Section from "./Section";
|
||||||
import Card from './Card';
|
import Card from "./Card";
|
||||||
import Form from './Form';
|
import Form from "./Form";
|
||||||
import TextInputRow from './TextInputRow';
|
import TextInputRow from "./TextInputRow";
|
||||||
import FileInputRow from './FileInputRow';
|
import FileInputRow from "./FileInputRow";
|
||||||
import DictInputRow from './DictInputRow';
|
import DictInputRow from "./DictInputRow";
|
||||||
|
import { csvFormat, csvParse, generateFile } from "./csv";
|
||||||
|
|
||||||
const PlantBlock = (props) => {
|
const PlantBlock = (props) => {
|
||||||
const emissions = {
|
const onChange = (val, field1, field2, field3) => {
|
||||||
"CO2": "0.05",
|
const newPlant = { ...props.value };
|
||||||
"CH4": "0.01",
|
if (field3 !== undefined) {
|
||||||
"N2O": "0.04",
|
newPlant[field1][field2][field3] = val;
|
||||||
};
|
} else if (field2 !== undefined) {
|
||||||
const output = {
|
newPlant[field1][field2] = val;
|
||||||
"Nickel": "0.5",
|
} else {
|
||||||
"Metal casing": "0.35",
|
newPlant[field1] = val;
|
||||||
};
|
}
|
||||||
return (
|
props.onChange(newPlant);
|
||||||
<>
|
};
|
||||||
<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 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>
|
const onCandidateLocationsFile = (contents) => {
|
||||||
<TextInputRow
|
const data = csvParse({
|
||||||
label="Input"
|
contents: contents,
|
||||||
tooltip="The name of the product that this plant takes as input. Only one input is accepted per plant."
|
requiredCols: [
|
||||||
disabled="disabled"
|
"name",
|
||||||
value="Battery"
|
"latitude (deg)",
|
||||||
/>
|
"longitude (deg)",
|
||||||
<DictInputRow
|
"area cost factor",
|
||||||
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."
|
const result = {};
|
||||||
value={output}
|
data.forEach((el) => {
|
||||||
disableKeys={true}
|
result[el["name"]] = {
|
||||||
default="0"
|
"latitude (deg)": el["latitude (deg)"],
|
||||||
/>
|
"longitude (deg)": el["longitude (deg)"],
|
||||||
|
"area cost factor": el["area cost factor"],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
onChange(result, "locations");
|
||||||
|
};
|
||||||
|
|
||||||
<h1>Capacity & costs</h1>
|
const onCandidateLocationsDownload = () => {
|
||||||
<TextInputRow
|
const result = [];
|
||||||
label="Minimum capacity"
|
for (const [locationName, locationDict] of Object.entries(
|
||||||
unit="tonne"
|
props.value["locations"]
|
||||||
tooltip="The minimum size of the plant."
|
)) {
|
||||||
default="0"
|
result.push({
|
||||||
/>
|
name: locationName,
|
||||||
<TextInputRow
|
"latitude (deg)": locationDict["latitude (deg)"],
|
||||||
label="Opening cost (min capacity)"
|
"longitude (deg)": locationDict["longitude (deg)"],
|
||||||
unit="$"
|
"area cost factor": locationDict["area cost factor"],
|
||||||
tooltip="The cost to open the plant at minimum capacity."
|
});
|
||||||
default="0.00"
|
}
|
||||||
/>
|
generateFile(`Candidate locations - ${props.name}.csv`, csvFormat(result));
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h1>Storage</h1>
|
const onCandidateLocationsClear = () => {
|
||||||
<TextInputRow
|
onChange({}, "locations");
|
||||||
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h1>Disposal</h1>
|
let description = "No locations set";
|
||||||
<DictInputRow
|
const nCenters = Object.keys(props.value["locations"]).length;
|
||||||
label="Disposal cost"
|
if (nCenters > 0) description = `${nCenters} locations`;
|
||||||
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}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h1>Emissions</h1>
|
const shouldDisableMaxCap =
|
||||||
<DictInputRow
|
props.value["minimum capacity (tonne)"] ===
|
||||||
label="Emissions"
|
props.value["maximum capacity (tonne)"];
|
||||||
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</Form>
|
return (
|
||||||
</Card>
|
<>
|
||||||
</>
|
<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;
|
export default PlantBlock;
|
@ -1,212 +1,177 @@
|
|||||||
import { useState } from 'react';
|
import Section from "./Section";
|
||||||
import Section from './Section';
|
import Card from "./Card";
|
||||||
import Card from './Card';
|
import Form from "./Form";
|
||||||
import Form from './Form';
|
import TextInputRow from "./TextInputRow";
|
||||||
import TextInputRow from './TextInputRow';
|
import FileInputRow from "./FileInputRow";
|
||||||
import FileInputRow from './FileInputRow';
|
import DictInputRow from "./DictInputRow";
|
||||||
import DictInputRow from './DictInputRow';
|
import { csvParse, extractNumericColumns, generateFile } from "./csv";
|
||||||
import * as d3 from 'd3';
|
import { csvFormat } from "d3";
|
||||||
|
|
||||||
const ProductBlock = (props) => {
|
const ProductBlock = (props) => {
|
||||||
const onChange = (field, val) => {
|
const onChange = (field, val) => {
|
||||||
const newProduct = { ...props.value };
|
const newProduct = { ...props.value };
|
||||||
newProduct[field] = val;
|
newProduct[field] = val;
|
||||||
props.onChange(newProduct);
|
props.onChange(newProduct);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onInitialAmountsFile = (contents) => {
|
const onInitialAmountsFile = (contents) => {
|
||||||
const data = d3.csvParse(contents);
|
const data = csvParse({
|
||||||
const T = data.columns.length - 3;
|
contents: contents,
|
||||||
|
requiredCols: ["latitude (deg)", "longitude (deg)", "name"],
|
||||||
// Construct list of required columns
|
});
|
||||||
let isValid = true;
|
const result = {};
|
||||||
const requiredCols = ["latitude (deg)", "longitude (deg)", "name"];
|
data.forEach((el) => {
|
||||||
for (let t = 0; t < T; t++) {
|
result[el["name"]] = {
|
||||||
requiredCols.push(t + 1);
|
"latitude (deg)": el["latitude (deg)"],
|
||||||
}
|
"longitude (deg)": el["longitude (deg)"],
|
||||||
|
"amount (tonne)": extractNumericColumns(el, "amount"),
|
||||||
// Check required columns
|
};
|
||||||
requiredCols.forEach(col => {
|
});
|
||||||
if (!(col in data[0])) {
|
onChange("initial amounts", result);
|
||||||
console.log(`Column "${col}" not found in CSV file.`);
|
};
|
||||||
isValid = false;
|
|
||||||
}
|
const onInitialAmountsClear = () => {
|
||||||
});
|
onChange("initial amounts", {});
|
||||||
if (!isValid) return;
|
};
|
||||||
|
|
||||||
// Construct initial amounts dict
|
const onInitialAmountsTemplate = () => {
|
||||||
const result = {};
|
generateFile(
|
||||||
data.forEach(el => {
|
"Initial amounts - Template.csv",
|
||||||
let amounts = [];
|
csvFormat([
|
||||||
for (let t = 0; t < T; t++) {
|
{
|
||||||
amounts.push(el[t + 1]);
|
name: "Washakie County",
|
||||||
}
|
"latitude (deg)": "43.8356",
|
||||||
result[el["name"]] = {
|
"longitude (deg)": "-107.6602",
|
||||||
"latitude (deg)": el["latitude (deg)"],
|
"amount 1": "21902",
|
||||||
"longitude (deg)": el["longitude (deg)"],
|
"amount 2": "6160",
|
||||||
"amount (tonne)": amounts,
|
"amount 3": "2721",
|
||||||
};
|
"amount 4": "12917",
|
||||||
});
|
"amount 5": "18048",
|
||||||
|
},
|
||||||
onChange("initial amounts", result);
|
{
|
||||||
};
|
name: "Platte County",
|
||||||
|
"latitude (deg)": "42.1314",
|
||||||
const onInitialAmountsClear = () => {
|
"longitude (deg)": "-104.9676",
|
||||||
onChange("initial amounts", {});
|
"amount 1": "16723",
|
||||||
};
|
"amount 2": "8709",
|
||||||
|
"amount 3": "22584",
|
||||||
const onInitialAmountsTemplate = () => {
|
"amount 4": "12278",
|
||||||
exportToCsv(
|
"amount 5": "7196",
|
||||||
"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"],
|
name: "Park County",
|
||||||
["Platte County", "42.1314", "-104.9676", "16723", "8709", "22584", "12278", "7196"],
|
"latitude (deg)": "44.4063",
|
||||||
["Park County", "44.4063", "-109.4153", "14731", "11729", "15562", "7703", "23349"],
|
"longitude (deg)": "-109.4153",
|
||||||
["Goshen County", "42.0853", "-104.3534", "23266", "16299", "11470", "20107", "21592"],
|
"amount 1": "14731",
|
||||||
]);
|
"amount 2": "11729",
|
||||||
};
|
"amount 3": "15562",
|
||||||
|
"amount 4": "7703",
|
||||||
const onInitialAmountsDownload = () => {
|
"amount 5": "23349",
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function exportToCsv(filename, rows) {
|
const onInitialAmountsDownload = () => {
|
||||||
var processRow = function (row) {
|
const results = [];
|
||||||
var finalVal = "";
|
for (const [locationName, locationDict] of Object.entries(
|
||||||
for (var j = 0; j < row.length; j++) {
|
props.value["initial amounts"]
|
||||||
var innerValue = row[j] === null ? "" : row[j].toString();
|
)) {
|
||||||
if (row[j] instanceof Date) {
|
const row = {
|
||||||
innerValue = row[j].toLocaleString();
|
name: locationName,
|
||||||
}
|
"latitude (deg)": locationDict["latitude (deg)"],
|
||||||
var result = innerValue.replace(/"/g, '""');
|
"longitude (deg)": locationDict["longitude (deg)"],
|
||||||
if (result.search(/("|,|\n)/g) >= 0) result = '"' + result + '"';
|
};
|
||||||
if (j > 0) finalVal += ",";
|
locationDict["amount (tonne)"].forEach((el, idx) => {
|
||||||
finalVal += result;
|
row[`amount ${idx + 1}`] = el;
|
||||||
}
|
});
|
||||||
return finalVal + "\n";
|
results.push(row);
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
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;
|
export default ProductBlock;
|
@ -1,7 +1,7 @@
|
|||||||
import styles from './Section.module.css';
|
import styles from "./Section.module.css";
|
||||||
|
|
||||||
const Section = (props) => {
|
const Section = (props) => {
|
||||||
return <h2 className={styles.Section}>{props.title}</h2>;
|
return <h2 className={styles.Section}>{props.title}</h2>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Section;
|
export default Section;
|
@ -1,6 +1,6 @@
|
|||||||
.Section {
|
.Section {
|
||||||
line-height: 36px;
|
line-height: 36px;
|
||||||
margin: 12px;
|
margin: 12px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
@ -1,40 +1,44 @@
|
|||||||
import form_styles from './Form.module.css';
|
import form_styles from "./Form.module.css";
|
||||||
import Button from './Button';
|
import Button from "./Button";
|
||||||
import { validate } from './Form';
|
import { validate } from "./Form";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
const TextInputRow = (props) => {
|
const TextInputRow = React.forwardRef((props, ref) => {
|
||||||
let unit = "";
|
let unit = "";
|
||||||
if (props.unit) {
|
if (props.unit) {
|
||||||
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
|
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tooltip = "";
|
let tooltip = "";
|
||||||
if (props.tooltip != undefined) {
|
if (props.tooltip !== undefined) {
|
||||||
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
if (props.validate !== undefined) {
|
if (!props.disabled && props.validate !== undefined) {
|
||||||
isValid = validate(props.validate, props.value);
|
isValid = validate(props.validate, props.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
let className = "";
|
let className = "";
|
||||||
if (!isValid) className = form_styles.invalid;
|
if (!isValid) className = form_styles.invalid;
|
||||||
|
|
||||||
return <div className={form_styles.FormRow}>
|
return (
|
||||||
<label>
|
<div className={form_styles.FormRow}>
|
||||||
{props.label} {unit}
|
<label>
|
||||||
</label>
|
{props.label} {unit}
|
||||||
<input
|
</label>
|
||||||
type="text"
|
<input
|
||||||
placeholder={props.default}
|
type="text"
|
||||||
disabled={props.disabled}
|
placeholder={props.default}
|
||||||
value={props.value}
|
disabled={props.disabled}
|
||||||
className={className}
|
value={props.value}
|
||||||
onChange={e => props.onChange(e.target.value)}
|
className={className}
|
||||||
/>
|
onChange={(e) => props.onChange(e.target.value)}
|
||||||
{tooltip}
|
ref={ref}
|
||||||
</div>;
|
/>
|
||||||
};
|
{tooltip}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default TextInputRow;
|
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 {
|
:root {
|
||||||
--site-width: 1200px;
|
--site-width: 1200px;
|
||||||
--box-border: 1px solid rgba(0, 0, 0, 0.2);
|
--box-border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
--box-shadow: 0px 2px 4px -3px rgba(0, 0, 0, 0.2);
|
--box-shadow: 0px 2px 4px -3px rgba(0, 0, 0, 0.2);
|
||||||
--border-radius: 4px;
|
--border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html,
|
||||||
margin: 0;
|
body {
|
||||||
padding: 0;
|
margin: 0;
|
||||||
border: 0;
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
color: rgba(0, 0, 0, 0.95);
|
color: rgba(0, 0, 0, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
max-width: var(--site-width);
|
max-width: var(--site-width);
|
||||||
min-width: 900px;
|
min-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__node.selected {
|
.react-flow__node.selected {
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2) !important;
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2) !important;
|
||||||
border-width: 2px !important;
|
border-width: 2px !important;
|
||||||
margin-top: -1px !important;
|
margin-top: -1px !important;
|
||||||
margin-left: -1px !important;
|
margin-left: -1px !important;
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__handle {
|
.react-flow__handle {
|
||||||
width: 8px !important;
|
width: 6px !important;
|
||||||
height: 8px !important;
|
height: 6px !important;
|
||||||
background-color: white !important;
|
background-color: white !important;
|
||||||
border: 1px solid black !important;
|
border: 1px solid black !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__handle:hover {
|
.react-flow__handle:hover {
|
||||||
background-color: black !important;
|
background-color: black !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__handle-right {
|
.react-flow__handle-right {
|
||||||
right: -5px !important;
|
right: -4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__handle-left {
|
.react-flow__handle-left {
|
||||||
left: -5px !important;
|
left: -4px !important;
|
||||||
}
|
}
|
@ -1,17 +1,11 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from "react-dom";
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
import Header from './Header';
|
import InputPage from "./InputPage";
|
||||||
import InputPage from './InputPage';
|
|
||||||
import Footer from './Footer';
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Header />
|
<InputPage />
|
||||||
<div id="content">
|
|
||||||
<InputPage />
|
|
||||||
</div>
|
|
||||||
<Footer />
|
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root')
|
document.getElementById("root")
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in new issue