Spaces:
Sleeping
Sleeping
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>
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
|
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='
|
619 |
)
|
620 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
621 |
# Sample particles for performance
|
622 |
import random
|
623 |
-
if len(particles) >
|
624 |
-
particles = random.sample(particles,
|
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 |
-
#
|
638 |
-
|
639 |
-
|
640 |
-
|
|
|
641 |
particle['lat'],
|
642 |
particle['lon'],
|
643 |
-
particle['
|
644 |
-
particle['v_velocity'] * 100,
|
645 |
-
particle.get('wave_height', 1)
|
646 |
])
|
647 |
|
648 |
-
# Add
|
649 |
-
|
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 |
-
|
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 = '
|
715 |
elif particle['wave_height'] < 4:
|
716 |
-
color = '
|
717 |
elif particle['wave_height'] < 6:
|
718 |
-
color = '
|
719 |
else:
|
720 |
-
color = '
|
721 |
|
722 |
popup_text = f"""
|
|
|
723 |
<b>π Wave Data</b><br>
|
724 |
-
Height
|
725 |
-
Direction
|
726 |
-
Period
|
727 |
-
Region
|
|
|
728 |
"""
|
729 |
|
730 |
folium.CircleMarker(
|
731 |
location=[particle['lat'], particle['lon']],
|
732 |
-
radius=max(
|
733 |
popup=folium.Popup(popup_text, max_width=200),
|
734 |
color=color,
|
735 |
fillColor=color,
|
736 |
-
fillOpacity=0.
|
737 |
-
weight=1
|
738 |
-
|
739 |
-
|
740 |
-
|
741 |
-
|
742 |
-
|
743 |
-
|
744 |
-
|
745 |
-
|
746 |
-
|
747 |
-
|
748 |
-
|
749 |
-
|
750 |
-
|
751 |
-
|
752 |
-
|
753 |
-
|
754 |
-
|
755 |
-
|
756 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
757 |
|
758 |
-
# Add
|
|
|
|
|
|
|
759 |
controls_html = f'''
|
760 |
-
<div
|
761 |
-
|
762 |
-
|
763 |
-
|
764 |
-
|
765 |
-
|
766 |
-
|
767 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
768 |
</div>
|
769 |
|
770 |
<script>
|
771 |
-
//
|
772 |
-
|
773 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
774 |
|
775 |
-
function
|
776 |
-
|
|
|
777 |
|
778 |
-
if (
|
779 |
-
|
780 |
-
|
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 |
-
|
791 |
-
initializeVelocityOnStartup();
|
792 |
}}
|
793 |
-
btn.textContent = 'βΈ Hide Particles';
|
794 |
-
particlesVisible = true;
|
795 |
}}
|
796 |
}}
|
797 |
|
798 |
-
function
|
799 |
-
|
800 |
-
var
|
801 |
|
802 |
-
if (
|
803 |
-
|
804 |
-
|
805 |
-
|
806 |
-
|
807 |
-
window.currentMapInstance = mapInstance;
|
808 |
}}
|
809 |
}}
|
|
|
|
|
|
|
|
|
|
|
810 |
|
811 |
-
if (
|
812 |
-
|
813 |
-
|
814 |
-
|
815 |
-
|
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 |
-
|
836 |
-
|
837 |
-
|
838 |
-
|
839 |
-
|
840 |
-
|
|
|
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
|