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:
@@ -414,6 +414,105 @@ function toRadians(degrees: number): number {
|
||||
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
|
||||
* This would integrate with Blue Flag, Green Coast Award databases
|
||||
@@ -476,8 +575,8 @@ export interface BiodiversityData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get marine biodiversity data for a location
|
||||
* This would integrate with GBIF, Marine Institute Ireland, or similar databases
|
||||
* Get marine biodiversity data for a location using GBIF API
|
||||
* GBIF (Global Biodiversity Information Facility) - Free, no API key required
|
||||
*/
|
||||
export const getBiodiversityData = async (
|
||||
latitude: number,
|
||||
@@ -487,36 +586,92 @@ export const getBiodiversityData = async (
|
||||
|
||||
try {
|
||||
// Check cache first
|
||||
const cached = await cache.get<BiodiversityData>(cacheKey);
|
||||
const cached = cache.get<BiodiversityData>(cacheKey);
|
||||
if (cached) {
|
||||
console.log('Biodiversity data retrieved from cache');
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Note: GBIF or Marine Institute API integration would go here
|
||||
// For now, return mock data structure
|
||||
// In production, you would integrate with:
|
||||
// - GBIF (Global Biodiversity Information Facility)
|
||||
// - Marine Institute Ireland
|
||||
// - National Biodiversity Data Centre
|
||||
|
||||
console.warn('Biodiversity API not fully implemented - returning placeholder');
|
||||
// Fetch marine species observations from GBIF within area
|
||||
// GBIF API: https://www.gbif.org/developer/occurrence
|
||||
// Use a bounding box instead of radius (more reliable)
|
||||
const latDelta = 0.05; // ~5km
|
||||
const lngDelta = 0.05;
|
||||
|
||||
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 = {
|
||||
species: [],
|
||||
marineProtectedArea: false,
|
||||
habitatTypes: [],
|
||||
source: 'Placeholder',
|
||||
species,
|
||||
marineProtectedArea,
|
||||
habitatTypes,
|
||||
conservationStatus: species.length > 0 ? 'Active biodiversity recorded' : 'Limited data',
|
||||
source: 'GBIF (Global Biodiversity Information Facility)',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Cache the result
|
||||
await cache.set(cacheKey, biodiversityData, CACHE_EXPIRATION);
|
||||
cache.set(cacheKey, biodiversityData, CACHE_EXPIRATION);
|
||||
|
||||
return biodiversityData;
|
||||
} catch (error) {
|
||||
console.error('Error fetching biodiversity data:', error);
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching biodiversity data:', error.message);
|
||||
|
||||
// Return minimal data on error
|
||||
return {
|
||||
species: [],
|
||||
marineProtectedArea: false,
|
||||
habitatTypes: ['Coastal'],
|
||||
source: 'GBIF (Global Biodiversity Information Facility)',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user