node-red-contrib-cii-calculator
Version:
A custom Node-RED node for calculating CII with user input.
463 lines (437 loc) • 14.4 kB
JavaScript
module.exports = function (RED) {
function CIICalculatorNode(config) {
RED.nodes.createNode(this, config);
this.name = config.name;
this.ship_type = config.ship_type;
this.gross_tonnage = config.gross_tonnage;
this.summer_load_dwt = config.summer_load_dwt;
this.reduction_factor = config.reduction_factor;
this.diesel_gas_oil_consumption = config.diesel_gas_oil_consumption;
this.light_fuel_oil_consumption = config.light_fuel_oil_consumption;
this.heavy_fuel_oil_consumption = config.heavy_fuel_oil_consumption;
this.lpg_propane_consumption = config.lpg_propane_consumption;
this.lpg_butane_consumption = config.lpg_butane_consumption;
this.lpg_consumption = config.lpg_consumption;
this.methanol_consumption = config.methanol_consumption;
this.ethanol_consumption = config.ethanol_consumption;
this.total_distance_travelled = config.total_distance_travelled;
const node = this;
node.on("input", function (msg) {
// 선박 정보 배열(알고리즘 고정 값)
const shipTable = [
{
type: "Bulk Carrier",
useMetric: "DWT",
capacityRanges: [
{
min: 279000,
max: Infinity,
a: 279000,
c: 0.622,
exp: [0.86, 0.94, 1.06, 1.18],
},
{
min: 0,
max: 278999,
a: 4745,
c: 0.622,
exp: [0.86, 0.94, 1.06, 1.18],
},
],
},
{
type: "Gas Carrier",
useMetric: "DWT",
capacityRanges: [
{
min: 65000,
max: Infinity,
a: 144100000000,
c: 2.071,
exp: [0.85, 0.95, 1.06, 1.25],
},
{
min: 0,
max: 64999,
a: 8104,
c: 0.639,
exp: [0.85, 0.95, 1.06, 1.25],
},
],
},
{
type: "Tanker",
useMetric: "DWT",
capacityRanges: [
{
min: 0,
max: Infinity,
a: 5247,
c: 0.61,
exp: [0.82, 0.93, 1.08, 1.28],
},
],
},
{
type: "Container Ship",
useMetric: "DWT",
capacityRanges: [
{
min: 0,
max: Infinity,
a: 1984,
c: 0.489,
exp: [0.83, 0.91, 1.12, 1.19],
},
],
},
{
type: "General Cargo Ship",
useMetric: "DWT",
capacityRanges: [
{
min: 20000,
max: Infinity,
a: 31948,
c: 0.792,
exp: [0.83, 0.94, 1.06, 1.19],
},
{
min: 0,
max: 19999,
a: 588,
c: 0.389,
exp: [0.83, 0.94, 1.06, 1.19],
},
],
},
{
type: "Refrigerated Cargo Carrier",
useMetric: "DWT",
capacityRanges: [
{
min: 0,
max: Infinity,
a: 4600,
c: 0.557,
exp: [0.78, 0.91, 1.07, 1.2],
},
],
},
{
type: "Combination Carrier",
useMetric: "DWT",
capacityRanges: [
{
min: 0,
max: Infinity,
a: 40853,
c: 0.812,
exp: [0.87, 0.96, 1.06, 1.14],
},
],
},
{
type: "LNG Carrier",
useMetric: "DWT",
capacityRanges: [
{
min: 100000,
max: Infinity,
a: 9.827,
c: 0.0,
exp: [0.89, 0.98, 1.06, 1.13],
},
{
min: 65000,
max: 99999,
a: 144800000000000,
c: 2.673,
exp: [0.89, 0.98, 1.06, 1.13],
},
{
min: 0,
max: 64999,
a: 144800000000000,
c: 2.673,
exp: [0.89, 0.98, 1.06, 1.13],
},
],
},
{
type: "Ro-Ro Cargo Ship (Vehicle Carrier)",
useMetric: "GT",
capacityRanges: [
{
min: 0,
max: Infinity,
a: 5739,
c: 0.631,
exp: [0.78, 0.92, 1.1, 1.37],
},
],
},
{
type: "Ro-Ro Cargo Ship",
useMetric: "DWT",
capacityRanges: [
{
min: 0,
max: Infinity,
a: 10952,
c: 0.637,
exp: [0.66, 0.9, 1.11, 1.37],
},
],
},
{
type: "Ro-Ro Passenger Ship",
useMetric: "GT",
capacityRanges: [
{
min: 0,
max: Infinity,
a: 7540,
c: 0.587,
exp: [0.72, 0.9, 1.09, 1.41],
},
],
},
{
type: "Cruise Passenger Ship",
useMetric: "GT",
capacityRanges: [
{
min: 0,
max: Infinity,
a: 930,
c: 0.383,
exp: [0.87, 0.95, 1.06, 1.16],
},
],
},
];
// 감소율 데이터
const reductionFactors = [
{ year: 2019, reduction: 0 },
{ year: 2020, reduction: 1 },
{ year: 2021, reduction: 2 },
{ year: 2022, reduction: 3 },
{ year: 2023, reduction: 5 },
{ year: 2024, reduction: 7 },
{ year: 2025, reduction: 9 },
{ year: 2026, reduction: 11 },
];
// 연료 데이터를 객체로 구성
const fuels = {
diesel_gas_oil_consumption:
parseFloat(node.diesel_gas_oil_consumption) ||
parseFloat(msg.payload.diesel_gas_oil_consumption) ||
0,
light_fuel_oil_consumption:
parseFloat(node.light_fuel_oil_consumption) ||
parseFloat(msg.payload.light_fuel_oil_consumption) ||
0,
heavy_fuel_oil_consumption:
parseFloat(node.heavy_fuel_oil_consumption) ||
parseFloat(msg.payload.heavy_fuel_oil_consumption) ||
0,
lpg_propane_consumption:
parseFloat(node.lpg_propane_consumption) ||
parseFloat(msg.payload.lpg_propane_consumption) ||
0,
lpg_butane_consumption:
parseFloat(node.lpg_butane_consumption) ||
parseFloat(msg.payload.lpg_butane_consumption) ||
0,
lpg_consumption:
parseFloat(node.lpg_consumption) ||
parseFloat(msg.payload.lpg_consumption) ||
0,
methanol_consumption:
parseFloat(node.methanol_consumption) ||
parseFloat(msg.payload.methanol_consumption) ||
0,
ethanol_consumption:
parseFloat(node.ethanol_consumption) ||
parseFloat(msg.payload.ethanol_consumption) ||
0,
};
const totalDistanceTravelled =
parseFloat(node.total_distance_travelled) ||
parseFloat(msg.payload.total_distance_travelled);
const reductionFactorYear =
parseInt(node.reduction_factor) ||
parseInt(msg.payload.reduction_factor);
const shipInfo = {
type: node.ship_type || msg.payload.ship_type,
GT:
parseFloat(node.gross_tonnage) ||
parseFloat(msg.payload.gross_tonnage),
DWT:
parseFloat(node.summer_load_dwt) ||
parseFloat(msg.payload.summer_load_dwt),
};
const findShipInfo = ({ type, GT, DWT }) => {
const ship = shipTable.find((ship) => ship.type === type);
if (!ship) return { a: null, c: null };
const metricValue = ship.useMetric === "DWT" ? DWT : GT;
const range = ship.capacityRanges.find(
(range) => metricValue >= range.min && metricValue <= range.max
);
return range
? { a: range.a, c: range.c, useMetric: ship.useMetric }
: { a: null, c: null, useMetric: null };
};
const { a, c, useMetric } = findShipInfo(shipInfo);
const getCurrentYearReduction = () => {
const currentReduction = reductionFactors.find(
(factor) => factor.year === reductionFactorYear
);
return currentReduction ? currentReduction.reduction : null;
};
const reductionFactor = getCurrentYearReduction();
const capacity = useMetric === "DWT" ? shipInfo.DWT : shipInfo.GT;
const requiredCII = (
((100 - reductionFactor) / 100) *
a *
Math.pow(capacity, -c)
).toFixed(2);
const fuelData = {
diesel_gas_oil_consumption: { CF: 3.206 },
light_fuel_oil_consumption: { CF: 3.151 },
heavy_fuel_oil_consumption: { CF: 3.114 },
lpg_propane_consumption: { CF: 3.0 },
lpg_butane_consumption: { CF: 3.03 },
lpg_consumption: { CF: 2.75 },
methanol_consumption: { CF: 1.375 },
ethanol_consumption: { CF: 1.913 },
};
function calculateTotalCO2Emissions(fuels) {
let totalEmissions = 0;
for (const [fuelKey, amount] of Object.entries(fuels)) {
const fuelInfo = fuelData[fuelKey];
if (fuelInfo) {
const emissions = fuelInfo.CF * amount;
totalEmissions += emissions;
} else {
node.error(`Unknown or unsupported fuel type: ${fuelKey}`);
}
}
return totalEmissions;
}
const totalEmissions = calculateTotalCO2Emissions(fuels) * 1000000;
function calculateCapacity(shipInfo, totalDistance) {
const ship = shipTable.find((s) => s.type === shipInfo.type);
if (!ship) {
node.error("Ship type not found");
return 0; // 오류 발생 시 0 반환
}
const metric = ship.useMetric === "GT" ? shipInfo.GT : shipInfo.DWT;
return metric * totalDistance;
}
const resultW = calculateCapacity(shipInfo, totalDistanceTravelled);
const attainedCII =
resultW > 0 ? (totalEmissions / resultW).toFixed(2) : 0;
function calculateCIRating(shipType, capacity, requiredCII) {
const ship = shipTable.find((s) => s.type === shipType);
if (!ship) {
node.error("Ship type not found");
return null;
}
const capacityRange = ship.capacityRanges.find(
(range) => capacity >= range.min && capacity <= range.max
);
if (!capacityRange) {
node.error("Capacity range not found for this ship type");
return null;
}
const ratings = {
A: Math.round(requiredCII * capacityRange.exp[0] * 100) / 100,
B: Math.round(requiredCII * capacityRange.exp[1] * 100) / 100,
C: Number(requiredCII),
D: Math.round(requiredCII * capacityRange.exp[2] * 100) / 100,
E: Math.round(requiredCII * capacityRange.exp[3] * 100) / 100,
};
return ratings;
}
const rating = calculateCIRating(shipInfo.type, capacity, requiredCII);
function determineCIRating(shipType, capacity, requiredCII, attainedCII) {
const ratings = calculateCIRating(shipType, capacity, requiredCII);
if (!ratings) {
return "Unable to determine rating: Ship type or capacity range not found.";
}
if (attainedCII <= ratings.A) {
return "A";
} else if (attainedCII <= ratings.B) {
return "B";
} else if (attainedCII <= ratings.C) {
return "C";
} else if (attainedCII <= ratings.D) {
return "D";
} else {
return "E";
}
}
const grade = determineCIRating(
shipInfo.type,
capacity,
requiredCII,
attainedCII
);
function calculateYearlyRequiredCIIAndRating(shipInfo) {
const { a, c, useMetric } = findShipInfo(shipInfo);
const capacity = useMetric === "DWT" ? shipInfo.DWT : shipInfo.GT;
const yearlyData = [];
for (let year = 2019; year <= 2026; year++) {
const yearReduction = reductionFactors.find(
(factor) => factor.year === year
);
if (yearReduction) {
const requiredCII = (
((100 - yearReduction.reduction) / 100) *
a *
Math.pow(capacity, -c)
).toFixed(2);
const ratings = calculateCIRating(
shipInfo.type,
capacity,
requiredCII
);
const data = {
year: year,
requiredCII: parseFloat(requiredCII),
ratings: ratings,
};
if (year === new Date().getFullYear()) {
data.attainedCII = parseFloat(attainedCII);
}
yearlyData.push(data);
}
}
return yearlyData;
}
const yearlyData = calculateYearlyRequiredCIIAndRating(shipInfo);
const chartData = yearlyData.map((data) => ({
year: data.year,
requiredCII: data.requiredCII,
attainedCII: data.attainedCII,
A: data.ratings.A,
B: data.ratings.B,
C: data.ratings.C,
D: data.ratings.D,
E: data.ratings.E,
}));
msg.payload = {
required_cii: requiredCII,
attained_cii: attainedCII,
grade,
rating,
reduction_factor: reductionFactor,
chartData,
};
node.send(msg);
});
}
RED.nodes.registerType("cii-calculator", CIICalculatorNode);
};