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:
@@ -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)
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user