application planning
This commit is contained in:
198
backend/src/utils/applicationCalculations.js
Normal file
198
backend/src/utils/applicationCalculations.js
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Application Calculation Engine
|
||||
* Calculates product amounts, water amounts, and application speeds
|
||||
* based on area, product rates, and equipment specifications
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculate liquid application requirements
|
||||
* @param {number} areaSquareFeet - Area to be treated in square feet
|
||||
* @param {number} rateAmount - Product rate amount
|
||||
* @param {string} rateUnit - Product rate unit (oz/gal, oz/1000sqft, etc.)
|
||||
* @param {Object} equipment - Equipment specifications
|
||||
* @param {Object} nozzle - Nozzle specifications (optional)
|
||||
* @returns {Object} Calculation results
|
||||
*/
|
||||
function calculateLiquidApplication(areaSquareFeet, rateAmount, rateUnit, equipment, nozzle) {
|
||||
let productOunces = 0;
|
||||
let waterGallons = 0;
|
||||
let applicationSpeedMph = 3; // Default speed
|
||||
|
||||
const areaAcres = areaSquareFeet / 43560; // Convert to acres
|
||||
const area1000sqft = areaSquareFeet / 1000; // Convert to 1000 sq ft units
|
||||
|
||||
console.log(`Calculating liquid application:
|
||||
Area: ${areaSquareFeet} sq ft (${areaAcres.toFixed(3)} acres, ${area1000sqft.toFixed(1)} x 1000sqft)
|
||||
Rate: ${rateAmount} ${rateUnit}
|
||||
Equipment: ${equipment?.categoryName}
|
||||
Nozzle GPM: ${nozzle?.flowRateGpm || 'N/A'}`);
|
||||
|
||||
// Calculate product amount based on rate unit
|
||||
if (rateUnit.includes('oz/gal')) {
|
||||
// Rate is ounces per gallon of spray solution
|
||||
// Need to determine gallons of water needed first
|
||||
const gallonsPerAcre = 20; // Default 20 GPA for lawn applications
|
||||
waterGallons = areaAcres * gallonsPerAcre;
|
||||
productOunces = waterGallons * rateAmount;
|
||||
} else if (rateUnit.includes('oz/1000sqft') || rateUnit.includes('oz per 1000sqft')) {
|
||||
// Rate is ounces per 1000 square feet
|
||||
productOunces = area1000sqft * rateAmount;
|
||||
// Calculate water needed (typical lawn application is 0.5-1 gallon per 1000 sqft)
|
||||
waterGallons = area1000sqft * 0.75; // 0.75 gal per 1000 sqft default
|
||||
} else if (rateUnit.includes('oz/acre')) {
|
||||
// Rate is ounces per acre
|
||||
productOunces = areaAcres * rateAmount;
|
||||
waterGallons = areaAcres * 20; // 20 GPA default
|
||||
} else if (rateUnit.includes('fl oz/1000sqft') || rateUnit.includes('fl oz per 1000sqft')) {
|
||||
// Rate is fluid ounces per 1000 square feet
|
||||
productOunces = area1000sqft * rateAmount;
|
||||
waterGallons = area1000sqft * 0.75;
|
||||
} else {
|
||||
// Fallback: assume rate is per 1000 sq ft
|
||||
productOunces = area1000sqft * rateAmount;
|
||||
waterGallons = area1000sqft * 0.75;
|
||||
}
|
||||
|
||||
// Calculate application speed based on equipment and nozzle
|
||||
if (equipment && nozzle) {
|
||||
const sprayWidthFeet = equipment.sprayWidthFeet || 3; // Default 3 ft width
|
||||
const nozzleGpm = nozzle.flowRateGpm || 0.25; // Default 0.25 GPM
|
||||
const gallonsPerAcre = (waterGallons / areaAcres) || 20;
|
||||
|
||||
// Speed calculation: MPH = (GPM × 5940) / (Width × GPA)
|
||||
// Where 5940 is a conversion constant
|
||||
applicationSpeedMph = (nozzleGpm * 5940) / (sprayWidthFeet * gallonsPerAcre);
|
||||
|
||||
// Limit speed to reasonable range (1-8 MPH for lawn applications)
|
||||
applicationSpeedMph = Math.max(1, Math.min(8, applicationSpeedMph));
|
||||
}
|
||||
|
||||
const result = {
|
||||
productAmountOunces: Math.round(productOunces * 100) / 100, // Round to 2 decimals
|
||||
waterAmountGallons: Math.round(waterGallons * 100) / 100,
|
||||
applicationSpeedMph: Math.round(applicationSpeedMph * 100) / 100,
|
||||
gallonsPerAcre: Math.round((waterGallons / areaAcres) * 100) / 100,
|
||||
calculationDetails: {
|
||||
areaSquareFeet,
|
||||
areaAcres: Math.round(areaAcres * 1000) / 1000,
|
||||
area1000sqft: Math.round(area1000sqft * 10) / 10,
|
||||
rateAmount,
|
||||
rateUnit
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Liquid calculation result:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate granular application requirements
|
||||
* @param {number} areaSquareFeet - Area to be treated in square feet
|
||||
* @param {number} rateAmount - Product rate amount
|
||||
* @param {string} rateUnit - Product rate unit (lbs/1000sqft, lbs/acre, etc.)
|
||||
* @param {Object} equipment - Equipment specifications
|
||||
* @returns {Object} Calculation results
|
||||
*/
|
||||
function calculateGranularApplication(areaSquareFeet, rateAmount, rateUnit, equipment) {
|
||||
let productPounds = 0;
|
||||
let applicationSpeedMph = 3; // Default speed
|
||||
|
||||
const areaAcres = areaSquareFeet / 43560;
|
||||
const area1000sqft = areaSquareFeet / 1000;
|
||||
|
||||
console.log(`Calculating granular application:
|
||||
Area: ${areaSquareFeet} sq ft (${areaAcres.toFixed(3)} acres, ${area1000sqft.toFixed(1)} x 1000sqft)
|
||||
Rate: ${rateAmount} ${rateUnit}
|
||||
Equipment: ${equipment?.categoryName}`);
|
||||
|
||||
// Calculate product amount based on rate unit
|
||||
if (rateUnit.includes('lbs/1000sqft') || rateUnit.includes('lbs per 1000sqft') || rateUnit.includes('lb/1000sqft')) {
|
||||
// Rate is pounds per 1000 square feet
|
||||
productPounds = area1000sqft * rateAmount;
|
||||
} else if (rateUnit.includes('lbs/acre') || rateUnit.includes('lb/acre')) {
|
||||
// Rate is pounds per acre
|
||||
productPounds = areaAcres * rateAmount;
|
||||
} else if (rateUnit.includes('oz/1000sqft') || rateUnit.includes('oz per 1000sqft')) {
|
||||
// Rate is ounces per 1000 square feet, convert to pounds
|
||||
productPounds = (area1000sqft * rateAmount) / 16; // 16 oz = 1 lb
|
||||
} else {
|
||||
// Fallback: assume rate is per 1000 sq ft
|
||||
productPounds = area1000sqft * rateAmount;
|
||||
}
|
||||
|
||||
// Calculate application speed for spreaders
|
||||
if (equipment) {
|
||||
const spreadWidthFeet = equipment.spreadWidth || equipment.sprayWidthFeet || 3;
|
||||
// For granular applications, speed is typically 2-4 MPH
|
||||
// This is more dependent on walking/driving speed than flow rate
|
||||
applicationSpeedMph = 3; // Standard walking speed for spreader applications
|
||||
}
|
||||
|
||||
const result = {
|
||||
productAmountPounds: Math.round(productPounds * 100) / 100,
|
||||
applicationSpeedMph: Math.round(applicationSpeedMph * 100) / 100,
|
||||
calculationDetails: {
|
||||
areaSquareFeet,
|
||||
areaAcres: Math.round(areaAcres * 1000) / 1000,
|
||||
area1000sqft: Math.round(area1000sqft * 10) / 10,
|
||||
rateAmount,
|
||||
rateUnit
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Granular calculation result:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main calculation function that determines application type and calls appropriate calculator
|
||||
* @param {Object} params - Calculation parameters
|
||||
* @returns {Object} Calculation results
|
||||
*/
|
||||
function calculateApplication(params) {
|
||||
const {
|
||||
areaSquareFeet,
|
||||
rateAmount,
|
||||
rateUnit,
|
||||
applicationType,
|
||||
equipment,
|
||||
nozzle
|
||||
} = params;
|
||||
|
||||
console.log('Starting application calculation with params:', params);
|
||||
|
||||
if (applicationType === 'liquid') {
|
||||
return {
|
||||
type: 'liquid',
|
||||
...calculateLiquidApplication(areaSquareFeet, rateAmount, rateUnit, equipment, nozzle)
|
||||
};
|
||||
} else if (applicationType === 'granular') {
|
||||
return {
|
||||
type: 'granular',
|
||||
...calculateGranularApplication(areaSquareFeet, rateAmount, rateUnit, equipment)
|
||||
};
|
||||
} else {
|
||||
// Auto-detect based on rate unit
|
||||
const isLiquid = rateUnit.toLowerCase().includes('oz/gal') ||
|
||||
rateUnit.toLowerCase().includes('fl oz') ||
|
||||
equipment?.categoryName?.toLowerCase() === 'sprayer';
|
||||
|
||||
if (isLiquid) {
|
||||
return {
|
||||
type: 'liquid',
|
||||
...calculateLiquidApplication(areaSquareFeet, rateAmount, rateUnit, equipment, nozzle)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'granular',
|
||||
...calculateGranularApplication(areaSquareFeet, rateAmount, rateUnit, equipment)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
calculateApplication,
|
||||
calculateLiquidApplication,
|
||||
calculateGranularApplication
|
||||
};
|
||||
@@ -94,13 +94,30 @@ const userProductSchema = Joi.object({
|
||||
const applicationPlanSchema = Joi.object({
|
||||
lawnSectionId: Joi.number().integer().positive().required(),
|
||||
equipmentId: Joi.number().integer().positive().required(),
|
||||
nozzleId: Joi.number().integer().positive().optional(),
|
||||
plannedDate: Joi.date().required(),
|
||||
notes: Joi.string().allow('').optional(),
|
||||
areaSquareFeet: Joi.number().positive().optional(),
|
||||
equipment: Joi.object({
|
||||
id: Joi.number().integer().positive().optional(),
|
||||
categoryName: Joi.string().optional(),
|
||||
tankSizeGallons: Joi.number().positive().allow(null).optional(),
|
||||
pumpGpm: Joi.number().positive().allow(null).optional(),
|
||||
sprayWidthFeet: Joi.number().positive().allow(null).optional(),
|
||||
capacityLbs: Joi.number().positive().allow(null).optional(),
|
||||
spreadWidth: Joi.number().positive().allow(null).optional()
|
||||
}).optional(),
|
||||
nozzle: Joi.object({
|
||||
id: Joi.number().integer().positive().optional(),
|
||||
flowRateGpm: Joi.number().positive().allow(null).optional(),
|
||||
sprayAngle: Joi.number().integer().allow(null).optional()
|
||||
}).allow(null).optional(),
|
||||
products: Joi.array().items(Joi.object({
|
||||
productId: Joi.number().integer().positive().optional(),
|
||||
userProductId: Joi.number().integer().positive().optional(),
|
||||
rateAmount: Joi.number().positive().required(),
|
||||
rateUnit: Joi.string().max(50).required()
|
||||
rateUnit: Joi.string().max(50).required(),
|
||||
applicationType: Joi.string().valid('liquid', 'granular').optional()
|
||||
})).min(1).required()
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user