application planning

This commit is contained in:
Jake Kasper
2025-08-23 13:18:53 -04:00
parent f2f3ee52cf
commit f480fddc2c
5 changed files with 294 additions and 26 deletions

View 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
};

View File

@@ -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()
});