nakas Claude commited on
Commit
ec9c90f
Β·
1 Parent(s): 31f1bd1

Recreate wave visualization using exact wind app toggle structure

Browse files

- Use FeatureGroup layers for Wave Heights and Wave Particles (like wind layers)
- Add proper checkbox controls exactly like wind app interface
- Create wave height markers with color coding and popups
- Add directional arrows as wave particle indicators
- Include LayerControl for built-in Folium layer management
- Implement proper layer toggle functions with checkbox states
- Add Dark/Light mode toggle matching wind app style
- Use proper Folium map initialization and layer detection

πŸ€– Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +167 -182
app.py CHANGED
@@ -590,10 +590,9 @@ def fetch_wave_particles():
590
  return f"❌ Error: {str(e)}", None, "Failed to fetch particle data"
591
 
592
  def create_particle_wave_map(data):
593
- """Create Folium map with wave particle visualization exactly like the wind app"""
594
  try:
595
  import folium
596
- from folium.plugins import HeatMap
597
  import numpy as np
598
 
599
  particles = data.get('particles', [])
@@ -611,237 +610,223 @@ def create_particle_wave_map(data):
611
  center_lat = (min(lats) + max(lats)) / 2
612
  center_lon = (min(lons) + max(lons)) / 2
613
 
614
- # Create Folium map like wind app
615
  m = folium.Map(
616
  location=[center_lat, center_lon],
617
  zoom_start=4,
618
- tiles='OpenStreetMap'
619
  )
620
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
621
  # Sample particles for performance
622
  import random
623
- if len(particles) > 800:
624
- particles = random.sample(particles, 800)
625
-
626
- # Create velocity data for leaflet-velocity (JSON format like wind app)
627
- velocity_json = {
628
- "data": [],
629
- "bounds": {
630
- "south": min(lats),
631
- "north": max(lats),
632
- "west": min(lons),
633
- "east": max(lons)
634
- }
635
- }
636
 
637
- # Convert wave data to velocity grid format
638
- for particle in particles:
639
- if all(key in particle for key in ['lat', 'lon', 'u_velocity', 'v_velocity']):
640
- velocity_json["data"].append([
 
641
  particle['lat'],
642
  particle['lon'],
643
- particle['u_velocity'] * 100, # Scale for visibility
644
- particle['v_velocity'] * 100,
645
- particle.get('wave_height', 1)
646
  ])
647
 
648
- # Add Leaflet-Velocity script and initialization
649
- velocity_script = f"""
650
- <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
651
- <script src="https://cdn.jsdelivr.net/npm/leaflet-velocity@1.8.4/dist/leaflet-velocity.js"></script>
652
 
653
- <script>
654
- // Store velocity data globally
655
- window.velocityData = {json.dumps(velocity_json)};
656
- window.velocityLayerGlobal = null;
657
-
658
- // Initialize velocity layer when map is ready
659
- function initializeVelocityOnStartup() {{
660
- // Find the map instance (Folium creates it with a specific pattern)
661
- var mapKeys = Object.keys(window).filter(key => key.startsWith('map_'));
662
- if (mapKeys.length > 0) {{
663
- var mapInstance = window[mapKeys[0]];
664
- console.log('Found map instance:', mapKeys[0]);
665
-
666
- // Create velocity layer
667
- window.velocityLayerGlobal = L.velocityLayer({{
668
- displayValues: true,
669
- displayOptions: {{
670
- velocityType: 'Ocean Waves',
671
- position: 'bottomleft',
672
- emptyString: 'No wave data'
673
- }},
674
- data: window.velocityData.data,
675
- maxVelocity: 15,
676
- colorScale: ['#3288bd', '#66c2a5', '#abdda4', '#e6f598', '#fee08b', '#fdae61', '#f46d43', '#d53e4f'],
677
- particleAge: 120,
678
- particleMultiplier: 0.005,
679
- lineWidth: 2
680
- }});
681
-
682
- // Add to map
683
- window.velocityLayerGlobal.addTo(mapInstance);
684
- window.currentMapInstance = mapInstance;
685
- console.log('Wave particles initialized and added to map');
686
-
687
- return true;
688
- }}
689
- return false;
690
- }}
691
-
692
- // Try to initialize multiple times with different delays
693
- setTimeout(initializeVelocityOnStartup, 1000);
694
- setTimeout(initializeVelocityOnStartup, 2000);
695
- setTimeout(initializeVelocityOnStartup, 3000);
696
- setTimeout(initializeVelocityOnStartup, 5000);
697
-
698
- // Also try when DOM is ready
699
- document.addEventListener('DOMContentLoaded', function() {{
700
- setTimeout(initializeVelocityOnStartup, 1000);
701
- setTimeout(initializeVelocityOnStartup, 2000);
702
- }});
703
- </script>
704
- """
705
-
706
- # Add the velocity script to the map
707
- m.get_root().html.add_child(folium.Element(velocity_script))
708
-
709
- # Add wave height markers with popup info
710
- for i, particle in enumerate(particles[::10]): # Every 10th particle for markers
711
  if particle.get('lat') and particle.get('lon') and particle.get('wave_height'):
