File size: 13,591 Bytes
b2ab6f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/**
 * Wave Particle Animation System for Leaflet
 * Based on wind particle systems but adapted for ocean wave data
 */

class WaveParticleSystem {
    constructor(map, options = {}) {
        this.map = map;
        this.particles = [];
        this.activeParticles = [];
        this.animationId = null;
        this.isAnimating = false;
        
        // Configuration
        this.config = {
            particleCount: options.particleCount || 1500,
            particleLife: options.particleLife || 50, // frames
            particleSpeed: options.particleSpeed || 1.0,
            particleSize: options.particleSize || 1.5,
            fadeOpacity: options.fadeOpacity || 0.98,
            colorScheme: options.colorScheme || 'ocean',
            trailLength: options.trailLength || 10,
            ...options
        };
        
        // Canvas setup
        this.canvas = null;
        this.ctx = null;
        this.setupCanvas();
        
        // Wave data
        this.waveData = null;
        this.bounds = null;
    }
    
    setupCanvas() {
        // Create canvas overlay
        this.canvas = document.createElement('canvas');
        this.canvas.style.position = 'absolute';
        this.canvas.style.top = '0';
        this.canvas.style.left = '0';
        this.canvas.style.pointerEvents = 'none';
        this.canvas.style.zIndex = '1000';
        
        this.ctx = this.canvas.getContext('2d');
        
        // Add to map
        this.map.getContainer().appendChild(this.canvas);
        
        // Resize handling
        this.map.on('resize', () => this.resizeCanvas());
        this.map.on('zoom', () => this.handleMapChange());
        this.map.on('move', () => this.handleMapChange());
        
        this.resizeCanvas();
    }
    
    resizeCanvas() {
        const container = this.map.getContainer();
        this.canvas.width = container.offsetWidth;
        this.canvas.height = container.offsetHeight;
        this.canvas.style.width = container.offsetWidth + 'px';
        this.canvas.style.height = container.offsetHeight + 'px';
    }
    
    handleMapChange() {
        // Recalculate particle positions when map changes
        this.updateParticleScreenPositions();
    }
    
    loadWaveData(waveData) {
        this.waveData = waveData;
        this.calculateBounds();
        this.initializeParticles();
        console.log(`Loaded ${waveData.particles ? waveData.particles.length : 0} wave data points for particle animation`);
    }
    
    calculateBounds() {
        if (!this.waveData || !this.waveData.particles) return;
        
        const lats = this.waveData.particles.map(p => p.lat);
        const lons = this.waveData.particles.map(p => p.lon);
        
        this.bounds = {
            north: Math.max(...lats),
            south: Math.min(...lats),
            east: Math.max(...lons),
            west: Math.min(...lons)
        };
    }
    
    initializeParticles() {
        this.particles = [];
        this.activeParticles = [];
        
        if (!this.waveData || !this.waveData.particles) return;
        
        // Create particle pool from wave data
        for (let i = 0; i < Math.min(this.config.particleCount, this.waveData.particles.length); i++) {
            const wavePoint = this.waveData.particles[i];
            const particle = this.createParticle(wavePoint);
            this.particles.push(particle);
        }
        
        // Initialize some active particles
        const initialActiveCount = Math.min(200, this.particles.length);
        for (let i = 0; i < initialActiveCount; i++) {
            this.activateParticle();
        }
    }
    
    createParticle(wavePoint) {
        return {
            // Geographic position
            lat: wavePoint.lat,
            lon: wavePoint.lon,
            
            // Screen position (calculated dynamically)
            x: 0,
            y: 0,
            
            // Wave properties
            waveHeight: wavePoint.wave_height,
            waveDirection: wavePoint.wave_direction,
            wavePeriod: wavePoint.wave_period,
            
            // Velocity components (adjusted for screen space)
            vx: wavePoint.u_velocity || 0,
            vy: wavePoint.v_velocity || 0,
            
            // Particle state
            life: 0,
            maxLife: this.config.particleLife + Math.random() * 20,
            size: wavePoint.particle_size || this.config.particleSize,
            opacity: 1.0,
            active: false,
            
            // Visual properties
            colorIntensity: wavePoint.color_intensity || 0.5,
            trail: [] // For particle trails
        };
    }
    
