fuck yeah!
This commit is contained in:
parent
ccb53d9091
commit
da10f5e132
44 changed files with 3260 additions and 448 deletions
16
views/__init__.py
Normal file
16
views/__init__.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# views/__init__.py
|
||||
from views.main_window import MainWindow
|
||||
from views.layer_widget import LayerWidget
|
||||
from views.control_panel import ControlPanel
|
||||
from views.canvas_widget import SolarCanvas
|
||||
from views.timelapse_dialog import TimelapseDialog
|
||||
from views.metadata_viewer import MetadataViewer # ← должен импортировать класс
|
||||
|
||||
__all__ = [
|
||||
'MainWindow',
|
||||
'LayerWidget',
|
||||
'ControlPanel',
|
||||
'SolarCanvas',
|
||||
'TimelapseDialog',
|
||||
'MetadataViewer'
|
||||
]
|
||||
BIN
views/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
views/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
views/__pycache__/canvas_widget.cpython-312.pyc
Normal file
BIN
views/__pycache__/canvas_widget.cpython-312.pyc
Normal file
Binary file not shown.
BIN
views/__pycache__/control_panel.cpython-312.pyc
Normal file
BIN
views/__pycache__/control_panel.cpython-312.pyc
Normal file
Binary file not shown.
BIN
views/__pycache__/layer_widget.cpython-312.pyc
Normal file
BIN
views/__pycache__/layer_widget.cpython-312.pyc
Normal file
Binary file not shown.
BIN
views/__pycache__/main_window.cpython-312.pyc
Normal file
BIN
views/__pycache__/main_window.cpython-312.pyc
Normal file
Binary file not shown.
BIN
views/__pycache__/metadata_viewer.cpython-312.pyc
Normal file
BIN
views/__pycache__/metadata_viewer.cpython-312.pyc
Normal file
Binary file not shown.
BIN
views/__pycache__/timelapse_dialog.cpython-312.pyc
Normal file
BIN
views/__pycache__/timelapse_dialog.cpython-312.pyc
Normal file
Binary file not shown.
149
views/canvas_widget.py
Normal file
149
views/canvas_widget.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"""
|
||||
Виджет для отображения и манипуляции солнечными изображениями
|
||||
Поддерживает: зум, панорамирование, композитинг слоев
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
|
||||
from PySide6.QtCore import Qt, QRectF, QPointF, Signal
|
||||
from PySide6.QtGui import QPixmap, QImage, QWheelEvent, QMouseEvent, QPainter
|
||||
import numpy as np
|
||||
|
||||
|
||||
class SolarCanvas(QGraphicsView):
|
||||
"""
|
||||
Кастомный Canvas для отображения солнечных изображений
|
||||
Поддерживает масштабирование, панорамирование и наложение слоев
|
||||
"""
|
||||
|
||||
zoom_changed = Signal(float)
|
||||
|
||||
def __init__(self, controller):
|
||||
super().__init__()
|
||||
self.controller = controller
|
||||
self.scene = QGraphicsScene(self)
|
||||
self.setScene(self.scene)
|
||||
|
||||
# Настройки отображения - ИСПРАВЛЕНО: используем QPainter
|
||||
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
|
||||
self.setDragMode(self.DragMode.ScrollHandDrag)
|
||||
self.setTransformationAnchor(self.ViewportAnchor.AnchorUnderMouse)
|
||||
self.setResizeAnchor(self.ViewportAnchor.AnchorUnderMouse)
|
||||
|
||||
# Переменные для зума
|
||||
self.current_zoom = 1.0
|
||||
self.min_zoom = 0.1
|
||||
self.max_zoom = 10.0
|
||||
self.zoom_factor = 1.1
|
||||
|
||||
# Словарь для хранения слоев (QGraphicsPixmapItem)
|
||||
self.layer_items = {}
|
||||
|
||||
# Фон для лучшего контраста (черный для астрономических изображений)
|
||||
self.setBackgroundBrush(Qt.GlobalColor.black)
|
||||
|
||||
# Дополнительные настройки для плавности
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
|
||||
def set_image(self, layer_id: int, image_data: np.ndarray):
|
||||
"""
|
||||
Устанавливает изображение для слоя
|
||||
|
||||
Args:
|
||||
layer_id: ID слоя
|
||||
image_data: numpy массив с изображением
|
||||
"""
|
||||
# Конвертируем numpy array в QImage
|
||||
height, width = image_data.shape[:2]
|
||||
|
||||
if len(image_data.shape) == 2:
|
||||
# Черно-белое изображение
|
||||
bytes_per_line = width
|
||||
qimage = QImage(image_data.data, width, height, bytes_per_line, QImage.Format.Format_Grayscale8)
|
||||
else:
|
||||
# RGB изображение
|
||||
bytes_per_line = 3 * width
|
||||
qimage = QImage(image_data.data, width, height, bytes_per_line, QImage.Format.Format_RGB888)
|
||||
|
||||
# Конвертируем в QPixmap
|
||||
pixmap = QPixmap.fromImage(qimage)
|
||||
|
||||
# Создаем или обновляем графический элемент
|
||||
if layer_id in self.layer_items:
|
||||
self.layer_items[layer_id].setPixmap(pixmap)
|
||||
else:
|
||||
item = self.scene.addPixmap(pixmap)
|
||||
self.layer_items[layer_id] = item
|
||||
|
||||
# Центрируем изображение при первом добавлении
|
||||
if len(self.layer_items) == 1:
|
||||
self.centerOn(item)
|
||||
self.fitInView(item, Qt.AspectRatioMode.KeepAspectRatio)
|
||||
|
||||
def set_layer_visibility(self, layer_id: int, visible: bool):
|
||||
"""Устанавливает видимость слоя"""
|
||||
if layer_id in self.layer_items:
|
||||
self.layer_items[layer_id].setVisible(visible)
|
||||
|
||||
def set_layer_opacity(self, layer_id: int, opacity: float):
|
||||
"""Устанавливает прозрачность слоя"""
|
||||
if layer_id in self.layer_items:
|
||||
self.layer_items[layer_id].setOpacity(opacity)
|
||||
|
||||
def remove_layer(self, layer_id: int):
|
||||
"""Удаляет слой"""
|
||||
if layer_id in self.layer_items:
|
||||
self.scene.removeItem(self.layer_items[layer_id])
|
||||
del self.layer_items[layer_id]
|
||||
|
||||
def clear_all_layers(self):
|
||||
"""Очищает все слои"""
|
||||
for item in self.layer_items.values():
|
||||
self.scene.removeItem(item)
|
||||
self.layer_items.clear()
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent):
|
||||
"""Обработка колесика мыши для зума"""
|
||||
# Определяем направление зума
|
||||
zoom_in = event.angleDelta().y() > 0
|
||||
|
||||
# Вычисляем новый уровень зума
|
||||
if zoom_in:
|
||||
new_zoom = self.current_zoom * self.zoom_factor
|
||||
else:
|
||||
new_zoom = self.current_zoom / self.zoom_factor
|
||||
|
||||
# Ограничиваем зум
|
||||
if self.min_zoom <= new_zoom <= self.max_zoom:
|
||||
self.current_zoom = new_zoom
|
||||
self.scale(self.zoom_factor if zoom_in else 1/self.zoom_factor,
|
||||
self.zoom_factor if zoom_in else 1/self.zoom_factor)
|
||||
self.zoom_changed.emit(self.current_zoom)
|
||||
|
||||
def mousePressEvent(self, event: QMouseEvent):
|
||||
"""Обработка нажатия мыши для панорамирования"""
|
||||
if event.button() == Qt.MouseButton.MiddleButton:
|
||||
self.setDragMode(self.DragMode.ScrollHandDrag)
|
||||
# Создаем фейковое событие с левой кнопкой
|
||||
fake_event = QMouseEvent(
|
||||
event.type(), event.pos(), Qt.MouseButton.LeftButton,
|
||||
Qt.MouseButton.LeftButton, event.modifiers()
|
||||
)
|
||||
super().mousePressEvent(fake_event)
|
||||
else:
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: QMouseEvent):
|
||||
"""Обработка отпускания мыши"""
|
||||
if event.button() == Qt.MouseButton.MiddleButton:
|
||||
self.setDragMode(self.DragMode.NoDrag)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def reset_view(self):
|
||||
"""Сбрасывает зум и позицию"""
|
||||
if self.layer_items:
|
||||
first_item = next(iter(self.layer_items.values()))
|
||||
self.fitInView(first_item, Qt.AspectRatioMode.KeepAspectRatio)
|
||||
self.current_zoom = 1.0
|
||||
self.zoom_changed.emit(self.current_zoom)
|
||||
159
views/control_panel.py
Normal file
159
views/control_panel.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
"""
|
||||
Панель управления - выбор даты, спектра, загрузка изображений
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox,
|
||||
QComboBox, QDateEdit, QTimeEdit, QPushButton,
|
||||
QLabel, QProgressBar, QFrame
|
||||
)
|
||||
from PySide6.QtCore import Qt, QDateTime, Signal
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ControlPanel(QWidget):
|
||||
"""Панель управления для выбора параметров загрузки"""
|
||||
|
||||
load_image_requested = Signal(int, datetime) # source_id, date
|
||||
|
||||
def __init__(self, controller):
|
||||
super().__init__()
|
||||
self.controller = controller
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
"""Инициализация UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(10)
|
||||
layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
# Группа выбора спектра
|
||||
spectrum_group = QGroupBox("🌞 Спектральный канал")
|
||||
spectrum_layout = QVBoxLayout(spectrum_group)
|
||||
|
||||
self.spectrum_combo = QComboBox()
|
||||
self.spectrum_combo.setMinimumHeight(30)
|
||||
self.populate_spectrums()
|
||||
spectrum_layout.addWidget(self.spectrum_combo)
|
||||
|
||||
layout.addWidget(spectrum_group)
|
||||
|
||||
# Группа выбора даты и времени
|
||||
date_group = QGroupBox("📅 Дата и время (UTC)")
|
||||
date_layout = QVBoxLayout(date_group)
|
||||
|
||||
# Дата
|
||||
date_label = QLabel("Дата:")
|
||||
date_layout.addWidget(date_label)
|
||||
|
||||
self.date_edit = QDateEdit()
|
||||
self.date_edit.setDateTime(QDateTime.currentDateTime())
|
||||
self.date_edit.setCalendarPopup(True)
|
||||
self.date_edit.setMinimumHeight(30)
|
||||
date_layout.addWidget(self.date_edit)
|
||||
|
||||
# Время
|
||||
time_label = QLabel("Время:")
|
||||
date_layout.addWidget(time_label)
|
||||
|
||||
self.time_edit = QTimeEdit()
|
||||
self.time_edit.setTime(QDateTime.currentDateTime().time())
|
||||
self.time_edit.setMinimumHeight(30)
|
||||
date_layout.addWidget(self.time_edit)
|
||||
|
||||
# Кнопки быстрого выбора
|
||||
quick_layout = QHBoxLayout()
|
||||
|
||||
now_button = QPushButton("Сейчас")
|
||||
now_button.setMinimumHeight(30)
|
||||
now_button.clicked.connect(self.set_now)
|
||||
quick_layout.addWidget(now_button)
|
||||
|
||||
today_button = QPushButton("Сегодня")
|
||||
today_button.setMinimumHeight(30)
|
||||
today_button.clicked.connect(self.set_today)
|
||||
quick_layout.addWidget(today_button)
|
||||
|
||||
date_layout.addLayout(quick_layout)
|
||||
|
||||
layout.addWidget(date_group)
|
||||
|
||||
# Кнопка загрузки
|
||||
self.load_button = QPushButton("📥 Загрузить изображение")
|
||||
self.load_button.setMinimumHeight(40)
|
||||
self.load_button.clicked.connect(self.request_load_image)
|
||||
self.load_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #2ecc71;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #27ae60;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #555;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(self.load_button)
|
||||
|
||||
# Прогресс бар (для загрузки)
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
self.progress_bar.setMinimumHeight(20)
|
||||
layout.addWidget(self.progress_bar)
|
||||
|
||||
# Информационная метка
|
||||
self.info_label = QLabel("Выберите спектральный канал и дату")
|
||||
self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.info_label.setStyleSheet("color: #888; font-size: 11px; padding: 5px;")
|
||||
layout.addWidget(self.info_label)
|
||||
|
||||
def populate_spectrums(self):
|
||||
"""Заполняет список доступных спектров"""
|
||||
from models.api_model import HelioviewerAPI
|
||||
|
||||
sources = HelioviewerAPI.get_available_sources()
|
||||
for source_id, info in sources.items():
|
||||
display_text = f"{info['observatory']} - {info['name']} ({info['wavelength']})"
|
||||
self.spectrum_combo.addItem(display_text, source_id)
|
||||
|
||||
def get_selected_source_id(self) -> int:
|
||||
"""Возвращает ID выбранного источника"""
|
||||
return self.spectrum_combo.currentData()
|
||||
|
||||
def get_selected_datetime(self) -> datetime:
|
||||
"""Возвращает выбранную дату и время"""
|
||||
qdatetime = QDateTime(self.date_edit.date(), self.time_edit.time())
|
||||
return qdatetime.toPython()
|
||||
|
||||
def set_now(self):
|
||||
"""Устанавливает текущее время"""
|
||||
now = QDateTime.currentDateTime()
|
||||
self.date_edit.setDate(now.date())
|
||||
self.time_edit.setTime(now.time())
|
||||
|
||||
def set_today(self):
|
||||
"""Устанавливает сегодняшнюю дату"""
|
||||
self.date_edit.setDate(QDateTime.currentDateTime().date())
|
||||
|
||||
def request_load_image(self):
|
||||
"""Запрашивает загрузку изображения"""
|
||||
source_id = self.get_selected_source_id()
|
||||
date = self.get_selected_datetime()
|
||||
self.load_image_requested.emit(source_id, date)
|
||||
|
||||
def show_progress(self, show: bool):
|
||||
"""Показывает/скрывает прогресс бар"""
|
||||
self.progress_bar.setVisible(show)
|
||||
self.load_button.setEnabled(not show)
|
||||
self.info_label.setText("Загрузка..." if show else "Готово")
|
||||
|
||||
def update_progress(self, value: int, maximum: int):
|
||||
"""Обновляет прогресс"""
|
||||
self.progress_bar.setRange(0, maximum)
|
||||
self.progress_bar.setValue(value)
|
||||
self.info_label.setText(f"Загрузка: {int(value/maximum*100)}%")
|
||||
219
views/layer_widget.py
Normal file
219
views/layer_widget.py
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
"""
|
||||
Виджет для управления слоями изображений
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
|
||||
QHBoxLayout, QCheckBox, QSlider, QLabel, QPushButton,
|
||||
QFrame
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, QSize
|
||||
|
||||
|
||||
class LayerWidgetItem(QWidget):
|
||||
"""Виджет для отдельного слоя в списке"""
|
||||
|
||||
visibility_toggled = Signal(int, bool)
|
||||
opacity_changed = Signal(int, float)
|
||||
|
||||
def __init__(self, layer_id: int, name: str, visible: bool = True):
|
||||
super().__init__()
|
||||
self.layer_id = layer_id
|
||||
self.init_ui(name, visible)
|
||||
|
||||
def init_ui(self, name: str, visible: bool):
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(8, 6, 8, 6)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# Чекбокс видимости
|
||||
self.visibility_checkbox = QCheckBox()
|
||||
self.visibility_checkbox.setChecked(visible)
|
||||
self.visibility_checkbox.setFixedSize(22, 22)
|
||||
self.visibility_checkbox.toggled.connect(self.on_visibility_toggled)
|
||||
layout.addWidget(self.visibility_checkbox)
|
||||
|
||||
# Название слоя
|
||||
self.name_label = QLabel(name)
|
||||
self.name_label.setStyleSheet("font-size: 11px; font-weight: 500;")
|
||||
self.name_label.setWordWrap(True)
|
||||
layout.addWidget(self.name_label, 2)
|
||||
|
||||
# Слайдер прозрачности
|
||||
self.opacity_slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self.opacity_slider.setRange(0, 100)
|
||||
self.opacity_slider.setValue(100)
|
||||
self.opacity_slider.setFixedWidth(100)
|
||||
self.opacity_slider.valueChanged.connect(self.on_opacity_changed)
|
||||
layout.addWidget(self.opacity_slider)
|
||||
|
||||
# Метка прозрачности
|
||||
self.opacity_label = QLabel("100%")
|
||||
self.opacity_label.setFixedWidth(35)
|
||||
self.opacity_label.setStyleSheet("font-size: 10px; font-family: monospace;")
|
||||
layout.addWidget(self.opacity_label)
|
||||
|
||||
# Кнопка удаления
|
||||
remove_button = QPushButton("✖")
|
||||
remove_button.setFixedSize(26, 26)
|
||||
remove_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #e74c3c;
|
||||
color: white;
|
||||
border-radius: 13px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #c0392b;
|
||||
}
|
||||
""")
|
||||
remove_button.clicked.connect(lambda: self.visibility_toggled.emit(self.layer_id, False))
|
||||
layout.addWidget(remove_button)
|
||||
|
||||
def on_visibility_toggled(self, checked):
|
||||
self.visibility_toggled.emit(self.layer_id, checked)
|
||||
|
||||
def on_opacity_changed(self, value):
|
||||
opacity = value / 100.0
|
||||
self.opacity_label.setText(f"{value}%")
|
||||
self.opacity_changed.emit(self.layer_id, opacity)
|
||||
|
||||
def set_visible(self, visible: bool):
|
||||
self.visibility_checkbox.setChecked(visible)
|
||||
|
||||
def set_opacity(self, opacity: float):
|
||||
value = int(opacity * 100)
|
||||
self.opacity_slider.setValue(value)
|
||||
self.opacity_label.setText(f"{value}%")
|
||||
|
||||
|
||||
class LayerWidget(QWidget):
|
||||
"""Виджет для отображения списка слоев"""
|
||||
|
||||
def __init__(self, controller):
|
||||
super().__init__()
|
||||
self.controller = controller
|
||||
self.layer_widgets = {}
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(5)
|
||||
|
||||
# Список слоев
|
||||
self.list_widget = QListWidget()
|
||||
self.list_widget.setStyleSheet("""
|
||||
QListWidget {
|
||||
border: 1px solid #555;
|
||||
border-radius: 5px;
|
||||
background-color: #2b2b2b;
|
||||
outline: none;
|
||||
}
|
||||
QListWidget::item {
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
QListWidget::item:selected {
|
||||
background-color: #3c5a8c;
|
||||
}
|
||||
QListWidget::item:hover {
|
||||
background-color: #3c3c3c;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(self.list_widget)
|
||||
|
||||
# Кнопка очистки
|
||||
clear_button = QPushButton("🗑️ Очистить все слои")
|
||||
clear_button.setMinimumHeight(32)
|
||||
clear_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #555;
|
||||
color: white;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
""")
|
||||
clear_button.clicked.connect(self.clear_all_layers)
|
||||
layout.addWidget(clear_button)
|
||||
|
||||
# Пустое состояние
|
||||
self.empty_label = QLabel("📭 Нет загруженных слоев\n\nНажмите 'Загрузить изображение'")
|
||||
self.empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.empty_label.setStyleSheet("color: #888; padding: 30px; background-color: #2b2b2b; border-radius: 5px;")
|
||||
self.empty_label.setVisible(True)
|
||||
layout.addWidget(self.empty_label)
|
||||
|
||||
def add_layer(self, layer_id: int, name: str, visible: bool = True):
|
||||
"""Добавляет новый слой в список"""
|
||||
self.empty_label.setVisible(False)
|
||||
self.list_widget.setVisible(True)
|
||||
|
||||
item = QListWidgetItem()
|
||||
item.setSizeHint(QSize(0, 55))
|
||||
self.list_widget.addItem(item)
|
||||
|
||||
widget = LayerWidgetItem(layer_id, name, visible)
|
||||
widget.visibility_toggled.connect(self.on_visibility_toggled)
|
||||
widget.opacity_changed.connect(self.on_opacity_changed)
|
||||
|
||||
self.list_widget.setItemWidget(item, widget)
|
||||
self.layer_widgets[layer_id] = (item, widget)
|
||||
self.list_widget.setCurrentItem(item)
|
||||
self.list_widget.scrollToItem(item)
|
||||
|
||||
def remove_layer(self, layer_id: int):
|
||||
"""Удаляет слой из списка"""
|
||||
if layer_id in self.layer_widgets:
|
||||
item, _ = self.layer_widgets[layer_id]
|
||||
row = self.list_widget.row(item)
|
||||
self.list_widget.takeItem(row)
|
||||
del self.layer_widgets[layer_id]
|
||||
|
||||
if len(self.layer_widgets) == 0:
|
||||
self.empty_label.setVisible(True)
|
||||
self.list_widget.setVisible(False)
|
||||
|
||||
def update_layers(self, layers):
|
||||
"""Обновляет весь список слоев"""
|
||||
self.clear_all_layers(keep_controller=False)
|
||||
for layer in layers:
|
||||
self.add_layer(layer.id, layer.name, layer.visible)
|
||||
|
||||
if layers:
|
||||
self.empty_label.setVisible(False)
|
||||
self.list_widget.setVisible(True)
|
||||
else:
|
||||
self.empty_label.setVisible(True)
|
||||
self.list_widget.setVisible(False)
|
||||
|
||||
def clear_all_layers(self, keep_controller: bool = True):
|
||||
"""Очищает список слоев"""
|
||||
self.list_widget.clear()
|
||||
self.layer_widgets.clear()
|
||||
|
||||
if keep_controller:
|
||||
self.controller.clear_all_layers()
|
||||
|
||||
self.empty_label.setVisible(True)
|
||||
self.list_widget.setVisible(False)
|
||||
|
||||
def on_visibility_toggled(self, layer_id: int, visible: bool):
|
||||
self.controller.set_layer_visibility(layer_id, visible)
|
||||
|
||||
def on_opacity_changed(self, layer_id: int, opacity: float):
|
||||
self.controller.set_layer_opacity(layer_id, opacity)
|
||||
|
||||
def get_selected_layer_id(self) -> int:
|
||||
current_item = self.list_widget.currentItem()
|
||||
if current_item:
|
||||
for layer_id, (item, _) in self.layer_widgets.items():
|
||||
if item == current_item:
|
||||
return layer_id
|
||||
return None
|
||||
|
||||
def get_layers_count(self) -> int:
|
||||
return len(self.layer_widgets)
|
||||
251
views/main_window.py
Normal file
251
views/main_window.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
"""
|
||||
Главное окно приложения - содержит меню, панели и canvas
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QSplitter, QMenuBar, QMenu, QStatusBar, QMessageBox,
|
||||
QFileDialog, QToolBar, QFrame, QScrollArea, QLabel
|
||||
)
|
||||
from PySide6.QtCore import Qt, QSize
|
||||
from PySide6.QtGui import QAction, QKeySequence
|
||||
|
||||
from views.layer_widget import LayerWidget
|
||||
from views.canvas_widget import SolarCanvas
|
||||
from views.control_panel import ControlPanel
|
||||
from views.timelapse_dialog import TimelapseDialog
|
||||
from views.metadata_viewer import MetadataViewer
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Главное окно приложения"""
|
||||
|
||||
def __init__(self, controller):
|
||||
super().__init__()
|
||||
self.controller = controller
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
"""Инициализация пользовательского интерфейса"""
|
||||
self.setWindowTitle("Helioviewer Solar Viewer - Профессиональный просмотрщик снимков Солнца")
|
||||
self.setMinimumSize(1200, 800)
|
||||
|
||||
# Центральный виджет
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
# Основной layout
|
||||
main_layout = QHBoxLayout(central_widget)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Создаем сплиттер для левой панели и canvas
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
main_layout.addWidget(splitter)
|
||||
|
||||
# Левая панель (с прокруткой)
|
||||
left_panel = self.create_left_panel_with_scroll()
|
||||
splitter.addWidget(left_panel)
|
||||
|
||||
# Устанавливаем начальную ширину сплиттера
|
||||
splitter.setSizes([400, self.width() - 400])
|
||||
|
||||
# Правая область (canvas)
|
||||
right_area = self.create_right_area()
|
||||
splitter.addWidget(right_area)
|
||||
|
||||
# Создаем меню
|
||||
self.create_menu_bar()
|
||||
|
||||
# Создаем статус бар
|
||||
self.status_bar = QStatusBar()
|
||||
self.setStatusBar(self.status_bar)
|
||||
self.status_bar.showMessage("Готов к работе")
|
||||
|
||||
# Добавляем тулбар для быстрого доступа
|
||||
self.create_toolbar()
|
||||
|
||||
def create_left_panel_with_scroll(self) -> QWidget:
|
||||
"""
|
||||
Создает левую панель с прокруткой, где все виджеты равномерно распределяют пространство
|
||||
"""
|
||||
# Внешний контейнер для скролла
|
||||
scroll_container = QWidget()
|
||||
scroll_layout = QVBoxLayout(scroll_container)
|
||||
scroll_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Создаем QScrollArea
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True) # Позволяет виджету изменять размер
|
||||
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
scroll_area.setStyleSheet("""
|
||||
QScrollArea {
|
||||
border: none;
|
||||
background-color: #2b2b2b;
|
||||
}
|
||||
QScrollBar:vertical {
|
||||
background-color: #2b2b2b;
|
||||
width: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background-color: #555;
|
||||
border-radius: 6px;
|
||||
min-height: 20px;
|
||||
}
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background-color: #777;
|
||||
}
|
||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
||||
height: 0px;
|
||||
}
|
||||
""")
|
||||
|
||||
# Внутренний контейнер для всех виджетов
|
||||
content_widget = QWidget()
|
||||
content_widget.setStyleSheet("background-color: #2b2b2b;")
|
||||
layout = QVBoxLayout(content_widget)
|
||||
layout.setSpacing(10)
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
|
||||
# Панель управления (выбор даты, спектра)
|
||||
self.control_panel = ControlPanel(self.controller)
|
||||
layout.addWidget(self.control_panel, 1) # stretch factor = 1
|
||||
|
||||
# Разделитель
|
||||
layout.addWidget(self.create_separator())
|
||||
|
||||
# Заголовок слоев
|
||||
title_layers = self.create_section_title("📁 Слои изображений")
|
||||
layout.addWidget(title_layers)
|
||||
|
||||
# Виджет слоев - будет занимать столько места, сколько нужно
|
||||
self.layer_widget = LayerWidget(self.controller)
|
||||
layout.addWidget(self.layer_widget, 2) # stretch factor = 2 (больше места)
|
||||
|
||||
# Разделитель
|
||||
layout.addWidget(self.create_separator())
|
||||
|
||||
# Заголовок метаданных
|
||||
title_metadata = self.create_section_title("📊 Метаданные FITS")
|
||||
layout.addWidget(title_metadata)
|
||||
|
||||
# Просмотрщик метаданных
|
||||
self.metadata_viewer = MetadataViewer()
|
||||
layout.addWidget(self.metadata_viewer, 1) # stretch factor = 1
|
||||
|
||||
# Растягивающийся спейсер внизу (опционально)
|
||||
layout.addStretch()
|
||||
|
||||
# Устанавливаем content_widget в scroll_area
|
||||
scroll_area.setWidget(content_widget)
|
||||
|
||||
# Добавляем scroll_area в контейнер
|
||||
scroll_layout.addWidget(scroll_area)
|
||||
|
||||
return scroll_container
|
||||
|
||||
def create_separator(self) -> QFrame:
|
||||
"""Создает линию-разделитель"""
|
||||
separator = QFrame()
|
||||
separator.setFrameShape(QFrame.Shape.HLine)
|
||||
separator.setFrameShadow(QFrame.Shadow.Sunken)
|
||||
separator.setStyleSheet("background-color: #555; max-height: 1px;")
|
||||
return separator
|
||||
|
||||
def create_section_title(self, title: str) -> QLabel:
|
||||
"""Создает заголовок секции"""
|
||||
label = QLabel(title)
|
||||
label.setStyleSheet("""
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding: 8px 5px;
|
||||
background-color: #3c3c3c;
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
""")
|
||||
label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
return label
|
||||
|
||||
def create_right_area(self) -> QWidget:
|
||||
"""Создает правую область с canvas"""
|
||||
area = QWidget()
|
||||
layout = QVBoxLayout(area)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Canvas для отображения изображений
|
||||
self.canvas = SolarCanvas(self.controller)
|
||||
layout.addWidget(self.canvas)
|
||||
|
||||
return area
|
||||
|
||||
def create_menu_bar(self):
|
||||
"""Создает меню приложения"""
|
||||
menubar = self.menuBar()
|
||||
|
||||
# Меню Файл
|
||||
file_menu = menubar.addMenu("📁 Файл")
|
||||
|
||||
# Действия для меню Файл
|
||||
self.setup_action = QAction("⚙️ Настройки", self)
|
||||
self.exit_action = QAction("🚪 Выход", self)
|
||||
self.exit_action.setShortcut(QKeySequence.StandardKey.Quit)
|
||||
self.exit_action.triggered.connect(self.close)
|
||||
|
||||
file_menu.addAction(self.setup_action)
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction(self.exit_action)
|
||||
|
||||
# Меню Инструменты
|
||||
tools_menu = menubar.addMenu("🛠️ Инструменты")
|
||||
|
||||
self.timelapse_action = QAction("🎬 Таймлапс...", self)
|
||||
self.timelapse_action.triggered.connect(self.open_timelapse_dialog)
|
||||
tools_menu.addAction(self.timelapse_action)
|
||||
|
||||
# Меню Справка
|
||||
help_menu = menubar.addMenu("❓ Справка")
|
||||
|
||||
about_action = QAction("ℹ️ О программе", self)
|
||||
about_action.triggered.connect(self.show_about)
|
||||
help_menu.addAction(about_action)
|
||||
|
||||
def create_toolbar(self):
|
||||
"""Создает панель инструментов"""
|
||||
toolbar = QToolBar("Быстрый доступ")
|
||||
self.addToolBar(toolbar)
|
||||
toolbar.setIconSize(QSize(24, 24))
|
||||
|
||||
def open_timelapse_dialog(self):
|
||||
"""Открывает диалог создания таймлапса"""
|
||||
dialog = TimelapseDialog(self.controller, self)
|
||||
dialog.exec()
|
||||
|
||||
def show_about(self):
|
||||
"""Показывает информацию о программе"""
|
||||
QMessageBox.about(
|
||||
self,
|
||||
"О программе",
|
||||
"Helioviewer Solar Viewer v1.0\n\n"
|
||||
"Профессиональный просмотрщик снимков Солнца\n"
|
||||
"Использует данные Helioviewer API\n\n"
|
||||
"Возможности:\n"
|
||||
"• Загрузка снимков в различных спектрах\n"
|
||||
"• Многослойный режим с наложением\n"
|
||||
"• Создание таймлапс-анимаций\n"
|
||||
"• Просмотр FITS-метаданных\n\n"
|
||||
"© 2024 SolarViewer Team"
|
||||
)
|
||||
|
||||
def update_status(self, message: str, timeout: int = 3000):
|
||||
"""Обновляет статус в статус-баре"""
|
||||
self.status_bar.showMessage(message, timeout)
|
||||
|
||||
def update_layer_list(self, layers):
|
||||
"""Обновляет список слоев"""
|
||||
self.layer_widget.update_layers(layers)
|
||||
|
||||
def get_control_panel(self):
|
||||
"""Возвращает панель управления"""
|
||||
return self.control_panel
|
||||
126
views/metadata_viewer.py
Normal file
126
views/metadata_viewer.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"""
|
||||
Виджет для отображения FITS-метаданных из JP2 файлов
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem,
|
||||
QGroupBox, QLabel, QScrollArea
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
|
||||
class MetadataViewer(QWidget):
|
||||
"""Просмотрщик метаданных FITS"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Используем QScrollArea для метаданных
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
scroll_area.setStyleSheet("""
|
||||
QScrollArea {
|
||||
border: 1px solid #555;
|
||||
border-radius: 5px;
|
||||
background-color: #2b2b2b;
|
||||
}
|
||||
""")
|
||||
|
||||
# Внутренний контейнер
|
||||
content_widget = QWidget()
|
||||
content_layout = QVBoxLayout(content_widget)
|
||||
content_layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
# Дерево для метаданных
|
||||
self.tree = QTreeWidget()
|
||||
self.tree.setHeaderLabels(["Параметр", "Значение"])
|
||||
self.tree.setAlternatingRowColors(True)
|
||||
self.tree.setIndentation(10)
|
||||
self.tree.setMinimumHeight(150)
|
||||
self.tree.setStyleSheet("""
|
||||
QTreeWidget {
|
||||
background-color: #2b2b2b;
|
||||
alternate-background-color: #252525;
|
||||
border: none;
|
||||
}
|
||||
QTreeWidget::item {
|
||||
padding: 3px;
|
||||
}
|
||||
""")
|
||||
content_layout.addWidget(self.tree)
|
||||
|
||||
# Информационная метка
|
||||
self.info_label = QLabel("Выберите слой для просмотра метаданных")
|
||||
self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.info_label.setStyleSheet("color: #888; padding: 20px;")
|
||||
self.info_label.setWordWrap(True)
|
||||
content_layout.addWidget(self.info_label)
|
||||
|
||||
scroll_area.setWidget(content_widget)
|
||||
layout.addWidget(scroll_area)
|
||||
|
||||
def display_metadata(self, metadata: dict):
|
||||
"""Отображает метаданные в дереве"""
|
||||
self.tree.clear()
|
||||
|
||||
if not metadata:
|
||||
self.info_label.setVisible(True)
|
||||
self.tree.setVisible(False)
|
||||
self.info_label.setText("Метаданные не найдены")
|
||||
return
|
||||
|
||||
self.info_label.setVisible(False)
|
||||
self.tree.setVisible(True)
|
||||
|
||||
# Группировка метаданных
|
||||
categories = {
|
||||
"📡 Инструмент": ["Телескоп", "Инструмент", "Детектор", "Обсерватория"],
|
||||
"🌊 Спектр": ["Длина волны", "Единица длины волны"],
|
||||
"📅 Время": ["Дата наблюдения", "Начало экспозиции", "Конец экспозиции", "Время экспозиции (сек)"],
|
||||
"📐 Геометрия": ["Центр X (пикс)", "Центр Y (пикс)", "Шаг пикселя X (arcsec)", "Шаг пикселя Y (arcsec)",
|
||||
"Угол поворота (град)"],
|
||||
"📊 Изображение": ["Ширина (пикс)", "Высота (пикс)", "Бит на пиксель"],
|
||||
"📝 Другое": []
|
||||
}
|
||||
|
||||
# Сортируем метаданные
|
||||
for category, keys in categories.items():
|
||||
category_items = []
|
||||
for key in keys:
|
||||
if key in metadata and metadata[key]:
|
||||
category_items.append((key, metadata[key]))
|
||||
|
||||
# Для категории "Другое" собираем остальные ключи
|
||||
if category == "📝 Другое":
|
||||
all_keys = [k for sublist in categories.values() for k in sublist if k != "Другое"]
|
||||
for key, value in metadata.items():
|
||||
if key not in all_keys and value:
|
||||
category_items.append((key, value))
|
||||
|
||||
if category_items:
|
||||
category_item = QTreeWidgetItem(self.tree)
|
||||
category_item.setText(0, category)
|
||||
category_item.setExpanded(True)
|
||||
|
||||
for key, value in category_items:
|
||||
param_item = QTreeWidgetItem(category_item)
|
||||
param_item.setText(0, key)
|
||||
param_item.setText(1, str(value))
|
||||
|
||||
self.tree.expandAll()
|
||||
self.tree.resizeColumnToContents(0)
|
||||
self.tree.resizeColumnToContents(1)
|
||||
|
||||
def clear(self):
|
||||
"""Очищает отображение метаданных"""
|
||||
self.tree.clear()
|
||||
self.info_label.setVisible(True)
|
||||
self.tree.setVisible(False)
|
||||
self.info_label.setText("Выберите слой для просмотра метаданных")
|
||||
370
views/timelapse_dialog.py
Normal file
370
views/timelapse_dialog.py
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
"""
|
||||
Диалог для создания таймлапс-анимации
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QGroupBox,
|
||||
QComboBox, QDateEdit, QPushButton, QLabel,
|
||||
QProgressBar, QFileDialog, QMessageBox, QCheckBox,
|
||||
QTextEdit
|
||||
)
|
||||
from PySide6.QtCore import Qt, QDateTime, QSettings, QThread, Signal
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
|
||||
class TimelapseWorker(QThread):
|
||||
progress = Signal(int, int, str)
|
||||
log = Signal(str)
|
||||
finished = Signal(bool, str)
|
||||
|
||||
def __init__(self, source_id, start_date, end_date, output_path, fps=10):
|
||||
super().__init__()
|
||||
self.source_id = source_id
|
||||
self.start_date = start_date
|
||||
self.end_date = end_date
|
||||
self.output_path = output_path
|
||||
self.fps = fps
|
||||
self._is_cancelled = False
|
||||
|
||||
def cancel(self):
|
||||
self._is_cancelled = True
|
||||
|
||||
def run(self):
|
||||
from models.api_model import HelioviewerAPI
|
||||
from utils.video_creator import VideoCreator
|
||||
|
||||
try:
|
||||
# Создаем временную папку
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="helioviewer_timelapse_"))
|
||||
self.log.emit(f"Создана временная папка: {temp_dir}")
|
||||
|
||||
# Генерируем даты - ИСПРАВЛЕНО
|
||||
dates = []
|
||||
|
||||
# Проверяем тип и преобразуем в datetime если нужно
|
||||
from datetime import datetime as dt
|
||||
if isinstance(self.start_date, dt):
|
||||
current = self.start_date.replace(hour=12, minute=0, second=0)
|
||||
else:
|
||||
# Если это date, конвертируем в datetime
|
||||
current = dt.combine(self.start_date, dt.min.time()).replace(hour=12)
|
||||
|
||||
# Проверяем конец
|
||||
if isinstance(self.end_date, dt):
|
||||
end = self.end_date
|
||||
else:
|
||||
end = dt.combine(self.end_date, dt.max.time())
|
||||
|
||||
while current <= end:
|
||||
dates.append(current)
|
||||
current += timedelta(days=1)
|
||||
|
||||
total = len(dates)
|
||||
self.log.emit(f"Всего файлов в очереди: {total}")
|
||||
|
||||
downloaded = []
|
||||
|
||||
for i, date in enumerate(dates):
|
||||
if self._is_cancelled:
|
||||
self.log.emit("Отменено пользователем")
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
self.finished.emit(False, "Отменено")
|
||||
return
|
||||
|
||||
current_num = i + 1
|
||||
percent = int(current_num / total * 100)
|
||||
self.progress.emit(current_num, total, f"Скачивание {current_num}/{total} ({percent}%)")
|
||||
self.log.emit(f"Скачивание {current_num}/{total}: {date.strftime('%Y-%m-%d')}")
|
||||
|
||||
filepath = HelioviewerAPI.download_image(self.source_id, date, temp_dir)
|
||||
if filepath:
|
||||
downloaded.append(filepath)
|
||||
self.log.emit(f" ✓ Успешно")
|
||||
else:
|
||||
self.log.emit(f" ✗ Ошибка")
|
||||
|
||||
if self._is_cancelled:
|
||||
self.log.emit("Отменено пользователем")
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
self.finished.emit(False, "Отменено")
|
||||
return
|
||||
|
||||
if not downloaded:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
self.finished.emit(False, "Не удалось скачать ни одного изображения")
|
||||
return
|
||||
|
||||
self.log.emit(f"Создание видео из {len(downloaded)} кадров...")
|
||||
self.progress.emit(total, total, "Создание видео...")
|
||||
|
||||
video_path = VideoCreator.create_timelapse(downloaded, self.output_path, self.fps)
|
||||
|
||||
# Очищаем временную папку
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
self.log.emit(f"Временные файлы удалены")
|
||||
|
||||
self.finished.emit(True, str(video_path))
|
||||
|
||||
except Exception as e:
|
||||
self.log.emit(f"Ошибка: {str(e)}")
|
||||
self.finished.emit(False, str(e))
|
||||
|
||||
|
||||
class TimelapseDialog(QDialog):
|
||||
"""Диалог для создания таймлапса"""
|
||||
|
||||
def __init__(self, controller, parent=None):
|
||||
super().__init__(parent)
|
||||
self.controller = controller
|
||||
self.settings = QSettings("SolarViewer", "Helioviewer")
|
||||
self.worker = None
|
||||
self.init_ui()
|
||||
self.load_settings()
|
||||
|
||||
def init_ui(self):
|
||||
"""Инициализация UI"""
|
||||
self.setWindowTitle("Создание таймлапс-анимации")
|
||||
self.setModal(True)
|
||||
self.setMinimumWidth(500)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Группа выбора спектра
|
||||
spectrum_group = QGroupBox("🌞 Спектральный канал")
|
||||
spectrum_layout = QVBoxLayout(spectrum_group)
|
||||
|
||||
self.spectrum_combo = QComboBox()
|
||||
self.populate_spectrums()
|
||||
spectrum_layout.addWidget(self.spectrum_combo)
|
||||
|
||||
layout.addWidget(spectrum_group)
|
||||
|
||||
# Группа выбора дат
|
||||
date_group = QGroupBox("📅 Период")
|
||||
date_layout = QVBoxLayout(date_group)
|
||||
|
||||
# Начальная дата
|
||||
start_layout = QHBoxLayout()
|
||||
start_layout.addWidget(QLabel("Начало:"))
|
||||
self.start_date = QDateEdit()
|
||||
self.start_date.setDateTime(QDateTime.currentDateTime().addDays(-7))
|
||||
self.start_date.setCalendarPopup(True)
|
||||
start_layout.addWidget(self.start_date)
|
||||
date_layout.addLayout(start_layout)
|
||||
|
||||
# Конечная дата
|
||||
end_layout = QHBoxLayout()
|
||||
end_layout.addWidget(QLabel("Конец:"))
|
||||
self.end_date = QDateEdit()
|
||||
self.end_date.setDateTime(QDateTime.currentDateTime())
|
||||
self.end_date.setCalendarPopup(True)
|
||||
end_layout.addWidget(self.end_date)
|
||||
date_layout.addLayout(end_layout)
|
||||
|
||||
layout.addWidget(date_group)
|
||||
|
||||
# Параметры видео
|
||||
video_group = QGroupBox("Параметры видео")
|
||||
video_layout = QHBoxLayout(video_group)
|
||||
|
||||
video_layout.addWidget(QLabel("FPS:"))
|
||||
self.fps_spin = QComboBox()
|
||||
self.fps_spin.addItems(['5', '10', '15', '24', '30'])
|
||||
self.fps_spin.setCurrentText('10')
|
||||
video_layout.addWidget(self.fps_spin)
|
||||
|
||||
video_layout.addStretch()
|
||||
|
||||
layout.addWidget(video_group)
|
||||
|
||||
# Выбор папки сохранения
|
||||
folder_group = QGroupBox("Папка сохранения")
|
||||
folder_layout = QVBoxLayout(folder_group)
|
||||
|
||||
folder_select_layout = QHBoxLayout()
|
||||
self.folder_path = QLabel("Не выбрана")
|
||||
self.folder_path.setStyleSheet("color: gray;")
|
||||
folder_select_layout.addWidget(self.folder_path, 1)
|
||||
|
||||
browse_button = QPushButton("Обзор...")
|
||||
browse_button.clicked.connect(self.browse_folder)
|
||||
folder_select_layout.addWidget(browse_button)
|
||||
|
||||
folder_layout.addLayout(folder_select_layout)
|
||||
|
||||
# Чекбокс "Запомнить путь"
|
||||
self.remember_path_checkbox = QCheckBox("Запомнить этот путь")
|
||||
self.remember_path_checkbox.setChecked(True)
|
||||
folder_layout.addWidget(self.remember_path_checkbox)
|
||||
|
||||
layout.addWidget(folder_group)
|
||||
|
||||
# Прогресс бар
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
layout.addWidget(self.progress_bar)
|
||||
|
||||
# Лог сообщений
|
||||
self.log_text = QTextEdit()
|
||||
self.log_text.setMaximumHeight(150)
|
||||
self.log_text.setReadOnly(True)
|
||||
self.log_text.setVisible(False)
|
||||
layout.addWidget(self.log_text)
|
||||
|
||||
# Кнопки
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.create_button = QPushButton("Создать")
|
||||
self.create_button.clicked.connect(self.start_timelapse)
|
||||
button_layout.addWidget(self.create_button)
|
||||
|
||||
self.cancel_button = QPushButton("Отмена")
|
||||
self.cancel_button.clicked.connect(self.cancel_timelapse)
|
||||
self.cancel_button.setEnabled(False)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
|
||||
close_button = QPushButton("Закрыть")
|
||||
close_button.clicked.connect(self.reject)
|
||||
button_layout.addWidget(close_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
self.selected_folder = None
|
||||
|
||||
def populate_spectrums(self):
|
||||
"""Заполняет список доступных спектров"""
|
||||
from models.api_model import HelioviewerAPI
|
||||
|
||||
sources = HelioviewerAPI.get_available_sources()
|
||||
for source_id, info in sources.items():
|
||||
display_text = f"{info['observatory']} - {info['name']} ({info['wavelength']})"
|
||||
self.spectrum_combo.addItem(display_text, source_id)
|
||||
|
||||
def browse_folder(self):
|
||||
"""Выбор папки для сохранения"""
|
||||
folder = QFileDialog.getExistingDirectory(self, "Выберите папку для сохранения")
|
||||
if folder:
|
||||
self.selected_folder = Path(folder)
|
||||
self.folder_path.setText(str(self.selected_folder))
|
||||
self.folder_path.setStyleSheet("color: green;")
|
||||
|
||||
if self.remember_path_checkbox.isChecked():
|
||||
self.save_settings()
|
||||
|
||||
def save_settings(self):
|
||||
"""Сохраняет настройки"""
|
||||
if self.selected_folder:
|
||||
self.settings.setValue("timelapse/last_folder", str(self.selected_folder))
|
||||
self.settings.setValue("timelapse/remember_path", self.remember_path_checkbox.isChecked())
|
||||
|
||||
def load_settings(self):
|
||||
"""Загружает настройки"""
|
||||
remember = self.settings.value("timelapse/remember_path", True, type=bool)
|
||||
self.remember_path_checkbox.setChecked(remember)
|
||||
|
||||
if remember:
|
||||
last_folder = self.settings.value("timelapse/last_folder", "")
|
||||
if last_folder and Path(last_folder).exists():
|
||||
self.selected_folder = Path(last_folder)
|
||||
self.folder_path.setText(str(self.selected_folder))
|
||||
self.folder_path.setStyleSheet("color: green;")
|
||||
|
||||
def start_timelapse(self):
|
||||
"""Запускает создание таймлапса"""
|
||||
if not self.selected_folder:
|
||||
QMessageBox.warning(self, "Внимание", "Выберите папку для сохранения")
|
||||
return
|
||||
|
||||
source_id = self.spectrum_combo.currentData()
|
||||
start_date = self.start_date.date().toPython()
|
||||
end_date = self.end_date.date().toPython()
|
||||
fps = int(self.fps_spin.currentText())
|
||||
|
||||
from datetime import datetime as dt
|
||||
|
||||
# Конвертируем date в datetime с правильным временем
|
||||
start_datetime = dt(start_date.year, start_date.month, start_date.day, 12, 0, 0)
|
||||
end_datetime = dt(end_date.year, end_date.month, end_date.day, 23, 59, 59)
|
||||
|
||||
days = (end_date - start_date).days + 1
|
||||
filename = f"timelapse_{source_id}_{start_date.strftime('%Y%m%d')}_to_{end_date.strftime('%Y%m%d')}.mp4"
|
||||
output_path = self.selected_folder / filename
|
||||
|
||||
self.save_settings()
|
||||
|
||||
# Обновляем UI
|
||||
self.create_button.setEnabled(False)
|
||||
self.cancel_button.setEnabled(True)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.log_text.setVisible(True)
|
||||
self.progress_bar.setValue(0)
|
||||
self.log_text.clear()
|
||||
|
||||
self.add_log(f"🚀 Запуск таймлапса: {days} файлов")
|
||||
self.add_log(f"📁 Папка: {self.selected_folder}")
|
||||
|
||||
# Создаем и запускаем поток
|
||||
self.worker = TimelapseWorker(source_id, start_datetime, end_datetime, output_path, fps)
|
||||
self.worker.progress.connect(self.update_progress)
|
||||
self.worker.log.connect(self.add_log)
|
||||
self.worker.finished.connect(self.on_finished)
|
||||
self.worker.start()
|
||||
|
||||
def cancel_timelapse(self):
|
||||
"""Отмена создания таймлапса"""
|
||||
if self.worker and self.worker.isRunning():
|
||||
reply = QMessageBox.question(
|
||||
self, "Отмена",
|
||||
"Вы уверены? Все уже скачанные файлы будут удалены.",
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
self.worker.cancel()
|
||||
self.add_log("⏹️ Отмена процесса...")
|
||||
self.cancel_button.setEnabled(False)
|
||||
|
||||
def update_progress(self, current, total, message):
|
||||
"""Обновление прогресса"""
|
||||
if total > 0:
|
||||
percent = int((current / total) * 100)
|
||||
self.progress_bar.setValue(percent)
|
||||
|
||||
def add_log(self, message):
|
||||
"""Добавление сообщения в лог"""
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
self.log_text.append(f"[{timestamp}] {message}")
|
||||
self.log_text.verticalScrollBar().setValue(
|
||||
self.log_text.verticalScrollBar().maximum()
|
||||
)
|
||||
|
||||
def on_finished(self, success, message):
|
||||
"""Обработка завершения"""
|
||||
self.create_button.setEnabled(True)
|
||||
self.cancel_button.setEnabled(False)
|
||||
|
||||
if success:
|
||||
self.add_log(f"✅ ГОТОВО: {message}")
|
||||
QMessageBox.information(self, "Готово", f"Таймлапс успешно создан!\n{message}")
|
||||
self.accept()
|
||||
else:
|
||||
self.add_log(f"❌ Ошибка: {message}")
|
||||
QMessageBox.critical(self, "Ошибка", message)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""При закрытии окна"""
|
||||
if self.worker and self.worker.isRunning():
|
||||
reply = QMessageBox.question(
|
||||
self, "Процесс выполняется",
|
||||
"Создание таймлапса еще не завершено.\n\nЗакрыть окно?",
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
self.worker.cancel()
|
||||
event.accept()
|
||||
else:
|
||||
event.ignore()
|
||||
else:
|
||||
event.accept()
|
||||
Loading…
Add table
Add a link
Reference in a new issue