nakas Claude commited on
Commit
fe7cdf9
ยท
1 Parent(s): 23b6a4d

Implement dual radar system: Canadian + American support

Browse files

- Added RadarAnalyzer class supporting both Canadian and US radar
- Created radar toggle switch in Gradio interface
- Integrated NOAA/NEXRAD WMS endpoints for US radar data
- Downloaded multiple US radar legend options for user cropping
- Added create_us_legend_data.py helper script for US legend processing
- Updated map configuration to switch between radar systems
- Maintained backward compatibility with CanadianRadarAnalyzer

Features:
- ๐Ÿ‡จ๐Ÿ‡ฆ Canadian Radar (ECCC) - Full North America coverage
- ๐Ÿ‡บ๐Ÿ‡ธ American Radar (NOAA/NEXRAD) - Continental US coverage
- Toggle switch between radar systems
- Automatic map bounds adjustment per radar type
- Ready for US legend mapping once cropped legend is provided

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

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

.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
__pycache__/app.cpython-313.pyc ADDED
Binary file (14.1 kB). View file
 
__pycache__/radar_analyzer.cpython-313.pyc CHANGED
Binary files a/__pycache__/radar_analyzer.cpython-313.pyc and b/__pycache__/radar_analyzer.cpython-313.pyc differ
 
app.py CHANGED
@@ -6,36 +6,74 @@ import numpy as np
6
  from PIL import Image
7
  import base64
8
  import io
9
- from radar_analyzer import CanadianRadarAnalyzer
10
 
11
  class RadarAnalysisApp:
12
  def __init__(self):
13
- self.wms_url = "https://geo.weather.gc.ca/geomet"
14
- self.analyzer = CanadianRadarAnalyzer()
15
 
