feat: implement GBIF biodiversity API integration (partial)

- Add GBIF API integration for marine species observations
- Implement species categorization (birds, fish, mammals, algae, etc.)
- Add Marine Protected Area detection for 7 Irish MPAs
- Determine habitat types from observations
- Add BiodiversityInfo component back to EnvironmentalDataPanel
- Free API, no key required

Note: GBIF API parameters need adjustment - currently returning 400 error
This commit is contained in:
2025-11-16 23:50:56 +00:00
parent 5812906758
commit b0eafebfc4
3 changed files with 178 additions and 21 deletions

View File

@@ -268,7 +268,7 @@
- Add notification preferences endpoint - Add notification preferences endpoint
- _Requirements: 6.3, 6.4, 6.5, 10.4_ - _Requirements: 6.3, 6.4, 6.5, 10.4_
- [x] 32. Integrate external APIs with caching - [ ] 32. Integrate external APIs with caching
- Set up Redis for caching external API responses - Set up Redis for caching external API responses
- Implement weather API integration (OpenWeatherMap) - Implement weather API integration (OpenWeatherMap)
- Build tide API integration (WorldTides or NOAA) - Build tide API integration (WorldTides or NOAA)

View File

@@ -414,6 +414,105 @@ function toRadians(degrees: number): number {
return degrees * (Math.PI / 180); return degrees * (Math.PI / 180);
} }
/**
* Determine species category from GBIF record
*/
function determineCategory(record: any): string {
const kingdom = record.kingdom?.toLowerCase() || '';
const phylum = record.phylum?.toLowerCase() || '';
const className = record.class?.toLowerCase() || '';
if (kingdom === 'animalia') {
if (className.includes('aves') || className.includes('bird')) return 'Bird';
if (className.includes('mammalia') || className.includes('mammal')) return 'Marine Mammal';
if (className.includes('actinopterygii') || phylum.includes('chordata')) return 'Fish';
if (phylum.includes('mollusca')) return 'Mollusk';
if (phylum.includes('arthropoda')) return 'Crustacean';
if (phylum.includes('echinodermata')) return 'Echinoderm';
if (phylum.includes('cnidaria')) return 'Cnidarian';
return 'Marine Animal';
}
if (kingdom === 'plantae') {
if (phylum.includes('rhodophyta')) return 'Red Algae';
if (phylum.includes('phaeophyta') || phylum.includes('ochrophyta')) return 'Brown Algae';
if (phylum.includes('chlorophyta')) return 'Green Algae';
return 'Marine Plant';
}
if (kingdom === 'chromista') return 'Algae';
return 'Marine Organism';
}
/**
* Check if location is in a Marine Protected Area
* Simplified check based on known Irish MPAs
*/
function checkMarineProtectedArea(latitude: number, longitude: number): boolean {
// Known Irish Marine Protected Areas (simplified list)
const irishMPAs = [
{ name: 'Dublin Bay', lat: 53.35, lng: -6.15, radius: 10 },
{ name: 'Galway Bay', lat: 53.25, lng: -9.05, radius: 15 },
{ name: 'Bantry Bay', lat: 51.68, lng: -9.45, radius: 12 },
{ name: 'Lough Hyne', lat: 51.50, lng: -9.30, radius: 5 },
{ name: 'Roaringwater Bay', lat: 51.52, lng: -9.55, radius: 10 },
{ name: 'Clew Bay', lat: 53.85, lng: -9.70, radius: 15 },
{ name: 'Donegal Bay', lat: 54.60, lng: -8.45, radius: 20 },
];
for (const mpa of irishMPAs) {
const distance = calculateDistance(latitude, longitude, mpa.lat, mpa.lng);
if (distance < mpa.radius) {
return true;
}
}
return false;
}
/**
* Determine habitat types based on location and observations
*/
function determineHabitatTypes(latitude: number, longitude: number, observations: any[]): string[] {
const habitats = new Set<string>();
// Always add coastal for Irish locations
habitats.add('Coastal');
// Check for specific habitat indicators in observations
observations.forEach((obs: any) => {
const habitat = obs.habitat?.toLowerCase() || '';
const locality = obs.locality?.toLowerCase() || '';
if (habitat.includes('rocky') || locality.includes('rocky')) {
habitats.add('Rocky Shore');
}
if (habitat.includes('sand') || locality.includes('sand') || locality.includes('beach')) {
habitats.add('Sandy Beach');
}
if (habitat.includes('estuar') || locality.includes('estuar')) {
habitats.add('Estuarine');
}
if (habitat.includes('reef') || locality.includes('reef')) {
habitats.add('Reef');
}
if (habitat.includes('kelp') || locality.includes('kelp')) {
habitats.add('Kelp Forest');
}
if (habitat.includes('mudflat') || locality.includes('mudflat')) {
habitats.add('Mudflat');
}
});
// If no specific habitats found, add generic ones
if (habitats.size === 1) {
habitats.add('Intertidal Zone');
}
return Array.from(habitats);
}
/** /**
* Get beach quality rating data * Get beach quality rating data
* This would integrate with Blue Flag, Green Coast Award databases * This would integrate with Blue Flag, Green Coast Award databases
@@ -476,8 +575,8 @@ export interface BiodiversityData {
} }
/** /**
* Get marine biodiversity data for a location * Get marine biodiversity data for a location using GBIF API
* This would integrate with GBIF, Marine Institute Ireland, or similar databases * GBIF (Global Biodiversity Information Facility) - Free, no API key required
*/ */
export const getBiodiversityData = async ( export const getBiodiversityData = async (
latitude: number, latitude: number,
@@ -487,36 +586,92 @@ export const getBiodiversityData = async (
try { try {
// Check cache first // Check cache first
const cached = await cache.get<BiodiversityData>(cacheKey); const cached = cache.get<BiodiversityData>(cacheKey);
if (cached) { if (cached) {
console.log('Biodiversity data retrieved from cache'); console.log('Biodiversity data retrieved from cache');
return cached; return cached;
} }
// Note: GBIF or Marine Institute API integration would go here // Fetch marine species observations from GBIF within area
// For now, return mock data structure // GBIF API: https://www.gbif.org/developer/occurrence
// In production, you would integrate with: // Use a bounding box instead of radius (more reliable)
// - GBIF (Global Biodiversity Information Facility) const latDelta = 0.05; // ~5km
// - Marine Institute Ireland const lngDelta = 0.05;
// - National Biodiversity Data Centre
console.warn('Biodiversity API not fully implemented - returning placeholder'); const response = await axios.get('https://api.gbif.org/v1/occurrence/search', {
params: {
decimalLatitude: `${latitude - latDelta},${latitude + latDelta}`,
decimalLongitude: `${longitude - lngDelta},${longitude + lngDelta}`,
limit: 50, // Get more results
country: 'IE', // Ireland
hasCoordinate: true,
hasGeospatialIssue: false,
},
timeout: 10000,
});
const results = response.data.results || [];
// Process species data
const speciesMap = new Map<string, any>();
results.forEach((record: any) => {
if (record.species && record.scientificName) {
const key = record.scientificName;
if (!speciesMap.has(key)) {
speciesMap.set(key, {
name: record.vernacularName || record.species,
scientificName: record.scientificName,
category: determineCategory(record),
count: 1,
});
} else {
const existing = speciesMap.get(key);
existing.count += 1;
}
}
});
// Convert to array and sort by count
const species = Array.from(speciesMap.values())
.sort((a, b) => b.count - a.count)
.slice(0, 10) // Top 10 species
.map(({ name, scientificName, category }) => ({
name,
scientificName,
category,
}));
// Check if location is in a Marine Protected Area (simplified check)
const marineProtectedArea = checkMarineProtectedArea(latitude, longitude);
// Determine habitat types based on location
const habitatTypes = determineHabitatTypes(latitude, longitude, results);
const biodiversityData: BiodiversityData = { const biodiversityData: BiodiversityData = {
species: [], species,
marineProtectedArea: false, marineProtectedArea,
habitatTypes: [], habitatTypes,
source: 'Placeholder', conservationStatus: species.length > 0 ? 'Active biodiversity recorded' : 'Limited data',
source: 'GBIF (Global Biodiversity Information Facility)',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}; };
// Cache the result // Cache the result
await cache.set(cacheKey, biodiversityData, CACHE_EXPIRATION); cache.set(cacheKey, biodiversityData, CACHE_EXPIRATION);
return biodiversityData; return biodiversityData;
} catch (error) { } catch (error: any) {
console.error('Error fetching biodiversity data:', error); console.error('Error fetching biodiversity data:', error.message);
return null;
// Return minimal data on error
return {
species: [],
marineProtectedArea: false,
habitatTypes: ['Coastal'],
source: 'GBIF (Global Biodiversity Information Facility)',
timestamp: new Date().toISOString(),
};
} }
}; };

View File

@@ -2,6 +2,7 @@
import { WeatherWidget } from './WeatherWidget'; import { WeatherWidget } from './WeatherWidget';
import { TideDisplay } from './TideDisplay'; import { TideDisplay } from './TideDisplay';
import { WaterQualityIndicator } from './WaterQualityIndicator'; import { WaterQualityIndicator } from './WaterQualityIndicator';
import { BiodiversityInfo } from './BiodiversityInfo';
interface EnvironmentalDataPanelProps { interface EnvironmentalDataPanelProps {
latitude: number; latitude: number;
@@ -19,13 +20,14 @@ export function EnvironmentalDataPanel({
<div className="mb-4"> <div className="mb-4">
<h2 className="text-xl font-bold text-gray-800 mb-2">Environmental Context</h2> <h2 className="text-xl font-bold text-gray-800 mb-2">Environmental Context</h2>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
Weather, tide, and water quality data for this location Weather, tide, water quality, and biodiversity data for this location
</p> </p>
</div> </div>
<WeatherWidget latitude={latitude} longitude={longitude} /> <WeatherWidget latitude={latitude} longitude={longitude} />
<TideDisplay latitude={latitude} longitude={longitude} /> <TideDisplay latitude={latitude} longitude={longitude} />
<WaterQualityIndicator latitude={latitude} longitude={longitude} /> <WaterQualityIndicator latitude={latitude} longitude={longitude} />
<BiodiversityInfo latitude={latitude} longitude={longitude} />
</div> </div>
); );
} }