Загрузка данных


# границы прогноза max. мин.
# начало прогноза (с начала проекта, с указанной даты)
# приклеить низ
#
#
# -*- coding: utf-8 -*-
# gui/forecast_window.py
from matplotlib.figure import Figure 
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg 
import tkinter as tk
import matplotlib.dates as mdates 
from tkinter import ttk
from datetime import datetime, timedelta

# --- ИМПОРТЫ НОВЫХ МОДУЛЕЙ ---
from modules.forecast_data_processing import generate_trajectory
from modules.forecast_modeling import monte_carlo_forecast
from modules.forecast_visualization import GraphUpdater


class ForecastWindow:
    """
    Окно для отображения прогноза.
    Принимает на вход родительское окно (root), чтобы иметь доступ к его данным.
    """
    def __init__(self, parent):
        # Сохраняем ссылку на главное окно
        self.root_window = parent
        # Создаем новое окно (Toplevel)
        self.window = tk.Toplevel(parent)
        self.window.title("Прогнозирование")
        self.window.geometry("1050x650")
        self.window.resizable(True, True)
        
        # --- ГЛАВНЫЙ КОНТЕЙНЕР ---
        main_container = ttk.Frame(self.window)
        main_container.pack(fill="both", expand=True)
        # Делим главное окно на две части: Верх (График+Настройки) и Низ (Результаты)
        main_container.grid_rowconfigure(0, weight=4) # Верхняя часть выше
        main_container.grid_rowconfigure(1, weight=1) # Нижняя часть ниже

        # --- ВЕРХНИЙ БЛОК (График и Настройки) ---
        top_block = ttk.Frame(main_container)
        top_block.grid(row=0, column=0, sticky="nsew")
        # Внутри верхнего блока делим на два фрейма: График (слева) и Настройки (справа)
        top_block.grid_columnconfigure(0, weight=3) # График шире
        top_block.grid_columnconfigure(1, weight=2) # Настройки уже

        # 1. Левая панель: График
        graph_frame = ttk.LabelFrame(top_block, text="График прогноза", padding="10")
        graph_frame.grid(row=0, column=0, sticky="nsew")
        self.figure = Figure(figsize=(5, 4), dpi=100)
        self.ax = self.figure.add_subplot(111)
        self.canvas = FigureCanvasTkAgg(self.figure, master=graph_frame)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(fill="both", expand=True)

        # --- ПРАВАЯ ПАНЕЛЬ ---
        settings_frame = ttk.LabelFrame(top_block, text="Параметры модели", padding="10")
        settings_frame.grid(row=0, column=1, sticky="ns")
        settings_frame.config(width=500)
        settings_frame.pack_propagate(False)

        # -- Блок выбора проекта --
        project_frame = ttk.Frame(settings_frame)
        project_frame.pack(fill="x", pady=(0, 10))
        ttk.Label(project_frame, text="Выбрать проект:").pack(side="left", anchor="w", padx=(0, 5))
        self.project_combobox = ttk.Combobox(project_frame, state="readonly", width=28)
        self.project_combobox.pack(side="left", anchor="w")
        self.refresh_button = ttk.Button(project_frame, text="Обновить список", command=self.load_projects)
        self.refresh_button.pack(side="left", anchor="w", padx=(10, 0))
        self.load_projects()

        # -- Поля ввода параметров --
        input_frame = ttk.Frame(settings_frame)
        input_frame.pack(fill="x", padx=10, pady=5)
        ttk.Label(input_frame, text="Интенсивность событий (в день):").pack(side="left", anchor="w", padx=(0, 10))
        self.lambda_entry = ttk.Entry(input_frame, width=10)
        self.lambda_entry.insert(0, "5")
        self.lambda_entry.pack(side="left", anchor="w")
        ttk.Label(input_frame, text="Шаг (дни):").pack(side="left", anchor="w", padx=(20, 10))
        self.step_entry = ttk.Entry(input_frame, width=10)
        self.step_entry.insert(0, "1")
        self.step_entry.pack(side="left", anchor="w")

        # Поле "Прогноз на:"
        period_frame = ttk.Frame(settings_frame)
        period_frame.pack(fill="x", padx=10, pady=(5, 0))
        ttk.Label(period_frame, text="Прогноз на:").pack(side="left", anchor="w", padx=(0, 10))
        self.period_combobox = ttk.Combobox(period_frame, state="readonly",
                                          values=["Месяц (30 дней)", "Квартал (90 дней)", "Полгода (180 дней)"],
                                          width=18)
        self.period_combobox.current(0)
        self.period_combobox.pack(side="left", anchor="w")

        # -- Настройки симуляции --
        sim_settings_frame = ttk.Frame(settings_frame)
        sim_settings_frame.pack(fill="x", padx=10, pady=5)
        ttk.Label(sim_settings_frame, text="Кол-во итераций:\n(в симуляции)", justify="left").pack(
            side="left", anchor="nw", padx=(0, 10))
        self.iterations_entry = ttk.Entry(sim_settings_frame, width=15)
        self.iterations_entry.insert(0, "10000")
        self.iterations_entry.pack(side="left", anchor="w")

        self.build_button = ttk.Button(settings_frame, text="Построить прогноз", command=self.run_forecast)
        self.build_button.pack(pady=30)

        # --- НИЖНИЙ БЛОК (Результаты расчета) ---
        result_frame = ttk.LabelFrame(main_container, text="Результаты расчета", padding="5")
        result_frame.grid(row=1, column=0, sticky="ew")
        self.result_text = tk.Text(result_frame, height=2, wrap='word', state='disabled')
        scrollbar = ttk.Scrollbar(result_frame, command=self.result_text.yview)
        scrollbar.pack(side="right", fill="y")
        self.result_text.configure(yscrollcommand=scrollbar.set)
        self.result_text.pack(padx=10, pady=5, fill="x")

    def load_projects(self):
        """Загружает список проектов из главного окна в выпадающий список."""
        self.project_combobox.set('')
        projects = self.root_window.project_manager.list_projects()
        project_names = [p.name for p in projects]
        self.project_combobox['values'] = project_names
        if project_names:
            self.project_combobox.current(0)

    def _insert_into_result(self, text):
        """Вставляет текст в поле результатов."""
        self.result_text.config(state='normal')
        self.result_text.insert(tk.END, text)
        self.result_text.config(state='disabled')
        self.result_text.see(tk.END)

    def _clear_result_text(self):
        """Очищает поле результатов перед новым расчетом."""
        self.result_text.config(state='normal')
        self.result_text.delete(1.0, tk.END)
        self.result_text.config(state='disabled')

    def run_forecast(self):
        """Финальная рабочая версия метода."""
        # Очищаем поле результатов и график перед новым расчетом
        self._clear_result_text()
        
        try:
            iterations = int(self.iterations_entry.get())
            lambda_val = float(self.lambda_entry.get())
            
            selected_period = self.period_combobox.get()
            days_forward_dict = {"Месяц": 30, "Квартал": 90, "Полгода": 180}
            period_key = next((k for k in days_forward_dict if k in selected_period), None)
            days_forward = days_forward_dict.get(period_key, 30)
                
            step_days = int(self.step_entry.get())
            from datetime import datetime
            start_date = datetime.now().date()

        except ValueError as e:
            error_msg = f"Ошибка ввода: {e}. Проверьте все поля."
            print(error_msg)
            self._insert_into_result("ОШИБКА: " + str(e) + "\n")
            return

        # --- 1. РАБОТА С ДАННЫМИ И МОДЕЛЬЮ ---
        from modules.forecast_data_processing import generate_trajectory
        from modules.forecast_modeling import monte_carlo_forecast
        from modules.forecast_visualization import GraphUpdater

        trajectory, daily_values_history = generate_trajectory(lambda_val, days_forward, step_days)

        if not trajectory:
            self._insert_into_result("Не удалось сгенерировать данные.\n")
            return

        sim_result = monte_carlo_forecast(
            pool_values=trajectory,
            num_simulations=iterations,
            p1=0.05,
            p2=0.95
        )

        final_lower = sim_result["lower_bound"]
        final_upper = sim_result["upper_bound"]
        final_median = sim_result["median"]

        # --- 2. ПОДГОТОВКА РЕЗУЛЬТАТОВ (ТЕКСТ) ---
        output_text = (
            f"ПРОГНОЗ ДОСТИЖЕНИЯ ЦЕЛИ:\n"
            f"   Цель (Нижняя граница): {final_lower:.2f}\n\n"
            f"   Прогнозируемый диапазон значений:\n"
            f"      Минимум: {final_lower:.2f}\n"
            f"      Максимум: {final_upper:.2f}\n\n"
        )

        # Поиск даты пересечения
        date_of_intersection = None
        for idx, val in enumerate(daily_values_history):
            if val >= final_lower:
                date_of_intersection = start_date + timedelta(days=idx)
                break

        if date_of_intersection:
            output_text += f"Достижение минимума прогнозируется: {date_of_intersection.strftime('%d.%m.%Y')}"
        else:
            output_text += "Достижение минимума в заданный период не прогнозируется."

        self._insert_into_result(output_text)

        # --- 3. ВИЗУАЛИЗАЦИЯ (ЭТО ГЛАВНАЯ ЧАСТЬ!) ---
        # Сначала очищаем оси графика
        self.ax.clear() 

        # Создаем объект-подготовщик
        graph_updater = GraphUpdater(start_date, step_days)
        # Получаем готовые данные для рисования
        plot_data = graph_updater.prepare_plot_data(sim_result, trajectory, daily_values_history)

        # --- КОМАНДЫ РИСОВАНИЯ ---
        # Рисуем горизонтальные линии (всегда)
        self.ax.axhline(y=plot_data["lower_bound"], color='red', linestyle='--', label=f'Нижняя граница')
        self.ax.axhline(y=plot_data["median"],     color='green', linestyle='-', label=f'Медиана')
        self.ax.axhline(y=plot_data["upper_bound"], color='blue', linestyle='--', label=f'Верхняя граница')

        # Рисуем траекторию (если есть данные)
        if plot_data["has_trajectory"]:
            self.ax.plot(plot_data["dates"], plot_data["trajectory"], color='orange',
                        marker='o', linewidth=2, markersize=6, label='Смоделированная динамика')
            # Настраиваем ось X
            self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%d.%m'))
            self.ax.xaxis.set_tick_params(rotation=45)
            self.ax.set_xticks(plot_data["dates"])

        # Рисуем точку пересечения (если она есть и есть траектория)
        if plot_data["intersection_date"] and plot_data["has_trajectory"]:
            x_coord = plot_data["dates"][plot_data["intersection_x_idx"]]
            y_coord = plot_data["intersection_y_val"]
            self.ax.scatter(x_coord, y_coord, color='darkred', s=150,
                          zorder=5, edgecolor='black', linewidth=1, alpha=0.7)
            self.ax.annotate(f'{plot_data["intersection_date"].strftime("%d.%m")}',
                            xy=(x_coord, y_coord),
                            xytext=(5, 5), textcoords="offset points",
                            fontsize=8, backgroundcolor='white')

        # Добавляем легенду
        handles, labels = self.ax.get_legend_handles_labels()
        if handles:
            self.ax.legend(loc='upper left')

        # --- 4. ФИНАЛЬНЫЙ ЭТАП ---
        # ОБЯЗАТЕЛЬНО сообщаем Canvas, что нужно перерисоваться!
        self.canvas.draw()