712
  # Color based on wave height
713
  if particle['wave_height'] < 2:
714
- color = 'blue'
715
  elif particle['wave_height'] < 4:
716
- color = 'lightblue'
717
  elif particle['wave_height'] < 6:
718
- color = 'orange'
719
  else:
720
- color = 'red'
721
 
722
  popup_text = f"""
 
723
  <b>🌊 Wave Data</b><br>
724
- Height: {particle['wave_height']:.2f}m<br>
725
- Direction: {particle.get('wave_direction', 0):.1f}Β°<br>
726
- Period: {particle.get('wave_period', 0):.1f}s<br>
727
- Region: {particle.get('region', 'Unknown')}
 
728
  """
729
 
730
  folium.CircleMarker(
731
  location=[particle['lat'], particle['lon']],
732
- radius=max(3, min(8, particle['wave_height'] * 2)),
733
  popup=folium.Popup(popup_text, max_width=200),
734
  color=color,
735
  fillColor=color,
736
- fillOpacity=0.6,
737
- weight=1
738
- ).add_to(m)
739
-
740
- # Add legend
741
- legend_html = f'''
742
- <div style="position: fixed;
743
- bottom: 50px; right: 50px; width: 180px; height: 140px;
744
- background-color: white; border:2px solid grey; z-index:9999;
745
- font-size:12px; padding: 10px; font-family: Arial;">
746
- <p><b>🌊 Wave Particle Data</b></p>
747
- <p><small>Particles: {len(particles)}</small></p>
748
- <p><small>Regions: {', '.join(data.get('regions_processed', ['Global']))}</small></p>
749
- <p><b>Wave Height:</b></p>
750
- <p><i style="color:blue">●</i> 0-2m (Low)</p>
751
- <p><i style="color:lightblue">●</i> 2-4m (Moderate)</p>
752
- <p><i style="color:orange">●</i> 4-6m (High)</p>
753
- <p><i style="color:red">●</i> 6+m (Extreme)</p>
754
- </div>
755
- '''
756
- m.get_root().html.add_child(folium.Element(legend_html))
 
 
 
 
 
 
 
 
 
 
 
 
 
757
 
758
- # Add working controls with proper functionality
 
 
 
