Spaces:
Sleeping
Sleeping
| /** | |
| * 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; |