const express = require('express'); const axios = require('axios'); const pool = require('../config/database'); const { AppError } = require('../middleware/errorHandler'); const router = express.Router(); // @route GET /api/weather/:propertyId // @desc Get current weather for property location // @access Private router.get('/:propertyId', async (req, res, next) => { try { const propertyId = req.params.propertyId; // Verify property belongs to user const propertyResult = await pool.query( 'SELECT id, name, latitude, longitude FROM properties WHERE id = $1 AND user_id = $2', [propertyId, req.user.id] ); if (propertyResult.rows.length === 0) { throw new AppError('Property not found', 404); } const property = propertyResult.rows[0]; if (!property.latitude || !property.longitude) { throw new AppError('Property location coordinates not set', 400); } const apiKey = process.env.WEATHER_API_KEY; if (!apiKey) { throw new AppError('Weather service not configured', 503); } // Fetch current weather from OpenWeatherMap const weatherResponse = await axios.get( `https://api.openweathermap.org/data/2.5/weather`, { params: { lat: property.latitude, lon: property.longitude, appid: apiKey, units: 'imperial' }, timeout: 5000 } ); const weatherData = weatherResponse.data; // Store weather data in cache const today = new Date().toISOString().split('T')[0]; await pool.query( `INSERT INTO weather_data (property_id, date, temperature_high, temperature_low, humidity, wind_speed, precipitation, conditions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (property_id, date) DO UPDATE SET temperature_high = EXCLUDED.temperature_high, temperature_low = EXCLUDED.temperature_low, humidity = EXCLUDED.humidity, wind_speed = EXCLUDED.wind_speed, precipitation = EXCLUDED.precipitation, conditions = EXCLUDED.conditions, created_at = CURRENT_TIMESTAMP`, [ propertyId, today, Math.round(weatherData.main.temp), Math.round(weatherData.main.temp_min), weatherData.main.humidity, weatherData.wind?.speed || 0, weatherData.rain?.['1h'] || 0, weatherData.weather[0].description ] ); res.json({ success: true, data: { weather: { location: { propertyId: property.id, propertyName: property.name, latitude: parseFloat(property.latitude), longitude: parseFloat(property.longitude) }, current: { temperature: Math.round(weatherData.main.temp), feelsLike: Math.round(weatherData.main.feels_like), humidity: weatherData.main.humidity, pressure: weatherData.main.pressure, windSpeed: weatherData.wind?.speed || 0, windDirection: weatherData.wind?.deg || 0, visibility: weatherData.visibility ? weatherData.visibility / 1609.34 : null, // Convert to miles uvIndex: weatherData.uvi || null, conditions: weatherData.weather[0].description, icon: weatherData.weather[0].icon }, precipitation: { current: weatherData.rain?.['1h'] || 0, forecast3h: weatherData.rain?.['3h'] || 0 }, timestamps: { sunrise: new Date(weatherData.sys.sunrise * 1000), sunset: new Date(weatherData.sys.sunset * 1000), lastUpdated: new Date() } } } }); } catch (error) { if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') { throw new AppError('Weather service temporarily unavailable', 503); } if (error.response?.status === 401) { throw new AppError('Weather service authentication failed', 503); } next(error); } }); // @route GET /api/weather/:propertyId/forecast // @desc Get 5-day weather forecast for property // @access Private router.get('/:propertyId/forecast', async (req, res, next) => { try { const propertyId = req.params.propertyId; // Verify property belongs to user const propertyResult = await pool.query( 'SELECT id, name, latitude, longitude FROM properties WHERE id = $1 AND user_id = $2', [propertyId, req.user.id] ); if (propertyResult.rows.length === 0) { throw new AppError('Property not found', 404); } const property = propertyResult.rows[0]; if (!property.latitude || !property.longitude) { throw new AppError('Property location coordinates not set', 400); } const apiKey = process.env.WEATHER_API_KEY; if (!apiKey) { throw new AppError('Weather service not configured', 503); } // Fetch 5-day forecast from OpenWeatherMap const forecastResponse = await axios.get( `https://api.openweathermap.org/data/2.5/forecast`, { params: { lat: property.latitude, lon: property.longitude, appid: apiKey, units: 'imperial' }, timeout: 5000 } ); const forecastData = forecastResponse.data; // Group forecast by day const dailyForecast = {}; forecastData.list.forEach(item => { const date = item.dt_txt.split(' ')[0]; if (!dailyForecast[date]) { dailyForecast[date] = { date, temperatures: [], humidity: [], windSpeed: [], precipitation: 0, conditions: [], timestamps: [] }; } dailyForecast[date].temperatures.push(item.main.temp); dailyForecast[date].humidity.push(item.main.humidity); dailyForecast[date].windSpeed.push(item.wind?.speed || 0); dailyForecast[date].precipitation += item.rain?.['3h'] || 0; dailyForecast[date].conditions.push(item.weather[0].description); dailyForecast[date].timestamps.push(new Date(item.dt * 1000)); }); // Process daily summaries const forecast = Object.values(dailyForecast).map(day => { const temps = day.temperatures; const humidity = day.humidity; const windSpeeds = day.windSpeed; return { date: day.date, temperatureHigh: Math.round(Math.max(...temps)), temperatureLow: Math.round(Math.min(...temps)), averageHumidity: Math.round(humidity.reduce((a, b) => a + b, 0) / humidity.length), maxWindSpeed: Math.round(Math.max(...windSpeeds)), totalPrecipitation: Math.round(day.precipitation * 100) / 100, conditions: day.conditions[0], // Use first condition of the day timestamps: day.timestamps }; }).slice(0, 5); // Limit to 5 days res.json({ success: true, data: { forecast: { location: { propertyId: property.id, propertyName: property.name, latitude: parseFloat(property.latitude), longitude: parseFloat(property.longitude) }, daily: forecast, lastUpdated: new Date() } } }); } catch (error) { if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') { throw new AppError('Weather service temporarily unavailable', 503); } if (error.response?.status === 401) { throw new AppError('Weather service authentication failed', 503); } next(error); } }); // @route GET /api/weather/:propertyId/history // @desc Get weather history for property // @access Private router.get('/:propertyId/history', async (req, res, next) => { try { const propertyId = req.params.propertyId; const { start_date, end_date, limit = 30 } = req.query; // Verify property belongs to user const propertyResult = await pool.query( 'SELECT id, name FROM properties WHERE id = $1 AND user_id = $2', [propertyId, req.user.id] ); if (propertyResult.rows.length === 0) { throw new AppError('Property not found', 404); } const property = propertyResult.rows[0]; let whereConditions = ['property_id = $1']; let queryParams = [propertyId]; let paramCount = 1; if (start_date) { paramCount++; whereConditions.push(`date >= $${paramCount}`); queryParams.push(start_date); } if (end_date) { paramCount++; whereConditions.push(`date <= $${paramCount}`); queryParams.push(end_date); } const whereClause = whereConditions.join(' AND '); paramCount++; queryParams.push(limit); const result = await pool.query( `SELECT * FROM weather_data WHERE ${whereClause} ORDER BY date DESC LIMIT $${paramCount}`, queryParams ); res.json({ success: true, data: { history: { location: { propertyId: property.id, propertyName: property.name }, records: result.rows.map(record => ({ date: record.date, temperatureHigh: record.temperature_high, temperatureLow: record.temperature_low, humidity: record.humidity, windSpeed: parseFloat(record.wind_speed), precipitation: parseFloat(record.precipitation), conditions: record.conditions, recordedAt: record.created_at })) } } }); } catch (error) { next(error); } }); // @route GET /api/weather/conditions/suitable // @desc Check if weather conditions are suitable for application // @access Private router.get('/conditions/suitable/:propertyId', async (req, res, next) => { try { const propertyId = req.params.propertyId; const { application_type = 'general' } = req.query; // Verify property belongs to user const propertyResult = await pool.query( 'SELECT id, name, latitude, longitude FROM properties WHERE id = $1 AND user_id = $2', [propertyId, req.user.id] ); if (propertyResult.rows.length === 0) { throw new AppError('Property not found', 404); } const property = propertyResult.rows[0]; if (!property.latitude || !property.longitude) { throw new AppError('Property location coordinates not set', 400); } const apiKey = process.env.WEATHER_API_KEY; if (!apiKey) { throw new AppError('Weather service not configured', 503); } // Get current weather const weatherResponse = await axios.get( `https://api.openweathermap.org/data/2.5/weather`, { params: { lat: property.latitude, lon: property.longitude, appid: apiKey, units: 'imperial' }, timeout: 5000 } ); const weather = weatherResponse.data; // Define suitability criteria based on application type const criteria = { general: { maxWindSpeed: 10, // mph maxTemperature: 85, // °F minTemperature: 45, // °F maxHumidity: 85, // % maxPrecipitation: 0.1 // inches }, herbicide: { maxWindSpeed: 7, maxTemperature: 80, minTemperature: 50, maxHumidity: 80, maxPrecipitation: 0 }, fertilizer: { maxWindSpeed: 15, maxTemperature: 90, minTemperature: 40, maxHumidity: 90, maxPrecipitation: 0 } }; const rules = criteria[application_type] || criteria.general; // Check conditions const windSpeed = weather.wind?.speed || 0; const temperature = weather.main.temp; const humidity = weather.main.humidity; const precipitation = weather.rain?.['1h'] || 0; const checks = { windSpeed: { suitable: windSpeed <= rules.maxWindSpeed, value: windSpeed, threshold: rules.maxWindSpeed, message: windSpeed > rules.maxWindSpeed ? `Wind speed too high (${windSpeed} mph > ${rules.maxWindSpeed} mph)` : 'Wind speed acceptable' }, temperature: { suitable: temperature >= rules.minTemperature && temperature <= rules.maxTemperature, value: temperature, range: [rules.minTemperature, rules.maxTemperature], message: temperature < rules.minTemperature ? `Temperature too low (${Math.round(temperature)}°F < ${rules.minTemperature}°F)` : temperature > rules.maxTemperature ? `Temperature too high (${Math.round(temperature)}°F > ${rules.maxTemperature}°F)` : 'Temperature acceptable' }, humidity: { suitable: humidity <= rules.maxHumidity, value: humidity, threshold: rules.maxHumidity, message: humidity > rules.maxHumidity ? `Humidity too high (${humidity}% > ${rules.maxHumidity}%)` : 'Humidity acceptable' }, precipitation: { suitable: precipitation <= rules.maxPrecipitation, value: precipitation, threshold: rules.maxPrecipitation, message: precipitation > rules.maxPrecipitation ? `Active precipitation (${precipitation} in/hr)` : 'No precipitation' } }; const overallSuitable = Object.values(checks).every(check => check.suitable); res.json({ success: true, data: { suitability: { overall: overallSuitable, applicationType: application_type, checks, recommendations: overallSuitable ? ['Conditions are suitable for application'] : Object.values(checks) .filter(check => !check.suitable) .map(check => check.message), lastUpdated: new Date() } } }); } catch (error) { if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') { throw new AppError('Weather service temporarily unavailable', 503); } if (error.response?.status === 401) { throw new AppError('Weather service authentication failed', 503); } next(error); } }); module.exports = router;