759
  controls_html = f'''
760
- <div id="waveControls" style="position: fixed;
761
- top: 10px; left: 50px; z-index:9999;
762
- background-color: rgba(255,255,255,0.9);
763
- padding: 10px; border-radius: 5px;
764
- font-size:12px; font-family: Arial; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
765
- <b>🌊 Wave Controls</b><br>
766
- <button id="toggleParticles" onclick="toggleWaveLayer()" style="margin:2px; padding:4px 8px; background:#007cba; color:white; border:none; border-radius:3px; cursor:pointer;">⏸ Hide Particles</button>
767
- <button id="toggleTheme" onclick="changeTheme()" style="margin:2px; padding:4px 8px; background:#007cba; color:white; border:none; border-radius:3px; cursor:pointer;">πŸŒ™ Dark Mode</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
768
  </div>
769
 
770
  <script>
771
- // Global variables for control state
772
- var particlesVisible = true;
773
- var isDarkTheme = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
774
 
775
- function toggleWaveLayer() {{
776
- const btn = document.getElementById('toggleParticles');
 
777
 
778
- if (particlesVisible) {{
779
- // Hide particles
780
- if (window.velocityLayerGlobal && window.currentMapInstance) {{
781
- window.currentMapInstance.removeLayer(window.velocityLayerGlobal);
782
- }}
783
- btn.textContent = 'β–Ά Show Particles';
784
- particlesVisible = false;
785
- }} else {{
786
- // Show particles
787
- if (window.velocityLayerGlobal && window.currentMapInstance) {{
788
- window.velocityLayerGlobal.addTo(window.currentMapInstance);
789
  }} else {{
790
- // Try to reinitialize if needed
791
- initializeVelocityOnStartup();
792
  }}
793
- btn.textContent = '⏸ Hide Particles';
794
- particlesVisible = true;
795
  }}
796
  }}
797
 
798
- function changeTheme() {{
799
- const btn = document.getElementById('toggleTheme');
800
- var mapInstance = window.currentMapInstance;
801
 
802
- if (!mapInstance) {{
803
- // Try to find map instance
804
- var mapKeys = Object.keys(window).filter(key => key.startsWith('map_'));
805
- if (mapKeys.length > 0) {{
806
- mapInstance = window[mapKeys[0]];
807
- window.currentMapInstance = mapInstance;
808
  }}
809
  }}
 
 
 
 
 
810
 
811
- if (!mapInstance) return;
812
-
813
- if (isDarkTheme) {{
814
- // Switch to light theme
815
- mapInstance.eachLayer(function(layer) {{
816
- if (layer._url && layer._url.includes('dark')) {{
817
- mapInstance.removeLayer(layer);
818
  }}
819
- }});
820
-
821
- L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
822
- attribution: 'Β© OpenStreetMap contributors'
823
- }}).addTo(mapInstance);
824
-
825
- btn.textContent = 'πŸŒ™ Dark Mode';
826
- isDarkTheme = false;
827
- }} else {{
828
- // Switch to dark theme
829
- mapInstance.eachLayer(function(layer) {{
830
- if (layer._url && layer._url.includes('openstreetmap')) {{
831
- mapInstance.removeLayer(layer);
832
  }}
833
- }});
834
-
835
- L.tileLayer('https://{{s}}.basemaps.cartocdn.com/dark_all/{{z}}/{{x}}/{{y}}{{r}}.png', {{
836
- attribution: 'Β© CARTO Β© OpenStreetMap contributors'
837
- }}).addTo(mapInstance);
838
-
839
- btn.textContent = 'β˜€ Light Mode';
840
- isDarkTheme = true;
 
841
  }}
842
  }}
843
  </script>
844
  '''
 
845
  m.get_root().html.add_child(folium.Element(controls_html))
846
 
847
  # Return HTML representation
 
590
  return f"❌ Error: {str(e)}", None, "Failed to fetch particle data"
591
 
592
  def create_particle_wave_map(data):
593
+ """Create Folium map with wave layer toggles exactly like the wind app"""
594
  try:
595
  import folium
 
596
  import numpy as np
597
 
598
  particles = data.get('particles', [])
 
610
  center_lat = (min(lats) + max(lats)) / 2
611
  center_lon = (min(lons) + max(lons)) / 2
612
 
613
+ # Create Folium map exactly like wind app
614
  m = folium.Map(
615
  location=[center_lat, center_lon],
616
  zoom_start=4,
617
+ tiles=None # We'll add tiles manually
618
  )
619
 
620
+ # Add base tile layers (like wind app)
621
+ light_tiles = folium.TileLayer(
622
+ 'https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png',
623
+ name='OpenStreetMap',
624
+ attr='Β© OpenStreetMap contributors'
625
+ )
626
+ light_tiles.add_to(m)
627
+
628
+ dark_tiles = folium.TileLayer(
629
+ 'https://{{s}}.basemaps.cartocdn.com/dark_all/{{z}}/{{x}}/{{y}}{{r}}.png',
630
+ name='Dark',
631
+ attr='Β© CARTO Β© OpenStreetMap contributors'
632
+ )
633
+ dark_tiles.add_to(m)
634
+
635
  # Sample particles for performance
