454 lines
14 KiB
JavaScript
454 lines
14 KiB
JavaScript
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; |