/**
* Strategies for initing and setting UI elements.
*
* @license BSD, see LICENSE.md.
*/
import {ParsedYear, YearMatcher} from "duration";
import {EngineNumber} from "engine_number";
import {NumberParseUtil} from "number_parse_util";
import {
Command,
LimitCommand,
RechargeCommand,
RecycleCommand,
ReplaceCommand,
} from "ui_translator_components";
import {
setFieldValue,
getFieldValue,
setEngineNumberValue,
getEngineNumberValue,
invertNumberString,
setDuring,
} from "ui_editor_util";
/**
* Initializes a set command UI element.
*
* @param {Object} itemObj - The command object to initialize from.
* @param {HTMLElement} root - The root element containing the UI.
* @param {Object} codeObj - Optional code object containing substances data.
* @param {string} context - Context for stream detection ('consumption' or 'policy').
* @param {Object} streamUpdater - StreamSelectionAvailabilityUpdater instance.
*/
function initSetCommandUi(itemObj, root, codeObj, context, streamUpdater) {
// Update stream options based on enabled streams - use context-aware detection
const enabledStreams = streamUpdater.getEnabledStreamsForCurrentContext(codeObj, null, context);
const targetSelect = root.querySelector(".set-target-input");
streamUpdater.updateStreamOptionStates(targetSelect, enabledStreams);
setFieldValue(root.querySelector(".set-target-input"), itemObj, "sales", (x) =>
x.getTarget(),
);
setEngineNumberValue(
root.querySelector(".set-amount-input"),
root.querySelector(".set-units-input"),
itemObj,
new EngineNumber(1, "mt"),
(x) => x.getValue(),
);
setDuring(
root.querySelector(".duration-subcomponent"),
itemObj,
new YearMatcher(new ParsedYear(1), new ParsedYear(1)),
true,
);
}
/**
* Reads values from a set command UI element.
*
* @param {HTMLElement} root - The root element containing the UI.
* @returns {Command} A new Command object with the UI values.
*/
function readSetCommandUi(root) {
const target = getFieldValue(root.querySelector(".set-target-input"));
const amount = getEngineNumberValue(
root.querySelector(".set-amount-input"),
root.querySelector(".set-units-input"),
);
const duration = readDurationUi(root.querySelector(".duration-subcomponent"));
return new Command("setVal", target, amount, duration);
}
/**
* Initializes a change command UI element.
*
* @param {Object} itemObj - The command object to initialize from.
* @param {HTMLElement} root - The root element containing the UI.
* @param {Object} codeObj - Optional code object containing substances data.
* @param {string} context - Context for stream detection ('consumption' or 'policy').
* @param {Object} streamUpdater - StreamSelectionAvailabilityUpdater instance.
*/
function initChangeCommandUi(itemObj, root, codeObj, context, streamUpdater) {
// Update stream options based on enabled streams - use context-aware detection
const enabledStreams = streamUpdater.getEnabledStreamsForCurrentContext(codeObj, null, context);
const targetSelect = root.querySelector(".change-target-input");
streamUpdater.updateStreamOptionStates(targetSelect, enabledStreams);
setFieldValue(root.querySelector(".change-target-input"), itemObj, "sales", (x) =>
x.getTarget(),
);
setFieldValue(root.querySelector(".change-sign-input"), itemObj, "+", (x) =>
x.getValue() < 0 ? "-" : "+",
);
setFieldValue(root.querySelector(".change-amount-input"), itemObj, 5, (x) => {
if (x.getValue() === null || x.getValue().getValue() === null) {
return 5;
}
const valueSigned = x.getValue().getValue();
const valueUnsigned = Math.abs(valueSigned);
return valueUnsigned;
});
setFieldValue(root.querySelector(".change-units-input"), itemObj, "% / year", (x) => {
if (x.getValue() === null) {
return "% / year";
}
return x.getValue().getUnits();
});
setDuring(
root.querySelector(".duration-subcomponent"),
itemObj,
new YearMatcher(new ParsedYear(2), new ParsedYear(10)),
true,
);
}
/**
* Reads values from a change command UI element.
*
* @param {HTMLElement} root - The root element containing the UI.
* @returns {Command} A new Command object with the UI values.
*/
function readChangeCommandUi(root) {
const target = getFieldValue(root.querySelector(".change-target-input"));
const invert = getFieldValue(root.querySelector(".change-sign-input")) === "-";
const numberParser = new NumberParseUtil();
const amountInput = getFieldValue(root.querySelector(".change-amount-input"));
const result = numberParser.parseFlexibleNumber(amountInput);
if (!result.isSuccess()) {
throw new Error(`Invalid amount format: ${result.getError()}`);
}
const amount = result.getNumber() * (invert ? -1 : 1);
const units = getFieldValue(root.querySelector(".change-units-input"));
// Preserve original string format, applying sign inversion if needed
const originalString = invert ? invertNumberString(amountInput) : amountInput.trim();
const amountWithUnits = new EngineNumber(amount, units, originalString);
const duration = readDurationUi(root.querySelector(".duration-subcomponent"));
return new Command("change", target, amountWithUnits, duration);
}
/**
* Initializes a limit command UI widget.
*
* @param {Object} itemObj - The command object to initialize from.
* @param {HTMLElement} root - The root element containing the UI.
* @param {Object} codeObj - The code object containing available substances.
* @param {string} context - Context for stream detection ('consumption' or 'policy').
* @param {Object} streamUpdater - StreamSelectionAvailabilityUpdater instance.
*/
function initLimitCommandUi(itemObj, root, codeObj, context, streamUpdater) {
const substances = codeObj.getSubstances();
const substanceNamesDup = substances.map((x) => x.getName());
const substanceNames = Array.of(...new Set(substanceNamesDup));
const substanceSelect = d3.select(root.querySelector(".substances-select"));
substanceSelect.html("");
substanceSelect
.selectAll("option")
.data(substanceNames)
.enter()
.append("option")
.attr("value", (x) => x)
.text((x) => x);
setFieldValue(root.querySelector(".limit-type-input"), itemObj, "cap", (x) => x.getTypeName());
setFieldValue(root.querySelector(".limit-target-input"), itemObj, "sales", (x) => x.getTarget());
setEngineNumberValue(
root.querySelector(".limit-amount-input"),
root.querySelector(".limit-units-input"),
itemObj,
new EngineNumber(1, "mt"),
(x) => x.getValue(),
);
setFieldValue(root.querySelector(".displacing-type-input"), itemObj, "", (x) =>
x && x.getDisplacingType ? x.getDisplacingType() : "",
);
setFieldValue(root.querySelector(".displacing-target-input"), itemObj, "", (x) =>
x && x.getDisplacing ? (x.getDisplacing() === null ? "" : x.getDisplacing()) : "",
);
setDuring(
root.querySelector(".duration-subcomponent"),
itemObj,
new YearMatcher(new ParsedYear(2), new ParsedYear(10)),
true,
);
// Add event listener to update options when substance changes
const substanceSelectElement = root.querySelector(".substances-select");
const updateLimitTargetOptions = () => {
const limitTargetSelect = root.querySelector(".limit-target-input");
const enabledStreams = streamUpdater.getEnabledStreamsForCurrentContext(codeObj, null, context);
streamUpdater.updateStreamOptionStates(limitTargetSelect, enabledStreams);
};
const updateDisplacingOptions = () => {
const displacingTargetSelect = root.querySelector(".displacing-target-input");
if (displacingTargetSelect) {
const enabledStreams = streamUpdater.getEnabledStreamsForCurrentContext(
codeObj, null, context,
);
streamUpdater.updateStreamOptionStates(displacingTargetSelect, enabledStreams);
}
};
substanceSelectElement.addEventListener("change", updateLimitTargetOptions);
substanceSelectElement.addEventListener("change", updateDisplacingOptions);
// Initial update of stream options
updateLimitTargetOptions();
updateDisplacingOptions();
}
/**
* Reads values from a limit command UI widget.
*
* @param {HTMLElement} root - The root element containing the UI.
* @returns {LimitCommand} A new LimitCommand object with the UI values.
*/
function readLimitCommandUi(root) {
const limitType = getFieldValue(root.querySelector(".limit-type-input"));
const target = getFieldValue(root.querySelector(".limit-target-input"));
const amount = getEngineNumberValue(
root.querySelector(".limit-amount-input"),
root.querySelector(".limit-units-input"),
);
const displacingTypeRaw = getFieldValue(root.querySelector(".displacing-type-input"));
const displacingType = displacingTypeRaw || "";
const displacingTargetRaw = getFieldValue(root.querySelector(".displacing-target-input"));
const displacingTarget = displacingTargetRaw === "" ? null : displacingTargetRaw;
const duration = readDurationUi(root.querySelector(".duration-subcomponent"));
return new LimitCommand(limitType, target, amount, duration, displacingTarget, displacingType);
}
/**
* Initializes a recharge command UI widget.
*
* @param {Object} itemObj - The recharge command object or null for new commands.
* @param {HTMLElement} root - The root element containing the UI.
* @param {Object} codeObj - The code object for context.
*/
function initRechargeCommandUi(itemObj, root, codeObj) {
// All recharge objects are now RechargeCommand instances
const populationGetter = (x) => {
const engineNumber = x.getPopulationEngineNumber();
return engineNumber.getOriginalString() || String(engineNumber.getValue());
};
const populationUnitsGetter = (x) => {
return x.getPopulationEngineNumber().getUnits();
};
const volumeGetter = (x) => {
const engineNumber = x.getVolumeEngineNumber();
return engineNumber.getOriginalString() || String(engineNumber.getValue());
};
const volumeUnitsGetter = (x) => {
return x.getVolumeEngineNumber().getUnits();
};
setFieldValue(
root.querySelector(".recharge-population-input"),
itemObj,
"5",
populationGetter,
);
setFieldValue(
root.querySelector(".recharge-population-units-input"),
itemObj,
"%",
populationUnitsGetter,
);
setFieldValue(
root.querySelector(".recharge-volume-input"),
itemObj,
"0.85",
volumeGetter,
);
setFieldValue(
root.querySelector(".recharge-volume-units-input"),
itemObj,
"kg / unit",
volumeUnitsGetter,
);
// Set up duration using standard pattern
setDuring(
root.querySelector(".duration-subcomponent"),
itemObj,
new YearMatcher(new ParsedYear(1), new ParsedYear(1)),
true,
);
}
/**
* Reads values from a recharge command UI widget.
*
* @param {HTMLElement} root - The root element containing the UI.
* @returns {RechargeCommand} A new RechargeCommand object with the UI values.
*/
function readRechargeCommandUi(root) {
const population = getFieldValue(root.querySelector(".recharge-population-input"));
const populationUnits = getFieldValue(
root.querySelector(".recharge-population-units-input"),
);
const volume = getFieldValue(root.querySelector(".recharge-volume-input"));
const volumeUnits = getFieldValue(
root.querySelector(".recharge-volume-units-input"),
);
// Read duration using standard pattern
const duration = readDurationUi(root.querySelector(".duration-subcomponent"));
// Create EngineNumber objects with original string preservation
const populationEngineNumber = new EngineNumber(population, populationUnits, population);
const volumeEngineNumber = new EngineNumber(volume, volumeUnits, volume);
return new RechargeCommand(
populationEngineNumber,
volumeEngineNumber,
duration,
);
}
/**
* Initializes a recycle command UI widget.
*
* @param {Object} itemObj - The command object to initialize from.
* @param {HTMLElement} root - The root element containing the UI.
* @param {Object} codeObj - Optional code object containing substances data.
* @param {string} context - Context for stream detection ('consumption' or 'policy').
* @param {Object} streamUpdater - StreamSelectionAvailabilityUpdater instance.
*/
function initRecycleCommandUi(itemObj, root, codeObj, context, streamUpdater) {
// Update stream options based on enabled streams - use context-aware detection
const enabledStreams = streamUpdater.getEnabledStreamsForCurrentContext(codeObj, null, context);
const displacingSelect = root.querySelector(".displacing-input");
streamUpdater.updateStreamOptionStates(displacingSelect, enabledStreams);
setEngineNumberValue(
root.querySelector(".recycle-amount-input"),
root.querySelector(".recycle-units-input"),
itemObj,
new EngineNumber(10, "%"),
(x) => x.getTarget(),
);
setEngineNumberValue(
root.querySelector(".recycle-reuse-amount-input"),
root.querySelector(".recycle-reuse-units-input"),
itemObj,
new EngineNumber(10, "%"),
(x) => x.getValue(),
);
setFieldValue(root.querySelector(".displacing-input"), itemObj, "", (x) =>
x && x.getDisplacing ? (x.getDisplacing() === null ? "" : x.getDisplacing()) : "",
);
setFieldValue(
root.querySelector(".recycle-induction-amount-input"),
itemObj,
"100",
(x) => {
if (x && x.getInduction) {
const induction = x.getInduction();
if (induction === null) {
return "100";
} else if (induction === "default") {
return "100";
} else if (induction instanceof EngineNumber) {
return induction.getValue().toString();
} else {
return induction.toString();
}
} else {
return "100";
}
},
);
setFieldValue(root.querySelector(".recycle-stage-input"), itemObj, "recharge", (x) =>
x && x.getStage ? x.getStage() : "recharge",
);
setDuring(
root.querySelector(".duration-subcomponent"),
itemObj,
new YearMatcher(new ParsedYear(2), new ParsedYear(10)),
true,
);
}
/**
* Validates and normalizes induction input values.
*
* @param {string} rawValue - The raw input value
* @returns {string|EngineNumber} Normalized induction value
*/
function validateInductionInput(rawValue) {
if (rawValue === "") {
throw new Error("Induction rate is required. Please enter a value between 0-100.");
}
if (rawValue === "default") {
return "default";
}
const numValue = parseFloat(rawValue);
if (isNaN(numValue)) {
throw new Error(`Invalid induction rate: "${rawValue}". Must be a number between 0-100.`);
}
if (numValue < 0 || numValue > 100) {
throw new Error(`Induction rate ${numValue}% is out of range. Must be between 0-100%.`);
}
return new EngineNumber(numValue, "%", rawValue.trim());
}
/**
* Reads values from a recycle command UI widget.
*
* @param {HTMLElement} root - The root element containing the UI.
* @returns {Command} A new Command object with the UI values.
*/
function readRecycleCommandUi(root) {
const collection = getEngineNumberValue(
root.querySelector(".recycle-amount-input"),
root.querySelector(".recycle-units-input"),
);
const reuse = getEngineNumberValue(
root.querySelector(".recycle-reuse-amount-input"),
root.querySelector(".recycle-reuse-units-input"),
);
// Add induction handling with validation
const inductionRaw = getFieldValue(
root.querySelector(".recycle-induction-amount-input"),
);
const induction = validateInductionInput(inductionRaw);
const stage = getFieldValue(root.querySelector(".recycle-stage-input"));
const duration = readDurationUi(root.querySelector(".duration-subcomponent"));
// RecycleCommand constructor: (target, value, duration, stage, induction)
return new RecycleCommand(collection, reuse, duration, stage, induction);
}
/**
* Initializes a replace command UI widget.
*
* @param {Object} itemObj - The command object to initialize from.
* @param {HTMLElement} root - The root element containing the UI.
* @param {Object} codeObj - The code object containing available substances.
* @param {string} context - Context for stream detection ('consumption' or 'policy').
* @param {Object} streamUpdater - StreamSelectionAvailabilityUpdater instance.
*/
function initReplaceCommandUi(itemObj, root, codeObj, context, streamUpdater) {
const substances = codeObj.getSubstances();
const substanceNamesDup = substances.map((x) => x.getName());
const substanceNames = Array.of(...new Set(substanceNamesDup));
const substanceSelect = d3.select(root.querySelector(".substances-select"));
substanceSelect.html("");
substanceSelect
.selectAll("option")
.data(substanceNames)
.enter()
.append("option")
.attr("value", (x) => x)
.text((x) => x);
setEngineNumberValue(
root.querySelector(".replace-amount-input"),
root.querySelector(".replace-units-input"),
itemObj,
new EngineNumber(10, "%"),
(x) => x.getVolume(),
);
// Add event listener to update options when substance changes
const substanceSelectElement = root.querySelector(".substances-select");
const updateReplaceTargetOptions = () => {
const replaceTargetSelect = root.querySelector(".replace-target-input");
const enabledStreams = streamUpdater.getEnabledStreamsForCurrentContext(codeObj, null, context);
streamUpdater.updateStreamOptionStates(replaceTargetSelect, enabledStreams);
};
const updateDisplacingOptions = () => {
const displacingSelect = root.querySelector(".displacing-input");
if (displacingSelect) {
const enabledStreams = streamUpdater.getEnabledStreamsForCurrentContext(
codeObj, null, context,
);
streamUpdater.updateStreamOptionStates(displacingSelect, enabledStreams);
}
};
substanceSelectElement.addEventListener("change", updateReplaceTargetOptions);
substanceSelectElement.addEventListener("change", updateDisplacingOptions);
setFieldValue(root.querySelector(".replace-target-input"), itemObj, "sales", (x) =>
x.getSource(),
);
setFieldValue(root.querySelector(".replace-replacement-input"), itemObj, substanceNames[0], (x) =>
x.getDestination(),
);
setDuring(
root.querySelector(".duration-subcomponent"),
itemObj,
new YearMatcher(new ParsedYear(2), new ParsedYear(10)),
true,
);
// Initial update of stream options
updateReplaceTargetOptions();
updateDisplacingOptions();
}
/**
* Reads values from a replace command UI widget.
*
* @param {HTMLElement} root - The root element containing the UI.
* @returns {ReplaceCommand} A new ReplaceCommand object with the UI values.
*/
function readReplaceCommandUi(root) {
const target = getFieldValue(root.querySelector(".replace-target-input"));
const amount = getEngineNumberValue(
root.querySelector(".replace-amount-input"),
root.querySelector(".replace-units-input"),
);
const replacement = getFieldValue(root.querySelector(".replace-replacement-input"));
const duration = readDurationUi(root.querySelector(".duration-subcomponent"));
return new ReplaceCommand(amount, target, replacement, duration);
}
/**
* Reads duration values from a duration UI widget.
*
* @param {HTMLElement} root - The root element containing the UI.
* @returns {YearMatcher} A new YearMatcher object with the UI values.
*/
function readDurationUi(root) {
const durationType = getFieldValue(root.querySelector(".duration-type-input"));
const targets = {
"in year": {min: "duration-start", max: "duration-start"},
"during all years": {min: null, max: null},
"starting in year": {min: "duration-start", max: null},
"ending in year": {min: null, max: "duration-end"},
"during years": {min: "duration-start", max: "duration-end"},
}[durationType];
const getYearValue = (x) => (x === null ? null : root.querySelector("." + x).value);
const minYear = getYearValue(targets["min"]);
const maxYear = getYearValue(targets["max"]);
return new YearMatcher(
minYear ? new ParsedYear(minYear) : null,
maxYear ? new ParsedYear(maxYear) : null,
);
}
export {
initSetCommandUi,
readSetCommandUi,
initChangeCommandUi,
readChangeCommandUi,
initLimitCommandUi,
readLimitCommandUi,
initRechargeCommandUi,
readRechargeCommandUi,
initRecycleCommandUi,
validateInductionInput,
readRecycleCommandUi,
initReplaceCommandUi,
readReplaceCommandUi,
readDurationUi,
};