636
  import random
637
+ if len(particles) > 500:
638
+ particles = random.sample(particles, 500)
 
 
 
 
 
 
 
 
 
 
 
639
 
640
+ # Create wave height layer (like 10m wind in wind app)
641
+ wave_points = []
642
+ for particle in particles[::2]: # Every 2nd particle for performance
643
+ if particle.get('lat') and particle.get('lon') and particle.get('wave_height'):
644
+ wave_points.append([
645
  particle['lat'],
646
  particle['lon'],
647
+ particle['wave_height']
 
 
648
  ])
649
 
650
+ # Add wave markers as a feature group (like wind layers)
651
+ wave_layer = folium.FeatureGroup(name="Wave Heights", show=True)
 
 
652
 
653
+ for i, particle in enumerate(particles[::5]): # Every 5th particle
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  if particle.get('lat') and particle.get('lon') and particle.get('wave_height'):
655
  # Color based on wave height
656
  if particle['wave_height'] < 2:
657
+ color = '#0066ff'
658
  elif particle['wave_height'] < 4:
659
+ color = '#00aaff'
660
  elif particle['wave_height'] < 6:
661
+ color = '#ffaa00'
662
  else:
663
+ color = '#ff0000'
664
 
665
  popup_text = f"""
666
+ <div style="font-family: Arial; font-size: 12px;">
667
  <b>🌊 Wave Data</b><br>
668
+ <b>Height:</b> {particle['wave_height']:.2f}m<br>
669
+ <b>Direction:</b> {particle.get('wave_direction', 0):.1f}Β°<br>
670
+ <b>Period:</b> {particle.get('wave_period', 0):.1f}s<br>
671
+ <b>Region:</b> {particle.get('region', 'Unknown')}
672
+ </div>
673
  """
674
 
675
  folium.CircleMarker(
676
  location=[particle['lat'], particle['lon']],
677
+ radius=max(2, min(6, particle['wave_height'] * 1.5)),
678
  popup=folium.Popup(popup_text, max_width=200),
679
  color=color,
680
  fillColor=color,
681
+ fillOpacity=0.7,
682
+ weight=1,
683
+ opacity=0.8
684
+ ).add_to(wave_layer)
685
+
686
+ wave_layer.add_to(m)
687
+
688
+ # Add particle flow layer (simulated particle effect)
689
+ particle_layer = folium.FeatureGroup(name="Wave Particles", show=True)
690
+
691
+ # Add direction arrows as particles
692
+ for i, particle in enumerate(particles[::8]): # Every 8th particle
693
+ if all(key in particle for key in ['lat', 'lon', 'wave_direction', 'wave_height']):
694
+ # Create arrow marker pointing in wave direction
695
+ if particle['wave_height'] > 1: # Only show for significant waves
696
+ direction = particle['wave_direction']
697
+
698
+ # Create custom arrow icon
699
+ arrow_html = f'''
700
+ <div style="transform: rotate({direction}deg); color: rgba(100, 200, 255, 0.8); font-size: 16px;">
701
+ ↑
702
+ </div>
703
+ '''
704
+
705
+ folium.Marker(
706
+ location=[particle['lat'], particle['lon']],
707
+ icon=folium.DivIcon(
708
+ html=arrow_html,
709
+ icon_size=(20, 20),
710
+ icon_anchor=(10, 10)
711
+ )
712
+ ).add_to(particle_layer)
713
+
714
+ particle_layer.add_to(m)
715
 
