feat: implement external API integration with static tide tables (task 32)

- Add in-memory caching for external API responses (6-hour expiration)
- Implement OpenWeatherMap integration for weather data
- Create static tide tables for 22 Irish coastal stations
- Add tide calculation based on semi-diurnal patterns and lunar cycles
- Implement water quality, beach quality, and biodiversity placeholder endpoints
- Add comprehensive API documentation (EXTERNAL_API_SETUP.md)
- Fix frontend WaterQualityIndicator crash
- Remove Redis dependency (using in-memory cache instead)
- All external data endpoints functional and tested
This commit is contained in:
2025-11-16 23:24:42 +00:00
parent 92426c5cc1
commit 3d487a468e
15 changed files with 2260 additions and 4 deletions

View File

@@ -0,0 +1,339 @@
import { Request, Response } from 'express';
import {
getWeatherData,
getTideData,
getWaterQualityData,
getBeachQualityData,
getBiodiversityData,
getAllEnvironmentalData,
} from '../services/externalDataService';
/**
* Get weather data for a location
* GET /api/external/weather/:lat/:lng
*/
export const getWeather = async (req: Request, res: Response) => {
try {
const { lat, lng } = req.params;
const latitude = parseFloat(lat);
const longitude = parseFloat(lng);
// Validate coordinates
if (isNaN(latitude) || isNaN(longitude)) {
return res.status(400).json({
error: {
code: 'INVALID_COORDINATES',
message: 'Invalid latitude or longitude',
timestamp: new Date().toISOString(),
},
});
}
// Validate coordinate ranges
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
return res.status(400).json({
error: {
code: 'INVALID_COORDINATES',
message: 'Coordinates out of valid range',
timestamp: new Date().toISOString(),
},
});
}
const weatherData = await getWeatherData(latitude, longitude);
if (!weatherData) {
return res.status(503).json({
error: {
code: 'EXTERNAL_API_ERROR',
message: 'Weather data temporarily unavailable',
timestamp: new Date().toISOString(),
},
});
}
res.json(weatherData);
} catch (error) {
console.error('Error in getWeather controller:', error);
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to fetch weather data',
timestamp: new Date().toISOString(),
},
});
}
};
/**
* Get tide data for a location
* GET /api/external/tides/:lat/:lng
*/
export const getTides = async (req: Request, res: Response) => {
try {
const { lat, lng } = req.params;
const latitude = parseFloat(lat);
const longitude = parseFloat(lng);
// Validate coordinates
if (isNaN(latitude) || isNaN(longitude)) {
return res.status(400).json({
error: {
code: 'INVALID_COORDINATES',
message: 'Invalid latitude or longitude',
timestamp: new Date().toISOString(),
},
});
}
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
return res.status(400).json({
error: {
code: 'INVALID_COORDINATES',
message: 'Coordinates out of valid range',
timestamp: new Date().toISOString(),
},
});
}
const tideData = await getTideData(latitude, longitude);
if (!tideData) {
return res.status(503).json({
error: {
code: 'EXTERNAL_API_ERROR',
message: 'Tide data temporarily unavailable',
timestamp: new Date().toISOString(),
},
});
}
res.json(tideData);
} catch (error) {
console.error('Error in getTides controller:', error);
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to fetch tide data',
timestamp: new Date().toISOString(),
},
});
}
};
/**
* Get water quality data for a location
* GET /api/external/water-quality/:lat/:lng
*/
export const getWaterQuality = async (req: Request, res: Response) => {
try {
const { lat, lng } = req.params;
const latitude = parseFloat(lat);
const longitude = parseFloat(lng);
// Validate coordinates
if (isNaN(latitude) || isNaN(longitude)) {
return res.status(400).json({
error: {
code: 'INVALID_COORDINATES',
message: 'Invalid latitude or longitude',
timestamp: new Date().toISOString(),
},
});
}
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
return res.status(400).json({
error: {
code: 'INVALID_COORDINATES',
message: 'Coordinates out of valid range',
timestamp: new Date().toISOString(),
},
});
}
const waterQualityData = await getWaterQualityData(latitude, longitude);
if (!waterQualityData) {
return res.status(503).json({
error: {
code: 'EXTERNAL_API_ERROR',
message: 'Water quality data temporarily unavailable',
timestamp: new Date().toISOString(),
},
});
}
res.json(waterQualityData);
} catch (error) {
console.error('Error in getWaterQuality controller:', error);
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to fetch water quality data',
timestamp: new Date().toISOString(),
},
});
}
};
/**
* Get beach quality data for a location
* GET /api/external/beach-quality/:lat/:lng
*/
export const getBeachQuality = async (req: Request, res: Response) => {
try {
const { lat, lng } = req.params;
const latitude = parseFloat(lat);
const longitude = parseFloat(lng);
// Validate coordinates
if (isNaN(latitude) || isNaN(longitude)) {
return res.status(400).json({
error: {
code: 'INVALID_COORDINATES',
message: 'Invalid latitude or longitude',
timestamp: new Date().toISOString(),
},
});
}
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
return res.status(400).json({
error: {
code: 'INVALID_COORDINATES',
message: 'Coordinates out of valid range',
timestamp: new Date().toISOString(),
},
});
}
const beachQualityData = await getBeachQualityData(latitude, longitude);
if (!beachQualityData) {
return res.status(503).json({
error: {
code: 'EXTERNAL_API_ERROR',
message: 'Beach quality data temporarily unavailable',
timestamp: new Date().toISOString(),
},
});
}
res.json({
data: beachQualityData,
location: { latitude, longitude },
});
} catch (error) {
console.error('Error in getBeachQuality controller:', error);
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to fetch beach quality data',
timestamp: new Date().toISOString(),
},
});
}
};
/**
* Get biodiversity data for a location
* GET /api/external/biodiversity/:lat/:lng
*/
export const getBiodiversity = async (req: Request, res: Response) => {
try {
const { lat, lng } = req.params;
const latitude = parseFloat(lat);
const longitude = parseFloat(lng);
// Validate coordinates
if (isNaN(latitude) || isNaN(longitude)) {
return res.status(400).json({
error: {
code: 'INVALID_COORDINATES',
message: 'Invalid latitude or longitude',
timestamp: new Date().toISOString(),
},
});
}
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
return res.status(400).json({
error: {
code: 'INVALID_COORDINATES',
message: 'Coordinates out of valid range',
timestamp: new Date().toISOString(),
},
});
}
const biodiversityData = await getBiodiversityData(latitude, longitude);
if (!biodiversityData) {
return res.status(503).json({
error: {
code: 'EXTERNAL_API_ERROR',
message: 'Biodiversity data temporarily unavailable',
timestamp: new Date().toISOString(),
},
});
}
res.json(biodiversityData);
} catch (error) {
console.error('Error in getBiodiversity controller:', error);
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to fetch biodiversity data',
timestamp: new Date().toISOString(),
},
});
}
};
/**
* Get all environmental data for a location
* GET /api/external/all/:lat/:lng
*/
export const getAllData = async (req: Request, res: Response) => {
try {
const { lat, lng } = req.params;
const latitude = parseFloat(lat);
const longitude = parseFloat(lng);
// Validate coordinates
if (isNaN(latitude) || isNaN(longitude)) {
return res.status(400).json({
error: {
code: 'INVALID_COORDINATES',
message: 'Invalid latitude or longitude',
timestamp: new Date().toISOString(),
},
});
}
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
return res.status(400).json({
error: {
code: 'INVALID_COORDINATES',
message: 'Coordinates out of valid range',
timestamp: new Date().toISOString(),
},
});
}
const allData = await getAllEnvironmentalData(latitude, longitude);
res.json(allData);
} catch (error) {
console.error('Error in getAllData controller:', error);
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to fetch environmental data',
timestamp: new Date().toISOString(),
},
});
}
};