Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| NASA Solar Image Viewer | |
| Simple image viewer with video-like playback controls. | |
| """ | |
| import sys | |
| import time | |
| import threading | |
| from pathlib import Path | |
| from datetime import datetime | |
| # Add src to Python path | |
| sys.path.insert(0, str(Path(__file__).parent / "src")) | |
| from src.storage.storage_organizer import StorageOrganizer | |
| try: | |
| import tkinter as tk | |
| from tkinter import ttk, messagebox | |
| from PIL import Image, ImageTk | |
| HAS_GUI = True | |
| except ImportError: | |
| HAS_GUI = False | |
| class ImageViewer: | |
| """Simple image viewer with video-like controls.""" | |
| def __init__(self, storage: StorageOrganizer): | |
| """Initialize the image viewer.""" | |
| self.storage = storage | |
| self.images = [] | |
| self.current_index = 0 | |
| self.is_playing = False | |
| self.fps = 2 # Default 2 FPS | |
| self.play_thread = None | |
| if not HAS_GUI: | |
| raise ImportError("GUI libraries not available. Install with: pip install pillow") | |
| # Create main window | |
| self.root = tk.Tk() | |
| self.root.title("NASA Solar Image Viewer") | |
| self.root.geometry("800x900") | |
| self.setup_ui() | |
| def setup_ui(self): | |
| """Set up the user interface.""" | |
| # Main frame | |
| main_frame = ttk.Frame(self.root) | |
| main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) | |
| # Title | |
| title_label = ttk.Label(main_frame, text="🌞 NASA Solar Image Viewer", | |
| font=("Arial", 16, "bold")) | |
| title_label.pack(pady=(0, 10)) | |
| # Date selection frame | |
| date_frame = ttk.LabelFrame(main_frame, text="Select Date", padding=10) | |
| date_frame.pack(fill=tk.X, pady=(0, 10)) | |
| self.date_var = tk.StringVar() | |
| self.date_combo = ttk.Combobox(date_frame, textvariable=self.date_var, | |
| state="readonly", width=30) | |
| self.date_combo.pack(side=tk.LEFT, padx=(0, 10)) | |
| load_btn = ttk.Button(date_frame, text="Load Images", command=self.load_images) | |
| load_btn.pack(side=tk.LEFT) | |
| # Image display | |
| self.image_frame = ttk.LabelFrame(main_frame, text="Solar Image", padding=10) | |
| self.image_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) | |
| self.image_label = ttk.Label(self.image_frame, text="No image loaded", | |
| background="black", foreground="white") | |
| self.image_label.pack(fill=tk.BOTH, expand=True) | |
| # Info frame | |
| info_frame = ttk.Frame(main_frame) | |
| info_frame.pack(fill=tk.X, pady=(0, 10)) | |
| self.info_label = ttk.Label(info_frame, text="Ready", font=("Arial", 10)) | |
| self.info_label.pack() | |
| # Controls frame | |
| controls_frame = ttk.LabelFrame(main_frame, text="Playback Controls", padding=10) | |
| controls_frame.pack(fill=tk.X) | |
| # Playback buttons | |
| btn_frame = ttk.Frame(controls_frame) | |
| btn_frame.pack(fill=tk.X, pady=(0, 10)) | |
| self.prev_btn = ttk.Button(btn_frame, text="⏮ Previous", command=self.prev_image) | |
| self.prev_btn.pack(side=tk.LEFT, padx=(0, 5)) | |
| self.play_btn = ttk.Button(btn_frame, text="▶ Play", command=self.toggle_play) | |
| self.play_btn.pack(side=tk.LEFT, padx=(0, 5)) | |
| self.next_btn = ttk.Button(btn_frame, text="Next ⏭", command=self.next_image) | |
| self.next_btn.pack(side=tk.LEFT, padx=(0, 5)) | |
| self.stop_btn = ttk.Button(btn_frame, text="⏹ Stop", command=self.stop_play) | |
| self.stop_btn.pack(side=tk.LEFT) | |
| # Speed control | |
| speed_frame = ttk.Frame(controls_frame) | |
| speed_frame.pack(fill=tk.X) | |
| ttk.Label(speed_frame, text="Speed (FPS):").pack(side=tk.LEFT, padx=(0, 5)) | |
| self.speed_var = tk.DoubleVar(value=2.0) | |
| speed_scale = ttk.Scale(speed_frame, from_=0.5, to=10.0, | |
| variable=self.speed_var, orient=tk.HORIZONTAL, | |
| command=self.update_speed) | |
| speed_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10)) | |
| self.speed_label = ttk.Label(speed_frame, text="2.0 FPS") | |
| self.speed_label.pack(side=tk.LEFT) | |
| # Progress bar | |
| self.progress_var = tk.DoubleVar() | |
| self.progress_bar = ttk.Progressbar(controls_frame, variable=self.progress_var, | |
| maximum=100) | |
| self.progress_bar.pack(fill=tk.X, pady=(10, 0)) | |
| # Load available dates | |
| self.load_available_dates() | |
| def load_available_dates(self): | |
| """Load available dates with images.""" | |
| dates = [] | |
| data_dir = self.storage.base_data_dir | |
| if data_dir.exists(): | |
| for year_dir in data_dir.iterdir(): | |
| if not year_dir.is_dir() or not year_dir.name.isdigit(): | |
| continue | |
| for month_dir in year_dir.iterdir(): | |
| if not month_dir.is_dir() or not month_dir.name.isdigit(): | |
| continue | |
| for day_dir in month_dir.iterdir(): | |
| if not day_dir.is_dir() or not day_dir.name.isdigit(): | |
| continue | |
| images = list(day_dir.glob("*_4096_0211.jpg")) | |
| if images: | |
| try: | |
| date = datetime(int(year_dir.name), int(month_dir.name), int(day_dir.name)) | |
| date_str = f"{date.strftime('%Y-%m-%d')} ({len(images)} images)" | |
| dates.append((date, date_str)) | |
| except ValueError: | |
| continue | |
| if dates: | |
| dates.sort() | |
| date_strings = [date_str for _, date_str in dates] | |
| self.date_combo['values'] = date_strings | |
| self.date_combo.current(0) # Select first date | |
| self.available_dates = {date_str: date for date, date_str in dates} | |
| else: | |
| messagebox.showwarning("No Images", "No downloaded images found!\n\nRun 'python download_real_images.py' first.") | |
| def load_images(self): | |
| """Load images for the selected date.""" | |
| selected = self.date_var.get() | |
| if not selected or selected not in self.available_dates: | |
| messagebox.showerror("Error", "Please select a valid date") | |
| return | |
| date = self.available_dates[selected] | |
| image_files = self.storage.list_local_images(date) | |
| if not image_files: | |
| messagebox.showerror("Error", f"No images found for {date.strftime('%Y-%m-%d')}") | |
| return | |
| # Load image paths | |
| self.images = [] | |
| date_path = self.storage.get_date_path(date) | |
| for filename in sorted(image_files): | |
| image_path = date_path / filename | |
| self.images.append((image_path, filename)) | |
| self.current_index = 0 | |
| self.update_display() | |
| self.info_label.config(text=f"Loaded {len(self.images)} images for {date.strftime('%Y-%m-%d')}") | |
| def update_display(self): | |
| """Update the image display.""" | |
| if not self.images: | |
| return | |
| image_path, filename = self.images[self.current_index] | |
| try: | |
| # Load and resize image | |
| pil_image = Image.open(image_path) | |
| # Calculate size to fit in display area (max 600x600) | |
| display_size = (600, 600) | |
| pil_image.thumbnail(display_size, Image.Resampling.LANCZOS) | |
| # Convert to PhotoImage | |
| photo = ImageTk.PhotoImage(pil_image) | |
| # Update display | |
| self.image_label.config(image=photo, text="") | |
| self.image_label.image = photo # Keep a reference | |
| # Update info | |
| progress = (self.current_index + 1) / len(self.images) * 100 | |
| self.progress_var.set(progress) | |
| # Extract timestamp from filename | |
| timestamp = filename.split('_')[1] if '_' in filename else "Unknown" | |
| if len(timestamp) == 6: | |
| formatted_time = f"{timestamp[:2]}:{timestamp[2:4]}:{timestamp[4:6]}" | |
| else: | |
| formatted_time = timestamp | |
| info_text = f"Image {self.current_index + 1}/{len(self.images)} - Time: {formatted_time}" | |
| self.image_frame.config(text=info_text) | |
| except Exception as e: | |
| self.image_label.config(text=f"Error loading image: {e}") | |
| def prev_image(self): | |
| """Go to previous image.""" | |
| if self.images and self.current_index > 0: | |
| self.current_index -= 1 | |
| self.update_display() | |
| def next_image(self): | |
| """Go to next image.""" | |
| if self.images and self.current_index < len(self.images) - 1: | |
| self.current_index += 1 | |
| self.update_display() | |
| def toggle_play(self): | |
| """Toggle play/pause.""" | |
| if self.is_playing: | |
| self.pause_play() | |
| else: | |
| self.start_play() | |
| def start_play(self): | |
| """Start playing images.""" | |
| if not self.images: | |
| messagebox.showwarning("No Images", "Please load images first") | |
| return | |
| self.is_playing = True | |
| self.play_btn.config(text="⏸ Pause") | |
| # Start play thread | |
| self.play_thread = threading.Thread(target=self._play_loop, daemon=True) | |
| self.play_thread.start() | |
| def pause_play(self): | |
| """Pause playing.""" | |
| self.is_playing = False | |
| self.play_btn.config(text="▶ Play") | |
| def stop_play(self): | |
| """Stop playing and reset to first image.""" | |
| self.is_playing = False | |
| self.play_btn.config(text="▶ Play") | |
| self.current_index = 0 | |
| self.update_display() | |
| def _play_loop(self): | |
| """Play loop running in background thread.""" | |
| while self.is_playing and self.images: | |
| if self.current_index >= len(self.images) - 1: | |
| # Reached end, loop back to start | |
| self.current_index = 0 | |
| else: | |
| self.current_index += 1 | |
| # Update display in main thread | |
| self.root.after(0, self.update_display) | |
| # Wait based on FPS | |
| delay = 1.0 / self.fps | |
| time.sleep(delay) | |
| def update_speed(self, value): | |
| """Update playback speed.""" | |
| self.fps = float(value) | |
| self.speed_label.config(text=f"{self.fps:.1f} FPS") | |
| def run(self): | |
| """Run the viewer.""" | |
| self.root.mainloop() | |
| def main(): | |
| """Main viewer application.""" | |
| if not HAS_GUI: | |
| print("❌ GUI libraries not available!") | |
| print("💡 Install with: pip install pillow") | |
| print("💡 Or use the video creator: python create_video.py") | |
| return | |
| try: | |
| storage = StorageOrganizer("data") | |
| viewer = ImageViewer(storage) | |
| viewer.run() | |
| except Exception as e: | |
| print(f"❌ Error: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| if __name__ == "__main__": | |
| main() |