/** * Aqua-Lens Local Database * IndexedDB-based storage for water quality data */ class WaterQualityDatabase { constructor() { this.dbName = 'AquaLensDB'; 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; // Water tests store if (!db.objectStoreNames.contains('waterTests')) { const testStore = db.createObjectStore('waterTests', { keyPath: 'id' }); testStore.createIndex('timestamp', 'timestamp', { unique: false }); testStore.createIndex('waterSource', 'waterSource', { unique: false }); testStore.createIndex('safetyLevel', 'safetyLevel', { unique: false }); testStore.createIndex('location', ['latitude', 'longitude'], { unique: false }); } // Alerts store if (!db.objectStoreNames.contains('alerts')) { const alertStore = db.createObjectStore('alerts', { keyPath: 'id' }); alertStore.createIndex('timestamp', 'timestamp', { unique: false }); alertStore.createIndex('severity', 'severity', { unique: false }); alertStore.createIndex('resolved', 'resolved', { unique: false }); } // User preferences store if (!db.objectStoreNames.contains('userPreferences')) { db.createObjectStore('userPreferences', { keyPath: 'key' }); } // Calibration data store if (!db.objectStoreNames.contains('calibrationData')) { const calStore = db.createObjectStore('calibrationData', { keyPath: 'id' }); calStore.createIndex('parameter', 'parameter', { unique: false }); } }; }); } /** * Save a water test result */ async saveWaterTest(testData) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['waterTests'], 'readwrite'); const store = transaction.objectStore('waterTests'); const testRecord = { id: testData.id || this.generateId(), timestamp: testData.timestamp || new Date().toISOString(), waterSource: testData.waterSource, latitude: testData.location?.latitude || null, longitude: testData.location?.longitude || null, results: testData.results, overallQuality: testData.overallQuality, safetyLevel: testData.safetyLevel, confidence: testData.confidence, alerts: testData.alerts || [], recommendations: testData.recommendations || [], imageData: testData.imageData || null, processingMethod: testData.processingMethod, colorAccuracy: testData.colorAccuracy, metadata: { colorChannels: testData.colorChannels, lightingConditions: testData.lightingConditions, regionsDetected: testData.regionsDetected } }; const request = store.add(testRecord); request.onsuccess = () => { // Create alerts if unsafe if (testData.safetyLevel === 'Unsafe') { this.createAlert(testRecord); } resolve(testRecord); }; request.onerror = () => reject(request.error); }); } /** * Get all water tests */ async getAllWaterTests(limit = 100) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['waterTests'], 'readonly'); const store = transaction.objectStore('waterTests'); 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 water tests by location (within radius) */ async getWaterTestsByLocation(latitude, longitude, radiusKm = 10) { const allTests = await this.getAllWaterTests(1000); return allTests.filter(test => { if (!test.latitude || !test.longitude) return false; const distance = this.calculateDistance( latitude, longitude, test.latitude, test.longitude ); return distance <= radiusKm; }); } /** * Get water tests by source type */ async getWaterTestsBySource(waterSource) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['waterTests'], 'readonly'); const store = transaction.objectStore('waterTests'); const index = store.index('waterSource'); const request = index.getAll(waterSource); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } /** * Create an alert for unsafe water */ async createAlert(testData) { if (!this.db) await this.init(); const alert = { id: this.generateId(), testId: testData.id, type: 'contamination', severity: testData.safetyLevel === 'Unsafe' ? 'high' : 'medium', message: `Unsafe water detected: ${testData.alerts.join(', ')}`, timestamp: new Date().toISOString(), latitude: testData.latitude, longitude: testData.longitude, resolved: false, waterSource: testData.waterSource }; return new Promise((resolve, reject) => { const transaction = this.db.transaction(['alerts'], 'readwrite'); const store = transaction.objectStore('alerts'); const request = store.add(alert); request.onsuccess = () => resolve(alert); request.onerror = () => reject(request.error); }); } /** * Get all alerts */ async getAllAlerts(includeResolved = false) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['alerts'], 'readonly'); const store = transaction.objectStore('alerts'); const index = store.index('timestamp'); const request = index.openCursor(null, 'prev'); const results = []; request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { const alert = cursor.value; if (includeResolved || !alert.resolved) { results.push(alert); } cursor.continue(); } else { resolve(results); } }; request.onerror = () => reject(request.error); }); } /** * Get alerts by location */ async getAlertsByLocation(latitude, longitude, radiusKm = 50) { const allAlerts = await this.getAllAlerts(); return allAlerts.filter(alert => { if (!alert.latitude || !alert.longitude) return false; const distance = this.calculateDistance( latitude, longitude, alert.latitude, alert.longitude ); return distance <= radiusKm; }); } /** * Resolve an alert */ async resolveAlert(alertId) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(['alerts'], 'readwrite'); const store = transaction.objectStore('alerts'); const getRequest = store.get(alertId); getRequest.onsuccess = () => { const alert = getRequest.result; if (alert) { alert.resolved = true; alert.resolvedTimestamp = new Date().toISOString(); const putRequest = store.put(alert); putRequest.onsuccess = () => resolve(alert); putRequest.onerror = () => reject(putRequest.error); } else { reject(new Error('Alert not found')); } }; getRequest.onerror = () => reject(getRequest.error); }); } /** * Save user preferences */ 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); }); } /** * Get statistics */ async getStatistics() { const allTests = await this.getAllWaterTests(10000); const allAlerts = await this.getAllAlerts(); const stats = { totalTests: allTests.length, safeTests: allTests.filter(t => t.safetyLevel === 'Safe').length, unsafeTests: allTests.filter(t => t.safetyLevel === 'Unsafe').length, activeAlerts: allAlerts.filter(a => !a.resolved).length, qualityDistribution: { excellent: allTests.filter(t => t.overallQuality === 'Excellent').length, good: allTests.filter(t => t.overallQuality === 'Good').length, fair: allTests.filter(t => t.overallQuality === 'Fair').length, poor: allTests.filter(t => t.overallQuality === 'Poor').length }, sourceDistribution: {}, averageConfidence: 0 }; // Calculate source distribution allTests.forEach(test => { const source = test.waterSource || 'Unknown'; stats.sourceDistribution[source] = (stats.sourceDistribution[source] || 0) + 1; }); // Calculate average confidence if (allTests.length > 0) { stats.averageConfidence = allTests.reduce((sum, test) => sum + (test.confidence || 0), 0) / allTests.length; } return stats; } /** * Calculate distance between two coordinates (Haversine formula) */ 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 'aqua_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } /** * Clear all data (for testing/reset) */ async clearAllData() { if (!this.db) await this.init(); const stores = ['waterTests', 'alerts', 'userPreferences', 'calibrationData']; 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); }); })); } /** * Export data as JSON */ async exportData() { const [tests, alerts, preferences] = await Promise.all([ this.getAllWaterTests(10000), this.getAllAlerts(true), this.getAllUserPreferences() ]); return { waterTests: tests, alerts: alerts, 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); }); } } // Create singleton instance export const waterQualityDB = new WaterQualityDatabase(); // Initialize database on import waterQualityDB.init().catch(console.error); export default waterQualityDB;