/** * Test Strip Image Generator for AquaLens Accuracy Testing * Generates realistic water test strip images with known values for testing accuracy */ export class TestStripGenerator { constructor() { // Color mappings for different parameters based on real test strips this.colorMappings = { ph: { 4.0: { r: 255, g: 100, b: 100 }, // Red 5.0: { r: 255, g: 150, b: 100 }, // Orange-red 6.0: { r: 255, g: 200, b: 100 }, // Orange 6.5: { r: 255, g: 255, b: 100 }, // Yellow 7.0: { r: 150, g: 255, b: 150 }, // Light green 7.5: { r: 100, g: 200, b: 100 }, // Green 8.0: { r: 100, g: 150, b: 200 }, // Blue-green 8.5: { r: 100, g: 100, b: 255 }, // Blue 9.0: { r: 150, g: 100, b: 255 } // Purple }, chlorine: { 0: { r: 255, g: 255, b: 255 }, // White 0.5: { r: 255, g: 250, b: 200 }, // Very light yellow 1.0: { r: 255, g: 240, b: 150 }, // Light yellow 2.0: { r: 255, g: 220, b: 100 }, // Yellow 3.0: { r: 255, g: 200, b: 50 }, // Dark yellow 5.0: { r: 255, g: 150, b: 0 }, // Orange 10.0: { r: 200, g: 100, b: 0 } // Dark orange }, nitrates: { 0: { r: 255, g: 255, b: 255 }, // White 5: { r: 255, g: 240, b: 240 }, // Very light pink 10: { r: 255, g: 200, b: 200 }, // Light pink 25: { r: 255, g: 150, b: 150 }, // Pink 50: { r: 255, g: 100, b: 100 }, // Red-pink 100: { r: 200, g: 50, b: 50 }, // Dark red 200: { r: 150, g: 0, b: 0 } // Very dark red }, hardness: { 0: { r: 100, g: 255, b: 100 }, // Green 50: { r: 150, g: 255, b: 150 }, // Light green 100: { r: 200, g: 255, b: 200 }, // Very light green 150: { r: 255, g: 255, b: 200 }, // Light yellow 200: { r: 255, g: 200, b: 150 }, // Yellow-orange 300: { r: 255, g: 150, b: 100 }, // Orange 500: { r: 255, g: 100, b: 50 } // Red-orange }, alkalinity: { 0: { r: 255, g: 100, b: 100 }, // Red 40: { r: 255, g: 150, b: 100 }, // Orange-red 80: { r: 255, g: 200, b: 100 }, // Orange 120: { r: 255, g: 255, b: 100 }, // Yellow 180: { r: 200, g: 255, b: 150 }, // Light green 240: { r: 150, g: 200, b: 200 }, // Blue-green 400: { r: 100, g: 150, b: 255 } // Blue }, bacteria: { 0: { r: 100, g: 255, b: 100 }, // Green (safe) 1: { r: 255, g: 100, b: 100 } // Red (unsafe) } }; // Test strip layout configuration this.stripConfig = { width: 400, height: 100, backgroundColor: { r: 240, g: 240, b: 230 }, // Off-white strip background pads: [ { name: 'ph', x: 50, y: 30, width: 40, height: 40 }, { name: 'chlorine', x: 110, y: 30, width: 40, height: 40 }, { name: 'nitrates', x: 170, y: 30, width: 40, height: 40 }, { name: 'hardness', x: 230, y: 30, width: 40, height: 40 }, { name: 'alkalinity', x: 290, y: 30, width: 40, height: 40 }, { name: 'bacteria', x: 350, y: 30, width: 40, height: 40 } ] }; // Predefined test scenarios this.testScenarios = { 'excellent_water': { ph: 7.2, chlorine: 1.0, nitrates: 0, hardness: 75, alkalinity: 120, bacteria: 0, description: 'Excellent quality drinking water' }, 'good_water': { ph: 7.5, chlorine: 2.0, nitrates: 5, hardness: 150, alkalinity: 180, bacteria: 0, description: 'Good quality water with minor issues' }, 'poor_water': { ph: 6.0, chlorine: 0, nitrates: 50, hardness: 300, alkalinity: 40, bacteria: 0, description: 'Poor quality water needing treatment' }, 'contaminated_water': { ph: 5.5, chlorine: 0, nitrates: 100, hardness: 400, alkalinity: 20, bacteria: 1, description: 'Contaminated water - unsafe for consumption' } }; } /** * Generate a test strip image with specified parameters */ generateTestStrip(parameters, options = {}) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // Set canvas size canvas.width = this.stripConfig.width; canvas.height = this.stripConfig.height; // Apply lighting and background variations if specified const lighting = options.lighting || 'normal'; const background = options.background || 'white'; const noise = options.noise || 0; // Draw background this.drawBackground(ctx, background); // Draw test strip base this.drawStripBase(ctx, lighting); // Draw parameter pads this.stripConfig.pads.forEach(pad => { if (parameters[pad.name] !== undefined) { this.drawParameterPad(ctx, pad, parameters[pad.name], lighting, noise); } }); // Add realistic imperfections if (options.addImperfections) { this.addRealisticImperfections(ctx, options.imperfectionLevel || 'low'); } // Add labels if requested if (options.addLabels) { this.addParameterLabels(ctx); } return { canvas: canvas, dataURL: canvas.toDataURL('image/png'), parameters: parameters, expectedResults: this.calculateExpectedResults(parameters), metadata: { lighting: lighting, background: background, noise: noise, timestamp: new Date().toISOString() } }; } /** * Generate a test strip from a predefined scenario */ generateFromScenario(scenarioName, options = {}) { const scenario = this.testScenarios[scenarioName]; if (!scenario) { throw new Error(`Unknown scenario: ${scenarioName}`); } return this.generateTestStrip(scenario, { ...options, scenarioName: scenarioName, description: scenario.description }); } /** * Draw the background */ drawBackground(ctx, backgroundType) { const { width, height } = this.stripConfig; switch (backgroundType) { case 'white': ctx.fillStyle = '#ffffff'; break; case 'colored': ctx.fillStyle = '#f0f8ff'; // Light blue break; case 'textured': // Create a subtle texture pattern const gradient = ctx.createLinearGradient(0, 0, width, height); gradient.addColorStop(0, '#f8f8f8'); gradient.addColorStop(0.5, '#ffffff'); gradient.addColorStop(1, '#f0f0f0'); ctx.fillStyle = gradient; break; default: ctx.fillStyle = '#ffffff'; } ctx.fillRect(0, 0, width, height); } /** * Draw the test strip base */ drawStripBase(ctx, lighting) { const { width, height, backgroundColor } = this.stripConfig; // Adjust base color based on lighting let baseColor = { ...backgroundColor }; switch (lighting) { case 'dim': baseColor.r *= 0.7; baseColor.g *= 0.7; baseColor.b *= 0.7; break; case 'bright': baseColor.r = Math.min(255, baseColor.r * 1.2); baseColor.g = Math.min(255, baseColor.g * 1.2); baseColor.b = Math.min(255, baseColor.b * 1.2); break; } ctx.fillStyle = `rgb(${Math.round(baseColor.r)}, ${Math.round(baseColor.g)}, ${Math.round(baseColor.b)})`; ctx.fillRect(10, 10, width - 20, height - 20); // Add strip border ctx.strokeStyle = '#cccccc'; ctx.lineWidth = 1; ctx.strokeRect(10, 10, width - 20, height - 20); } /** * Draw a parameter pad with appropriate color */ drawParameterPad(ctx, pad, value, lighting, noise) { const color = this.getColorForValue(pad.name, value); // Apply lighting effects let adjustedColor = { ...color }; switch (lighting) { case 'dim': adjustedColor.r *= 0.6; adjustedColor.g *= 0.6; adjustedColor.b *= 0.6; break; case 'bright': adjustedColor.r = Math.min(255, adjustedColor.r * 1.3); adjustedColor.g = Math.min(255, adjustedColor.g * 1.3); adjustedColor.b = Math.min(255, adjustedColor.b * 1.3); break; } // Add noise if specified if (noise > 0) { adjustedColor.r += (Math.random() - 0.5) * noise * 50; adjustedColor.g += (Math.random() - 0.5) * noise * 50; adjustedColor.b += (Math.random() - 0.5) * noise * 50; // Clamp values adjustedColor.r = Math.max(0, Math.min(255, adjustedColor.r)); adjustedColor.g = Math.max(0, Math.min(255, adjustedColor.g)); adjustedColor.b = Math.max(0, Math.min(255, adjustedColor.b)); } // Draw the pad ctx.fillStyle = `rgb(${Math.round(adjustedColor.r)}, ${Math.round(adjustedColor.g)}, ${Math.round(adjustedColor.b)})`; ctx.fillRect(pad.x, pad.y, pad.width, pad.height); // Add subtle border ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)'; ctx.lineWidth = 0.5; ctx.strokeRect(pad.x, pad.y, pad.width, pad.height); } /** * Get color for a specific parameter value */ getColorForValue(parameter, value) { const colorMap = this.colorMappings[parameter]; if (!colorMap) { return { r: 128, g: 128, b: 128 }; // Gray fallback } // Find the closest color values for interpolation const values = Object.keys(colorMap).map(Number).sort((a, b) => a - b); if (value <= values[0]) { return colorMap[values[0]]; } if (value >= values[values.length - 1]) { return colorMap[values[values.length - 1]]; } // Interpolate between two closest values let lowerValue = values[0]; let upperValue = values[values.length - 1]; for (let i = 0; i < values.length - 1; i++) { if (value >= values[i] && value <= values[i + 1]) { lowerValue = values[i]; upperValue = values[i + 1]; break; } } const lowerColor = colorMap[lowerValue]; const upperColor = colorMap[upperValue]; const ratio = (value - lowerValue) / (upperValue - lowerValue); return { r: lowerColor.r + (upperColor.r - lowerColor.r) * ratio, g: lowerColor.g + (upperColor.g - lowerColor.g) * ratio, b: lowerColor.b + (upperColor.b - lowerColor.b) * ratio }; } /** * Add realistic imperfections to make the image more authentic */ addRealisticImperfections(ctx, level) { const { width, height } = this.stripConfig; const intensity = level === 'high' ? 0.3 : level === 'medium' ? 0.15 : 0.05; // Add subtle shadows ctx.fillStyle = `rgba(0, 0, 0, ${intensity * 0.1})`; ctx.fillRect(12, 12, width - 24, 2); // Top shadow ctx.fillRect(12, height - 14, width - 24, 2); // Bottom shadow // Add slight color variations for (let i = 0; i < intensity * 100; i++) { const x = Math.random() * width; const y = Math.random() * height; const size = Math.random() * 2 + 1; ctx.fillStyle = `rgba(${Math.random() * 50}, ${Math.random() * 50}, ${Math.random() * 50}, ${intensity})`; ctx.fillRect(x, y, size, size); } } /** * Add parameter labels */ addParameterLabels(ctx) { ctx.fillStyle = '#333333'; ctx.font = '10px Arial'; ctx.textAlign = 'center'; this.stripConfig.pads.forEach(pad => { const labelY = pad.y + pad.height + 15; ctx.fillText(pad.name.toUpperCase(), pad.x + pad.width / 2, labelY); }); } /** * Calculate expected analysis results */ calculateExpectedResults(parameters) { return { ph: parameters.ph, chlorine: parameters.chlorine, nitrates: parameters.nitrates, hardness: parameters.hardness, alkalinity: parameters.alkalinity, bacteria: parameters.bacteria, overallQuality: this.assessOverallQuality(parameters), safetyLevel: this.assessSafetyLevel(parameters) }; } /** * Assess overall water quality */ assessOverallQuality(params) { let score = 100; // pH scoring if (params.ph < 6.5 || params.ph > 8.5) score -= 20; else if (params.ph < 7.0 || params.ph > 8.0) score -= 10; // Chlorine scoring if (params.chlorine < 0.2 || params.chlorine > 4.0) score -= 15; // Nitrates scoring if (params.nitrates > 50) score -= 25; else if (params.nitrates > 25) score -= 15; else if (params.nitrates > 10) score -= 5; // Hardness scoring if (params.hardness > 300) score -= 10; else if (params.hardness > 200) score -= 5; // Alkalinity scoring if (params.alkalinity < 50 || params.alkalinity > 300) score -= 10; // Bacteria scoring if (params.bacteria > 0) score -= 30; if (score >= 90) return 'Excellent'; if (score >= 75) return 'Good'; if (score >= 60) return 'Fair'; if (score >= 40) return 'Poor'; return 'Very Poor'; } /** * Assess safety level */ assessSafetyLevel(params) { if (params.bacteria > 0) return 'Unsafe'; if (params.ph < 6.0 || params.ph > 9.0) return 'Unsafe'; if (params.nitrates > 100) return 'Unsafe'; if (params.chlorine > 5.0) return 'Caution'; if (params.ph < 6.5 || params.ph > 8.5) return 'Caution'; if (params.nitrates > 50) return 'Caution'; return 'Safe'; } /** * Generate multiple test strips with variations */ generateTestSuite(options = {}) { const testSuite = []; const scenarios = options.scenarios || Object.keys(this.testScenarios); const lightingConditions = options.lightingConditions || ['dim', 'normal', 'bright']; const backgrounds = options.backgrounds || ['white', 'colored']; scenarios.forEach(scenarioName => { lightingConditions.forEach(lighting => { backgrounds.forEach(background => { try { const testStrip = this.generateFromScenario(scenarioName, { lighting: lighting, background: background, addImperfections: true, imperfectionLevel: 'medium', noise: Math.random() * 0.1 }); testSuite.push({ id: `${scenarioName}_${lighting}_${background}`, ...testStrip, testConditions: { scenario: scenarioName, lighting: lighting, background: background } }); } catch (error) { console.error(`Failed to generate test strip for ${scenarioName}_${lighting}_${background}:`, error); } }); }); }); return testSuite; } /** * Export test suite as downloadable files */ exportTestSuite(testSuite, format = 'json') { const exportData = { generatedAt: new Date().toISOString(), totalTests: testSuite.length, tests: testSuite.map(test => ({ id: test.id, parameters: test.parameters, expectedResults: test.expectedResults, testConditions: test.testConditions, imageDataURL: test.dataURL })) }; if (format === 'json') { return JSON.stringify(exportData, null, 2); } return exportData; } /** * Get available test scenarios */ getAvailableScenarios() { return Object.keys(this.testScenarios).map(key => ({ name: key, description: this.testScenarios[key].description, parameters: { ...this.testScenarios[key] } })); } } // Create singleton instance export const testStripGenerator = new TestStripGenerator(); // Export convenience functions export const generateTestStrip = (parameters, options) => { return testStripGenerator.generateTestStrip(parameters, options); }; export const generateFromScenario = (scenarioName, options) => { return testStripGenerator.generateFromScenario(scenarioName, options); }; export default testStripGenerator;