/**
* Data structures for report and visualization functionality.
*
* @license BSD, see LICENSE.md.
*/
import {EngineNumber, makeNumberUnambiguousString} from "engine_number";
import {AggregatedResult, AttributeToExporterResult} from "engine_struct";
/**
* Builder class for creating metric computation strategies.
*
* Builder which handles the construction of metric processing pipelines
* including transformations and unit conversions.
*/
class MetricStrategyBuilder {
/**
* Create a new MetricStrategyBuilder instance.
*
* Create a new MetricStrategyBuilder instance, initalizing all strategy
* components to null requiring them to be specified later.
*/
constructor() {
const self = this;
self._strategies = {};
self._metric = null;
self._submetric = null;
self._units = null;
self._strategy = null;
self._transformation = null;
}
/**
* Set the metric name for the strategy.
*
* @param {string} metric - The metric name (e.g., 'sales', 'emissions').
*/
setMetric(metric) {
const self = this;
self._metric = metric;
}
/**
* Set the submetric name for the strategy.
*
* @param {string} submetric - The submetric name (e.g., 'all', 'import').
*/
setSubmetric(submetric) {
const self = this;
self._submetric = submetric;
}
/**
* Set the units for the strategy output.
*
* @param {string} units - The units specification (e.g., 'MtCO2e / yr').
*/
setUnits(units) {
const self = this;
self._units = units;
}
/**
* Set the core computation strategy.
*
* @param {Function} strategy - The function that implements the core metric
* computation.
*/
setStrategy(strategy) {
const self = this;
self._strategy = strategy;
}
/**
* Set the transformation to apply after the core strategy.
*
* @param {Function} transformation - The function that transforms the strategy
* output.
*/
setTransformation(transformation) {
const self = this;
self._transformation = transformation;
}
/**
* Add the configured strategy to the strategies collection.
*
* @throws {string} If any required component is null.
*/
add() {
const self = this;
self._requireCompleteDefinition();
const fullNamePieces = [self._metric, self._submetric, self._units];
const fullName = fullNamePieces.join(":");
const innerStrategy = self._strategy;
const innerTransformation = self._transformation;
const execute = (filterSet) => {
const result = innerStrategy(filterSet);
if (result === null) {
return null;
}
const transformed = innerTransformation(result);
return transformed;
};
self._strategies[fullName] = execute;
// Also register "/ year" variant if "/ yr" key exists
if (fullName.includes("/ yr")) {
const yearVariant = fullName.replace("/ yr", "/ year");
self._strategies[yearVariant] = execute;
}
}
/**
* Build and return the complete strategies object.
* @returns {Object} The strategies object containing all added strategies.
*/
build() {
const self = this;
return self._strategies;
}
/**
* Verify that all required components have been set.
* @private
* @throws {string} If any required component is null.
*/
_requireCompleteDefinition() {
const self = this;
const pieces = [
self._metric,
self._submetric,
self._units,
self._strategy,
self._transformation,
];
const nullPieces = pieces.filter((x) => x === null);
const numNullPieces = nullPieces.map((x) => 1).reduce((a, b) => a + b, 0);
if (numNullPieces > 0) {
throw "Encountered null values on MetricStrategyBuilder";
}
}
}
/**
* Facade which simplifies access to engine outputs.
*
* Wrapper class for report data that provides filtering and aggregation
* capabilities over simplified engine outputs.
*/
class ReportDataWrapper {
/**
* Create a new report data wrapper.
*
* @param {Array<EngineResult>} innerData - The raw report data to wrap.
*/
constructor(innerData) {
const self = this;
self._innerData = innerData;
self._innerDataExporterAttributed = null;
const strategyBuilder = new MetricStrategyBuilder();
self._addEmissionsStrategies(strategyBuilder);
self._addSalesStrategies(strategyBuilder);
self._addConsumptionStrategies(strategyBuilder);
self._addPopulationStrategies(strategyBuilder);
self._addBankStrategies(strategyBuilder);
self._metricStrategies = strategyBuilder.build();
}
/**
* Get the raw underlying data.
*
* @param {FilterSet} filterSet - Filter set with attribution settings to
* apply.
* @returns {Array<EngineResult>} The raw data.
*/
getRawData(filterSet) {
const self = this;
if (filterSet.getAttributeImporter()) {
return self._innerData;
} else {
if (self._innerDataExporterAttributed === null) {
self._innerDataExporterAttributed = self._buildExporterAttributed(self._innerData);
}
return self._innerDataExporterAttributed;
}
}
/**
* Get metric value based on filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber} The filtered metric value.
*/
getMetric(filterSet) {
const self = this;
const metric = filterSet.getFullMetricName();
const metricStrategy = self._metricStrategies[metric];
// Check if metricStrategy exists and is a function
if (typeof metricStrategy !== "function") {
console.warn("MetricStrategy is not a function for metric:", metric);
// Trigger global error recovery if available
const kigaliAppFound = window.kigaliApp;
const resetAvailable = kigaliAppFound &&
typeof kigaliAppFound.resetVisualizationState === "function";
if (resetAvailable) {
kigaliAppFound.resetVisualizationState();
}
return null;
}
try {
const value = metricStrategy(filterSet);
return value;
} catch (error) {
// Handle "metricStrategy is not a function" and other strategy execution errors
if (self._isMetricStrategyError(error)) {
console.warn("MetricStrategy execution failed - not a function:", error.message);
// Trigger global error recovery if available
const kigaliAppFound = window.kigaliApp;
const resetAvailable = kigaliAppFound &&
typeof kigaliAppFound.resetVisualizationState === "function";
if (resetAvailable) {
kigaliAppFound.resetVisualizationState();
}
return null;
} else {
// Re-throw other errors as they may be legitimate validation errors
throw error;
}
}
}
/**
* Get dimension values based on a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {Set<*>} Set of dimension values.
*/
getDimensionValues(filterSet) {
const self = this;
const dimension = filterSet.getDimension();
const strategy = {
simulations: () => self.getScenarios(filterSet),
applications: () => self.getApplications(filterSet),
substances: () => self.getSubstances(filterSet),
}[dimension];
const value = strategy();
return value;
}
/**
* Get scenarios matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {Set<string>} Set of scenario names.
*/
getScenarios(filterSet) {
const self = this;
if (filterSet.getScenario() === null) {
return new Set(self.getRawData(filterSet).map((x) => x.getScenarioName()));
} else {
return new Set([filterSet.getScenario()]);
}
}
/**
* Get the name of the first scenario available without applying any filter.
*
* @param {FilterSet} filterSet - The filter set indicating preprocessing
* options even though a filter is not applied.
* @returns {string|null} The first scenario name or null if no scenarios
* present.
*/
getFirstScenario(filterSet) {
const self = this;
const scenarios = self.getScenarios(filterSet);
for (const scenario of scenarios) {
return scenario;
}
return null;
}
/**
* Get applications matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {Set<string>} Set of application names.
*/
getApplications(filterSet) {
const self = this;
return self._getSetAfterFilter(filterSet, (x) => x.getApplication());
}
/**
* Get substances matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {Set<string>} Set of substance names.
*/
getSubstances(filterSet) {
const self = this;
return self._getSetAfterFilter(filterSet, (x) => x.getSubstance());
}
/**
* Get years matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {Set<number>} Set of years.
*/
getYears(filterSet) {
const self = this;
return self._getSetAfterFilter(filterSet, (x) => x.getYear());
}
/**
* Get consumption value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The consumption value, or null if no matching
* results.
*/
getGhgConsumption(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getGhgConsumption();
}
/**
* Get the domestic consumption value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The domestic consumption value, or null if no
* matching results.
*/
getDomesticConsumption(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getDomesticConsumption();
}
/**
* Get the import consumption value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The import consumption value, or null if no
* matching results.
*/
getImportConsumption(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getImportConsumption();
}
/**
* Get the recycled consumption value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The recycled consumption value, or null if no
* matching results.
*/
getRecycleConsumption(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getRecycleConsumption();
}
/**
* Get export consumption value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The export consumption value, or null if no matching
* results.
*/
getExportConsumption(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getExportConsumption();
}
/**
* Get total emissions value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The total emissions value, or null if no matching
* results.
*/
getTotalEmissions(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getTotalEmissions();
}
/**
* Get recharge emissions value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The recharge emissions value, or null if no
* matching results.
*/
getRechargeEmissions(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getRechargeEmissions();
}
/**
* Get end-of-life emissions value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The end-of-life emissions value, or null if
* no matching results.
*/
getEolEmissions(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getEolEmissions();
}
/**
* Get export emissions value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The export emissions value, or null if no matching
* results.
*/
getExportEmissions(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getExportConsumption();
}
/**
* Get initial charge emissions value matching a given filter set.
*
* This is an informational metric representing the GHG potential of substance
* initially charged into equipment. Actual emissions occur later during recharge
* (leakage between servicings) or at end-of-life disposal.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The initial charge emissions value, or null if no matching
* results.
*/
getInitialChargeEmissions(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getInitialChargeEmissions();
}
/**
* Get sales value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The sales value, or null if no matching
* results.
*/
getSales(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getSales();
}
/**
* Get sales from imports matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The imports component of sales, or null if no
* matching results.
*/
getImport(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getImport();
}
/**
* Get sales from domestic manufacture matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The domestic manufacture component of sales,
* or null if no matching results.
*/
getDomestic(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getDomestic();
}
/**
* Get the recycled sales value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The recycled sales value, or null if no
* matching results.
*/
getRecycle(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getRecycle();
}
/**
* Get export value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The export value, or null if no matching
* results.
*/
getExport(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getExport();
}
/**
* Get population value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The population value, or null if no matching
* results.
*/
getPopulation(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getPopulation();
}
/**
* Get the amount of new equipment added.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The new equipment added, or null if no matching
* results.
*/
getPopulationNew(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getPopulationNew();
}
/**
* Get energy consumption value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The energy consumption value, or null if no
* matching results.
*/
getEnergyConsumption(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getEnergyConsumption();
}
/**
* Get bank kg value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The bank kg value, or null if no matching
* results.
*/
getBankKg(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getBankKg();
}
/**
* Get bank tCO2e value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The bank tCO2e value, or null if no matching
* results.
*/
getBankTco2e(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getBankTco2e();
}
/**
* Get bank change kg value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The bank change kg value, or null if no matching
* results.
*/
getBankChangeKg(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getBankChangeKg();
}
/**
* Get bank change tCO2e value matching a given filter set.
*
* @param {FilterSet} filterSet - The filter criteria to apply.
* @returns {EngineNumber|null} The bank change tCO2e value, or null if no matching
* results.
*/
getBankChangeTco2e(filterSet) {
const self = this;
const aggregated = self._getAggregatedAfterFilter(filterSet);
return aggregated === null ? null : aggregated.getBankChangeTco2e();
}
/**
* Normalize time units by removing " / year" and " / yr" suffixes.
*
* @private
* @param {string} units - The units string to normalize.
* @returns {string} The normalized units string without time suffixes.
*/
_normalizeTimeUnits(units) {
const self = this;
return units
.replace(" / year", "")
.replace(" / yr", "");
}
/**
* Validate that emissions units are in expected format.
*
* Verifies that the provided value has units in tCO2e format after time unit
* normalization, throwing an error if units are unexpected.
*
* @private
* @param {EngineNumber} val - The value to validate.
* @throws {string} If units are not tCO2e.
*/
_validateEmissionsUnits(val) {
const self = this;
const normalizedUnits = self._normalizeTimeUnits(val.getUnits());
if (normalizedUnits !== "tCO2e") {
throw "Unexpected emissions source units: " + val.getUnits();
}
}
/**
* Add emissions conversion strategies to the builder.
*
* Adds unit conversion strategies for emissions data including tCO2e, ktCO2e,
* MtCO2e, and kgCO2e scales with appropriate validation.
*
* @private
* @param {MetricStrategyBuilder} strategyBuilder - The strategy builder to
* configure.
*/
_addEmissionsConversion(strategyBuilder) {
const self = this;
// tCO2e scale conversions
strategyBuilder.setUnits("tCO2e / yr");
strategyBuilder.setTransformation((val) => {
self._validateEmissionsUnits(val);
return new EngineNumber(
val.getValue(), "tCO2e / yr", makeNumberUnambiguousString(val.getValue()),
);
});
strategyBuilder.add();
strategyBuilder.setUnits("ktCO2e / yr");
strategyBuilder.setTransformation((val) => {
self._validateEmissionsUnits(val);
const convertedValue = val.getValue() / 1000;
return new EngineNumber(
convertedValue, "ktCO2e / yr", makeNumberUnambiguousString(convertedValue),
);
});
strategyBuilder.add();
strategyBuilder.setUnits("MtCO2e / yr");
strategyBuilder.setTransformation((val) => {
self._validateEmissionsUnits(val);
const convertedValue = val.getValue() / 1000000;
return new EngineNumber(
convertedValue, "MtCO2e / yr", makeNumberUnambiguousString(convertedValue),
);
});
strategyBuilder.add();
// kgCO2e scale conversions
strategyBuilder.setUnits("kgCO2e / yr");
strategyBuilder.setTransformation((val) => {
self._validateEmissionsUnits(val);
const convertedValue = val.getValue() * 1000;
return new EngineNumber(
convertedValue, "kgCO2e / yr", makeNumberUnambiguousString(convertedValue),
);
});
strategyBuilder.add();
}
/**
* Add emissions strategies to the builder.
*
* Adds emission metrics including recharge, eol, export, initial charge, and
* custom emissions with appropriate unit conversions.
*
* @private
* @param {MetricStrategyBuilder} strategyBuilder - The strategy builder to
* configure.
*/
_addEmissionsStrategies(strategyBuilder) {
const self = this;
strategyBuilder.setMetric("emissions");
strategyBuilder.setSubmetric("recharge");
strategyBuilder.setStrategy((x) => self.getRechargeEmissions(x));
self._addEmissionsConversion(strategyBuilder);
strategyBuilder.setSubmetric("eol");
strategyBuilder.setStrategy((x) => self.getEolEmissions(x));
self._addEmissionsConversion(strategyBuilder);
strategyBuilder.setSubmetric("export");
strategyBuilder.setStrategy((x) => self.getExportEmissions(x));
self._addEmissionsConversion(strategyBuilder);
strategyBuilder.setSubmetric("initial charge");
strategyBuilder.setStrategy((x) => self.getInitialChargeEmissions(x));
self._addEmissionsConversion(strategyBuilder);
strategyBuilder.setSubmetric("custom");
strategyBuilder.setStrategy((filterSet) => {
const customDef = filterSet.getCustomDefinition("emissions");
if (!customDef || customDef.length === 0) {
return null;
}
// Map submetrics to their raw emission methods
const emissionMethods = {
"recharge": (x) => self.getRechargeEmissions(x),
"eol": (x) => self.getEolEmissions(x),
"export": (x) => self.getExportEmissions(x),
"initial charge": (x) => self.getInitialChargeEmissions(x),
};
const results = customDef.map((submetric) => {
const method = emissionMethods[submetric];
return method ? method(filterSet) : null;
}).filter((result) => result !== null);
if (results.length === 0) {
return null;
}
return results.reduce((a, b) => {
if (!a) {
return b;
}
if (!b) {
return a;
}
if (a.getUnits() !== b.getUnits()) {
throw new Error(
`Cannot combine incompatible units: ${a.getUnits()} and ${b.getUnits()}`,
);
}
const result = a.getValue() + b.getValue();
return new EngineNumber(result, a.getUnits(), makeNumberUnambiguousString(result));
});
});
self._addEmissionsConversion(strategyBuilder);
}
/**
* Validate that sales units are in expected format.
*
* Verifies that the provided value has units in kg format after time unit
* normalization, throwing an error if units are unexpected.
*
* @private
* @param {EngineNumber} value - The value to validate.
* @throws {string} If units are not kg.
*/
_validateSalesUnits(value) {
const self = this;
const normalizedUnits = self._normalizeTimeUnits(value.getUnits());
if (normalizedUnits !== "kg") {
throw "Unexpected sales units: " + value.getUnits();
}
}
/**
* Add kg and mt unit conversions to the builder.
*
* Creates strategies for converting sales data between kilogram and metric tonne
* units with validation.
*
* @private
* @param {MetricStrategyBuilder} strategyBuilder - The strategy builder to
* configure.
*/
_makeForKgAndMt(strategyBuilder) {
const self = this;
strategyBuilder.setUnits("mt / yr");
strategyBuilder.setTransformation((value) => {
self._validateSalesUnits(value);
const convertedValue = value.getValue() / 1000;
return new EngineNumber(
convertedValue, "mt / yr", makeNumberUnambiguousString(convertedValue),
);
});
strategyBuilder.add();
strategyBuilder.setUnits("kg / yr");
strategyBuilder.setTransformation((value) => {
self._validateSalesUnits(value);
return new EngineNumber(
value.getValue(), "kg / yr", makeNumberUnambiguousString(value.getValue()),
);
});
strategyBuilder.add();
}
/**
* Add sales strategies to the builder.
*
* Adds sales metrics including import, domestic, recycle, export, and custom
* sales with appropriate unit conversions.
*
* @private
* @param {MetricStrategyBuilder} strategyBuilder - The strategy builder to
* configure.
*/
_addSalesStrategies(strategyBuilder) {
const self = this;
strategyBuilder.setMetric("sales");
strategyBuilder.setSubmetric("import");
strategyBuilder.setStrategy((x) => self.getImport(x));
self._makeForKgAndMt(strategyBuilder);
strategyBuilder.setSubmetric("domestic");
strategyBuilder.setStrategy((x) => self.getDomestic(x));
self._makeForKgAndMt(strategyBuilder);
strategyBuilder.setSubmetric("recycle");
strategyBuilder.setStrategy((x) => self.getRecycle(x));
self._makeForKgAndMt(strategyBuilder);
strategyBuilder.setSubmetric("export");
strategyBuilder.setStrategy((x) => self.getExport(x));
self._makeForKgAndMt(strategyBuilder);
strategyBuilder.setSubmetric("custom");
strategyBuilder.setStrategy((filterSet) => {
const customDef = filterSet.getCustomDefinition("sales");
if (!customDef || customDef.length === 0) {
return null;
}
// Map submetrics to their raw sales methods
const salesMethods = {
"domestic": (x) => self.getDomestic(x),
"import": (x) => self.getImport(x),
"export": (x) => self.getExport(x),
"recycle": (x) => self.getRecycle(x),
};
const results = customDef.map((submetric) => {
const method = salesMethods[submetric];
return method ? method(filterSet) : null;
}).filter((result) => result !== null);
if (results.length === 0) {
return null;
}
return results.reduce((a, b) => {
if (!a) {
return b;
}
if (!b) {
return a;
}
if (a.getUnits() !== b.getUnits()) {
throw new Error(
`Cannot combine incompatible units: ${a.getUnits()} and ${b.getUnits()}`,
);
}
const result = a.getValue() + b.getValue();
return new EngineNumber(result, a.getUnits(), makeNumberUnambiguousString(result));
});
});
self._makeForKgAndMt(strategyBuilder);
}
/**
* Add consumption strategies to the builder.
*
* Adds consumption metrics including import, domestic, recycle, export, and
* custom consumption with appropriate unit conversions.
*
* @private
* @param {MetricStrategyBuilder} strategyBuilder - The strategy builder to
* configure.
*/
_addConsumptionStrategies(strategyBuilder) {
const self = this;
strategyBuilder.setMetric("sales");
strategyBuilder.setSubmetric("import");
strategyBuilder.setStrategy((x) => self.getImportConsumption(x));
self._addEmissionsConversion(strategyBuilder);
strategyBuilder.setSubmetric("domestic");
strategyBuilder.setStrategy((x) => self.getDomesticConsumption(x));
self._addEmissionsConversion(strategyBuilder);
strategyBuilder.setSubmetric("recycle");
strategyBuilder.setStrategy((x) => self.getRecycleConsumption(x));
self._addEmissionsConversion(strategyBuilder);
strategyBuilder.setSubmetric("export");
strategyBuilder.setStrategy((x) => self.getExportConsumption(x));
self._addEmissionsConversion(strategyBuilder);
strategyBuilder.setSubmetric("custom");
strategyBuilder.setStrategy((filterSet) => {
const customDef = filterSet.getCustomDefinition("sales");
if (!customDef || customDef.length === 0) {
return null;
}
// Map submetrics to their consumption equivalents
const consumptionMethods = {
"domestic": (x) => self.getDomesticConsumption(x),
"import": (x) => self.getImportConsumption(x),
"export": (x) => self.getExportConsumption(x),
"recycle": (x) => self.getRecycleConsumption(x),
};
const results = customDef.map((submetric) => {
const method = consumptionMethods[submetric];
return method ? method(filterSet) : null;
}).filter((result) => result !== null);
if (results.length === 0) {
return null;
}
return results.reduce((a, b) => {
if (!a) {
return b;
}
if (!b) {
return a;
}
if (a.getUnits() !== b.getUnits()) {
throw new Error(
`Cannot combine incompatible units: ${a.getUnits()} and ${b.getUnits()}`,
);
}
const result = a.getValue() + b.getValue();
return new EngineNumber(result, a.getUnits(), makeNumberUnambiguousString(result));
});
});
self._addEmissionsConversion(strategyBuilder);
}
/**
* Add thousand and million unit conversions to the builder.
*
* Creates strategies for converting population data to units, thousands of units,
* and millions of units with validation.
*
* @private
* @param {MetricStrategyBuilder} strategyBuilder - The strategy builder to
* configure.
*/
_makeForThousandAndMillion(strategyBuilder) {
const self = this;
strategyBuilder.setUnits("units");
strategyBuilder.setTransformation((value) => {
if (value.getUnits() !== "units") {
throw "Unexpected population units: " + value.getUnits();
}
return value;
});
strategyBuilder.add();
strategyBuilder.setUnits("thousand units");
strategyBuilder.setTransformation((value) => {
if (value.getUnits() !== "units") {
throw "Unexpected population units: " + value.getUnits();
}
const convertedValue = value.getValue() / 1000;
return new EngineNumber(
convertedValue, "thousands of units", makeNumberUnambiguousString(convertedValue),
);
});
strategyBuilder.add();
strategyBuilder.setUnits("million units");
strategyBuilder.setTransformation((value) => {
if (value.getUnits() !== "units") {
throw "Unexpected population units: " + value.getUnits();
}
const convertedValue = value.getValue() / 1000000;
return new EngineNumber(
convertedValue, "millions of units", makeNumberUnambiguousString(convertedValue),
);
});
strategyBuilder.add();
}
/**
* Convert energy value to kwh/yr.
*
* Validates that the provided value has kwh units after time unit normalization
* and returns a normalized EngineNumber in kwh/yr format.
*
* @private
* @param {EngineNumber} value - The energy value to convert.
* @returns {EngineNumber} The value in kwh/yr units.
* @throws {string} If units are not kwh.
*/
_getKwhYr(value) {
const self = this;
const normalizedUnits = self._normalizeTimeUnits(value.getUnits());
if (normalizedUnits !== "kwh") {
throw "Unexpected energy units: " + value.getUnits();
}
return new EngineNumber(
value.getValue(), "kwh / yr", makeNumberUnambiguousString(value.getValue()),
);
}
/**
* Add energy unit conversions to the builder.
*
* Creates strategies for converting energy consumption data to kwh/yr, mwh/yr,
* and gwh/yr units with validation.
*
* @private
* @param {MetricStrategyBuilder} strategyBuilder - The strategy builder to
* configure.
*/
_makeForEnergyUnits(strategyBuilder) {
const self = this;
strategyBuilder.setStrategy((x) => self.getEnergyConsumption(x));
strategyBuilder.setUnits("kwh / year");
strategyBuilder.setTransformation((value) => self._getKwhYr(value));
strategyBuilder.add();
strategyBuilder.setUnits("mwh / year");
strategyBuilder.setTransformation((value) => {
const kwhValue = self._getKwhYr(value);
const convertedValue = kwhValue.getValue() / 1000;
return new EngineNumber(
convertedValue, "mwh / yr", makeNumberUnambiguousString(convertedValue),
);
});
strategyBuilder.add();
strategyBuilder.setUnits("gwh / year");
strategyBuilder.setTransformation((value) => {
const kwhValue = self._getKwhYr(value);
const convertedValue = kwhValue.getValue() / 1000000;
return new EngineNumber(
convertedValue, "gwh / yr", makeNumberUnambiguousString(convertedValue),
);
});
strategyBuilder.add();
}
/**
* Add population strategies to the builder.
*
* Adds population and energy metrics including all population, new population,
* and energy consumption with appropriate unit conversions.
*
* @private
* @param {MetricStrategyBuilder} strategyBuilder - The strategy builder to
* configure.
*/
_addPopulationStrategies(strategyBuilder) {
const self = this;
strategyBuilder.setMetric("population");
strategyBuilder.setSubmetric("all");
strategyBuilder.setStrategy((x) => self.getPopulation(x));
self._makeForThousandAndMillion(strategyBuilder);
self._makeForEnergyUnits(strategyBuilder);
strategyBuilder.setSubmetric("new");
strategyBuilder.setStrategy((x) => self.getPopulationNew(x));
self._makeForThousandAndMillion(strategyBuilder);
self._makeForEnergyUnits(strategyBuilder);
}
/**
* Create a bank strategy that dispatches to kg or tCO2e methods.
*
* Returns a function that determines which method to use based on the requested
* units in the filterSet, supporting both substance weight (kg/mt) and emissions
* potential (tCO2e) representations.
*
* @private
* @param {Function} kgMethod - Method to call for kg-based units.
* @param {Function} tco2eMethod - Method to call for tCO2e-based units.
* @returns {Function} Strategy function that dispatches based on units.
* @throws {string} If units are unsupported.
*/
_makeBankStrategy(kgMethod, tco2eMethod) {
const self = this;
return (filterSet) => {
const units = filterSet.getUnits();
const normalizedUnits = self._normalizeTimeUnits(units);
// Determine which method to use based on requested units
if (normalizedUnits === "kg" || normalizedUnits === "mt") {
return kgMethod(filterSet);
} else if (normalizedUnits === "tCO2e") {
return tco2eMethod(filterSet);
} else {
throw "Unsupported bank units: " + units;
}
};
}
/**
* Add bank strategies to the builder.
*
* Adds bank metrics including all bank and new bank values with support for both
* substance weight (kg/mt) and emissions potential (tCO2e) units.
*
* @private
* @param {MetricStrategyBuilder} strategyBuilder - The strategy builder to
* configure.
*/
_addBankStrategies(strategyBuilder) {
const self = this;
strategyBuilder.setMetric("population");
// kg -> mt conversion for substance bank
strategyBuilder.setSubmetric("all");
strategyBuilder.setStrategy(self._makeBankStrategy(
(x) => self.getBankKg(x),
(x) => self.getBankTco2e(x),
));
strategyBuilder.setUnits("mt / yr");
strategyBuilder.setTransformation((value) => {
const normalizedUnits = self._normalizeTimeUnits(value.getUnits());
if (normalizedUnits !== "kg") {
throw "Unexpected bank units: " + value.getUnits();
}
const convertedValue = value.getValue() / 1000;
return new EngineNumber(
convertedValue, "mt / yr", makeNumberUnambiguousString(convertedValue),
);
});
strategyBuilder.add();
strategyBuilder.setSubmetric("all");
strategyBuilder.setStrategy(self._makeBankStrategy(
(x) => self.getBankKg(x),
(x) => self.getBankTco2e(x),
));
strategyBuilder.setUnits("kg / yr");
strategyBuilder.setTransformation((value) => {
const normalizedUnits = self._normalizeTimeUnits(value.getUnits());
if (normalizedUnits !== "kg") {
throw "Unexpected bank units: " + value.getUnits();
}
return new EngineNumber(
value.getValue(), "kg / yr", makeNumberUnambiguousString(value.getValue()),
);
});
strategyBuilder.add();
strategyBuilder.setSubmetric("all");
strategyBuilder.setStrategy(self._makeBankStrategy(
(x) => self.getBankKg(x),
(x) => self.getBankTco2e(x),
));
strategyBuilder.setUnits("tCO2e / yr");
strategyBuilder.setTransformation((value) => {
const normalizedUnits = self._normalizeTimeUnits(value.getUnits());
if (normalizedUnits !== "tCO2e") {
throw "Unexpected bank units: " + value.getUnits();
}
return new EngineNumber(
value.getValue(), "tCO2e / yr", makeNumberUnambiguousString(value.getValue()),
);
});
strategyBuilder.add();
// Same pattern for "new" submetric
strategyBuilder.setSubmetric("new");
strategyBuilder.setStrategy(self._makeBankStrategy(
(x) => self.getBankChangeKg(x),
(x) => self.getBankChangeTco2e(x),
));
strategyBuilder.setUnits("mt / yr");
strategyBuilder.setTransformation((value) => {
const normalizedUnits = self._normalizeTimeUnits(value.getUnits());
if (normalizedUnits !== "kg") {
throw "Unexpected bank units: " + value.getUnits();
}
const convertedValue = value.getValue() / 1000;
return new EngineNumber(
convertedValue, "mt / yr", makeNumberUnambiguousString(convertedValue),
);
});
strategyBuilder.add();
strategyBuilder.setSubmetric("new");
strategyBuilder.setStrategy(self._makeBankStrategy(
(x) => self.getBankChangeKg(x),
(x) => self.getBankChangeTco2e(x),
));
strategyBuilder.setUnits("kg / yr");
strategyBuilder.setTransformation((value) => {
const normalizedUnits = self._normalizeTimeUnits(value.getUnits());
if (normalizedUnits !== "kg") {
throw "Unexpected bank units: " + value.getUnits();
}
return new EngineNumber(
value.getValue(), "kg / yr", makeNumberUnambiguousString(value.getValue()),
);
});
strategyBuilder.add();
strategyBuilder.setSubmetric("new");
strategyBuilder.setStrategy(self._makeBankStrategy(
(x) => self.getBankChangeKg(x),
(x) => self.getBankChangeTco2e(x),
));
strategyBuilder.setUnits("tCO2e / yr");
strategyBuilder.setTransformation((value) => {
const normalizedUnits = self._normalizeTimeUnits(value.getUnits());
if (normalizedUnits !== "tCO2e") {
throw "Unexpected bank units: " + value.getUnits();
}
return new EngineNumber(
value.getValue(), "tCO2e / yr", makeNumberUnambiguousString(value.getValue()),
);
});
strategyBuilder.add();
}
/**
* Get filtered set of values using a getter function.
*
* @private
* @param {FilterSet} filterSet - The filter criteria.
* @param {Function} getter - Function to get values from filtered results.
* @returns {Set<*>} Set of filtered values.
*/
_getSetAfterFilter(filterSet, getter) {
const self = this;
const afterFilter = self._applyFilterSet(filterSet);
const values = afterFilter.map(getter);
return new Set(values);
}
/**
* Get the initial aggregated result from the first one or two items.
*
* Creates the starting aggregated result using either a single item or the
* combination of the first two items, depending on array length.
*
* @private
* @param {Array<*>} afterFilter - The filtered results array.
* @returns {AggregatedResult} Initial aggregated result.
*/
_getStartAggregated(afterFilter) {
const self = this;
if (afterFilter.length === 1) {
return new AggregatedResult(afterFilter[0]);
} else {
return new AggregatedResult(afterFilter[0], afterFilter[1]);
}
}
/**
* Get aggregated result after applying filters.
*
* @private
* @param {FilterSet} filterSet - The filter criteria.
* @returns {AggregatedResult|null} Aggregated result or null if no matches.
*/
_getAggregatedAfterFilter(filterSet) {
const self = this;
const afterFilter = self._applyFilterSet(filterSet);
if (afterFilter.length === 0) {
return null;
}
// Combine all results using the AggregatedResult constructor which takes two objects
// and combines them. Start with the first two results and keep combining with the rest.
let aggregated = self._getStartAggregated(afterFilter);
// Continue combining with remaining results
for (let i = 2; i < afterFilter.length; i++) {
aggregated = new AggregatedResult(aggregated, afterFilter[i]);
}
return aggregated;
}
/**
* Apply filter set to get matching results.
*
* @private
* @param {FilterSet} filterSet - The filter criteria.
* @returns {Array<*>} Array of matching results.
*/
_applyFilterSet(filterSet) {
const self = this;
const step = (target, filterVal, getter) => {
if (filterVal === null) {
return target;
}
return target.filter((record) => {
const candidateVal = getter(record);
return candidateVal === filterVal;
});
};
const stepWithSubtype = (target, filterVal, getter) => {
if (filterVal === null) {
return target;
}
const isMetaGroup = filterVal.endsWith(" - All");
const withAllReplace = filterVal.replaceAll(" - All", " - ");
return target.filter((record) => {
const candidateVal = getter(record);
if (isMetaGroup) {
return candidateVal.startsWith(withAllReplace);
} else {
return candidateVal === filterVal;
}
});
};
const allRecords = self.getRawData(filterSet);
const scenario = filterSet.getScenario();
const withScenario = step(allRecords, scenario, (x) => x.getScenarioName());
const year = filterSet.getYear();
const withYear = step(withScenario, year, (x) => x.getYear());
const app = filterSet.getApplication();
const withApp = stepWithSubtype(withYear, app, (x) => x.getApplication());
const sub = filterSet.getSubstance();
const withSub = stepWithSubtype(withApp, sub, (x) => x.getSubstance());
return withSub;
}
/**
* Decorate the inner data (EngineResult) to attribute to exporter.
*
* @private
* @param {Array<EngineResult>} rawResults - The results with attribute
* to importer that should be decorated.
* @returns {Array<AttributeToExporterResult>} Decorated version.
*/
_buildExporterAttributed(rawResults) {
const self = this;
return rawResults.map((x) => new AttributeToExporterResult(x));
}
/**
* Check if an error is related to metricStrategy function issues.
*
* @private
* @param {Error} error - The error to check.
* @returns {boolean} True if this is a metricStrategy error that should trigger reset.
*/
_isMetricStrategyError(error) {
const self = this;
if (!error.message) {
return false;
}
if (!error.message.includes("not a function")) {
return false;
}
if (!error.message.toLowerCase().includes("metricstrategy")) {
return false;
}
return true;
}
}
export {ReportDataWrapper, MetricStrategyBuilder};