Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "private" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 830
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "private" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 814
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "private" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 804
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "private" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 789
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "public" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 756
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "private" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 730
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "private" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 707
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "private" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 657
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "private" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 618
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "private" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 600
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "private" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 568
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "private" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 533
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0106 Модификатор "private" недопустим для этого элемента. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 511
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS1002 Требуется ";". magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 508
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS1026 Требуется ")" magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 508
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0103 Имя "AddCombinedDataTab" не существует в текущем контексте. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 336
Серьезность Код Описание Проект Файл Строка Состояние подавления
Ошибка (активно) CS0103 Имя "ConvertToCombinedDataTable" не существует в текущем контексте. magn C:\Users\bonia\source\repos\magn\MainWindow.xaml.cs 332

// MainWindow.xaml.cs
using Microsoft.Win32;
using Newtonsoft.Json;
using ProjNet.CoordinateSystems;
using ProjNet.CoordinateSystems.Transformations;
using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace TxtProcessorApp
{
public partial class MainWindow : Window
{
// Dictionaries to store data from files
private Dictionary<string, List<string[]>> txtFileData = new Dictionary<string, List<string[]>>();
private Dictionary<string, List<string[]>> magneteFileData = new Dictionary<string, List<string[]>>();

    // Storage for combined data for further editing
    public DataTable combinedTxtDataTable = null;
    public DataTable combinedMagneteDataTable = null;

    public MainWindow()
    {
        InitializeComponent();
    }

    #region File Loading Handlers

    // Handler for loading files
    private async void LoadFiles_Click(object sender, RoutedEventArgs e)
    {
        if (sender is Button button && button.Tag is string fileType)
        {
            string filter = fileType == "txt" ? "Текстовые файлы (*.txt)|*.txt" : "Magnete файлы (*.magnete)|*.magnete";
            OpenFileDialog ofd = new OpenFileDialog
            {
                Multiselect = true,
                Filter = filter
            };

            if (ofd.ShowDialog() == true)
            {
                foreach (var file in ofd.FileNames)
                {
                    await ProcessFileAsync(file, fileType);
                }

                // Update the respective TabControl
                if (fileType == "txt")
                {
                    PopulateTabControl(FilesTabControl, txtFileData, CreateTxtDataGrid);
                    combinedTxtDataTable = null; // Reset combined data
                }
                else
                {
                    PopulateTabControl(MagneteFilesTabControl, magneteFileData, CreateMagneteDataGrid);
                    combinedMagneteDataTable = null; // Reset combined data
                }

                EditExportButton.IsEnabled = false;
                StatusTextBlock.Text = $"Загружено {(fileType == "txt" ? txtFileData.Count : magneteFileData.Count)} {(fileType == "txt" ? ".txt" : ".magnete")} файлов.";
            }
        }
    }

    // Universal method for processing files
    private Task ProcessFileAsync(string filePath, string fileType)
    {
        return Task.Run(() =>
        {
            List<string[]> processedLines = new List<string[]>();

            try
            {
                var lines = File.ReadAllLines(filePath);
                if (fileType == "txt")
                {
                    lines = lines.Skip(11).ToArray(); // Skip first 11 lines
                }

                foreach (var line in lines)
                {
                    if (string.IsNullOrWhiteSpace(line))
                        continue;

                    var columns = line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

                    if (columns.Length == 0)
                        continue;

                    // Check the first column
                    if (columns[0].ToLower() == "nan" || string.IsNullOrWhiteSpace(columns[0]))
                        continue;

                    // Add columns based on file type
                    if (fileType == "txt")
                    {
                        // For .txt, skip the first column
                        processedLines.Add(columns.Skip(1).ToArray());
                    }
                    else
                    {
                        // For .magnete, add all columns
                        processedLines.Add(columns);
                    }
                }

                // Add processed data to the dictionary
                string fileName = Path.GetFileName(filePath);
                string uniqueFileName = GetUniqueFileName(fileName, fileType == "txt" ? txtFileData : magneteFileData);
                if (fileType == "txt")
                {
                    lock (txtFileData)
                    {
                        txtFileData[uniqueFileName] = processedLines;
                    }
                }
                else
                {
                    lock (magneteFileData)
                    {
                        magneteFileData[uniqueFileName] = processedLines;
                    }
                }
            }
            catch (Exception ex)
            {
                Dispatcher.Invoke(() =>
                    MessageBox.Show($"Ошибка при обработке файла {filePath}:\n{ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error));
            }
        });
    }

    // Method to generate unique file name
    private string GetUniqueFileName(string fileName, Dictionary<string, List<string[]>> existingData)
    {
        string originalFileName = fileName;
        int duplicateCount = 1;
        while (existingData.ContainsKey(fileName))
        {
            fileName = $"{Path.GetFileNameWithoutExtension(originalFileName)}_{duplicateCount}{Path.GetExtension(originalFileName)}";
            duplicateCount++;
        }
        return fileName;
    }

    // Method to populate TabControl
    private void PopulateTabControl(TabControl tabControl, Dictionary<string, List<string[]>> fileData, Func<List<string[]>, DataGrid> createGrid)
    {
        Dispatcher.Invoke(() =>
        {
            tabControl.Items.Clear();
            foreach (var kvp in fileData)
            {
                TabItem tabItem = new TabItem
                {
                    Header = kvp.Key,
                    Content = createGrid(kvp.Value)
                };
                tabControl.Items.Add(tabItem);
            }
        });
    }

    // Create DataGrid for .txt files
    private DataGrid CreateTxtDataGrid(List<string[]> data)
    {
        if (combinedTxtDataTable == null)
        {
            combinedTxtDataTable = ConvertToDataTable(data, isTxt: true);
        }

        DataGrid dataGrid = new DataGrid
        {
            AutoGenerateColumns = true,
            ItemsSource = combinedTxtDataTable.DefaultView,
            IsReadOnly = true,
            FontFamily = new System.Windows.Media.FontFamily("Consolas"),
            FontSize = 12,
            Margin = new Thickness(5)
        };
        return dataGrid;
    }

    // Create DataGrid for .magnete files
    private DataGrid CreateMagneteDataGrid(List<string[]> data)
    {
        if (combinedMagneteDataTable == null)
        {
            combinedMagneteDataTable = ConvertToDataTable(data, isTxt: false);
        }

        DataGrid dataGrid = new DataGrid
        {
            AutoGenerateColumns = true,
            ItemsSource = combinedMagneteDataTable.DefaultView,
            IsReadOnly = true,
            FontFamily = new System.Windows.Media.FontFamily("Consolas"),
            FontSize = 12,
            Margin = new Thickness(5)
        };
        return dataGrid;
    }

    // Universal method to convert data to DataTable
    private DataTable ConvertToDataTable(List<string[]> data, bool isTxt)
    {
        DataTable dt = new DataTable();
        if (data == null || data.Count == 0)
            return dt;

        if (isTxt)
        {
            dt.Columns.Add("№", typeof(int));
            dt.Columns.Add("BASE-1", typeof(string));
            dt.Columns.Add("Noise", typeof(string));
            dt.Columns.Add("Time", typeof(string));
            dt.Columns.Add("Time UTC", typeof(string));
        }
        else
        {
            // Define columns for .magnete, assuming fixed number of columns
            dt.Columns.Add("Stan", typeof(string));
            dt.Columns.Add("Zeit", typeof(string));
            dt.Columns.Add("Noise", typeof(string));
            dt.Columns.Add("Time", typeof(string));
            dt.Columns.Add("TimeUTC", typeof(string));
        }

        int number = 1;
        foreach (var row in data)
        {
            DataRow dr = dt.NewRow();
            if (isTxt)
            {
                dr["№"] = number++;
                dr["BASE-1"] = row.Length > 0 ? row[0] : string.Empty;
                dr["Noise"] = row.Length > 1 ? row[1] : string.Empty;
                dr["Time"] = row.Length > 2 ? row[2] : string.Empty;
                dr["Time UTC"] = row.Length > 3 ? row[3] : string.Empty;
            }
            else
            {
                dr["Stan"] = row.Length > 0 ? row[0] : string.Empty;
                dr["Zeit"] = row.Length > 1 ? row[1] : string.Empty;
                dr["Noise"] = row.Length > 2 ? row[2] : string.Empty;
                dr["Time"] = row.Length > 3 ? row[3] : string.Empty;
                dr["TimeUTC"] = row.Length > 4 ? row[4] : string.Empty;
            }
            dt.Rows.Add(dr);
        }

        return dt;
    }

    #endregion

    #region File Combining Handler

    // Handler for Combine Files button
    private async void CombineFiles_Click(object sender, RoutedEventArgs e)
    {
        if (!txtFileData.Any() && !magneteFileData.Any())
        {
            MessageBox.Show("Нет загруженных файлов для объединения.", "Информация", MessageBoxButton.OK, MessageBoxImage.Information);
            return;
        }

        // Dialog to select file type to combine
        SelectEditTypeWindow selectWindow = new SelectEditTypeWindow("Выберите тип файлов для объединения:");
        selectWindow.Owner = this;

        if (selectWindow.ShowDialog() == true)
        {
            string selectedOption = selectWindow.SelectedOption;

            if (selectedOption == ".txt файлы" && txtFileData.Any())
            {
                await CombineTxtFilesAsync();
            }
            else if (selectedOption == ".magnete файлы" && magneteFileData.Any())
            {
                await CombineMagneteFilesAsync();
            }
            else
            {
                MessageBox.Show("Выбранная опция недоступна или нет загруженных файлов данного типа.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }
    }

    // Asynchronous method to combine .txt files
    private Task CombineTxtFilesAsync()
    {
        return Task.Run(() =>
        {
            List<string[]> combinedData = new List<string[]>();
            int lineNumber = 1;

            foreach (var file in txtFileData.Values)
            {
                foreach (var row in file)
                {
                    if (row.Length < 3)
                        continue;

                    string secondColumn = row[1];
                    string thirdColumn = row.Length > 1 ? row[2] : string.Empty;
                    string fourthColumn = row.Length > 2 ? row[3] : "0";

                    string timeString = "InvalidNumber";
                    if (double.TryParse(fourthColumn, NumberStyles.Any, CultureInfo.InvariantCulture, out double number))
                    {
                        double divided = number / 24;
                        TimeSpan time = TimeSpan.FromDays(divided);
                        timeString = time.ToString(@"hh\:mm\:ss");
                    }

                    combinedData.Add(new string[]
                    {
                        lineNumber.ToString(),
                        secondColumn,
                        thirdColumn,
                        fourthColumn,
                        timeString
                    });

                    lineNumber++;
                }
            }

            combinedTxtDataTable = ConvertToCombinedDataTable(combinedData, isTxt: true);

            Dispatcher.Invoke(() =>
            {
                AddCombinedDataTab(combinedTxtDataTable, ".txt данные объединённые", FilesTabControl);
                EditExportButton.IsEnabled = true;
                StatusTextBlock.Text = $"Объединены .txt файлы. Строк: {combinedTxtDataTable.Rows.Count}.";
            });
        });
    }

    // Asynchronous method to combine .magnete files
    private Task CombineMagneteFilesAsync()
    {
        return Task.Run(() =>
        {
            if (!magneteFileData.Any())
            {
                Dispatcher.Invoke(() =>
                    MessageBox.Show("Нет загруженных .magnete файлов для объединения.", "Информация", MessageBoxButton.OK, MessageBoxImage.Information));
                return;
            }

            DateTime fixedDate = new DateTime(1980, 1, 6, 0, 0, 0, DateTimeKind.Utc);
            DataTable combinedTable = new DataTable();

            // Define columns
            Dispatcher.Invoke(() =>
            {
                combinedTable.Columns.Add("DATA", typeof(string));
                combinedTable.Columns.Add("DATA UTC", typeof(string));
                combinedTable.Columns.Add("TIME", typeof(string));
                combinedTable.Columns.Add("TIME UTC", typeof(string));
                combinedTable.Columns.Add("Долгота WGS", typeof(string));
                combinedTable.Columns.Add("Широта WGS", typeof(string));
                combinedTable.Columns.Add("Долгота UTM", typeof(double));
                combinedTable.Columns.Add("Широта UTM", typeof(double));
                combinedTable.Columns.Add("№1", typeof(int));
                combinedTable.Columns.Add("Альтитуда", typeof(string));
                combinedTable.Columns.Add("№2", typeof(int));
                combinedTable.Columns.Add("Shark", typeof(string));
            });

            int sequentialNumber = 1;
            List<string> errorMessages = new List<string>();

            foreach (var file in magneteFileData.Values)
            {
                foreach (var row in file)
                {
                    DataRow newRow = combinedTable.NewRow();
                    newRow["DATA UTC"] = fixedDate.ToString("dd.MM.yyyy");
                    newRow["TIME"] = row.Length > 0 ? row[0].Trim() : string.Empty;
                    newRow["Широта WGS"] = row.Length > 1 ? row[1].Trim() : string.Empty;
                    newRow["Долгота WGS"] = row.Length > 2 ? row[2].Trim() : string.Empty;
                    newRow["№1"] = sequentialNumber;
                    newRow["№2"] = sequentialNumber;
                    newRow["Альтитуда"] = row.Length > 3 ? row[3].Trim() : string.Empty;
                    newRow["Shark"] = row.Length > 4 ? row[4].Trim() : string.Empty;
                    newRow["Долгота UTM"] = 0.0;
                    newRow["Широта UTM"] = 0.0;

                    string stanValue = row.Length > 0 ? row[0].Trim() : string.Empty;
                    string zeitValue = row.Length > 1 ? row[1].Trim() : string.Empty;
                    string noiseValue = row.Length > 2 ? row[2].Trim() : string.Empty;

                    double longitude, latitude;
                    bool validCoordinates = true;

                    if (!double.TryParse(noiseValue, NumberStyles.Any, CultureInfo.InvariantCulture, out longitude))
                    {
                        string error = $"Не удалось распарсить Долгота WGS='{noiseValue}' в строке: {string.Join("\t", row)}.";
                        errorMessages.Add(error);
                        LogCalculation(error);
                        validCoordinates = false;
                    }

                    if (!double.TryParse(zeitValue, NumberStyles.Any, CultureInfo.InvariantCulture, out latitude))
                    {
                        string error = $"Не удалось распарсить Широта WGS='{zeitValue}' в строке: {string.Join("\t", row)}.";
                        errorMessages.Add(error);
                        LogCalculation(error);
                        validCoordinates = false;
                    }

                    if (validCoordinates)
                    {
                        try
                        {
                            int utmZone = GetUTMZone(longitude);
                            var utmCoordinates = ConvertWGS84ToUTM(longitude, latitude, utmZone);
                            newRow["Долгота UTM"] = utmCoordinates.Easting;
                            newRow["Широта UTM"] = utmCoordinates.Northing;
                        }
                        catch (Exception ex)
                        {
                            string error = $"Ошибка при преобразовании координат (Долгота WGS='{noiseValue}', Широта WGS='{zeitValue}') в строке: {string.Join("\t", row)}. Текст ошибки: {ex.Message}.";
                            errorMessages.Add(error);
                            LogCalculation(error);
                            validCoordinates = false;
                        }
                    }

                    // Handle "Stan" column
                    if (double.TryParse(stanValue, NumberStyles.Any, CultureInfo.InvariantCulture, out double stanSeconds))
                    {
                        if (stanSeconds < 0)
                        {
                            string warning = $"Отрицательное значение TIME='{stanSeconds}' в строке: {string.Join("\t", row)}. Установка даты как фиксированной.";
                            errorMessages.Add(warning);
                            LogCalculation(warning);

                            newRow["DATA"] = fixedDate.ToString("dd.MM.yyyy");
                            newRow["TIME UTC"] = fixedDate.ToString("HH:mm:ss.f");
                        }
                        else
                        {
                            try
                            {
                                DateTime computedDate = fixedDate.AddSeconds(stanSeconds);
                                newRow["DATA"] = computedDate.ToString("dd.MM.yyyy");

                                TimeSpan offsetTime = TimeSpan.FromSeconds(18);
                                TimeSpan computedTime = (computedDate.TimeOfDay >= offsetTime) ? computedDate.TimeOfDay - offsetTime : TimeSpan.Zero;
                                double roundedSeconds = Math.Round(computedTime.TotalSeconds, 1, MidpointRounding.AwayFromZero);
                                TimeSpan roundedTime = TimeSpan.FromSeconds(roundedSeconds);
                                newRow["TIME UTC"] = roundedTime.ToString(@"hh\:mm\:ss\.f");
                            }
                            catch (Exception ex)
                            {
                                string error = $"Ошибка при расчёте даты для TIME='{stanSeconds}' в строке: {string.Join("\t", row)}. Текст ошибки: {ex.Message}. Установка даты как фиксированной.";
                                errorMessages.Add(error);
                                LogCalculation(error);

                                newRow["DATA"] = fixedDate.ToString("dd.MM.yyyy");
                                newRow["TIME UTC"] = fixedDate.ToString("HH:mm:ss.f");
                            }
                        }
                    }
                    else
                    {
                        string error = $"Не удалось распарсить TIME='{stanValue}' в строке: {string.Join("\t", row)}. Установка даты как фиксированной.";
                        errorMessages.Add(error);
                        LogCalculation(error);

                        newRow["DATA"] = fixedDate.ToString("dd.MM.yyyy");
                        newRow["TIME UTC"] = fixedDate.ToString("HH:mm:ss.f");
                    }

                    combinedTable.Rows.Add(newRow);
                    sequentialNumber++;

                    // Log calculations for each row
                    string logMessage = $"TIME: '{stanValue}', ComputedDate: '{newRow["DATA"]}', ComputedTime: '{newRow["TIME UTC"]}'";
                    LogCalculation(logMessage);
                }
            }

            Dispatcher.Invoke(() =>
            {
                combinedMagneteDataTable = combinedTable;
                AddCombinedDataTab(combinedMagneteDataTable, ".magnete данные объединённые", MagneteFilesTabControl);
                EditExportButton.IsEnabled = true;
                StatusTextBlock.Text = $"Объединены .magnete файлы. Строк: {combinedMagneteDataTable.Rows.Count}.";

                if (errorMessages.Any())
                {
                    string allErrors = string.Join("\n", errorMessages);
                    LogCalculation($"Обнаружены следующие проблемы:\n{allErrors}");
                    MessageBox.Show($"Файлы .magnete объединены с ошибками. Подробности в файле логов.", "Внимание", MessageBoxButton.OK, MessageBoxImage.Warning);
                }
                else
                {
                    MessageBox.Show($"Файлы .magnete успешно объединены. Строк: {combinedMagneteDataTable.Rows.Count}", "Успех", MessageBoxButton.OK, MessageBoxImage.Information);
                }
            });
        }

        // Method to add a combined data tab
        private void AddCombinedDataTab(DataTable combinedData, string header, TabControl parentTabControl)
        {
            // Check if the combined data tab already exists
            foreach (TabItem tab in parentTabControl.Items)
            {
                if (tab.Header.ToString() == header)
                    return; // Tab already exists
            }

            // Create DataGrid bound to the combined DataTable
            DataGrid dataGrid = CreateDataGrid(combinedData, isEditable: false);

            TabItem combinedTab = new TabItem
            {
                Header = header,
                Content = dataGrid
            };

            parentTabControl.Items.Add(combinedTab);
        }

        // Method to convert combined data to DataTable
        private DataTable ConvertToCombinedDataTable(List<string[]> data, bool isTxt)
        {
            DataTable dt = new DataTable();

            if (isTxt)
            {
                dt.Columns.Add("№", typeof(int));
                dt.Columns.Add("BASE-1", typeof(string));
                dt.Columns.Add("Noise", typeof(string));
                dt.Columns.Add("Time", typeof(string));
                dt.Columns.Add("Time UTC", typeof(string));
            }

            foreach (var row in data)
            {
                DataRow dr = dt.NewRow();
                if (isTxt)
                {
                    dr["№"] = int.Parse(row[0]);
                    dr["BASE-1"] = row[1];
                    dr["Noise"] = row[2];
                    dr["Time"] = row[3];
                    dr["Time UTC"] = row[4];
                }
                dt.Rows.Add(dr);
            }

            return dt;
        }

        #endregion

        #region Edit and Export Handler

        // Handler for Edit and Export button
        private void EditAndExport_Click(object sender, RoutedEventArgs e)
        {
            if (combinedTxtDataTable == null && combinedMagneteDataTable == null)
            {
                MessageBox.Show("Нет объединённых данных для редактирования.", "Информация", MessageBoxButton.OK, MessageBoxImage.Information);
                return;
            }

            // Create and open a dialog to select the data type to edit
            SelectEditTypeWindow selectWindow = new SelectEditTypeWindow("Выберите данные для редактирования:");
            selectWindow.Owner = this;

            if (selectWindow.ShowDialog() == true)
            {
                string selectedOption = selectWindow.SelectedOption;

                if (selectedOption == ".txt файлы" && combinedTxtDataTable != null)
                {
                    OpenEditWindow(combinedTxtDataTable, "Редактирование .txt данных");
                }
                else if (selectedOption == ".magnete файлы" && combinedMagneteDataTable != null)
                {
                    OpenEditWindow(combinedMagneteDataTable, "Редактирование .magnete данных");
                }
                else
                {
                    MessageBox.Show("Выбранная опция недоступна или данные отсутствуют.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
                }
            }
        }

        // Method to open the edit window
        private void OpenEditWindow(DataTable dataTable, string windowTitle)
        {
            EditWindow editWindow = new EditWindow(dataTable)
            {
                Title = windowTitle,
                Owner = this
            };
            editWindow.ShowDialog();

            // After editing, refresh the DataGrid in case of changes
            RefreshCombinedDataTabs();
        }

        #endregion

        #region Project Handling

        // Handler for Save Project button
        private async void SaveProject_Click(object sender, RoutedEventArgs e)
        {
            SaveFileDialog saveFileDialog = new SaveFileDialog
            {
                Filter = "Magnte Project Files (*.magn)|*.magn",
                DefaultExt = "magn",
                FileName = "project.magn"
            };

            if (saveFileDialog.ShowDialog() == true)
            {
                ProjectData project = new ProjectData
                {
                    TxtFileData = this.txtFileData,
                    MagneteFileData = this.magneteFileData,
                    CombinedTxtDataTable = this.combinedTxtDataTable,
                    CombinedMagneteDataTable = this.combinedMagneteDataTable
                };

                try
                {
                    // Asynchronously serialize the object to JSON
                    string json = await Task.Run(() => JsonConvert.SerializeObject(project, Formatting.Indented, new JsonSerializerSettings
                    {
                        PreserveReferencesHandling = PreserveReferencesHandling.Objects,
                        ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                    }));
                    await File.WriteAllTextAsync(saveFileDialog.FileName, json, System.Text.Encoding.UTF8);
                    StatusTextBlock.Text = $"Проект сохранён: {saveFileDialog.FileName}";
                    MessageBox.Show("Проект успешно сохранён.", "Успех", MessageBoxButton.OK, MessageBoxImage.Information);
                }
                catch (Exception ex)
                {
                    MessageBox.Show($"Ошибка при сохранении проекта:\n{ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
                }
            }
        }

        // Handler for Load Project button
        private async void LoadProject_Click(object sender, RoutedEventArgs e)
        {
            OpenFileDialog openFileDialog = new OpenFileDialog
            {
                Filter = "Magnte Project Files (*.magn)|*.magn",
                DefaultExt = "magn"
            };

            if (openFileDialog.ShowDialog() == true)
            {
                try
                {
                    string json = await File.ReadAllTextAsync(openFileDialog.FileName, System.Text.Encoding.UTF8);
                    ProjectData project = JsonConvert.DeserializeObject<ProjectData>(json);

                    if (project != null)
                    {
                        // Load .txt data
                        this.txtFileData = project.TxtFileData ?? new Dictionary<string, List<string[]>>();
                        PopulateTabControl(FilesTabControl, txtFileData, CreateTxtDataGrid);

                        // Load .magnete data
                        this.magneteFileData = project.MagneteFileData ?? new Dictionary<string, List<string[]>>();
                        PopulateTabControl(MagneteFilesTabControl, magneteFileData, CreateMagneteDataGrid);

                        // Load combined DataTables
                        this.combinedTxtDataTable = project.CombinedTxtDataTable;
                        this.combinedMagneteDataTable = project.CombinedMagneteDataTable;

                        // Update the combined data tabs
                        RefreshCombinedDataTabs();

                        EditExportButton.IsEnabled = combinedTxtDataTable != null || combinedMagneteDataTable != null;

                        StatusTextBlock.Text = $"Проект загружен: {openFileDialog.FileName}";
                        MessageBox.Show("Проект успешно загружен.", "Успех", MessageBoxButton.OK, MessageBoxImage.Information);
                    }
                    else
                    {
                        MessageBox.Show("Не удалось загрузить данные проекта.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
                    }
                }
                catch (Exception ex)
                {
                    MessageBox.Show($"Ошибка при загрузке проекта:\n{ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
                }
            }
        }

        #endregion

        #region Graph Window Handler

        // Handler for Show Graphs button
        private void ShowGraphs_Click(object sender, RoutedEventArgs e)
        {
            if (combinedTxtDataTable == null && combinedMagneteDataTable == null)
            {
                MessageBox.Show("Нет объединённых данных для построения графиков.", "Информация", MessageBoxButton.OK, MessageBoxImage.Information);
                return;
            }

            // Создание экземпляра GraphWindow
            GraphWindow graphWindow = new GraphWindow(combinedTxtDataTable, combinedMagneteDataTable)
            {
                Owner = this
            };

            // Подписка на событие DataModified
            graphWindow.DataModified += RefreshCombinedDataTabs;

            graphWindow.ShowDialog();

            // После закрытия GraphWindow обновляем TabControl с объединёнными данными
            RefreshCombinedDataTabs();

            StatusTextBlock.Text = $"Данные обновлены после редактирования графиков.";
        }

        // Method to refresh combined data tabs
        public void RefreshCombinedDataTabs()
        {
            Dispatcher.Invoke(() =>
            {
                // Refresh .txt combined tab
                foreach (TabItem tab in FilesTabControl.Items)
                {
                    if (tab.Header.ToString().Contains("объединённые") && combinedTxtDataTable != null)
                    {
                        var newDataGrid = CreateTxtDataGrid(ConvertDataTableToList(combinedTxtDataTable));
                        tab.Content = newDataGrid;
                    }
                }

                // Refresh .magnete combined tab
                foreach (TabItem tab in MagneteFilesTabControl.Items)
                {
                    if (tab.Header.ToString().Contains("объединённые") && combinedMagneteDataTable != null)
                    {
                        var newDataGrid = CreateMagneteDataGrid(ConvertDataTableToList(combinedMagneteDataTable));
                        tab.Content = newDataGrid;
                    }
                }

                EditExportButton.IsEnabled = combinedTxtDataTable != null || combinedMagneteDataTable != null;
            });
        }

        #endregion

        #region Graph Combining Helper Methods

        // Method for logging calculations and errors
        private void LogCalculation(string message, string logFileName = "calculation_log.txt")
        {
            string logFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, logFileName);
            try
            {
                File.AppendAllText(logFilePath, $"{DateTime.Now}: {message}{Environment.NewLine}");
            }
            catch (Exception ex)
            {
                Dispatcher.Invoke(() =>
                    MessageBox.Show($"Не удалось записать лог: {ex.Message}", "Ошибка логирования", MessageBoxButton.OK, MessageBoxImage.Error));
            }
        }

        // Method to determine UTM zone based on longitude
        private int GetUTMZone(double longitude)
        {
            if (longitude < -180 || longitude > 180)
                throw new ArgumentOutOfRangeException("Долгота должна быть в диапазоне от -180 до 180 градусов.");

            int zone = (int)Math.Floor((longitude + 180) / 6) + 1;
            return zone;
        }

        // Method to convert WGS84 coordinates to UTM
        private (double Easting, double Northing) ConvertWGS84ToUTM(double longitude, double latitude, int utmZone)
        {
            var wgs84 = GeographicCoordinateSystem.WGS84;
            var utmCS = ProjectedCoordinateSystem.WGS84_UTM(utmZone, latitude >= 0);
            var transformFactory = new CoordinateTransformationFactory();
            var transformation = transformFactory.CreateFromCoordinateSystems(wgs84, utmCS);
            double[] fromPoint = new double[] { longitude, latitude };
            double[] toPoint = transformation.MathTransform.Transform(fromPoint);
            return (toPoint[0], toPoint[1]);
        }

        #endregion

        #region Combined Data Binding

        // Create DataGrid bound to DataTable
        private DataGrid CreateDataGrid(DataTable dataTable, bool isEditable)
        {
            DataGrid dataGrid = new DataGrid
            {
                AutoGenerateColumns = true,
                ItemsSource = dataTable.DefaultView,
                IsReadOnly = !isEditable,
                FontFamily = new System.Windows.Media.FontFamily("Consolas"),
                FontSize = 12,
                Margin = new Thickness(5)
            };
            return dataGrid;
        }

        #endregion

        #region Helper Methods

        // Method to convert DataTable to List<string[]>
        private List<string[]> ConvertDataTableToList(DataTable table)
        {
            var list = new List<string[]>();
            foreach (DataRow row in table.Rows)
            {
                if (row.RowState != DataRowState.Deleted)
                {
                    var rowData = new string[table.Columns.Count];
                    for (int i = 0; i < table.Columns.Count; i++)
                    {
                        rowData[i] = row[i]?.ToString() ?? string.Empty;
                    }
                    list.Add(rowData);
                }
            }
            return list;
        }

        #endregion
    }
}

}

<!-- MainWindow.xaml -->
<Window x:Class="TxtProcessorApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TxtProcessorApp"
        Title="Txt Processor" Height="800" Width="1200">
    <DockPanel Margin="10">
        <!-- Верхняя панель кнопок -->
        <ToolBar DockPanel.Dock="Top">
            <Button Content="Загрузить .txt файлы" Width="150" Click="LoadFiles_Click" Tag="txt" />
            <Button Content="Загрузить .magnete файлы" Width="180" Click="LoadFiles_Click" Tag="magnete" Margin="10,0,0,0"/>
            <Button Content="Проект" Width="120" Margin="10,0,0,0">
                <Button.ContextMenu>
                    <ContextMenu>
                        <MenuItem Header="Сохранить проект" Click="SaveProject_Click"/>
                        <MenuItem Header="Загрузить проект" Click="LoadProject_Click"/>
                    </ContextMenu>
                </Button.ContextMenu>
            </Button>
            <Button Content="Сформировать файл" Width="150" Click="CombineFiles_Click" Margin="10,0,0,0"/>
            <Button Content="Редактировать и Экспортировать" Width="220" Click="EditAndExport_Click" IsEnabled="False" x:Name="EditExportButton" Margin="10,0,0,0"/>
            <Button Content="Графики" Width="100" Click="ShowGraphs_Click" Margin="10,0,0,0"/>
        </ToolBar>

        <!-- Строка состояния -->
        <StatusBar DockPanel.Dock="Bottom">
            <StatusBarItem>
                <TextBlock x:Name="StatusTextBlock" Text="Готово"/>
            </StatusBarItem>
        </StatusBar>

        <!-- Основная область с данными -->
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="5"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <!-- Левая область для файлов -->
            <GroupBox Header=".txt Файлы" Margin="0,10,5,10">
                <TabControl x:Name="FilesTabControl"/>
            </GroupBox>

            <!-- Правая область для файлов -->
            <GroupBox Header=".magnete Файлы" Grid.Column="2" Margin="5,10,0,10">
                <TabControl x:Name="MagneteFilesTabControl"/>
            </GroupBox>

            <!-- Разделитель -->
            <GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" Background="Gray"/>
        </Grid>
    </DockPanel>
</Window>

// ProjectData.cs
using System.Collections.Generic;
using System.Data;

namespace TxtProcessorApp
{
    public class ProjectData
    {
        public Dictionary<string, List<string[]>> TxtFileData { get; set; }
        public Dictionary<string, List<string[]>> MagneteFileData { get; set; }
        public DataTable CombinedTxtDataTable { get; set; }
        public DataTable CombinedMagneteDataTable { get; set; }
    }
}

<Window x:Class="TxtProcessorApp.GraphWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:oxy="http://oxyplot.org/wpf"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Graph Window" Height="600" Width="800">
    <DockPanel>
        <!-- Панель инструментов с кнопками Undo, Redo и выбором графика -->
        <StackPanel Orientation="Horizontal" DockPanel.Dock="Top" Margin="5">
            <Button x:Name="UndoButton" Content="Undo" Width="75" Margin="0,0,5,0" Click="UndoButton_Click" IsEnabled="False"/>
            <Button x:Name="RedoButton" Content="Redo" Width="75" Margin="0,0,15,0" Click="RedoButton_Click" IsEnabled="False"/>

            <Label Content="Выберите график:" VerticalAlignment="Center" Margin="0,0,5,0"/>
            <ComboBox x:Name="ChartSelector" Width="250" SelectionChanged="ChartSelector_SelectionChanged">
                <ComboBoxItem Content=".txt Time UTC vs BASE-1"/>
                <ComboBoxItem Content=".txt Time UTC vs Noise"/>
                <ComboBoxItem Content=".magnete Долгота UTM vs Широта UTM (Альтитуда)"/>
                <ComboBoxItem Content=".magnete Долгота UTM vs Широта UTM (Shark)"/>
            </ComboBox>
        </StackPanel>

        <!-- Область для отображения графика -->
        <oxy:PlotView x:Name="MainPlotView" DockPanel.Dock="Bottom" Margin="10" />
    </DockPanel>
</Window>

// GraphWindow.xaml.cs
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace TxtProcessorApp
{
    /// <summary>
    /// Класс, представляющий GraphWindow с поддержкой Undo и Redo для удаления точек.
    /// </summary>
    public partial class GraphWindow : Window
    {
        public PlotModel TxtBase1Model { get; private set; }
        public PlotModel TxtNoiseModel { get; private set; }
        public PlotModel MagneteAltitudeModel { get; private set; }
        public PlotModel MagneteSharkModel { get; private set; }

        // Сопоставление ScatterSeries с соответствующими DataRow
        private Dictionary<ScatterSeries, List<DataRow>> seriesDataRows = new Dictionary<ScatterSeries, List<DataRow>>();

        // Стэки для Undo и Redo
        private readonly Stack<GraphAction> undoStack = new Stack<GraphAction>();
        private readonly Stack<GraphAction> redoStack = new Stack<GraphAction>();

        // Ссылки на выбранную серию и точку
        private ScatterSeries selectedSeries = null;
        private ScatterPoint selectedPoint = null;

        // Событие для обновления MainWindow при изменениях
        public event Action DataModified;

        /// <summary>
        /// Конструктор класса.
        /// </summary>
        /// <param name="txtData">DataTable с данными .txt файлов.</param>
        /// <param name="magneteData">DataTable с данными .magnete файлов.</param>
        public GraphWindow(DataTable txtData, DataTable magneteData)
        {
            InitializeComponent();
            InitializePlotModels(txtData, magneteData); // Инициализация графиков
            this.DataContext = this;

            // Подписка на событие нажатия мыши на PlotView
            MainPlotView.MouseDown += PlotView_MouseDown;

            UpdateUndoRedoButtons();
        }

        /// <summary>
        /// Метод для обновления состояния кнопок Undo и Redo.
        /// </summary>
        private void UpdateUndoRedoButtons()
        {
            UndoButton.IsEnabled = undoStack.Any();
            RedoButton.IsEnabled = redoStack.Any();
        }

        /// <summary>
        /// Устанавливает равный масштаб для осей X и Y.
        /// </summary>
        private void SetEqualAxisScaling(LinearAxis xAxis, LinearAxis yAxis, IEnumerable<DataRow> dataRows, string xColumn, string yColumn)
        {
            try
            {
                // Вычисление минимальных и максимальных значений
                double minX = dataRows.Min(row => Convert.ToDouble(row[xColumn]));
                double maxX = dataRows.Max(row => Convert.ToDouble(row[xColumn]));
                double minY = dataRows.Min(row => Convert.ToDouble(row[yColumn]));
                double maxY = dataRows.Max(row => Convert.ToDouble(row[yColumn]));

                double rangeX = maxX - minX;
                double rangeY = maxY - minY;

                // Определение максимального диапазона
                double maxRange = Math.Max(rangeX, rangeY);

                // Вычисление центра диапазона
                double centerX = (minX + maxX) / 2;
                double centerY = (minY + maxY) / 2;

                // Установка диапазонов осей для равного масштаба
                xAxis.Minimum = centerX - maxRange / 2;
                xAxis.Maximum = centerX + maxRange / 2;
                yAxis.Minimum = centerY - maxRange / 2;
                yAxis.Maximum = centerY + maxRange / 2;

                // Установка основных шагов
                xAxis.MajorStep = maxRange / 10;
                yAxis.MajorStep = maxRange / 10;
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Ошибка при установке масштаба осей: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

        /// <summary>
        /// Инициализирует модели графиков.
        /// </summary>
        private void InitializePlotModels(DataTable txtData, DataTable magneteData)
        {
            // График 1: .txt Time UTC vs BASE-1
            TxtBase1Model = new PlotModel { Title = ".txt Time UTC vs BASE-1" };
            TxtBase1Model.Axes.Add(new DateTimeAxis
            {
                Position = AxisPosition.Bottom,
                StringFormat = "HH:mm:ss",
                Title = "Time UTC",
                IntervalType = DateTimeIntervalType.Hours,
                MinorIntervalType = DateTimeIntervalType.Minutes
            });
            TxtBase1Model.Axes.Add(new LinearAxis
            {
                Position = AxisPosition.Left,
                Title = "BASE-1"
            });
            var txtBase1Series = new ScatterSeries
            {
                Title = "BASE-1",
                MarkerType = MarkerType.Circle,
                MarkerSize = 5,
                TrackerFormatString = "Time UTC: {2:HH:mm:ss}\nBASE-1: {4}"
            };
            List<DataRow> txtBase1DataRows = new List<DataRow>();

            foreach (DataRow row in txtData.Rows)
            {
                if (row.RowState != DataRowState.Deleted &&
                    DateTime.TryParse(row["Time UTC"].ToString(), out DateTime timeUtc) &&
                    double.TryParse(row["BASE-1"].ToString(), out double base1))
                {
                    double timeInDouble = DateTimeAxis.ToDouble(timeUtc);
                    txtBase1Series.Points.Add(new ScatterPoint(timeInDouble, base1));
                    txtBase1DataRows.Add(row);
                }
            }
            TxtBase1Model.Series.Add(txtBase1Series);
            seriesDataRows[txtBase1Series] = txtBase1DataRows;

            // График 2: .txt Time UTC vs Noise
            TxtNoiseModel = new PlotModel { Title = ".txt Time UTC vs Noise" };
            TxtNoiseModel.Axes.Add(new DateTimeAxis
            {
                Position = AxisPosition.Bottom,
                StringFormat = "HH:mm:ss",
                Title = "Time UTC",
                IntervalType = DateTimeIntervalType.Hours,
                MinorIntervalType = DateTimeIntervalType.Minutes
            });
            TxtNoiseModel.Axes.Add(new LinearAxis
            {
                Position = AxisPosition.Left,
                Title = "Noise"
            });
            var txtNoiseSeries = new ScatterSeries
            {
                Title = "Noise",
                MarkerType = MarkerType.Circle,
                MarkerSize = 5,
                TrackerFormatString = "Time UTC: {2:HH:mm:ss}\nNoise: {4}"
            };
            List<DataRow> txtNoiseDataRows = new List<DataRow>();

            foreach (DataRow row in txtData.Rows)
            {
                if (row.RowState != DataRowState.Deleted &&
                    DateTime.TryParse(row["Time UTC"].ToString(), out DateTime timeUtc) &&
                    double.TryParse(row["Noise"].ToString(), out double noise))
                {
                    double timeInDouble = DateTimeAxis.ToDouble(timeUtc);
                    txtNoiseSeries.Points.Add(new ScatterPoint(timeInDouble, noise));
                    txtNoiseDataRows.Add(row);
                }
            }
            TxtNoiseModel.Series.Add(txtNoiseSeries);
            seriesDataRows[txtNoiseSeries] = txtNoiseDataRows;

            // График 3: .magnete Долгота UTM vs Широта UTM (Альтитуда)
            MagneteAltitudeModel = new PlotModel { Title = ".magnete Долгота UTM vs Широта UTM (Альтитуда)" };
            var xAxisAltitude = new LinearAxis
            {
                Position = AxisPosition.Bottom,
                Title = "Долгота UTM"
            };
            var yAxisAltitude = new LinearAxis
            {
                Position = AxisPosition.Left,
                Title = "Широта UTM"
            };
            MagneteAltitudeModel.Axes.Add(xAxisAltitude);
            MagneteAltitudeModel.Axes.Add(yAxisAltitude);

            var colorAxisAltitude = new LinearColorAxis
            {
                Position = AxisPosition.Right,
                Palette = OxyPalettes.Jet(500),
                Title = "Альтитуда"
            };
            MagneteAltitudeModel.Axes.Add(colorAxisAltitude);

            var magneteAltitudeSeries = new ScatterSeries
            {
                Title = "Альтитуда",
                MarkerType = MarkerType.Circle,
                MarkerSize = 5,
                TrackerFormatString = "Долгота UTM: {2}\nШирота UTM: {3}\nАльтитуда: {4}"
            };
            List<DataRow> magneteAltitudeDataRows = new List<DataRow>();

            foreach (DataRow row in magneteData.Rows)
            {
                if (row.RowState != DataRowState.Deleted &&
                    double.TryParse(row["Долгота UTM"].ToString(), out double longitudeUTM) &&
                    double.TryParse(row["Широта UTM"].ToString(), out double latitudeUTM) &&
                    double.TryParse(row["Альтитуда"].ToString(), out double altitude))
                {
                    magneteAltitudeSeries.Points.Add(new ScatterPoint(longitudeUTM, latitudeUTM, 5, altitude));
                    magneteAltitudeDataRows.Add(row);
                }
            }
            MagneteAltitudeModel.Series.Add(magneteAltitudeSeries);
            seriesDataRows[magneteAltitudeSeries] = magneteAltitudeDataRows;

            // Установка равного масштаба для осей
            SetEqualAxisScaling(xAxisAltitude, yAxisAltitude, magneteData.Rows.Cast<DataRow>(), "Долгота UTM", "Широта UTM");

            // График 4: .magnete Долгота UTM vs Широта UTM (Shark)
            MagneteSharkModel = new PlotModel { Title = ".magnete Долгота UTM vs Широта UTM (Shark)" };
            var xAxisShark = new LinearAxis
            {
                Position = AxisPosition.Bottom,
                Title = "Долгота UTM"
            };
            var yAxisShark = new LinearAxis
            {
                Position = AxisPosition.Left,
                Title = "Широта UTM"
            };
            MagneteSharkModel.Axes.Add(xAxisShark);
            MagneteSharkModel.Axes.Add(yAxisShark);

            var colorAxisShark = new LinearColorAxis
            {
                Position = AxisPosition.Right,
                Palette = OxyPalettes.Jet(500),
                Title = "Shark"
            };
            MagneteSharkModel.Axes.Add(colorAxisShark);

            var magneteSharkSeries = new ScatterSeries
            {
                Title = "Shark",
                MarkerType = MarkerType.Circle,
                MarkerSize = 5,
                TrackerFormatString = "Долгота UTM: {2}\nШирота UTM: {3}\nShark: {4}"
            };
            List<DataRow> magneteSharkDataRows = new List<DataRow>();

            foreach (DataRow row in magneteData.Rows)
            {
                if (row.RowState != DataRowState.Deleted &&
                    double.TryParse(row["Долгота UTM"].ToString(), out double longitudeUTM) &&
                    double.TryParse(row["Широта UTM"].ToString(), out double latitudeUTM) &&
                    double.TryParse(row["Shark"].ToString(), out double shark))
                {
                    magneteSharkSeries.Points.Add(new ScatterPoint(longitudeUTM, latitudeUTM, 5, shark));
                    magneteSharkDataRows.Add(row);
                }
            }
            MagneteSharkModel.Series.Add(magneteSharkSeries);
            seriesDataRows[magneteSharkSeries] = magneteSharkDataRows;

            // Установка равного масштаба для осей
            SetEqualAxisScaling(xAxisShark, yAxisShark, magneteData.Rows.Cast<DataRow>(), "Долгота UTM", "Широта UTM");

            // Настройка PlotView по умолчанию
            MainPlotView.Model = TxtBase1Model;
        }

        /// <summary>
        /// Обработчик выбора графика из ComboBox.
        /// </summary>
        private void ChartSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var selectedItem = (sender as ComboBox)?.SelectedItem as ComboBoxItem;
            if (selectedItem != null)
            {
                switch (selectedItem.Content.ToString())
                {
                    case ".txt Time UTC vs BASE-1":
                        MainPlotView.Model = TxtBase1Model;
                        break;
                    case ".txt Time UTC vs Noise":
                        MainPlotView.Model = TxtNoiseModel;
                        break;
                    case ".magnete Долгота UTM vs Широта UTM (Альтитуда)":
                        MainPlotView.Model = MagneteAltitudeModel;
                        break;
                    case ".magnete Долгота UTM vs Широта UTM (Shark)":
                        MainPlotView.Model = MagneteSharkModel;
                        break;
                    default:
                        break;
                }
            }
        }

        /// <summary>
        /// Обработчик события MouseDown для PlotView.
        /// </summary>
        private void PlotView_MouseDown(object sender, MouseButtonEventArgs e)
        {
            var plotModel = MainPlotView.Model;
            if (plotModel == null)
                return;

            // Получить позицию клика относительно PlotView
            var mousePos = e.GetPosition(MainPlotView);

            // Преобразование координат клика в данные координаты
            double xData, yData;
            try
            {
                xData = plotModel.DefaultXAxis.InverseTransform(mousePos.X);
                yData = plotModel.DefaultYAxis.InverseTransform(mousePos.Y);
            }
            catch
            {
                // Если преобразование не удалось, выйти
                return;
            }

            // Определить ближайшую точку и расстояние до неё (в пикселях)
            double thresholdInPixels = 10; // Пороговое расстояние в пикселях
            double minDistance = double.MaxValue;
            ScatterSeries nearestSeries = null;
            ScatterPoint nearestPoint = null;

            foreach (var series in plotModel.Series)
            {
                if (series is ScatterSeries scatterSeries)
                {
                    foreach (var point in scatterSeries.Points)
                    {
                        // Перевести данные координаты в экранные
                        double screenX = plotModel.DefaultXAxis.Transform(point.X);
                        double screenY = plotModel.DefaultYAxis.Transform(point.Y);

                        // Вычисление расстояния между кликом и точкой
                        double distance = Math.Sqrt(Math.Pow(mousePos.X - screenX, 2) + Math.Pow(mousePos.Y - screenY, 2));

                        if (distance < minDistance && distance <= thresholdInPixels)
                        {
                            minDistance = distance;
                            nearestSeries = scatterSeries;
                            nearestPoint = point;
                        }
                    }
                }
            }

            if (nearestSeries != null && nearestPoint != null)
            {
                // Сохранить выбранную серию и точку
                selectedSeries = nearestSeries;
                selectedPoint = nearestPoint;

                // Показать контекстное меню в позиции клика
                // Convert mouse position relative to window
                var windowPos = this.PointToScreen(mousePos);

                ShowContextMenu(new System.Windows.Point(windowPos.X, windowPos.Y));

                // Обработка события завершена
                e.Handled = true;
            }
        }

        /// <summary>
        /// Отображает контекстное меню с опцией удаления точки.
        /// </summary>
        /// <param name="position">Позиция клика для отображения меню (в координатах экрана).</param>
        private void ShowContextMenu(System.Windows.Point position)
        {
            if (selectedSeries == null || selectedPoint == null)
                return;

            // Создание контекстного меню
            ContextMenu contextMenu = new ContextMenu();

            MenuItem deleteItem = new MenuItem
            {
                Header = "Удалить точку"
            };
            deleteItem.Click += DeleteItem_Click;

            contextMenu.Items.Add(deleteItem);

            // Установка позиции контекстного меню
            contextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.AbsolutePoint;
            contextMenu.HorizontalOffset = position.X;
            contextMenu.VerticalOffset = position.Y;

            // Открытие контекстного меню
            contextMenu.IsOpen = true;
        }

        /// <summary>
        /// Обработчик события удаления точки из контекстного меню.
        /// </summary>
        private void DeleteItem_Click(object sender, RoutedEventArgs e)
        {
            if (selectedSeries != null && selectedPoint != null)
            {
                var result = MessageBox.Show("Вы уверены, что хотите удалить эту точку?", "Подтверждение удаления", MessageBoxButton.YesNo, MessageBoxImage.Question);
                if (result == MessageBoxResult.Yes)
                {
                    // Найти соответствующую DataRow
                    var dataRows = seriesDataRows[selectedSeries];
                    int pointIndex = selectedSeries.Points.IndexOf(selectedPoint);
                    if (pointIndex >= 0 && pointIndex < dataRows.Count)
                    {
                        DataRow rowToDelete = dataRows[pointIndex];

                        // Удаление строки из DataTable
                        rowToDelete.Delete(); // Помечает строку для удаления

                        // Удаление точки из ScatterSeries и списка DataRows
                        selectedSeries.Points.RemoveAt(pointIndex);
                        dataRows.RemoveAt(pointIndex);

                        // Запись действия в Undo Stack
                        var action = new GraphAction
                        {
                            Series = selectedSeries,
                            Point = selectedPoint,
                            PointIndex = pointIndex,
                            DataRow = rowToDelete
                        };
                        undoStack.Push(action);

                        // Очистка Redo Stack, так как новое действие делает историю Redo неактуальной
                        redoStack.Clear();

                        // Обновление графика
                        MainPlotView.Model.InvalidatePlot(true);

                        // Вызов события о модификации данных
                        DataModified?.Invoke();
                    }
                }

                // Сброс выбранной серии и точки
                selectedSeries = null;
                selectedPoint = null;

                // Обновление состояния кнопок
                UpdateUndoRedoButtons();
            }
        }

        /// <summary>
        /// Обработчик кнопки Undo.
        /// </summary>
        private void UndoButton_Click(object sender, RoutedEventArgs e)
        {
            if (!undoStack.Any())
                return;

            var action = undoStack.Pop();
            redoStack.Push(action); // Переносим действие в Redo Stack

            // Восстановление точки
            action.Series.Points.Insert(action.PointIndex, action.Point);
            seriesDataRows[action.Series].Insert(action.PointIndex, action.DataRow);

            // Восстановление строки в DataTable
            action.DataRow.RejectChanges(); // Восстанавливаем удалённую строку

            // Обновление графика
            MainPlotView.Model.InvalidatePlot(true);

            // Вызов события о модификации данных
            DataModified?.Invoke();

            // Обновление состояния кнопок
            UpdateUndoRedoButtons();
        }

        /// <summary>
        /// Обработчик кнопки Redo.
        /// </summary>
        private void RedoButton_Click(object sender, RoutedEventArgs e)
        {
            if (!redoStack.Any())
                return;

            var action = redoStack.Pop();
            undoStack.Push(action); // Переносим действие обратно в Undo Stack

            // Удаление точки снова
            action.Series.Points.RemoveAt(action.PointIndex);
            seriesDataRows[action.Series].RemoveAt(action.PointIndex);

            // Удаление строки из DataTable
            action.DataRow.Delete();

            // Обновление графика
            MainPlotView.Model.InvalidatePlot(true);

            // Вызов события о модификации данных
            DataModified?.Invoke();

            // Обновление состояния кнопок
            UpdateUndoRedoButtons();
        }
    }
}

// GraphAction.cs
using System.Data;
using OxyPlot.Series;

namespace TxtProcessorApp
{
    /// <summary>
    /// Класс, представляющий действие на графике для реализации Undo и Redo.
    /// </summary>
    public class GraphAction
    {
        /// <summary>
        /// Серия, к которой относится действие.
        /// </summary>
        public ScatterSeries Series { get; set; }

        /// <summary>
        /// Удалённая точка.
        /// </summary>
        public ScatterPoint Point { get; set; }

        /// <summary>
        /// Индекс точки в серии до её удаления.
        /// </summary>
        public int PointIndex { get; set; }

        /// <summary>
        /// Строка данных, связанная с удалённой точкой.
        /// </summary>
        public DataRow DataRow { get; set; }
    }
}