16
- # Extended North America bounds for map centering - covers Alaska to southeastern US
17
- self.canada_bounds = {
18
- "center": [52.5, -105.0],
19
- "southwest": [20, -170],
20
- "northeast": [85, -40]
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  def fetch_current_radar(self):
24
- """Fetch the most recent Canadian radar image."""
25
  try:
26
- url = f"{self.wms_url}?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&LAYERS=RADAR_1KM_RRAI&STYLES=&CRS=EPSG:4326&BBOX=20,-170,85,-40&WIDTH=1200&HEIGHT=800&FORMAT=image/png"
 
 
 
 
27
 
 
 
 
 
 
28
  response = requests.get(url, timeout=30)
29
  response.raise_for_status()
30
 
31
- # Save the radar image
32
- with open('current_radar_fetch.png', 'wb') as f:
 
33
  f.write(response.content)
34
 
35
- return 'current_radar_fetch.png'
36
 
37
  except Exception as e:
38
- print(f"Error fetching radar: {e}")
39
  return None
40
 
41
  def analyze_current_radar(self):
@@ -91,10 +129,11 @@ class RadarAnalysisApp:
91
  def create_radar_map_with_analysis(self, show_analysis=True):
92
  """Create a Folium map with radar overlay and optional analysis."""
93
  try:
94
- # Create base map
 
95
  m = folium.Map(
96
- location=self.canada_bounds["center"],
97
- zoom_start=4,
98
  tiles="OpenStreetMap"
99
  )
100
 
@@ -109,17 +148,18 @@ class RadarAnalysisApp:
109
  pass
110
 
111
  # Add live radar overlay with hover functionality
 
112
  wms_layer = folium.raster_layers.WmsTileLayer(
113
- url=self.wms_url,
114
- layers="RADAR_1KM_RRAI",
115
  version="1.3.0",
116
  transparent=True,
117
  format="image/png",
118
- name="Canadian Radar - Rain",
119
  overlay=True,
120
  control=True,
121
  opacity=0.7,
122
- attr='<a href="https://eccc-msc.github.io/open-data/licence/readme_en/">ECCC</a>'
123
  )
124
 
125
  wms_layer.add_to(m)
@@ -198,7 +238,8 @@ class RadarAnalysisApp:
198
 
199
  except Exception as e:
200
  # Return error map
201
- error_map = folium.Map(location=self.canada_bounds["center"], zoom_start=4)
 
202
 
203
  error_html = f"""
204
  <div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
@@ -237,18 +278,47 @@ def show_live_radar():
237
  radar_map = app.create_radar_map_with_analysis(show_analysis=False)
238
  return radar_map._repr_html_()
239
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  # Create Gradio interface
241
- with gr.Blocks(title="Canadian Weather Radar Analyzer", theme=gr.themes.Soft()) as interface:
242
- gr.Markdown("# ๐Ÿ‡จ๐Ÿ‡ฆ Canadian Weather Radar Analyzer")
243
- gr.Markdown("Precise pixel-level analysis of Canadian radar data with dBZ reflectivity mapping")
244
 
245
  with gr.Row():
246
  with gr.Column(scale=1):
247
  gr.Markdown("## Controls")
248
 
 
 
 
 
 
 
 
 
 
 
249
  analyze_btn = gr.Button("๐Ÿ” Analyze Current Radar", variant="primary", size="lg")
250
  live_btn = gr.Button("๐Ÿ“ก Show Live Radar", variant="secondary")
251
 
 
 
 
 
 
 
 
252
  gr.Markdown("---")
253
 
254
  gr.Markdown("### Features:")
@@ -282,6 +352,12 @@ with gr.Blocks(title="Canadian Weather Radar Analyzer", theme=gr.themes.Soft())
282
  )
283
 
284
  # Event handlers
 
 
 
 
 
 
285
  analyze_btn.click(
286
  fn=update_radar_analysis,
287
  outputs=[radar_map, analysis_output, annotated_image]
 
6
  from PIL import Image
7
  import base64
8
  import io
9
+ from radar_analyzer import RadarAnalyzer, CanadianRadarAnalyzer
10
 
11
  class RadarAnalysisApp:
12
  def __init__(self):
13
+ self.current_radar_type = "canadian"
14
+ self.analyzer = RadarAnalyzer(radar_type=self.current_radar_type)
15
 
16
+ # Radar configurations
17
+ self.radar_configs = {
18
+ "canadian": {
19
+ "wms_url": "https://geo.weather.gc.ca/geomet",
20
+ "layer": "RADAR_1KM_RRAI",
21
+ "bounds": (20.0, 85.0, -170.0, -40.0), # lat_min, lat_max, lon_min, lon_max
22
+ "center": [52.5, -105.0],
23
+ "zoom": 3,
24
+ "name": "๐Ÿ‡จ๐Ÿ‡ฆ Canadian Radar (ECCC)"
25
+ },
26
+ "american": {
27
+ "wms_url": "https://nowcoast.noaa.gov/arcgis/services/nowcoast/radar_meteo_imagery_nexrad_time/MapServer/WMSServer",
28
+ "layer": "1",
29
+ "bounds": (20.0, 50.0, -130.0, -65.0), # Continental US
30
+ "center": [39.0, -98.0],
31
+ "zoom": 4,
32
+ "name": "๐Ÿ‡บ๐Ÿ‡ธ American Radar (NOAA/NEXRAD)"
33
+ }
34
  }
35
+
36
+ # Current configuration
37
+ self.current_config = self.radar_configs[self.current_radar_type]
38
+
39
+ def switch_radar_type(self, radar_type: str):
40
+ """Switch between Canadian and American radar systems."""
41
+ if radar_type.lower() in self.radar_configs:
42
+ self.current_radar_type = radar_type.lower()
43
+ self.current_config = self.radar_configs[self.current_radar_type]
44
+ self.analyzer = RadarAnalyzer(radar_type=self.current_radar_type)
45
+ print(f"โœ… Switched to {self.current_config['name']}")
46
+ return True
47
+ else:
48
+ print(f"โŒ Unknown radar type: {radar_type}")
49
+ return False
50
 
51
  def fetch_current_radar(self):
52
+ """Fetch the most recent radar image based on current radar type."""
53
  try:
54
+ config = self.current_config
55
+ lat_min, lat_max, lon_min, lon_max = config["bounds"]
56
+
57
+ if self.current_radar_type == "canadian":
58
+ url = f"{config['wms_url']}?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&LAYERS={config['layer']}&STYLES=&CRS=EPSG:4326&BBOX={lat_min},{lon_min},{lat_max},{lon_max}&WIDTH=1200&HEIGHT=800&FORMAT=image/png"
59
 
60
+ elif self.current_radar_type == "american":
61
+ # NOAA NEXRAD WMS request
62
+ url = f"{config['wms_url']}?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&LAYERS={config['layer']}&STYLES=&CRS=EPSG:4326&BBOX={lat_min},{lon_min},{lat_max},{lon_max}&WIDTH=1200&HEIGHT=800&FORMAT=image/png&TIME=current"
63
+
64
+ print(f"๐Ÿ“ก Fetching {config['name']} data...")
65
  response = requests.get(url, timeout=30)
66
  response.raise_for_status()
67
 
68
+ # Save the radar image with radar type prefix
69
+ filename = f'{self.current_radar_type}_radar_fetch.png'
70
+ with open(filename, 'wb') as f:
71
  f.write(response.content)
72
 
73
+ return filename
74
 
75
  except Exception as e:
76
+ print(f"Error fetching {self.current_radar_type} radar: {e}")
77
  return None
78
 
79
  def analyze_current_radar(self):
 
129
  def create_radar_map_with_analysis(self, show_analysis=True):
130
  """Create a Folium map with radar overlay and optional analysis."""
131
  try:
132
+ # Create base map using current radar configuration
133
+ config = self.current_config
134
  m = folium.Map(
135
+ location=config["center"],
136
+ zoom_start=config["zoom"],
137
  tiles="OpenStreetMap"
138
  )
139
 
 
148
  pass
149
 
150
  # Add live radar overlay with hover functionality
151
+ config = self.current_config
152
  wms_layer = folium.raster_layers.WmsTileLayer(
153
+ url=config["wms_url"],
154
+ layers=config["layer"],
155
  version="1.3.0",
156
  transparent=True,
157
  format="image/png",
158
+ name=config["name"],
159
  overlay=True,
160
  control=True,
161
  opacity=0.7,
162
+ attr=f'<a href="#">{config["name"]}</a>'
163
  )
164
 
165
  wms_layer.add_to(m)
 
238
 
239
  except Exception as e:
240
  # Return error map
241
+ config = self.current_config
242
+ error_map = folium.Map(location=config["center"], zoom_start=config["zoom"])
243
 
244
  error_html = f"""
245
  <div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
 
278
  radar_map = app.create_radar_map_with_analysis(show_analysis=False)
279
  return radar_map._repr_html_()
280
 
281
+ def switch_radar_system(radar_type):
282
+ """Switch between Canadian and American radar systems."""
283
+ success = app.switch_radar_type(radar_type)
284
+ if success:
285
+ config = app.current_config
286
+ status_msg = f"โœ… Switched to {config['name']}"
287
+ # Update the map view
288
+ radar_map = app.create_radar_map_with_analysis(show_analysis=False)
289
+ return status_msg, radar_map._repr_html_()
290
+ else:
291
+ return f"โŒ Failed to switch to {radar_type} radar", ""
292
+
293
  # Create Gradio interface
294
+ with gr.Blocks(title="North American Weather Radar Analyzer", theme=gr.themes.Soft()) as interface:
295
+ gr.Markdown("# ๐ŸŒŽ North American Weather Radar Analyzer")
296
+ gr.Markdown("Dual radar system supporting Canadian (ECCC) and American (NOAA/NEXRAD) weather data")
297
 
298
  with gr.Row():
299
  with gr.Column(scale=1):
300
  gr.Markdown("## Controls")
301
 
302
+ # Radar system selector
303
+ radar_selector = gr.Radio(
304
+ choices=["canadian", "american"],
305
+ value="canadian",
306
+ label="๐ŸŒ Radar System",
307
+ info="Switch between Canadian and American radar data"
308
+ )
309
+
310
+ gr.Markdown("---")
311
+
312
  analyze_btn = gr.Button("๐Ÿ” Analyze Current Radar", variant="primary", size="lg")
313
  live_btn = gr.Button("๐Ÿ“ก Show Live Radar", variant="secondary")
314
 
315
+ # Status display
316
+ status_display = gr.Textbox(
317
+ value="๐Ÿ‡จ๐Ÿ‡ฆ Canadian Radar Active",
318
+ label="System Status",
319
+ interactive=False
320
+ )
321
+
322
  gr.Markdown("---")
323
 
324
  gr.Markdown("### Features:")
 
352
  )
