\n
RELOG: Reverse Logistics Optimization
\n
\n Copyright © 2020—2022, UChicago Argonne, LLC. All Rights\n Reserved.\n
\n
\n );\n};\n\nexport default Footer;\n","export const defaultProduct = {\n \"initial amounts\": {},\n \"disposal cost ($/tonne)\": \"0\",\n \"disposal limit (tonne)\": \"0\",\n \"disposal limit (%)\": \"\",\n \"transportation cost ($/km/tonne)\": \"0\",\n \"transportation energy (J/km/tonne)\": \"0\",\n \"transportation emissions (tonne/km/tonne)\": {},\n x: 0,\n y: 0,\n};\n\nexport const defaultPlantLocation = {\n \"area cost factor\": 1.0,\n \"latitude (deg)\": 0,\n \"longitude (deg)\": 0,\n};\n\nexport const defaultPlant = {\n locations: {},\n \"outputs (tonne/tonne)\": {},\n \"disposal cost ($/tonne)\": {},\n \"disposal limit (tonne)\": {},\n \"emissions (tonne/tonne)\": {},\n storage: {\n \"cost ($/tonne)\": 0,\n \"limit (tonne)\": 0,\n },\n \"maximum capacity (tonne)\": 0,\n \"minimum capacity (tonne)\": 0,\n \"opening cost (max capacity) ($)\": 0,\n \"opening cost (min capacity) ($)\": 0,\n \"fixed operating cost (max capacity) ($)\": 0,\n \"fixed operating cost (min capacity) ($)\": 0,\n \"variable operating cost ($/tonne)\": 0,\n \"energy (GJ/tonne)\": 0,\n x: 0,\n y: 0,\n};\n\nexport const defaultData = {\n parameters: {\n \"time horizon (years)\": \"1\",\n \"building period (years)\": \"[1]\",\n \"inflation rate (%)\": \"0\",\n },\n products: {},\n plants: {},\n};\n","import arrayWithoutHoles from \"./arrayWithoutHoles.js\";\nimport iterableToArray from \"./iterableToArray.js\";\nimport unsupportedIterableToArray from \"./unsupportedIterableToArray.js\";\nimport nonIterableSpread from \"./nonIterableSpread.js\";\nexport default function _toConsumableArray(arr) {\n return arrayWithoutHoles(arr) || iterableToArray(arr) || unsupportedIterableToArray(arr) || nonIterableSpread();\n}","import arrayLikeToArray from \"./arrayLikeToArray.js\";\nexport default function _arrayWithoutHoles(arr) {\n if (Array.isArray(arr)) return arrayLikeToArray(arr);\n}","export default function _iterableToArray(iter) {\n if (typeof Symbol !== \"undefined\" && iter[Symbol.iterator] != null || iter[\"@@iterator\"] != null) return Array.from(iter);\n}","export default function _nonIterableSpread() {\n throw new TypeError(\"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\");\n}","const isNumeric = (val) => {\n return String(val).length > 0 && !isNaN(val);\n};\n\nconst keysToList = (obj) => {\n const result = [];\n for (const key of Object.keys(obj)) {\n result.push(key);\n }\n return result;\n};\n\nexport const exportValue = (original, T, R = 1) => {\n if (isNumeric(original)) {\n if (T) {\n let v = parseFloat(original);\n const result = [];\n for (let i = 0; i < T; i++) {\n result.push(v);\n v *= R;\n }\n return result;\n } else {\n return parseFloat(original);\n }\n }\n\n try {\n const parsed = JSON.parse(original);\n return parsed;\n } catch {\n // ignore\n }\n return original;\n};\n\nconst exportValueDict = (original, T) => {\n const result = {};\n for (const [key, val] of Object.entries(original)) {\n if (key.length === 0) continue;\n result[key] = exportValue(val, T);\n }\n if (Object.keys(result).length > 0) {\n return result;\n } else {\n return null;\n }\n};\n\nconst computeTotalInitialAmount = (prod) => {\n let total = null;\n for (const locDict of Object.values(prod[\"initial amounts\"])) {\n const locAmount = locDict[\"amount (tonne)\"];\n if (!total) total = [...locAmount];\n else {\n for (let i = 0; i < locAmount.length; i++) {\n total[i] += locAmount[i];\n }\n }\n }\n return total;\n};\n\nexport const importList = (args, R = 1) => {\n if (args === undefined) return \"\";\n if (Array.isArray(args) && args.length > 0) {\n let isConstant = true;\n for (let i = 1; i < args.length; i++) {\n if (Math.abs(args[i - 1] - args[i] / R) > 1e-3) {\n isConstant = false;\n break;\n }\n }\n if (isConstant) {\n return String(args[0]);\n } else {\n return JSON.stringify(args);\n }\n } else {\n return args;\n }\n};\n\nexport const importDict = (args) => {\n if (!args) return {};\n const result = {};\n for (const [key, val] of Object.entries(args)) {\n result[key] = importList(val);\n }\n return result;\n};\n\nconst computeAbsDisposal = (prod) => {\n const disposalPerc = prod[\"disposal limit (%)\"];\n const total = computeTotalInitialAmount(prod);\n const disposalAbs = [];\n for (let i = 0; i < total.length; i++) {\n disposalAbs[i] = (total[i] * disposalPerc) / 100;\n }\n return disposalAbs;\n};\n\nconst computeInflationAndTimeHorizon = (obj, keys) => {\n for (let i = 0; i < keys.length; i++) {\n const list = obj[keys[i]];\n if (\n Array.isArray(list) &&\n list.length > 1 &&\n isNumeric(list[0]) &&\n isNumeric(list[1]) &&\n Math.abs(list[0]) > 0\n ) {\n return [list[1] / list[0], list.length];\n }\n }\n return [1, 1];\n};\n\nexport const exportProduct = (original, parameters) => {\n const result = {};\n\n // Read time horizon\n let T = parameters[\"time horizon (years)\"];\n if (isNumeric(T)) T = parseInt(T);\n else T = 1;\n\n // Read inflation\n let R = parameters[\"inflation rate (%)\"];\n if (isNumeric(R)) R = parseFloat(R) / 100 + 1;\n else R = 1;\n\n // Copy constant time series\n result[\"initial amounts\"] = original[\"initial amounts\"];\n [\"disposal limit (tonne)\", \"transportation energy (J/km/tonne)\"].forEach(\n (key) => {\n const v = exportValue(original[key], T);\n if (v.length > 0) result[key] = v;\n }\n );\n\n // Copy cost time series (with inflation)\n [\"disposal cost ($/tonne)\", \"transportation cost ($/km/tonne)\"].forEach(\n (key) => {\n const v = exportValue(original[key], T, R);\n if (v.length > 0) result[key] = v;\n }\n );\n\n // Copy dictionaries\n [\"transportation emissions (tonne/km/tonne)\"].forEach((key) => {\n const v = exportValueDict(original[key], T);\n if (v) result[key] = v;\n });\n\n // Transform percentage disposal limits into absolute\n if (isNumeric(original[\"disposal limit (%)\"])) {\n result[\"disposal limit (tonne)\"] = computeAbsDisposal(original);\n }\n return result;\n};\n\nexport const exportPlant = (original, parameters) => {\n const result = {};\n\n // Read time horizon\n let T = parameters[\"time horizon (years)\"];\n if (isNumeric(T)) T = parseInt(T);\n else T = 1;\n\n // Read inflation\n let R = parameters[\"inflation rate (%)\"];\n if (isNumeric(R)) R = parseFloat(R) / 100 + 1;\n else R = 1;\n\n // Copy scalar values\n [\"input\"].forEach((key) => {\n result[key] = original[key];\n });\n\n // Copy time series values\n [\"energy (GJ/tonne)\"].forEach((key) => {\n result[key] = exportValue(original[key], T);\n if (result[key] === undefined) {\n delete result[key];\n }\n });\n\n // Copy scalar dicts\n [\"outputs (tonne/tonne)\"].forEach((key) => {\n const v = exportValueDict(original[key]);\n if (v) result[key] = v;\n });\n\n // Copy time series dicts\n [\"emissions (tonne/tonne)\"].forEach((key) => {\n const v = exportValueDict(original[key], T);\n if (v) result[key] = v;\n });\n\n const minCap = original[\"minimum capacity (tonne)\"];\n const maxCap = original[\"maximum capacity (tonne)\"];\n\n result.locations = {};\n for (const [locName, origDict] of Object.entries(original[\"locations\"])) {\n const resDict = (result.locations[locName] = {});\n const capDict = (resDict[\"capacities (tonne)\"] = {});\n\n const acf = origDict[\"area cost factor\"];\n\n const exportValueAcf = (obj) => {\n const v = exportValue(obj, T, R);\n if (Array.isArray(v)) {\n return v.map((v) => v * acf);\n }\n return \"\";\n };\n\n // Copy scalar values\n [\"latitude (deg)\", \"longitude (deg)\"].forEach((key) => {\n resDict[key] = origDict[key];\n });\n\n // Copy minimum capacity dict\n capDict[minCap] = {};\n for (const [resKeyName, origKeyName] of Object.entries({\n \"opening cost ($)\": \"opening cost (min capacity) ($)\",\n \"fixed operating cost ($)\": \"fixed operating cost (min capacity) ($)\",\n \"variable operating cost ($/tonne)\": \"variable operating cost ($/tonne)\",\n })) {\n capDict[minCap][resKeyName] = exportValueAcf(original[origKeyName]);\n }\n\n if (maxCap !== minCap) {\n // Copy maximum capacity dict\n capDict[maxCap] = {};\n for (const [resKeyName, origKeyName] of Object.entries({\n \"opening cost ($)\": \"opening cost (max capacity) ($)\",\n \"fixed operating cost ($)\": \"fixed operating cost (max capacity) ($)\",\n \"variable operating cost ($/tonne)\":\n \"variable operating cost ($/tonne)\",\n })) {\n capDict[maxCap][resKeyName] = exportValueAcf(original[origKeyName]);\n }\n }\n\n // Copy disposal\n resDict.disposal = {};\n for (const [dispName, dispCost] of Object.entries(\n original[\"disposal cost ($/tonne)\"]\n )) {\n if (dispName.length === 0) continue;\n const v = exportValueAcf(dispCost, T);\n if (v) {\n resDict.disposal[dispName] = { \"cost ($/tonne)\": v };\n const limit = original[\"disposal limit (tonne)\"][dispName];\n if (isNumeric(limit)) {\n resDict.disposal[dispName][\"limit (tonne)\"] = exportValue(limit, T);\n }\n }\n }\n\n // Copy storage\n resDict.storage = {\n \"cost ($/tonne)\": exportValueAcf(original[\"storage\"][\"cost ($/tonne)\"]),\n };\n const storLimit = original[\"storage\"][\"limit (tonne)\"];\n if (isNumeric(storLimit)) {\n resDict.storage[\"limit (tonne)\"] = exportValue(storLimit);\n }\n }\n\n return result;\n};\n\nexport const exportData = (original) => {\n const result = {\n parameters: {},\n products: {},\n plants: {},\n };\n\n // Export parameters\n [\"time horizon (years)\", \"building period (years)\"].forEach((key) => {\n result.parameters[key] = exportValue(original.parameters[key]);\n });\n\n // Read time horizon\n let T = result.parameters[\"time horizon (years)\"];\n if (!isNumeric(T)) T = 1;\n\n // Export products\n for (const [prodName, prodDict] of Object.entries(original.products)) {\n result.products[prodName] = exportProduct(prodDict, original.parameters);\n }\n\n // Export plants\n for (const [plantName, plantDict] of Object.entries(original.plants)) {\n result.plants[plantName] = exportPlant(plantDict, original.parameters);\n }\n return result;\n};\n\nconst compressDisposalLimits = (original, result) => {\n if (!(\"disposal limit (tonne)\" in original)) {\n return;\n }\n const total = computeTotalInitialAmount(original);\n if (!total) return;\n const limit = original[\"disposal limit (tonne)\"];\n let perc = Math.round((limit[0] / total[0]) * 1e6) / 1e6;\n for (let i = 1; i < limit.length; i++) {\n if (Math.abs(limit[i] / total[i] - perc) > 1e-5) {\n return;\n }\n }\n result[\"disposal limit (tonne)\"] = \"\";\n result[\"disposal limit (%)\"] = String(perc * 100);\n};\n\nexport const importProduct = (original) => {\n const prod = {};\n const parameters = {};\n\n prod[\"initial amounts\"] = { ...original[\"initial amounts\"] };\n\n // Initialize null values\n [\"x\", \"y\"].forEach((key) => {\n prod[key] = null;\n });\n\n // Initialize empty values\n [\"disposal limit (%)\"].forEach((key) => {\n prod[key] = \"\";\n });\n\n // Import constant lists\n [\"transportation energy (J/km/tonne)\", \"disposal limit (tonne)\"].forEach(\n (key) => {\n prod[key] = importList(original[key]);\n }\n );\n\n // Compute inflation and time horizon\n const [R, T] = computeInflationAndTimeHorizon(original, [\n \"transportation cost ($/km/tonne)\",\n \"disposal cost ($/tonne)\",\n ]);\n parameters[\"inflation rate (%)\"] = String((R - 1) * 100);\n parameters[\"time horizon (years)\"] = String(T);\n\n // Import cost lists\n [\"transportation cost ($/km/tonne)\", \"disposal cost ($/tonne)\"].forEach(\n (key) => {\n prod[key] = importList(original[key], R);\n }\n );\n\n // Import dicts\n [\"transportation emissions (tonne/km/tonne)\"].forEach((key) => {\n prod[key] = importDict(original[key]);\n });\n\n // Attempt to convert absolute disposal limits to relative\n compressDisposalLimits(original, prod);\n\n return [prod, parameters];\n};\n\nexport const importPlant = (original) => {\n const plant = {};\n const parameters = {};\n\n plant[\"storage\"] = {};\n plant[\"storage\"][\"cost ($/tonne)\"] = 0;\n plant[\"storage\"][\"limit (tonne)\"] = 0;\n plant[\"disposal cost ($/tonne)\"] = 0;\n plant[\"disposal limit (tonne)\"] = 0;\n\n // Initialize null values\n [\"x\", \"y\"].forEach((key) => {\n plant[key] = null;\n });\n\n // Import scalar values\n [\"input\"].forEach((key) => {\n plant[key] = original[key];\n });\n\n // Import timeseries values\n [\"energy (GJ/tonne)\"].forEach((key) => {\n plant[key] = importList(original[key]);\n if (plant[key] === \"\") {\n delete plant[key];\n }\n });\n\n // Import dicts\n [\"outputs (tonne/tonne)\", \"emissions (tonne/tonne)\"].forEach((key) => {\n plant[key] = importDict(original[key]);\n });\n\n let costsInitialized = false;\n let R = null;\n\n // Read locations\n const resLocDict = (plant.locations = {});\n for (const [locName, origLocDict] of Object.entries(original[\"locations\"])) {\n resLocDict[locName] = {};\n\n // Import latitude and longitude\n [\"latitude (deg)\", \"longitude (deg)\"].forEach((key) => {\n resLocDict[locName][key] = origLocDict[key];\n });\n\n const capacities = keysToList(origLocDict[\"capacities (tonne)\"]);\n const last = capacities.length - 1;\n const minCap = capacities[0];\n const maxCap = capacities[last];\n const minCapDict = origLocDict[\"capacities (tonne)\"][minCap];\n const maxCapDict = origLocDict[\"capacities (tonne)\"][maxCap];\n\n // Import min/max capacity\n if (\"minimum capacity (tonne)\" in plant) {\n if (\n plant[\"minimum capacity (tonne)\"] !== minCap ||\n plant[\"maximum capacity (tonne)\"] !== maxCap\n ) {\n throw \"Data loss\";\n }\n } else {\n plant[\"minimum capacity (tonne)\"] = minCap;\n plant[\"maximum capacity (tonne)\"] = maxCap;\n }\n\n // Compute area cost factor\n let acf = 1;\n if (costsInitialized) {\n acf = plant[\"opening cost (max capacity) ($)\"];\n if (Array.isArray(acf)) acf = acf[0];\n acf = maxCapDict[\"opening cost ($)\"][0] / acf;\n }\n resLocDict[locName][\"area cost factor\"] = acf;\n\n const [R, T] = computeInflationAndTimeHorizon(maxCapDict, [\n \"opening cost ($)\",\n \"fixed operating cost ($)\",\n \"variable operating cost ($/tonne)\",\n ]);\n parameters[\"inflation rate (%)\"] = String((R - 1) * 100);\n parameters[\"time horizon (years)\"] = String(T);\n\n // Read adjusted costs\n const importListAcf = (obj) =>\n importList(\n obj.map((v) => v / acf),\n R\n );\n const openCostMax = importListAcf(maxCapDict[\"opening cost ($)\"]);\n const openCostMin = importListAcf(minCapDict[\"opening cost ($)\"]);\n const fixCostMax = importListAcf(maxCapDict[\"fixed operating cost ($)\"]);\n const fixCostMin = importListAcf(minCapDict[\"fixed operating cost ($)\"]);\n const storCost = importListAcf(origLocDict.storage[\"cost ($/tonne)\"]);\n const storLimit = String(origLocDict.storage[\"limit (tonne)\"]);\n const varCost = importListAcf(\n minCapDict[\"variable operating cost ($/tonne)\"]\n );\n\n const dispCost = {};\n const dispLimit = {};\n for (const prodName of Object.keys(original[\"outputs (tonne/tonne)\"])) {\n dispCost[prodName] = \"\";\n dispLimit[prodName] = \"\";\n\n if (prodName in origLocDict[\"disposal\"]) {\n const prodDict = origLocDict[\"disposal\"][prodName];\n dispCost[prodName] = importListAcf(prodDict[\"cost ($/tonne)\"]);\n if (\"limit (tonne)\" in prodDict)\n dispLimit[prodName] = importList(prodDict[\"limit (tonne)\"]);\n }\n }\n\n const check = (left, right) => {\n let valid = true;\n if (isNumeric(left) && isNumeric(right)) {\n valid = Math.abs(left - right) < 1.0;\n } else {\n valid = left === right;\n }\n if (!valid)\n console.warn(`Data loss detected: ${locName}, ${left} != ${right}`);\n };\n\n if (costsInitialized) {\n // Verify that location costs match the previously initialized ones\n check(plant[\"opening cost (max capacity) ($)\"], openCostMax);\n check(plant[\"opening cost (min capacity) ($)\"], openCostMin);\n check(plant[\"fixed operating cost (max capacity) ($)\"], fixCostMax);\n check(plant[\"fixed operating cost (min capacity) ($)\"], fixCostMin);\n check(plant[\"variable operating cost ($/tonne)\"], varCost);\n check(plant[\"storage\"][\"cost ($/tonne)\"], storCost);\n check(plant[\"storage\"][\"limit (tonne)\"], storLimit);\n check(String(plant[\"disposal cost ($/tonne)\"]), String(dispCost));\n check(String(plant[\"disposal limit (tonne)\"]), String(dispLimit));\n } else {\n // Initialize plant costs\n costsInitialized = true;\n plant[\"opening cost (max capacity) ($)\"] = openCostMax;\n plant[\"opening cost (min capacity) ($)\"] = openCostMin;\n plant[\"fixed operating cost (max capacity) ($)\"] = fixCostMax;\n plant[\"fixed operating cost (min capacity) ($)\"] = fixCostMin;\n plant[\"variable operating cost ($/tonne)\"] = varCost;\n plant[\"storage\"] = {};\n plant[\"storage\"][\"cost ($/tonne)\"] = storCost;\n plant[\"storage\"][\"limit (tonne)\"] = storLimit;\n plant[\"disposal cost ($/tonne)\"] = dispCost;\n plant[\"disposal limit (tonne)\"] = dispLimit;\n parameters[\"inflation rate (%)\"] = String((R - 1) * 100);\n }\n }\n\n return [plant, parameters];\n};\n\nexport const importData = (original) => {\n [\"parameters\", \"plants\", \"products\"].forEach((key) => {\n if (!(key in original)) {\n throw \"File not recognized.\";\n }\n });\n\n const result = {};\n result.parameters = importDict(original.parameters);\n [\"building period (years)\"].forEach((k) => {\n result.parameters[k] = JSON.stringify(original.parameters[k]);\n });\n result.parameters[\"inflation rate (%)\"] = \"0\";\n\n // Import products\n result.products = {};\n for (const [prodName, origProdDict] of Object.entries(original.products)) {\n const [recoveredProd, recoveredParams] = importProduct(origProdDict);\n result.products[prodName] = recoveredProd;\n result.parameters = { ...result.parameters, ...recoveredParams };\n }\n\n // Import plants\n result.plants = {};\n for (const [plantName, origPlantDict] of Object.entries(original.plants)) {\n const [recoveredPlant, recoveredParams] = importPlant(origPlantDict);\n result.plants[plantName] = recoveredPlant;\n result.parameters = { ...result.parameters, ...recoveredParams };\n }\n\n return result;\n};\n","import React, { useState, useRef, useEffect } from \"react\";\nimport { openDB, deleteDB, wrap, unwrap } from \"idb\";\n\nimport \"./index.css\";\nimport PipelineBlock from \"./PipelineBlock\";\nimport ParametersBlock from \"./ParametersBlock\";\nimport ProductBlock from \"./ProductBlock\";\nimport PlantBlock from \"./PlantBlock\";\nimport Button from \"./Button\";\nimport Header from \"./Header\";\nimport Footer from \"./Footer\";\nimport { defaultData, defaultPlant, defaultProduct } from \"./defaults\";\nimport { randomPosition } from \"./PipelineBlock\";\nimport { exportData, importData } from \"./export\";\nimport { generateFile } from \"./csv\";\n\nconst setDefaults = (actualDict, defaultDict) => {\n for (const [key, defaultValue] of Object.entries(defaultDict)) {\n if (!(key in actualDict)) {\n if (typeof defaultValue === \"object\") {\n actualDict[key] = { ...defaultValue };\n } else {\n actualDict[key] = defaultValue;\n }\n }\n }\n};\n\nconst cleanDict = (dict, defaultDict) => {\n for (const key of Object.keys(dict)) {\n if (!(key in defaultDict)) {\n delete dict[key];\n }\n }\n};\n\nconst fixLists = (dict, blacklist, stringify) => {\n for (const [key, val] of Object.entries(dict)) {\n if (blacklist.includes(key)) continue;\n if (Array.isArray(val)) {\n // Replace constant lists by a single number\n let isConstant = true;\n for (let i = 1; i < val.length; i++) {\n if (val[i - 1] !== val[i]) {\n isConstant = false;\n break;\n }\n }\n if (isConstant) dict[key] = val[0];\n\n // Convert lists to JSON strings\n if (stringify) dict[key] = JSON.stringify(dict[key]);\n }\n if (typeof val === \"object\") {\n fixLists(val, blacklist, stringify);\n }\n }\n};\n\nconst openRelogDB = async () => {\n const dbPromise = await openDB(\"RELOG\", 1, {\n upgrade(db) {\n db.createObjectStore(\"casebuilder\");\n },\n });\n return dbPromise;\n};\n\nconst InputPage = () => {\n const fileElem = useRef();\n let [data, setData] = useState(defaultData);\n\n const save = async (data) => {\n const db = await openRelogDB();\n await db.put(\"casebuilder\", data, \"data\");\n };\n\n useEffect(async () => {\n const db = await openRelogDB();\n const data = await db.get(\"casebuilder\", \"data\");\n if (data) setData(data);\n }, []);\n\n const promptName = (prevData) => {\n const name = prompt(\"Name\");\n if (!name || name.length === 0) return;\n if (name in prevData.products || name in prevData.plants) return;\n return name;\n };\n\n const onAddPlant = () => {\n setData((prevData) => {\n const name = promptName(prevData);\n if (name === undefined) return prevData;\n const newData = { ...prevData };\n const [x, y] = randomPosition();\n newData.plants[name] = {\n ...defaultPlant,\n x: x,\n y: y,\n };\n save(newData);\n return newData;\n });\n };\n\n const onAddProduct = () => {\n setData((prevData) => {\n const name = promptName(prevData);\n if (name === undefined) return prevData;\n const newData = { ...prevData };\n const [x, y] = randomPosition();\n console.log(x, y);\n newData.products[name] = {\n ...defaultProduct,\n x: x,\n y: y,\n };\n save(newData);\n return newData;\n });\n };\n\n const onRenamePlant = (prevName, newName) => {\n setData((prevData) => {\n const newData = { ...prevData };\n newData.plants[newName] = newData.plants[prevName];\n delete newData.plants[prevName];\n save(newData);\n return newData;\n });\n };\n\n const onRenameProduct = (prevName, newName) => {\n setData((prevData) => {\n const newData = { ...prevData };\n newData.products[newName] = newData.products[prevName];\n delete newData.products[prevName];\n for (const [, plant] of Object.entries(newData.plants)) {\n if (plant.input === prevName) {\n plant.input = newName;\n }\n let outputFound = false;\n for (const [outputName] of Object.entries(\n plant[\"outputs (tonne/tonne)\"]\n )) {\n if (outputName === prevName) outputFound = true;\n }\n if (outputFound) {\n plant[\"outputs (tonne/tonne)\"][newName] =\n plant[\"outputs (tonne/tonne)\"][prevName];\n delete plant[\"outputs (tonne/tonne)\"][prevName];\n }\n }\n save(newData);\n return newData;\n });\n };\n\n const onMovePlant = (plantName, x, y) => {\n setData((prevData) => {\n const newData = { ...prevData };\n newData.plants[plantName].x = x;\n newData.plants[plantName].y = y;\n save(newData);\n return newData;\n });\n };\n\n const onMoveProduct = (productName, x, y) => {\n setData((prevData) => {\n const newData = { ...prevData };\n newData.products[productName].x = x;\n newData.products[productName].y = y;\n save(newData);\n return newData;\n });\n };\n\n const onRemovePlant = (plantName) => {\n setData((prevData) => {\n const newData = { ...prevData };\n delete newData.plants[plantName];\n save(newData);\n return newData;\n });\n };\n\n const onRemoveProduct = (productName) => {\n setData((prevData) => {\n const newData = { ...prevData };\n delete newData.products[productName];\n for (const [, plant] of Object.entries(newData.plants)) {\n if (plant.input === productName) {\n delete plant.input;\n }\n let outputFound = false;\n for (const [outputName] of Object.entries(\n plant[\"outputs (tonne/tonne)\"]\n )) {\n if (outputName === productName) outputFound = true;\n }\n if (outputFound) {\n delete plant[\"outputs (tonne/tonne)\"][productName];\n }\n }\n save(newData);\n return newData;\n });\n };\n\n const onSetPlantInput = (plantName, productName) => {\n setData((prevData) => {\n const newData = { ...prevData };\n newData.plants[plantName].input = productName;\n save(newData);\n return newData;\n });\n };\n\n const onAddPlantOutput = (plantName, productName) => {\n setData((prevData) => {\n if (productName in prevData.plants[plantName][\"outputs (tonne/tonne)\"]) {\n return prevData;\n }\n const newData = { ...prevData };\n [\n \"outputs (tonne/tonne)\",\n \"disposal cost ($/tonne)\",\n \"disposal limit (tonne)\",\n ].forEach((key) => {\n newData.plants[plantName][key] = { ...newData.plants[plantName][key] };\n newData.plants[plantName][key][productName] = 0;\n });\n save(newData);\n return newData;\n });\n };\n\n const onSave = () => {\n generateFile(\"case.json\", JSON.stringify(exportData(data), null, 2));\n };\n\n const onClear = () => {\n const newData = JSON.parse(JSON.stringify(defaultData));\n setData(newData);\n save(newData);\n };\n\n const onLoad = (contents) => {\n const newData = importData(JSON.parse(contents));\n setData(newData);\n save(newData);\n };\n\n const onChange = (val, field1, field2) => {\n setData((prevData) => {\n const newData = { ...prevData };\n if (field2 !== undefined) {\n newData[field1][field2] = val;\n } else {\n newData[field1] = val;\n }\n save(newData);\n return newData;\n });\n };\n let productComps = [];\n for (const [prodName, prod] of Object.entries(data.products)) {\n productComps.push(\n