/**
* Utilities for handling entity duplication and name conflict resolution.
*
* @license BSD, see LICENSE.md.
*/
import {validateNumericInputs} from "ui_editor_util";
import {
Application,
DefinitionalStanza,
SimulationScenario,
Substance,
} from "ui_translator_components";
/**
* Result of name conflict resolution.
*/
class NameConflictResolution {
/**
* Create a new NameConflictResolution.
*
* @param {string} originalName - The original name that was requested.
* @param {string} resolvedName - The final name after conflict resolution.
*/
constructor(originalName, resolvedName) {
const self = this;
self._originalName = originalName;
self._resolvedName = resolvedName;
}
/**
* Check if the name was changed during conflict resolution.
*
* @returns {boolean} True if the name was changed, false otherwise.
*/
getNameChanged() {
const self = this;
return self._originalName !== self._resolvedName;
}
/**
* Get the final resolved name.
*
* @returns {string} The resolved name.
*/
getNewName() {
const self = this;
return self._resolvedName;
}
}
/**
* Resolves name conflicts by appending incrementing numbers until finding a unique name.
*
* @param {string} baseName - The initial desired name.
* @param {Set<string>} existingNames - Set of existing names to avoid conflicts with.
* @returns {NameConflictResolution} A resolution object with the result.
*/
function resolveNameConflict(baseName, existingNames) {
if (!existingNames.has(baseName)) {
return new NameConflictResolution(baseName, baseName);
}
let counter = 1;
let candidate = `${baseName} (${counter})`;
while (existingNames.has(candidate)) {
counter++;
candidate = `${baseName} (${counter})`;
}
return new NameConflictResolution(baseName, candidate);
}
/**
* Resolves substance name conflicts with special handling for effective substance names.
* This function handles the combination of substance and equipment model names.
*
* @param {string} baseName - The initial desired substance name.
* @param {Set<string>} existingNames - Set of existing substance names to avoid conflicts with.
* @returns {NameConflictResolution} A resolution object with the result.
*/
function resolveSubstanceNameConflict(baseName, existingNames) {
return resolveNameConflict(baseName, existingNames);
}
/**
* Presenter for managing entity duplication dialog functionality.
*/
class DuplicateEntityPresenter {
/**
* Creates a new DuplicateEntityPresenter.
*
* @param {Function} getCodeObj - Callback to get the current code object.
* @param {Function} onCodeObjUpdate - Callback when code object is updated.
*/
constructor(getCodeObj, onCodeObjUpdate) {
const self = this;
self._getCodeObj = getCodeObj;
self._onCodeObjUpdate = onCodeObjUpdate;
self._dialog = document.getElementById("duplicate-entity-dialog");
self._duplicateLink = document.querySelector(".duplicate-entity-link");
self._setupDialog();
}
/**
* Set up dialog event handlers and dynamic behavior.
*
* @private
*/
_setupDialog() {
const self = this;
self._setupDuplicateLink();
self._setupEntityType();
self._setupSourceEntity();
self._setupEquipmentModel();
self._setupSaveButton();
self._setupCancelButton();
}
/**
* Set up duplicate link click handler.
*
* @private
*/
_setupDuplicateLink() {
const self = this;
self._duplicateLink.addEventListener("click", (event) => {
event.preventDefault();
self._refreshEntityDropdown();
self._dialog.showModal();
});
}
/**
* Set up entity type change handler.
*
* @private
*/
_setupEntityType() {
const self = this;
const entityTypeInput = self._dialog.querySelector(".duplicate-entity-type-input");
entityTypeInput.addEventListener("change", () => {
self._refreshEntityDropdown();
self._updateNewNameSuggestion();
self._toggleSubstanceSpecificFields();
});
}
/**
* Set up source entity change handler.
*
* @private
*/
_setupSourceEntity() {
const self = this;
const sourceEntityInput = self._dialog.querySelector(".duplicate-source-entity-input");
sourceEntityInput.addEventListener("change", () => {
self._updateNewNameSuggestion();
const entityType = self._dialog.querySelector(".duplicate-entity-type-input").value;
if (entityType === "substance") {
self._refreshApplicationDropdown();
}
});
}
/**
* Set up equipment model change handler.
*
* @private
*/
_setupEquipmentModel() {
const self = this;
const equipmentModelInput = self._dialog.querySelector(".duplicate-equipment-model-input");
equipmentModelInput.addEventListener("input", () => {
self._updateNewNameSuggestion();
});
}
/**
* Set up save button click handler.
*
* @private
*/
_setupSaveButton() {
const self = this;
const saveButton = self._dialog.querySelector(".save-button");
saveButton.addEventListener("click", (event) => {
event.preventDefault();
self._duplicateEntity();
});
}
/**
* Set up cancel button click handler.
*
* @private
*/
_setupCancelButton() {
const self = this;
const cancelButton = self._dialog.querySelector(".cancel-button");
cancelButton.addEventListener("click", (event) => {
event.preventDefault();
self._dialog.close();
});
}
/**
* Refresh the source entity dropdown based on selected entity type.
*
* @private
*/
_refreshEntityDropdown() {
const self = this;
const entityType = self._dialog.querySelector(".duplicate-entity-type-input").value;
const sourceDropdown = self._dialog.querySelector(".duplicate-source-entity-input");
const codeObj = self._getCodeObj();
// Clear existing options
sourceDropdown.innerHTML = "";
const entityMappers = {
application: () => codeObj.getApplications().map((app) => ({
name: app.getName(),
value: app.getName(),
})),
policy: () => codeObj.getPolicies().map((policy) => ({
name: policy.getName(),
value: policy.getName(),
})),
simulation: () => codeObj.getScenarios().map((scenario) => ({
name: scenario.getName(),
value: scenario.getName(),
})),
substance: () => codeObj.getSubstances().map((substance) => ({
name: substance.getName(),
value: substance.getName(),
application: self._findSubstanceApplication(codeObj, substance.getName()),
})),
};
const mapper = entityMappers[entityType];
const entities = mapper ? mapper() : [];
// Add options to dropdown
if (entities.length === 0) {
const option = document.createElement("option");
option.value = "";
option.textContent = `No ${entityType}s available`;
option.disabled = true;
sourceDropdown.appendChild(option);
} else {
entities.forEach((entity) => {
const option = document.createElement("option");
option.value = entity.value;
option.textContent = entity.name;
sourceDropdown.appendChild(option);
});
}
}
/**
* Update the new name suggestion based on selected source entity.
*
* @private
*/
_updateNewNameSuggestion() {
const self = this;
const entityType = self._dialog.querySelector(".duplicate-entity-type-input").value;
const sourceEntity = self._dialog.querySelector(".duplicate-source-entity-input").value;
const newNameInput = self._dialog.querySelector(".duplicate-new-name-input");
const equipmentModel = self._dialog.querySelector(".duplicate-equipment-model-input").value;
if (sourceEntity && sourceEntity !== "") {
if (entityType === "substance" && equipmentModel && equipmentModel.trim() !== "") {
newNameInput.value = `${sourceEntity} - ${equipmentModel.trim()}`;
} else {
newNameInput.value = `${sourceEntity} Copy`;
}
}
}
/**
* Execute the entity duplication operation.
*
* @private
*/
_duplicateEntity() {
const self = this;
const entityType = self._dialog.querySelector(".duplicate-entity-type-input").value;
const sourceEntityName = self._dialog.querySelector(".duplicate-source-entity-input").value;
const newName = self._dialog.querySelector(".duplicate-new-name-input").value.trim();
// Validation
if (!sourceEntityName) {
alert("Please select a source entity to duplicate.");
return;
}
if (!newName) {
alert("Please enter a name for the new entity.");
return;
}
const codeObj = self._getCodeObj();
try {
const duplicators = {
application: () => self._duplicateApplication(codeObj, sourceEntityName, newName),
policy: () => self._duplicatePolicy(codeObj, sourceEntityName, newName),
simulation: () => self._duplicateSimulation(codeObj, sourceEntityName, newName),
substance: () => self._duplicateSubstance(codeObj, sourceEntityName, newName),
};
const duplicator = duplicators[entityType];
if (!duplicator) {
throw new Error(`Unknown entity type: ${entityType}`);
}
duplicator();
if (!self._validateBeforeUpdate(entityType)) {
return; // User cancelled validation, keep dialog open
}
self._onCodeObjUpdate(codeObj);
self._dialog.close();
} catch (error) {
console.error("Error duplicating entity:", error);
alert(`Error duplicating ${entityType}: ${error.message}`);
}
}
/**
* Validate the duplicate dialog before updating code object.
* Runs validation checks that may show confirmation dialogs.
*
* @param {string} entityType - The type of entity being duplicated
* @returns {boolean} True if validation passes or user confirms, false if user cancels
* @private
*/
_validateBeforeUpdate(entityType) {
const self = this;
// Validate numeric inputs and get user confirmation for potentially invalid values
if (!validateNumericInputs(self._dialog, entityType)) {
return false; // User cancelled numeric input validation
}
// For simulations, also check duration
if (entityType === "simulation") {
if (!validateSimulationDuration(self._dialog)) {
return false; // User cancelled simulation duration validation
}
}
return true;
}
/**
* Duplicate an application with deep copy of all substances and commands.
*
* @param {Program} codeObj - The program object to modify
* @param {string} sourceAppName - Name of source application
* @param {string} newName - Name for the duplicated application
* @private
*/
_duplicateApplication(codeObj, sourceAppName, newName) {
const self = this;
const sourceApp = codeObj.getApplication(sourceAppName);
if (!sourceApp) {
throw new Error(`Application "${sourceAppName}" not found`);
}
// Deep copy substances with all their commands
const duplicatedSubstances = sourceApp.getSubstances().map((substance) => {
return self._deepCopySubstance(substance);
});
// Create new application with copied substances
const newApplication = new Application(
newName,
duplicatedSubstances,
sourceApp._isModification,
sourceApp._isCompatible,
);
codeObj.addApplication(newApplication);
}
/**
* Duplicate a policy with deep copy of all applications and commands.
*
* @param {Program} codeObj - The program object to modify
* @param {string} sourcePolicyName - Name of source policy
* @param {string} newName - Name for the duplicated policy
* @private
*/
_duplicatePolicy(codeObj, sourcePolicyName, newName) {
const self = this;
const sourcePolicy = codeObj.getPolicy(sourcePolicyName);
if (!sourcePolicy) {
throw new Error(`Policy "${sourcePolicyName}" not found`);
}
// Deep copy applications within the policy
const duplicatedApplications = sourcePolicy.getApplications().map((app) => {
const duplicatedSubstances = app.getSubstances().map((substance) => {
return self._deepCopySubstance(substance);
});
return new Application(
app.getName(),
duplicatedSubstances,
app._isModification,
app._isCompatible,
);
});
// Create new policy stanza
const newPolicy = new DefinitionalStanza(
newName,
duplicatedApplications,
sourcePolicy._isCompatible,
);
codeObj.insertPolicy(null, newPolicy);
}
/**
* Duplicate a simulation scenario.
*
* @param {Program} codeObj - The program object to modify
* @param {string} sourceSimName - Name of source simulation
* @param {string} newName - Name for the duplicated simulation
* @private
*/
_duplicateSimulation(codeObj, sourceSimName, newName) {
const self = this;
const sourceSimulation = codeObj.getScenario(sourceSimName);
if (!sourceSimulation) {
throw new Error(`Simulation "${sourceSimName}" not found`);
}
const newSimulation = new SimulationScenario(
newName,
self._copyPolicyArray(sourceSimulation),
sourceSimulation.getYearStart(),
sourceSimulation.getYearEnd(),
sourceSimulation._isCompatible,
);
codeObj.insertScenario(null, newSimulation);
}
/**
* Create a copy of the policy names array from a simulation scenario.
*
* @param {SimulationScenario} sourceSimulation - The source simulation to copy from
* @returns {Array<string>} A new array containing copied policy names
* @private
*/
_copyPolicyArray(sourceSimulation) {
const self = this;
return [...sourceSimulation.getPolicyNames()];
}
/**
* Show/hide substance-specific fields (equipment model and application selection)
* based on entity type selection.
*
* @private
*/
_toggleSubstanceSpecificFields() {
const self = this;
const entityType = self._dialog.querySelector(".duplicate-entity-type-input").value;
const equipmentSection = self._dialog.querySelector(".equipment-model-section");
const applicationSection = self._dialog.querySelector(".application-selection-section");
if (entityType === "substance") {
equipmentSection.style.display = "block";
applicationSection.style.display = "block";
self._refreshApplicationDropdown();
} else {
equipmentSection.style.display = "none";
applicationSection.style.display = "none";
self._dialog.querySelector(".duplicate-equipment-model-input").value = "";
self._dialog.querySelector(".duplicate-target-application-input").innerHTML = "";
}
}
/**
* Find which application contains a specific substance.
*
* @param {Program} codeObj - The program object
* @param {string} substanceName - Name of substance to find
* @returns {string} Application name containing the substance
* @private
*/
_findSubstanceApplication(codeObj, substanceName) {
const self = this;
const applications = codeObj.getApplications();
for (const app of applications) {
if (app.getSubstances().some((sub) => sub.getName() === substanceName)) {
return app.getName();
}
}
return null;
}
/**
* Refresh the application dropdown with available applications.
*
* @private
*/
_refreshApplicationDropdown() {
const self = this;
const applicationDropdown = self._dialog.querySelector(".duplicate-target-application-input");
const sourceEntityName = self._dialog.querySelector(".duplicate-source-entity-input").value;
const codeObj = self._getCodeObj();
// Clear existing options
applicationDropdown.innerHTML = "";
const applications = codeObj.getApplications().map((app) => ({
name: app.getName(),
value: app.getName(),
}));
if (applications.length === 0) {
const option = document.createElement("option");
option.value = "";
option.textContent = "No applications available";
option.disabled = true;
applicationDropdown.appendChild(option);
return;
}
// Find source substance's application for default selection
const sourceApplicationName = self._findSubstanceApplication(codeObj, sourceEntityName);
// Add application options
applications.forEach((app) => {
const option = document.createElement("option");
option.value = app.value;
option.textContent = app.name;
// Select source application by default
if (app.value === sourceApplicationName) {
option.selected = true;
}
applicationDropdown.appendChild(option);
});
}
/**
* Duplicate a substance with optional equipment model for compound naming.
*
* @param {Program} codeObj - The program object to modify
* @param {string} sourceSubstanceName - Name of source substance
* @param {string} newName - Name for the duplicated substance (may be compound)
* @private
*/
_duplicateSubstance(codeObj, sourceSubstanceName, newName) {
const self = this;
// Get selected target application
const targetApplicationName = self._dialog.querySelector(
".duplicate-target-application-input",
).value;
if (!targetApplicationName) {
throw new Error("No applications available for substance duplication");
}
// Find target application
const targetApplication = codeObj.getApplications().find(
(app) => app.getName() === targetApplicationName,
);
if (!targetApplication) {
throw new Error(`Target application "${targetApplicationName}" not found`);
}
// Find the source substance across all applications
let sourceSubstance = null;
for (const app of codeObj.getApplications()) {
const substance = app.getSubstances().find((sub) => sub.getName() === sourceSubstanceName);
if (substance) {
sourceSubstance = substance;
break;
}
}
if (!sourceSubstance) {
throw new Error(`Substance "${sourceSubstanceName}" not found`);
}
// Check for duplicate names within target application (not globally)
const existingSubstancesInTarget = targetApplication.getSubstances();
if (existingSubstancesInTarget.some((sub) => sub.getName() === newName)) {
throw new Error(
`Substance "${newName}" already exists in application "${targetApplicationName}"`,
);
}
// Deep copy the substance with new name
const duplicatedSubstance = self._deepCopySubstance(sourceSubstance);
duplicatedSubstance._name = newName;
// Add to the selected target application
targetApplication.insertSubstance(null, duplicatedSubstance);
}
/**
* Deep copy a substance with all its commands and properties.
*
* @param {Substance} sourceSubstance - The substance to copy
* @returns {Substance} Deep copied substance
* @private
*/
_deepCopySubstance(sourceSubstance) {
const self = this;
// Commands are immutable, so we can share references
const copiedCharges = sourceSubstance.getInitialCharges();
const copiedLimits = sourceSubstance.getLimits();
const copiedChanges = sourceSubstance.getChanges();
const copiedEqualsGhg = sourceSubstance.getEqualsGhg();
const copiedEqualsKwh = sourceSubstance.getEqualsKwh();
const copiedRecharges = sourceSubstance.getRecharges();
const copiedRecycles = sourceSubstance.getRecycles();
const copiedReplaces = sourceSubstance.getReplaces();
const copiedRetire = sourceSubstance.getRetire();
const copiedSetVals = sourceSubstance.getSetVals();
const copiedEnables = sourceSubstance.getEnables();
const copiedAssumeMode = sourceSubstance.getAssumeMode();
// Create new substance with copied commands (matching constructor parameter order)
return new Substance(
sourceSubstance.getName(),
copiedCharges,
copiedLimits,
copiedChanges,
copiedEqualsGhg,
copiedEqualsKwh,
copiedRecharges,
copiedRecycles,
copiedReplaces,
copiedRetire,
copiedSetVals,
copiedEnables,
sourceSubstance._isModification,
sourceSubstance._isCompatible,
copiedAssumeMode,
);
}
}
export {
NameConflictResolution,
resolveNameConflict,
resolveSubstanceNameConflict,
DuplicateEntityPresenter,
};