UNPKG

node-red-contrib-cii-calculator

Version:

A custom Node-RED node for calculating CII with user input.

463 lines (437 loc) 14.4 kB
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); };