solarviewer 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solar_radio_image_viewer/__init__.py +12 -0
- solar_radio_image_viewer/assets/add_tab_default.png +0 -0
- solar_radio_image_viewer/assets/add_tab_default_light.png +0 -0
- solar_radio_image_viewer/assets/add_tab_hover.png +0 -0
- solar_radio_image_viewer/assets/add_tab_hover_light.png +0 -0
- solar_radio_image_viewer/assets/browse.png +0 -0
- solar_radio_image_viewer/assets/browse_light.png +0 -0
- solar_radio_image_viewer/assets/close_tab_default.png +0 -0
- solar_radio_image_viewer/assets/close_tab_default_light.png +0 -0
- solar_radio_image_viewer/assets/close_tab_hover.png +0 -0
- solar_radio_image_viewer/assets/close_tab_hover_light.png +0 -0
- solar_radio_image_viewer/assets/ellipse_selection.png +0 -0
- solar_radio_image_viewer/assets/ellipse_selection_light.png +0 -0
- solar_radio_image_viewer/assets/icons8-ellipse-90.png +0 -0
- solar_radio_image_viewer/assets/icons8-ellipse-90_light.png +0 -0
- solar_radio_image_viewer/assets/icons8-info-90.png +0 -0
- solar_radio_image_viewer/assets/icons8-info-90_light.png +0 -0
- solar_radio_image_viewer/assets/profile.png +0 -0
- solar_radio_image_viewer/assets/profile_light.png +0 -0
- solar_radio_image_viewer/assets/rectangle_selection.png +0 -0
- solar_radio_image_viewer/assets/rectangle_selection_light.png +0 -0
- solar_radio_image_viewer/assets/reset.png +0 -0
- solar_radio_image_viewer/assets/reset_light.png +0 -0
- solar_radio_image_viewer/assets/ruler.png +0 -0
- solar_radio_image_viewer/assets/ruler_light.png +0 -0
- solar_radio_image_viewer/assets/search.png +0 -0
- solar_radio_image_viewer/assets/search_light.png +0 -0
- solar_radio_image_viewer/assets/settings.png +0 -0
- solar_radio_image_viewer/assets/settings_light.png +0 -0
- solar_radio_image_viewer/assets/splash.fits +0 -0
- solar_radio_image_viewer/assets/zoom_60arcmin.png +0 -0
- solar_radio_image_viewer/assets/zoom_60arcmin_light.png +0 -0
- solar_radio_image_viewer/assets/zoom_in.png +0 -0
- solar_radio_image_viewer/assets/zoom_in_light.png +0 -0
- solar_radio_image_viewer/assets/zoom_out.png +0 -0
- solar_radio_image_viewer/assets/zoom_out_light.png +0 -0
- solar_radio_image_viewer/create_video.py +1345 -0
- solar_radio_image_viewer/dialogs.py +2665 -0
- solar_radio_image_viewer/from_simpl/__init__.py +184 -0
- solar_radio_image_viewer/from_simpl/caltable_visualizer.py +1001 -0
- solar_radio_image_viewer/from_simpl/dynamic_spectra_dialog.py +332 -0
- solar_radio_image_viewer/from_simpl/make_dynamic_spectra.py +351 -0
- solar_radio_image_viewer/from_simpl/pipeline_logger_gui.py +1232 -0
- solar_radio_image_viewer/from_simpl/simpl_theme.py +352 -0
- solar_radio_image_viewer/from_simpl/utils.py +984 -0
- solar_radio_image_viewer/from_simpl/view_dynamic_spectra_GUI.py +1975 -0
- solar_radio_image_viewer/helioprojective.py +1916 -0
- solar_radio_image_viewer/helioprojective_viewer.py +817 -0
- solar_radio_image_viewer/helioviewer_browser.py +1514 -0
- solar_radio_image_viewer/main.py +148 -0
- solar_radio_image_viewer/move_phasecenter.py +1269 -0
- solar_radio_image_viewer/napari_viewer.py +368 -0
- solar_radio_image_viewer/noaa_events/__init__.py +32 -0
- solar_radio_image_viewer/noaa_events/noaa_events.py +430 -0
- solar_radio_image_viewer/noaa_events/noaa_events_gui.py +1922 -0
- solar_radio_image_viewer/norms.py +293 -0
- solar_radio_image_viewer/radio_data_downloader/__init__.py +25 -0
- solar_radio_image_viewer/radio_data_downloader/radio_data_downloader.py +756 -0
- solar_radio_image_viewer/radio_data_downloader/radio_data_downloader_gui.py +528 -0
- solar_radio_image_viewer/searchable_combobox.py +220 -0
- solar_radio_image_viewer/solar_context/__init__.py +41 -0
- solar_radio_image_viewer/solar_context/active_regions.py +371 -0
- solar_radio_image_viewer/solar_context/cme_alerts.py +234 -0
- solar_radio_image_viewer/solar_context/context_images.py +297 -0
- solar_radio_image_viewer/solar_context/realtime_data.py +528 -0
- solar_radio_image_viewer/solar_data_downloader/__init__.py +35 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader.py +1667 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_cli.py +901 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_gui.py +1210 -0
- solar_radio_image_viewer/styles.py +643 -0
- solar_radio_image_viewer/utils/__init__.py +32 -0
- solar_radio_image_viewer/utils/rate_limiter.py +255 -0
- solar_radio_image_viewer/utils.py +952 -0
- solar_radio_image_viewer/video_dialog.py +2629 -0
- solar_radio_image_viewer/video_utils.py +656 -0
- solar_radio_image_viewer/viewer.py +11174 -0
- solarviewer-1.0.2.dist-info/METADATA +343 -0
- solarviewer-1.0.2.dist-info/RECORD +82 -0
- solarviewer-1.0.2.dist-info/WHEEL +5 -0
- solarviewer-1.0.2.dist-info/entry_points.txt +8 -0
- solarviewer-1.0.2.dist-info/licenses/LICENSE +21 -0
- solarviewer-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CME Alerts Parser - Fetches Coronal Mass Ejection data from NASA DONKI API.
|
|
4
|
+
|
|
5
|
+
Data source:
|
|
6
|
+
- NASA DONKI (Space Weather Database of Notifications, Knowledge, Information)
|
|
7
|
+
- Endpoint: kauai.ccmc.gsfc.nasa.gov/DONKI/WS/get/CME
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import urllib.request
|
|
12
|
+
import urllib.error
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime, date, timedelta
|
|
15
|
+
from typing import List, Optional, Dict, Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CMEEvent:
|
|
20
|
+
"""Represents a Coronal Mass Ejection event."""
|
|
21
|
+
activity_id: str
|
|
22
|
+
start_time: datetime
|
|
23
|
+
source_location: str # e.g., "S17W72"
|
|
24
|
+
active_region: Optional[str]
|
|
25
|
+
speed: float # km/s
|
|
26
|
+
half_angle: float # degrees (angular width)
|
|
27
|
+
latitude: float
|
|
28
|
+
longitude: float
|
|
29
|
+
is_earth_directed: bool
|
|
30
|
+
earth_arrival_time: Optional[datetime]
|
|
31
|
+
note: str
|
|
32
|
+
analysis_link: str
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def speed_category(self) -> str:
|
|
36
|
+
"""Categorize CME speed."""
|
|
37
|
+
if self.speed < 500:
|
|
38
|
+
return "Slow"
|
|
39
|
+
elif self.speed < 1000:
|
|
40
|
+
return "Moderate"
|
|
41
|
+
elif self.speed < 2000:
|
|
42
|
+
return "Fast"
|
|
43
|
+
else:
|
|
44
|
+
return "Extreme"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def color_code(self) -> str:
|
|
48
|
+
"""Get color based on speed and Earth-directed status."""
|
|
49
|
+
if self.is_earth_directed:
|
|
50
|
+
if self.speed >= 1500:
|
|
51
|
+
return "#F44336" # Red - fast Earth-directed
|
|
52
|
+
elif self.speed >= 1000:
|
|
53
|
+
return "#FF9800" # Orange
|
|
54
|
+
else:
|
|
55
|
+
return "#FFC107" # Amber
|
|
56
|
+
else:
|
|
57
|
+
return "#4CAF50" # Green - not Earth-directed
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def start_time_str(self) -> str:
|
|
61
|
+
"""Format start time."""
|
|
62
|
+
return self.start_time.strftime("%Y-%m-%d %H:%M")
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def arrival_str(self) -> str:
|
|
66
|
+
"""Format Earth arrival time."""
|
|
67
|
+
if self.earth_arrival_time:
|
|
68
|
+
return self.earth_arrival_time.strftime("%Y-%m-%d %H:%M")
|
|
69
|
+
return "—"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def fetch_cme_data(event_date: date, days_range: int = 3) -> Optional[List[Dict]]:
|
|
73
|
+
"""
|
|
74
|
+
Fetch CME data from NASA DONKI API.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
event_date: Center date to search around
|
|
78
|
+
days_range: Number of days before and after to search
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Raw JSON data or None if fetch failed
|
|
82
|
+
"""
|
|
83
|
+
start_date = (event_date - timedelta(days=days_range)).strftime("%Y-%m-%d")
|
|
84
|
+
end_date = (event_date + timedelta(days=days_range)).strftime("%Y-%m-%d")
|
|
85
|
+
|
|
86
|
+
url = f"https://kauai.ccmc.gsfc.nasa.gov/DONKI/WS/get/CME?startDate={start_date}&endDate={end_date}"
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
from ..utils import get_global_session
|
|
90
|
+
session = get_global_session()
|
|
91
|
+
response = session.get(url)
|
|
92
|
+
return json.loads(response.text)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
if hasattr(e, 'response') and e.response is not None:
|
|
95
|
+
if e.response.status_code == 404:
|
|
96
|
+
return []
|
|
97
|
+
print(f"CME fetch error: {e}")
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def parse_cme_events(raw_data: List[Dict]) -> List[CMEEvent]:
|
|
102
|
+
"""
|
|
103
|
+
Parse CME JSON data into CMEEvent objects.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
raw_data: Raw JSON from DONKI API
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of CMEEvent objects
|
|
110
|
+
"""
|
|
111
|
+
events = []
|
|
112
|
+
|
|
113
|
+
for cme in raw_data:
|
|
114
|
+
try:
|
|
115
|
+
activity_id = cme.get("activityID", "Unknown")
|
|
116
|
+
start_time_str = activity_id.split("-CME-")[0] if "-CME-" in activity_id else None
|
|
117
|
+
|
|
118
|
+
if start_time_str:
|
|
119
|
+
try:
|
|
120
|
+
start_time = datetime.fromisoformat(start_time_str)
|
|
121
|
+
except ValueError:
|
|
122
|
+
start_time = datetime.now()
|
|
123
|
+
else:
|
|
124
|
+
start_time = datetime.now()
|
|
125
|
+
|
|
126
|
+
# Get the most accurate analysis
|
|
127
|
+
analyses = cme.get("cmeAnalyses") or []
|
|
128
|
+
best_analysis = None
|
|
129
|
+
for analysis in analyses:
|
|
130
|
+
if analysis.get("isMostAccurate"):
|
|
131
|
+
best_analysis = analysis
|
|
132
|
+
break
|
|
133
|
+
if not best_analysis and analyses:
|
|
134
|
+
best_analysis = analyses[0]
|
|
135
|
+
|
|
136
|
+
if not best_analysis:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
speed = best_analysis.get("speed", 0) or 0
|
|
140
|
+
half_angle = best_analysis.get("halfAngle", 0) or 0
|
|
141
|
+
latitude = best_analysis.get("latitude", 0) or 0
|
|
142
|
+
longitude = best_analysis.get("longitude", 0) or 0
|
|
143
|
+
note = best_analysis.get("note", "") or ""
|
|
144
|
+
analysis_link = best_analysis.get("link", "")
|
|
145
|
+
|
|
146
|
+
# Check for Earth impact
|
|
147
|
+
is_earth_directed = False
|
|
148
|
+
earth_arrival_time = None
|
|
149
|
+
|
|
150
|
+
enlil_list = best_analysis.get("enlilList") or []
|
|
151
|
+
for enlil in enlil_list:
|
|
152
|
+
if enlil.get("isEarthGB") or enlil.get("isEarthMinorImpact"):
|
|
153
|
+
is_earth_directed = True
|
|
154
|
+
impact_list = enlil.get("impactList") or []
|
|
155
|
+
for impact in impact_list:
|
|
156
|
+
if "Earth" in impact.get("location", ""):
|
|
157
|
+
is_earth_directed = True
|
|
158
|
+
arrival_str = impact.get("arrivalTime")
|
|
159
|
+
if arrival_str:
|
|
160
|
+
try:
|
|
161
|
+
earth_arrival_time = datetime.fromisoformat(arrival_str.replace("Z", ""))
|
|
162
|
+
except ValueError:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
# Determine if likely Earth-directed based on longitude
|
|
166
|
+
if abs(longitude) < 45: # CME centered within ±45° of Sun-Earth line
|
|
167
|
+
is_earth_directed = True
|
|
168
|
+
|
|
169
|
+
# Format source location
|
|
170
|
+
lat_hem = "N" if latitude >= 0 else "S"
|
|
171
|
+
lon_hem = "E" if longitude <= 0 else "W"
|
|
172
|
+
source_location = f"{lat_hem}{abs(int(latitude))}{lon_hem}{abs(int(longitude))}"
|
|
173
|
+
|
|
174
|
+
active_region = cme.get("activeRegionNum")
|
|
175
|
+
if active_region:
|
|
176
|
+
active_region = str(active_region)
|
|
177
|
+
|
|
178
|
+
events.append(CMEEvent(
|
|
179
|
+
activity_id=activity_id,
|
|
180
|
+
start_time=start_time,
|
|
181
|
+
source_location=source_location,
|
|
182
|
+
active_region=active_region,
|
|
183
|
+
speed=speed,
|
|
184
|
+
half_angle=half_angle,
|
|
185
|
+
latitude=latitude,
|
|
186
|
+
longitude=longitude,
|
|
187
|
+
is_earth_directed=is_earth_directed,
|
|
188
|
+
earth_arrival_time=earth_arrival_time,
|
|
189
|
+
note=note[:200] + "..." if len(note) > 200 else note,
|
|
190
|
+
analysis_link=analysis_link,
|
|
191
|
+
))
|
|
192
|
+
except Exception as e:
|
|
193
|
+
print(f"Error parsing CME: {e}")
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
# Sort by start time
|
|
197
|
+
events.sort(key=lambda e: e.start_time, reverse=True)
|
|
198
|
+
return events
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def fetch_and_parse_cme_events(event_date: date, days_range: int = 3) -> Optional[List[CMEEvent]]:
|
|
202
|
+
"""
|
|
203
|
+
Fetch and parse CME events for a date range.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
event_date: Center date to search around
|
|
207
|
+
days_range: Number of days before and after to search
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
List of CMEEvent objects or None if fetch failed
|
|
211
|
+
"""
|
|
212
|
+
raw_data = fetch_cme_data(event_date, days_range)
|
|
213
|
+
if raw_data is None:
|
|
214
|
+
return None
|
|
215
|
+
if not raw_data:
|
|
216
|
+
return []
|
|
217
|
+
return parse_cme_events(raw_data)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
# Test with sample date
|
|
222
|
+
from datetime import date
|
|
223
|
+
|
|
224
|
+
test_date = date(2024, 6, 9)
|
|
225
|
+
print(f"Fetching CME events around {test_date}...")
|
|
226
|
+
|
|
227
|
+
events = fetch_and_parse_cme_events(test_date)
|
|
228
|
+
if events:
|
|
229
|
+
print(f"Found {len(events)} CME events:")
|
|
230
|
+
for e in events[:5]:
|
|
231
|
+
earth_str = "🌍 Earth-directed" if e.is_earth_directed else ""
|
|
232
|
+
print(f" {e.start_time_str} | {e.speed:.0f} km/s ({e.speed_category}) | {e.source_location} {earth_str}")
|
|
233
|
+
else:
|
|
234
|
+
print("No CME events found or fetch failed")
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Context Images Fetcher - Get solar context imagery from Helioviewer API or SolarMonitor.org.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import date, datetime
|
|
8
|
+
from typing import List, Optional, Dict, Tuple
|
|
9
|
+
import requests
|
|
10
|
+
import re
|
|
11
|
+
from urllib.parse import urljoin
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ContextImage:
|
|
16
|
+
"""Represents a context image."""
|
|
17
|
+
title: str
|
|
18
|
+
thumb_url: str
|
|
19
|
+
page_url: str # URL to the full disk page or full-res image
|
|
20
|
+
instrument: str
|
|
21
|
+
description: str
|
|
22
|
+
credits: str = "Helioviewer" # Credit source
|
|
23
|
+
source_id: Optional[int] = None # Helioviewer sourceId if applicable
|
|
24
|
+
|
|
25
|
+
# Essential instruments for context viewing
|
|
26
|
+
# Format: (nickname, layer_path, description)
|
|
27
|
+
# layer_path format: [observatory,instrument,detector,measurement,visible,opacity]
|
|
28
|
+
ESSENTIAL_INSTRUMENTS = [
|
|
29
|
+
("AIA 304", "[SDO,AIA,AIA,304,1,100]", "SDO", "304Å Chromosphere & Transition Region"),
|
|
30
|
+
("AIA 193", "[SDO,AIA,AIA,193,1,100]", "SDO", "193Å Corona & Hot Flare Plasma"),
|
|
31
|
+
("AIA 171", "[SDO,AIA,AIA,171,1,100]", "SDO", "171Å Quiet Corona"),
|
|
32
|
+
("AIA 335", "[SDO,AIA,AIA,335,1,100]", "SDO", "335Å Active Region Corona"),
|
|
33
|
+
("HMI Mag", "[SDO,HMI,HMI,magnetogram,1,100]", "SDO", "Magnetogram (Magnetic Field)"),
|
|
34
|
+
("HMI Int", "[SDO,HMI,HMI,continuum,1,100]", "SDO", "Continuum (Photosphere)"),
|
|
35
|
+
("LASCO C2", "[SOHO,LASCO,C2,white-light,1,100]", "SOHO", "Inner Coronagraph (2-6 Rs)"),
|
|
36
|
+
("LASCO C3", "[SOHO,LASCO,C3,white-light,1,100]", "SOHO", "Outer Coronagraph (4-30 Rs)"),
|
|
37
|
+
("SUVI 171", "[GOES,SUVI,SUVI,171,1,100]", "GOES", "171Å Corona"),
|
|
38
|
+
("SUVI 304", "[GOES,SUVI,SUVI,304,1,100]", "GOES", "304Å Chromosphere"),
|
|
39
|
+
("EUVI-A 171", "[STEREO_A,SECCHI,EUVI,171,1,100]", "STEREO_A", "171Å from STEREO-A"),
|
|
40
|
+
("EUVI-A 304", "[STEREO_A,SECCHI,EUVI,304,1,100]", "STEREO_A", "304Å from STEREO-A"),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
def fetch_from_helioviewer(event_date: date) -> List[ContextImage]:
|
|
44
|
+
"""
|
|
45
|
+
Fetch essential context images from Helioviewer API as PNG screenshots.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
event_date: Date to fetch images for
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of ContextImage objects with PNG image URLs
|
|
52
|
+
"""
|
|
53
|
+
# Format date for Helioviewer API (ISO 8601 UTC)
|
|
54
|
+
date_str = f"{event_date.strftime('%Y-%m-%d')}T12:00:00Z"
|
|
55
|
+
|
|
56
|
+
images = []
|
|
57
|
+
|
|
58
|
+
# Fetch only essential instruments with PNG screenshots
|
|
59
|
+
for nickname, layer_path, observatory, description in ESSENTIAL_INSTRUMENTS:
|
|
60
|
+
# Determine image type for appropriate field of view
|
|
61
|
+
is_lasco_c2 = 'LASCO,C2' in layer_path
|
|
62
|
+
is_lasco_c3 = 'LASCO,C3' in layer_path
|
|
63
|
+
|
|
64
|
+
# Thumbnail parameters - optimized for each image type
|
|
65
|
+
base_url = "https://api.helioviewer.org/v2/takeScreenshot/"
|
|
66
|
+
|
|
67
|
+
if is_lasco_c2:
|
|
68
|
+
# LASCO C2: Inner coronagraph - needs MORE FoV than before
|
|
69
|
+
thumb_params = {
|
|
70
|
+
'date': date_str,
|
|
71
|
+
'imageScale': '75',
|
|
72
|
+
'layers': layer_path,
|
|
73
|
+
'x0': '0',
|
|
74
|
+
'y0': '0',
|
|
75
|
+
'width': '160',
|
|
76
|
+
'height': '160',
|
|
77
|
+
'display': 'true',
|
|
78
|
+
'watermark': 'false'
|
|
79
|
+
}
|
|
80
|
+
full_params = {
|
|
81
|
+
'date': date_str,
|
|
82
|
+
'imageScale': '6',
|
|
83
|
+
'layers': layer_path,
|
|
84
|
+
'x0': '0',
|
|
85
|
+
'y0': '0',
|
|
86
|
+
'width': '2048',
|
|
87
|
+
'height': '2048',
|
|
88
|
+
'display': 'true',
|
|
89
|
+
'watermark': 'false'
|
|
90
|
+
}
|
|
91
|
+
elif is_lasco_c3:
|
|
92
|
+
# LASCO C3: Outer coronagraph - needs A LOT MORE FoV
|
|
93
|
+
thumb_params = {
|
|
94
|
+
'date': date_str,
|
|
95
|
+
'imageScale': '240',
|
|
96
|
+
'layers': layer_path,
|
|
97
|
+
'x0': '0',
|
|
98
|
+
'y0': '0',
|
|
99
|
+
'width': '160',
|
|
100
|
+
'height': '160',
|
|
101
|
+
'display': 'true',
|
|
102
|
+
'watermark': 'false'
|
|
103
|
+
}
|
|
104
|
+
full_params = {
|
|
105
|
+
'date': date_str,
|
|
106
|
+
'imageScale': '28.0',
|
|
107
|
+
'layers': layer_path,
|
|
108
|
+
'x0': '0',
|
|
109
|
+
'y0': '0',
|
|
110
|
+
'width': '2048',
|
|
111
|
+
'height': '2048',
|
|
112
|
+
'display': 'true',
|
|
113
|
+
'watermark': 'false'
|
|
114
|
+
}
|
|
115
|
+
else:
|
|
116
|
+
# Solar disk images - need LESS FoV (less black space)
|
|
117
|
+
thumb_params = {
|
|
118
|
+
'date': date_str,
|
|
119
|
+
'imageScale': '15.0',
|
|
120
|
+
'layers': layer_path,
|
|
121
|
+
'x0': '0',
|
|
122
|
+
'y0': '0',
|
|
123
|
+
'width': '160',
|
|
124
|
+
'height': '160',
|
|
125
|
+
'display': 'true',
|
|
126
|
+
'watermark': 'false'
|
|
127
|
+
}
|
|
128
|
+
full_params = {
|
|
129
|
+
'date': date_str,
|
|
130
|
+
'imageScale': '1.6',
|
|
131
|
+
'layers': layer_path,
|
|
132
|
+
'x0': '0',
|
|
133
|
+
'y0': '0',
|
|
134
|
+
'width': '1600',
|
|
135
|
+
'height': '1600',
|
|
136
|
+
'display': 'true',
|
|
137
|
+
'watermark': 'false'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Construct URLs with parameters
|
|
141
|
+
from urllib.parse import urlencode
|
|
142
|
+
thumb_url = f"{base_url}?{urlencode(thumb_params)}"
|
|
143
|
+
full_url = f"{base_url}?{urlencode(full_params)}"
|
|
144
|
+
|
|
145
|
+
images.append(ContextImage(
|
|
146
|
+
title=nickname,
|
|
147
|
+
thumb_url=thumb_url,
|
|
148
|
+
page_url=full_url,
|
|
149
|
+
instrument=observatory,
|
|
150
|
+
description=description,
|
|
151
|
+
credits="Helioviewer",
|
|
152
|
+
source_id=None
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
return images
|
|
156
|
+
|
|
157
|
+
def fetch_from_solarmonitor(event_date: date) -> List[ContextImage]:
|
|
158
|
+
"""
|
|
159
|
+
Scrape SolarMonitor index page to find all available context images for the date.
|
|
160
|
+
Returns list of ContextImage objects with thumbnail and page URLs.
|
|
161
|
+
|
|
162
|
+
This is the legacy/fallback method.
|
|
163
|
+
"""
|
|
164
|
+
date_str = event_date.strftime("%Y%m%d")
|
|
165
|
+
base_url = "https://solarmonitor.org/"
|
|
166
|
+
index_url = f"{base_url}index.php?date={date_str}"
|
|
167
|
+
|
|
168
|
+
images = []
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
from ..utils import get_global_session
|
|
172
|
+
except ImportError:
|
|
173
|
+
from solar_radio_image_viewer.utils import get_global_session
|
|
174
|
+
|
|
175
|
+
session = get_global_session()
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
response = session.get(index_url)
|
|
179
|
+
|
|
180
|
+
if response.status_code != 200:
|
|
181
|
+
print(f"SolarMonitor index fetch failed: {response.status_code}")
|
|
182
|
+
return []
|
|
183
|
+
|
|
184
|
+
html = response.text
|
|
185
|
+
|
|
186
|
+
# Regex to find links to full_disk.php wrapping a thumbnail
|
|
187
|
+
pattern = re.compile(r'href=["\']?(full_disk\.php[^"\'>]+)["\']?[^>]*>[\s\S]*?<img[^>]+src=["\']?([^"\'> ]+thumb\.png)["\']?', re.IGNORECASE)
|
|
188
|
+
|
|
189
|
+
matches = pattern.findall(html)
|
|
190
|
+
|
|
191
|
+
seen_types = set()
|
|
192
|
+
|
|
193
|
+
for page_link, thumb_path in matches:
|
|
194
|
+
# Extract 'type' from page_link to use as title/ID
|
|
195
|
+
type_match = re.search(r'type=([a-zA-Z0-9_]+)', page_link)
|
|
196
|
+
img_type = type_match.group(1) if type_match else "unknown"
|
|
197
|
+
|
|
198
|
+
if img_type in seen_types:
|
|
199
|
+
continue
|
|
200
|
+
seen_types.add(img_type)
|
|
201
|
+
|
|
202
|
+
# Construct absolute URLs
|
|
203
|
+
full_page_url = urljoin(base_url, page_link)
|
|
204
|
+
full_thumb_url = urljoin(base_url, thumb_path)
|
|
205
|
+
|
|
206
|
+
# Metadata lookup
|
|
207
|
+
instrument, desc = _identify_instrument(img_type)
|
|
208
|
+
|
|
209
|
+
images.append(ContextImage(
|
|
210
|
+
title=img_type.replace('_', ' ').upper(),
|
|
211
|
+
thumb_url=full_thumb_url,
|
|
212
|
+
page_url=full_page_url,
|
|
213
|
+
instrument=instrument,
|
|
214
|
+
description=desc,
|
|
215
|
+
credits="SolarMonitor"
|
|
216
|
+
))
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
print(f"Error scraping SolarMonitor: {e}")
|
|
220
|
+
|
|
221
|
+
return images
|
|
222
|
+
|
|
223
|
+
def fetch_context_images(event_date: date, use_helioviewer: bool = True) -> List[ContextImage]:
|
|
224
|
+
"""
|
|
225
|
+
Fetch context images for a given date.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
event_date: Date to fetch images for
|
|
229
|
+
use_helioviewer: If True, use Helioviewer API (default, returns PNG).
|
|
230
|
+
If False, use SolarMonitor (strict backup only).
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of ContextImage objects
|
|
234
|
+
"""
|
|
235
|
+
if use_helioviewer:
|
|
236
|
+
images = fetch_from_helioviewer(event_date)
|
|
237
|
+
if images:
|
|
238
|
+
return images
|
|
239
|
+
# Fall back to SolarMonitor if Helioviewer fails
|
|
240
|
+
print("Helioviewer fetch failed, falling back to SolarMonitor")
|
|
241
|
+
|
|
242
|
+
return fetch_from_solarmonitor(event_date)
|
|
243
|
+
|
|
244
|
+
def resolve_full_image_url(page_url: str) -> Optional[str]:
|
|
245
|
+
"""
|
|
246
|
+
Given an image page URL, resolve to the actual full-resolution image URL.
|
|
247
|
+
|
|
248
|
+
For Helioviewer URLs, returns the URL directly.
|
|
249
|
+
For SolarMonitor URLs, scrapes the page to find the image.
|
|
250
|
+
"""
|
|
251
|
+
# Check if it's a Helioviewer URL
|
|
252
|
+
if 'helioviewer.org' in page_url:
|
|
253
|
+
# It's already a direct image URL from Helioviewer
|
|
254
|
+
return page_url
|
|
255
|
+
|
|
256
|
+
# Otherwise, it's a SolarMonitor URL - scrape it
|
|
257
|
+
try:
|
|
258
|
+
from ..utils import get_global_session
|
|
259
|
+
except ImportError:
|
|
260
|
+
from solar_radio_image_viewer.utils import get_global_session
|
|
261
|
+
session = get_global_session()
|
|
262
|
+
response = session.get(page_url)
|
|
263
|
+
if response.status_code != 200:
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
html = response.text
|
|
267
|
+
|
|
268
|
+
# Find all PNG images
|
|
269
|
+
all_imgs = re.findall(r'src=["\']?([^"\'> ]+\.png)["\']?', html, re.IGNORECASE)
|
|
270
|
+
|
|
271
|
+
for img_path in all_imgs:
|
|
272
|
+
if "thmb" not in img_path and "common_files" not in img_path:
|
|
273
|
+
return urljoin("https://solarmonitor.org/", img_path)
|
|
274
|
+
|
|
275
|
+
except Exception as e:
|
|
276
|
+
print(f"Error resolving full image: {e}")
|
|
277
|
+
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def _identify_instrument(img_type: str) -> Tuple[str, str]:
|
|
281
|
+
"""Refine instrument name and description based on code (for SolarMonitor)."""
|
|
282
|
+
code = img_type.lower()
|
|
283
|
+
if 'saia' in code:
|
|
284
|
+
if '193' in code: return "SDO AIA", "193Å (Corona)"
|
|
285
|
+
if '094' in code: return "SDO AIA", "94Å (Hot Flare)"
|
|
286
|
+
if '335' in code: return "SDO AIA", "335Å (Active Region)"
|
|
287
|
+
return "SDO AIA", "EUV Image"
|
|
288
|
+
if 'seit' in code: return "SOHO EIT", "EUV (Historical)"
|
|
289
|
+
if 'shmi' in code: return "SDO HMI", "Magnetogram"
|
|
290
|
+
if 'smdi' in code: return "SOHO MDI", "Magnetogram (Historical)"
|
|
291
|
+
if 'gong' in code: return "GONG", "H-Alpha (Chromosphere)"
|
|
292
|
+
if 'bbso' in code: return "BBSO", "H-Alpha"
|
|
293
|
+
if 'swap' in code: return "Proba-2 SWAP", "174Å (Corona)"
|
|
294
|
+
if 'trce' in code: return "TRACE", "EUV (Historical)"
|
|
295
|
+
if 'sxi' in code or 'suvi' in code or 'goes' in code: return "GOES", "X-Ray Imager"
|
|
296
|
+
if 'lasc' in code or 'c2' in code or 'c3' in code: return "SOHO LASCO", "Coronagraph"
|
|
297
|
+
return "Unknown", "Solar Context"
|