    activateParticle() {
        // Find inactive particle
        const inactive = this.particles.find(p => !p.active);
        if (!inactive) return;
        
        // Reset particle properties
        inactive.active = true;
        inactive.life = 0;
        inactive.opacity = 1.0;
        inactive.trail = [];
        
        // Random starting position within bounds
        const randomWavePoint = this.waveData.particles[Math.floor(Math.random() * this.waveData.particles.length)];
        inactive.lat = randomWavePoint.lat + (Math.random() - 0.5) * 2; // Small random offset
        inactive.lon = randomWavePoint.lon + (Math.random() - 0.5) * 2;
        
        // Update screen position
        this.updateParticleScreenPosition(inactive);
        
        this.activeParticles.push(inactive);
    }
    
    updateParticleScreenPosition(particle) {
        const point = this.map.latLngToContainerPoint([particle.lat, particle.lon]);
        particle.x = point.x;
        particle.y = point.y;
    }
    
    updateParticleScreenPositions() {
        this.activeParticles.forEach(particle => {
            this.updateParticleScreenPosition(particle);
        });
    }
    
    interpolateWaveField(lat, lon) {
        if (!this.waveData || !this.waveData.particles) return { u: 0, v: 0, height: 0 };
        
        // Simple nearest neighbor for now (could be improved with bilinear interpolation)\n        let nearest = null;
        let minDistance = Infinity;
        \n        for (const point of this.waveData.particles) {
            const distance = Math.sqrt(\n                Math.pow(point.lat - lat, 2) + Math.pow(point.lon - lon, 2)\n            );
            if (distance < minDistance) {\n                minDistance = distance;\n                nearest = point;\n            }\n        }\n        \n        if (nearest && minDistance < 2.0) { // Within 2 degrees\n            return {\n                u: nearest.u_velocity || 0,\n                v: nearest.v_velocity || 0,\n                height: nearest.wave_height || 0,\n                direction: nearest.wave_direction || 0\n            };\n        }\n        \n        return { u: 0, v: 0, height: 0, direction: 0 };\n    }\n    \n    updateParticles() {\n        const zoom = this.map.getZoom();\n        const speedScale = Math.pow(2, zoom - 2) * this.config.particleSpeed;\n        \n        this.activeParticles = this.activeParticles.filter(particle => {\n            // Age the particle\n            particle.life++;\n            \n            // Check if particle should die\n            if (particle.life > particle.maxLife) {\n                particle.active = false;\n                return false;\n            }\n            \n            // Get wave field at current position\n            const field = this.interpolateWaveField(particle.lat, particle.lon);\n            \n            // Update velocity based on local wave field\n            particle.vx = field.u * speedScale * 100; // Scale for screen pixels\n            particle.vy = -field.v * speedScale * 100; // Negative because screen Y increases downward\n            \n            // Update position\n            particle.x += particle.vx;\n            particle.y += particle.vy;\n            \n            // Convert back to geographic coordinates\n            const latlng = this.map.containerPointToLatLng([particle.x, particle.y]);\n            particle.lat = latlng.lat;\n            particle.lon = latlng.lng;\n            \n            // Update trail\n            particle.trail.push({ x: particle.x, y: particle.y });\n            if (particle.trail.length > this.config.trailLength) {\n                particle.trail.shift();\n            }\n            \n            // Update opacity (fade with age)\n            particle.opacity = 1.0 - (particle.life / particle.maxLife);\n            \n            // Check bounds\n            const bounds = this.map.getBounds();\n            const margin = 100; // pixels\n            if (particle.x < -margin || particle.x > this.canvas.width + margin ||\n                particle.y < -margin || particle.y > this.canvas.height + margin) {\n                // Particle is off screen, deactivate\n                particle.active = false;\n                return false;\n            }\n            \n            return true;\n        });\n        \n        // Maintain particle count by activating new ones\n        const targetActive = Math.min(300, this.particles.length);\n        while (this.activeParticles.length < targetActive) {\n            this.activateParticle();\n        }\n    }\n    \n    getParticleColor(particle) {\n        const intensity = particle.colorIntensity;\n        const opacity = particle.opacity;\n        \n        switch (this.config.colorScheme) {\n            case 'ocean':\n                // Blue to white based on wave height\n                const r = Math.floor(intensity * 255);\n                const g = Math.floor(100 + intensity * 155);\n                const b = 255;\n                return `rgba(${r}, ${g}, ${b}, ${opacity})`;\n                \n            case 'heatmap':\n                // Heat map colors\n                if (intensity < 0.3) {\n                    return `rgba(0, 0, ${Math.floor(255 * intensity / 0.3)}, ${opacity})`;\n                } else if (intensity < 0.7) {\n                    const green = Math.floor(255 * (intensity - 0.3) / 0.4);\n                    return `rgba(0, ${green}, 255, ${opacity})`;\n                } else {\n                    const red = Math.floor(255 * (intensity - 0.7) / 0.3);\n                    return `rgba(${red}, 255, 255, ${opacity})`;\n                }\n                \n            default:\n                return `rgba(255, 255, 255, ${opacity})`;\n        }\n    }\n    \n    drawParticles() {\n        // Clear canvas with fade effect\n        this.ctx.fillStyle = `rgba(0, 0, 0, ${1 - this.config.fadeOpacity})`;\n        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n        \n        // Draw particles\n        this.activeParticles.forEach(particle => {\n            this.drawParticle(particle);\n        });\n    }\n    \n    drawParticle(particle) {\n        const color = this.getParticleColor(particle);\n        \n        // Draw particle trail\n        if (particle.trail.length > 1) {\n            this.ctx.beginPath();\n            this.ctx.strokeStyle = color;\n            this.ctx.lineWidth = 1;\n            this.ctx.globalAlpha = particle.opacity * 0.3;\n            \n            this.ctx.moveTo(particle.trail[0].x, particle.trail[0].y);\n            for (let i = 1; i < particle.trail.length; i++) {\n                this.ctx.lineTo(particle.trail[i].x, particle.trail[i].y);\n            }\n            this.ctx.stroke();\n        }\n        \n        // Draw particle\n        this.ctx.beginPath();\n        this.ctx.fillStyle = color;\n        this.ctx.globalAlpha = particle.opacity;\n        this.ctx.arc(particle.x, particle.y, particle.size, 0, 2 * Math.PI);\n        this.ctx.fill();\n        \n        // Reset alpha\n        this.ctx.globalAlpha = 1.0;\n    }\n    \n    animate() {\n        if (!this.isAnimating) return;\n        \n        this.updateParticles();\n        this.drawParticles();\n        \n        this.animationId = requestAnimationFrame(() => this.animate());\n    }\n    \n    start() {\n        if (this.isAnimating) return;\n        \n        this.isAnimating = true;\n        console.log('Starting wave particle animation');\n        this.animate();\n    }\n    \n    stop() {\n        if (!this.isAnimating) return;\n        \n        this.isAnimating = false;\n        if (this.animationId) {\n            cancelAnimationFrame(this.animationId);\n            this.animationId = null;\n        }\n        console.log('Stopped wave particle animation');\n    }\n    \n    setColorScheme(scheme) {\n        this.config.colorScheme = scheme;\n    }\n    \n    setParticleSpeed(speed) {\n        this.config.particleSpeed = speed;\n    }\n    \n    setParticleCount(count) {\n        this.config.particleCount = count;\n        // Reinitialize if needed\n        if (this.waveData) {\n            this.initializeParticles();\n        }\n    }\n    \n    destroy() {\n        this.stop();\n        \n        if (this.canvas && this.canvas.parentNode) {\n            this.canvas.parentNode.removeChild(this.canvas);\n        }\n        \n        this.map.off('resize');\n        this.map.off('zoom');\n        this.map.off('move');\n        \n        this.particles = [];\n        this.activeParticles = [];\n        this.waveData = null;\n    }\n}\n\n// Export for use in other modules\nif (typeof module !== 'undefined' && module.exports) {\n    module.exports = WaveParticleSystem;\n} else if (typeof window !== 'undefined') {\n    window.WaveParticleSystem = WaveParticleSystem;\n}