716
+ # Add layer control (like wind app)
717
+ folium.LayerControl(collapsed=False).add_to(m)
718
+
719
+ # Add custom controls HTML (exactly like wind app structure)
720
  controls_html = f'''
721
+ <div style="position: fixed; top: 10px; left: 10px; z-index: 9999;
722
+ background: rgba(255,255,255,0.95); padding: 15px; border-radius: 8px;
723
+ box-shadow: 0 4px 8px rgba(0,0,0,0.2); font-family: Arial, sans-serif; font-size: 13px;">
724
+
725
+ <div style="margin-bottom: 10px;">
726
+ <strong>🌊 Wave Visualization</strong>
727
+ </div>
728
+
729
+ <div style="margin: 8px 0;">
730
+ <label style="display: flex; align-items: center; margin-bottom: 5px;">
731
+ <input type="checkbox" id="waveHeightToggle" checked onchange="toggleWaveHeight()"
732
+ style="margin-right: 8px;">
733
+ Wave Heights
734
+ </label>
735
+
736
+ <label style="display: flex; align-items: center;">
737
+ <input type="checkbox" id="waveParticleToggle" checked onchange="toggleWaveParticles()"
738
+ style="margin-right: 8px;">
739
+ Wave Particles
740
+ </label>
741
+ </div>
742
+
743
+ <div style="margin-top: 12px; padding-top: 10px; border-top: 1px solid #ddd;">
744
+ <label style="display: flex; align-items: center;">
745
+ <input type="checkbox" id="darkModeToggle" onchange="toggleDarkMode()"
746
+ style="margin-right: 8px;">
747
+ Dark Mode
748
+ </label>
749
+ </div>
750
+
751
+ <div style="margin-top: 8px; font-size: 11px; color: #666;">
752
+ Particles: {len(particles)} | Regions: {len(data.get('regions_processed', []))}
753
+ </div>
754
  </div>
755
 
756
  <script>
757
+ // Wait for map to be fully loaded
758
+ setTimeout(function() {{
759
+ // Get the map instance
760
+ var mapElement = document.querySelector('.folium-map');
761
+ if (mapElement && mapElement._leaflet_id) {{
762
+ var map = window[mapElement._leaflet_id];
763
+ window.waveMap = map;
764
+
765
+ // Get layer references
766
+ window.waveLayers = {{}};
767
+ map.eachLayer(function(layer) {{
768
+ if (layer.options && layer.options.name) {{
769
+ window.waveLayers[layer.options.name] = layer;
770
+ }}
771
+ }});
772
+
773
+ console.log('Wave map initialized with layers:', Object.keys(window.waveLayers));
774
+ }}
775
+ }}, 1000);
776
 
777
+ function toggleWaveHeight() {{
778
+ var checkbox = document.getElementById('waveHeightToggle');
779
+ var map = window.waveMap;
780
 
781
+ if (map && window.waveLayers['Wave Heights']) {{
782
+ if (checkbox.checked) {{
783
+ map.addLayer(window.waveLayers['Wave Heights']);
 
 
 
 
 
 
 
 
784
  }} else {{
785
+ map.removeLayer(window.waveLayers['Wave Heights']);
 
786
  }}
 
 
787
  }}
788
  }}
789
 
790
+ function toggleWaveParticles() {{
791
+ var checkbox = document.getElementById('waveParticleToggle');
792
+ var map = window.waveMap;
793
 
794
+ if (map && window.waveLayers['Wave Particles']) {{
795
+ if (checkbox.checked) {{
796
+ map.addLayer(window.waveLayers['Wave Particles']);
797
+ }} else {{
798
+ map.removeLayer(window.waveLayers['Wave Particles']);
 
799
  }}
800
  }}
801
+ }}
802
+
803
+ function toggleDarkMode() {{
804
+ var checkbox = document.getElementById('darkModeToggle');
805
+ var map = window.waveMap;
806
 
807
+ if (map && window.waveLayers) {{
808
+ if (checkbox.checked) {{
809
+ // Switch to dark
810
+ if (window.waveLayers['OpenStreetMap']) {{
811
+ map.removeLayer(window.waveLayers['OpenStreetMap']);
 
 
812
  }}
813
+ if (window.waveLayers['Dark']) {{
814
+ map.addLayer(window.waveLayers['Dark']);
 
 
 
 
 
 
 
 
 
 
 
815
  }}
816
+ }} else {{
817
+ // Switch to light
818
+ if (window.waveLayers['Dark']) {{
819
+ map.removeLayer(window.waveLayers['Dark']);
820
+ }}
821
+ if (window.waveLayers['OpenStreetMap']) {{
822
+ map.addLayer(window.waveLayers['OpenStreetMap']);
823
+ }}
824
+ }}
825
  }}
826
  }}
827
  </script>
828
  '''
829
+
830
  m.get_root().html.add_child(folium.Element(controls_html))
831
 
832
  # Return HTML representation