353
 
354
  # Event handlers
355
+ radar_selector.change(
356
+ fn=switch_radar_system,
357
+ inputs=[radar_selector],
358
+ outputs=[status_display, radar_map]
359
+ )
360
+
361
  analyze_btn.click(
362
  fn=update_radar_analysis,
363
  outputs=[radar_map, analysis_output, annotated_image]
create_us_legend_data.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Helper script to create US radar legend data from a cropped legend image.
4
+ Run this after the user provides a cropped US radar legend.
5
+ """
6
+
7
+ from radar_analyzer import RadarAnalyzer
8
+ import json
9
+ import os
10
+
11
+ def create_us_legend_data(legend_image_path: str):
12
+ """Create legend data JSON file from US radar legend image."""
13
+
14
+ if not os.path.exists(legend_image_path):
15
+ print(f"โŒ Legend file not found: {legend_image_path}")
16
+ return False
17
+
18
+ print(f"๐ŸŽจ Processing US radar legend: {legend_image_path}")
19
+
20
+ try:
21
+ # Create US radar analyzer
22
+ analyzer = RadarAnalyzer(radar_type="american", legend_path=legend_image_path)
23
+
24
+ # Extract colors using the precise extraction method
25
+ color_ranges = analyzer._extract_precise_legend_colors()
26
+
27
+ if color_ranges:
28
+ print(f"โœ… Extracted {len(color_ranges)} color ranges from US legend")
29
+
30
+ # Show sample colors and dBZ ranges
31
+ print('\n๐Ÿ“Š Sample US radar color ranges:')
32
+ for i, cr in enumerate(color_ranges[::len(color_ranges)//10]): # Show every 10th
33
+ print(f' {cr.name}: {cr.min_value:.1f}-{cr.max_value:.1f} dBZ -> RGB{cr.rgb}')
34
+
35
+ # Save the US legend data
36
+ cache_data = {
37
+ 'source_file': legend_image_path,
38
+ 'radar_type': 'american',
39
+ 'total_colors': len(color_ranges),
40
+ 'dbz_range': [color_ranges[0].min_value, color_ranges[-1].max_value] if color_ranges else [0, 0],
41
+ 'colors': []
42
+ }
43
+
44
+ for cr in color_ranges:
45
+ cache_data['colors'].append({
46
+ 'min_value': cr.min_value,
47
+ 'max_value': cr.max_value,
48
+ 'rgb': list(cr.rgb),
49
+ 'name': cr.name,
50
+ 'dbz_center': (cr.min_value + cr.max_value) / 2
51
+ })
52
+
53
+ # Save with descriptive filename
54
+ output_file = 'us_radar_legend_data.json'
55
+ with open(output_file, 'w') as f:
56
+ json.dump(cache_data, f, indent=2)
57
+
58
+ print(f'๐Ÿ’พ Saved US legend data: {output_file}')
59
+
60
+ # Show dBZ range
61
+ if color_ranges:
62
+ min_dbz = min(cr.min_value for cr in color_ranges)
63
+ max_dbz = max(cr.max_value for cr in color_ranges)
64
+ print(f'๐Ÿ“Š US dBZ Range: {min_dbz:.1f} to {max_dbz:.1f}')
65
+
66
+ print('\nโœ… US radar legend data created successfully!')
67
+ print('๐Ÿ”„ The dual radar system is now ready to use!')
68
+
69
+ return True
70
+
71
+ else:
72
+ print(f'โŒ No colors extracted from {legend_image_path}')
73
+ return False
74
+
75
+ except Exception as e:
76
+ print(f'โŒ Error processing US legend: {e}')
77
+ return False
78
+
79
+ if __name__ == "__main__":
80
+ import sys
81
+
82
+ if len(sys.argv) != 2:
83
+ print("Usage: python create_us_legend_data.py <path_to_cropped_us_legend.png>")
84
+ print("Example: python create_us_legend_data.py us_radar_legend_cropped.png")
85
+ sys.exit(1)
86
+
87
+ legend_path = sys.argv[1]
88
+ success = create_us_legend_data(legend_path)
89
+
90
+ if success:
91
+ sys.exit(0)
92
+ else:
93
+ sys.exit(1)
radar_analyzer.py CHANGED
@@ -15,22 +15,37 @@ class ColorRange:
15
  rgb: Tuple[int, int, int]
16
  name: str
17
 
18
- class CanadianRadarAnalyzer:
19
  """
20
- Precise pixel analyzer for Canadian weather radar data.
21
- Analyzes dBZ reflectivity values using color mapping from ECCC radar legend.
22
  """
23
 
24
- def __init__(self, legend_path: str = "radar_legendwellcropped.png"):
25
- # ECCC Radar WMS endpoints
26
- self.wms_base_url = "https://geo.weather.gc.ca/geomet"
27
- self.radar_layer = "RADAR_1KM_RRAI" # 1km Rain Radar
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- # Load pre-computed legend data (much faster!)
30
- self.legend_path = legend_path
31
  self.precipitation_scale = self._load_precomputed_legend_data()
32
 
33
- # Color tolerance for matching (RGB distance) - increased for better detection
34
  self.color_tolerance = 40
35
 
36
  # Initialize reference colors from legend
@@ -38,6 +53,13 @@ class CanadianRadarAnalyzer:
38
  if not self.precipitation_scale:
39
  # Fallback to manual colors if legend extraction fails
40
  self._extract_legend_colors()
 
 
 
 
 
 
 
41
 
42
  def _load_precomputed_legend_data(self) -> List[ColorRange]:
43
  """
 
15
  rgb: Tuple[int, int, int]
16
  name: str
17
 
18
+ class RadarAnalyzer:
19
  """
20
+ Dual radar analyzer for Canadian and US weather radar data.
21
+ Analyzes dBZ reflectivity values using color mapping from radar legends.
22
  """
23
 
24
+ def __init__(self, radar_type: str = "canadian", legend_path: str = None):
25
+ self.radar_type = radar_type.lower()
26
+
27
+ if self.radar_type == "canadian":
28
+ # ECCC Radar WMS endpoints
29
+ self.wms_base_url = "https://geo.weather.gc.ca/geomet"
30
+ self.radar_layer = "RADAR_1KM_RRAI" # 1km Rain Radar
31
+ self.default_legend = "radar_legendwellcropped.png"
32
+ self.bounds = (20.0, 85.0, -170.0, -40.0) # lat_min, lat_max, lon_min, lon_max
33
+
34
+ elif self.radar_type == "american" or self.radar_type == "us":
35
+ # NOAA/NWS Radar endpoints (will be added)
36
+ self.wms_base_url = "https://nowcoast.noaa.gov/arcgis/services/nowcoast/radar_meteo_imagery_nexrad_time/MapServer/WMSServer"
37
+ self.radar_layer = "1" # NEXRAD reflectivity
38
+ self.default_legend = "us_radar_legend_cropped.png" # Will be provided by user
39
+ self.bounds = (20.0, 50.0, -130.0, -65.0) # Continental US bounds
40
+
41
+ else:
42
+ raise ValueError(f"Unsupported radar type: {radar_type}. Use 'canadian' or 'american'")
43
 
44
+ # Load legend data
45
+ self.legend_path = legend_path or self.default_legend
46
  self.precipitation_scale = self._load_precomputed_legend_data()
47
 
48
+ # Color tolerance for matching (RGB distance)
49
  self.color_tolerance = 40
50
 
51
  # Initialize reference colors from legend
 
53
  if not self.precipitation_scale:
54
  # Fallback to manual colors if legend extraction fails
55
  self._extract_legend_colors()
56
+
57
+
58
+ # Keep backward compatibility
59
+ class CanadianRadarAnalyzer(RadarAnalyzer):
60
+ """Backward compatibility class for Canadian radar."""
61
+ def __init__(self, legend_path: str = "radar_legendwellcropped.png"):
62
+ super().__init__(radar_type="canadian", legend_path=legend_path)
63
 
64
  def _load_precomputed_legend_data(self) -> List[ColorRange]:
65
  """
radar_legend_fresh_1.png ADDED
radar_legend_variant_1.png ADDED
radar_legend_variant_3.png ADDED
us_radar_legend_6.png ADDED
us_radar_legend_7.png ADDED
us_radar_legend_reference.png ADDED