GreenPlusbyGXS / web /src /utils /biodiversityDatabase.js
gaialive's picture
Upload 106 files
759768a verified
/**
* BiodiversityEar Local Database
* IndexedDB-based storage for biodiversity data
*/
class BiodiversityDatabase {
constructor() {
this.dbName = 'BiodiversityEarDB';
this.version = 1;
this.db = null;
}
/**
* Initialize the database
*/
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Audio recordings store
if (!db.objectStoreNames.contains('audioRecordings')) {
const recordingStore = db.createObjectStore('audioRecordings', { keyPath: 'id' });
recordingStore.createIndex('timestamp', 'timestamp', { unique: false });
recordingStore.createIndex('location', ['latitude', 'longitude'], { unique: false });
recordingStore.createIndex('habitat', 'habitat', { unique: false });
recordingStore.createIndex('speciesCount', 'speciesCount', { unique: false });
}
// Species observations store
if (!db.objectStoreNames.contains('speciesObservations')) {
const speciesStore = db.createObjectStore('speciesObservations', { keyPath: 'id' });
speciesStore.createIndex('speciesName', 'speciesName', { unique: false });
speciesStore.createIndex('timestamp', 'timestamp', { unique: false });
speciesStore.createIndex('confidence', 'confidence', { unique: false });
speciesStore.createIndex('location', ['latitude', 'longitude'], { unique: false });
}
// Biodiversity hotspots store
if (!db.objectStoreNames.contains('biodiversityHotspots')) {
const hotspotStore = db.createObjectStore('biodiversityHotspots', { keyPath: 'id' });
hotspotStore.createIndex('location', ['latitude', 'longitude'], { unique: false });
hotspotStore.createIndex('speciesRichness', 'speciesRichness', { unique: false });
}
// User preferences store
if (!db.objectStoreNames.contains('userPreferences')) {
db.createObjectStore('userPreferences', { keyPath: 'key' });
}
// Species library store (for offline access)
if (!db.objectStoreNames.contains('speciesLibrary')) {
const libraryStore = db.createObjectStore('speciesLibrary', { keyPath: 'id' });
libraryStore.createIndex('scientificName', 'scientificName', { unique: false });
libraryStore.createIndex('region', 'region', { unique: false });
libraryStore.createIndex('conservationStatus', 'conservationStatus', { unique: false });
}
};
});
}
/**
* Save an audio recording analysis
*/
async saveRecording(recordingData) {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['audioRecordings', 'speciesObservations'], 'readwrite');
const recordingStore = transaction.objectStore('audioRecordings');
const speciesStore = transaction.objectStore('speciesObservations');
const recordingRecord = {
id: recordingData.id || this.generateId(),
timestamp: recordingData.timestamp || new Date().toISOString(),
latitude: recordingData.location?.latitude || null,
longitude: recordingData.location?.longitude || null,
habitat: recordingData.habitat || 'Unknown',
duration: recordingData.duration || 0,
audioQuality: recordingData.audioQuality || 'Good',
speciesCount: recordingData.detectedSpecies?.length || 0,
biodiversityScore: recordingData.biodiversityMetrics?.biodiversityScore || 0,
shannonIndex: recordingData.biodiversityMetrics?.shannonIndex || 0,
ecosystemHealth: recordingData.biodiversityMetrics?.ecosystemHealth || 'Unknown',
detectedSpecies: recordingData.detectedSpecies || [],
acousticFeatures: recordingData.acousticFeatures || {},
recommendations: recordingData.recommendations || [],
confidence: recordingData.confidence || 0,
audioData: recordingData.audioData || null // Store audio blob if needed
};
// Save main recording
const recordingRequest = recordingStore.add(recordingRecord);
recordingRequest.onsuccess = () => {
// Save individual species observations
const speciesPromises = (recordingData.detectedSpecies || []).map(species => {
return new Promise((resolveSpecies, rejectSpecies) => {
const speciesRecord = {
id: this.generateId(),
recordingId: recordingRecord.id,
speciesName: species.name,
scientificName: species.scientificName,
confidence: species.confidence,
timestamp: recordingRecord.timestamp,
latitude: recordingRecord.latitude,
longitude: recordingRecord.longitude,
habitat: recordingRecord.habitat,
callType: species.callType || 'unknown',
frequency: species.frequency,
conservationStatus: species.conservationStatus,
matchedFeatures: species.matchedFeatures || []
};
const speciesRequest = speciesStore.add(speciesRecord);
speciesRequest.onsuccess = () => resolveSpecies(speciesRecord);
speciesRequest.onerror = () => rejectSpecies(speciesRequest.error);
});
});
Promise.all(speciesPromises)
.then(() => resolve(recordingRecord))
.catch(reject);
};
recordingRequest.onerror = () => reject(recordingRequest.error);
});
}
/**
* Get all recordings
*/
async getAllRecordings(limit = 100) {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['audioRecordings'], 'readonly');
const store = transaction.objectStore('audioRecordings');
const index = store.index('timestamp');
const request = index.openCursor(null, 'prev'); // Newest first
const results = [];
let count = 0;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && count < limit) {
results.push(cursor.value);
count++;
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = () => reject(request.error);
});
}
/**
* Get recordings by location
*/
async getRecordingsByLocation(latitude, longitude, radiusKm = 10) {
const allRecordings = await this.getAllRecordings(1000);
return allRecordings.filter(recording => {
if (!recording.latitude || !recording.longitude) return false;
const distance = this.calculateDistance(
latitude, longitude,
recording.latitude, recording.longitude
);
return distance <= radiusKm;
});
}
/**
* Get species observations
*/
async getSpeciesObservations(speciesName = null, limit = 100) {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['speciesObservations'], 'readonly');
const store = transaction.objectStore('speciesObservations');
let request;
if (speciesName) {
const index = store.index('speciesName');
request = index.getAll(speciesName);
} else {
const index = store.index('timestamp');
request = index.openCursor(null, 'prev');
}
if (speciesName) {
request.onsuccess = () => resolve(request.result.slice(0, limit));
request.onerror = () => reject(request.error);
} else {
const results = [];
let count = 0;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && count < limit) {
results.push(cursor.value);
count++;
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = () => reject(request.error);
}
});
}
/**
* Get biodiversity statistics
*/
async getStatistics() {
const allRecordings = await this.getAllRecordings(10000);
const allObservations = await this.getSpeciesObservations(null, 10000);
const stats = {
totalRecordings: allRecordings.length,
totalSpeciesObservations: allObservations.length,
uniqueSpecies: new Set(allObservations.map(obs => obs.speciesName)).size,
averageBiodiversityScore: 0,
averageSpeciesPerRecording: 0,
ecosystemHealthDistribution: {
excellent: 0,
good: 0,
fair: 0,
poor: 0
},
conservationStatusDistribution: {},
habitatDistribution: {},
topSpecies: [],
recordingsByMonth: {}
};
if (allRecordings.length > 0) {
// Calculate averages
stats.averageBiodiversityScore = allRecordings.reduce((sum, rec) => sum + (rec.biodiversityScore || 0), 0) / allRecordings.length;
stats.averageSpeciesPerRecording = allRecordings.reduce((sum, rec) => sum + (rec.speciesCount || 0), 0) / allRecordings.length;
// Ecosystem health distribution
allRecordings.forEach(recording => {
const health = (recording.ecosystemHealth || 'unknown').toLowerCase();
if (stats.ecosystemHealthDistribution[health] !== undefined) {
stats.ecosystemHealthDistribution[health]++;
}
});
// Habitat distribution
allRecordings.forEach(recording => {
const habitat = recording.habitat || 'Unknown';
stats.habitatDistribution[habitat] = (stats.habitatDistribution[habitat] || 0) + 1;
});
// Recordings by month
allRecordings.forEach(recording => {
const month = new Date(recording.timestamp).toISOString().slice(0, 7); // YYYY-MM
stats.recordingsByMonth[month] = (stats.recordingsByMonth[month] || 0) + 1;
});
}
if (allObservations.length > 0) {
// Conservation status distribution
allObservations.forEach(observation => {
const status = observation.conservationStatus || 'Unknown';
stats.conservationStatusDistribution[status] = (stats.conservationStatusDistribution[status] || 0) + 1;
});
// Top species
const speciesCounts = {};
allObservations.forEach(observation => {
const species = observation.speciesName;
speciesCounts[species] = (speciesCounts[species] || 0) + 1;
});
stats.topSpecies = Object.entries(speciesCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([species, count]) => ({ species, count }));
}
return stats;
}
/**
* Find biodiversity hotspots
*/
async findBiodiversityHotspots(latitude, longitude, radiusKm = 50) {
const nearbyRecordings = await this.getRecordingsByLocation(latitude, longitude, radiusKm);
// Group recordings by approximate location (grid-based)
const gridSize = 0.01; // ~1km grid
const locationGroups = {};
nearbyRecordings.forEach(recording => {
if (recording.latitude && recording.longitude) {
const gridLat = Math.round(recording.latitude / gridSize) * gridSize;
const gridLng = Math.round(recording.longitude / gridSize) * gridSize;
const key = `${gridLat.toFixed(3)}_${gridLng.toFixed(3)}`;
if (!locationGroups[key]) {
locationGroups[key] = {
latitude: gridLat,
longitude: gridLng,
recordings: [],
totalSpecies: new Set(),
avgBiodiversityScore: 0
};
}
locationGroups[key].recordings.push(recording);
recording.detectedSpecies.forEach(species => {
locationGroups[key].totalSpecies.add(species.name);
});
}
});
// Calculate hotspot metrics
const hotspots = Object.values(locationGroups)
.map(group => {
const avgScore = group.recordings.reduce((sum, rec) => sum + (rec.biodiversityScore || 0), 0) / group.recordings.length;
const distance = this.calculateDistance(latitude, longitude, group.latitude, group.longitude);
return {
latitude: group.latitude,
longitude: group.longitude,
speciesRichness: group.totalSpecies.size,
recordingCount: group.recordings.length,
averageBiodiversityScore: Math.round(avgScore),
distance: Math.round(distance * 100) / 100,
lastRecorded: Math.max(...group.recordings.map(r => new Date(r.timestamp).getTime())),
topSpecies: [...group.totalSpecies].slice(0, 5)
};
})
.filter(hotspot => hotspot.speciesRichness >= 2) // Minimum 2 species
.sort((a, b) => b.speciesRichness - a.speciesRichness)
.slice(0, 10);
return hotspots;
}
/**
* Get species migration patterns
*/
async getSpeciesMigrationPatterns(speciesName) {
const observations = await this.getSpeciesObservations(speciesName, 1000);
// Group by month
const monthlyData = {};
observations.forEach(obs => {
const month = new Date(obs.timestamp).getMonth();
if (!monthlyData[month]) {
monthlyData[month] = {
count: 0,
locations: [],
avgConfidence: 0
};
}
monthlyData[month].count++;
if (obs.latitude && obs.longitude) {
monthlyData[month].locations.push({
lat: obs.latitude,
lng: obs.longitude
});
}
monthlyData[month].avgConfidence += obs.confidence;
});
// Calculate averages
Object.keys(monthlyData).forEach(month => {
const data = monthlyData[month];
data.avgConfidence = Math.round(data.avgConfidence / data.count);
});
return monthlyData;
}
/**
* Save user preference
*/
async saveUserPreference(key, value) {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['userPreferences'], 'readwrite');
const store = transaction.objectStore('userPreferences');
const request = store.put({ key, value, timestamp: new Date().toISOString() });
request.onsuccess = () => resolve(value);
request.onerror = () => reject(request.error);
});
}
/**
* Get user preference
*/
async getUserPreference(key, defaultValue = null) {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['userPreferences'], 'readonly');
const store = transaction.objectStore('userPreferences');
const request = store.get(key);
request.onsuccess = () => {
const result = request.result;
resolve(result ? result.value : defaultValue);
};
request.onerror = () => reject(request.error);
});
}
/**
* Calculate distance between two coordinates
*/
calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Earth's radius in kilometers
const dLat = this.toRadians(lat2 - lat1);
const dLon = this.toRadians(lon2 - lon1);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* Convert degrees to radians
*/
toRadians(degrees) {
return degrees * (Math.PI / 180);
}
/**
* Generate unique ID
*/
generateId() {
return 'bio_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
/**
* Export data as JSON
*/
async exportData() {
const [recordings, observations, preferences] = await Promise.all([
this.getAllRecordings(10000),
this.getSpeciesObservations(null, 10000),
this.getAllUserPreferences()
]);
return {
audioRecordings: recordings,
speciesObservations: observations,
userPreferences: preferences,
exportTimestamp: new Date().toISOString(),
version: this.version
};
}
/**
* Get all user preferences
*/
async getAllUserPreferences() {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['userPreferences'], 'readonly');
const store = transaction.objectStore('userPreferences');
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
/**
* Clear all data
*/
async clearAllData() {
if (!this.db) await this.init();
const stores = ['audioRecordings', 'speciesObservations', 'biodiversityHotspots', 'userPreferences', 'speciesLibrary'];
return Promise.all(stores.map(storeName => {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}));
}
}
// Create singleton instance
export const biodiversityDB = new BiodiversityDatabase();
// Initialize database on import
biodiversityDB.init().catch(console.error);
export default biodiversityDB;