можешь сделать так, чтобы когда я захожу в Graphwindow и удаляю точки, файл по которому строились графики никак не изменялся пока я не нажму кнопку сохранить






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

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

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

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

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

        <!-- Нижняя область для объединённых данных -->
        <Grid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="5"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="5"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <!-- Объединённые .txt данные -->
            <GroupBox Header="Объединённые .txt Файлы" Grid.Row="0" Grid.Column="0" Margin="0,10,5,10">
                <DataGrid x:Name="CombinedTxtDataGrid" 
                          AutoGenerateColumns="True" 
                          IsReadOnly="True" 
                          ItemsSource="{Binding CombinedTxtDataTable.DefaultView}" 
                          FontFamily="Consolas" 
                          FontSize="12" 
                          Margin="5"/>
            </GroupBox>

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

            <!-- Объединённые .magnete данные -->
            <GroupBox Header="Объединённые .magnete Файлы" Grid.Row="0" Grid.Column="2" Margin="5,10,0,10">
                <DataGrid x:Name="CombinedMagneteDataGrid" 
                          AutoGenerateColumns="True" 
                          IsReadOnly="True" 
                          ItemsSource="{Binding CombinedMagneteDataTable.DefaultView}" 
                          FontFamily="Consolas" 
                          FontSize="12" 
                          Margin="5"/>
            </GroupBox>
        </Grid>
    </Grid>
</DockPanel>

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

namespace TxtProcessorApp
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
// Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string name)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }

    // 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[]>>();

    // Backing fields for Combined DataTables
    private DataTable combinedTxtDataTable = new DataTable();
    private DataTable combinedMagneteDataTable = new DataTable();

    // Properties for binding
    public DataTable CombinedTxtDataTable
    {
        get => combinedTxtDataTable;
        set
        {
            combinedTxtDataTable = value;
            OnPropertyChanged(nameof(CombinedTxtDataTable));
        }
    }

    public DataTable CombinedMagneteDataTable
    {
        get => combinedMagneteDataTable;
        set
        {
            combinedMagneteDataTable = value;
            OnPropertyChanged(nameof(CombinedMagneteDataTable));
        }
    }

    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = this; // Set DataContext for binding

        // Initialize columns for combined .txt data
        InitializeCombinedTxtDataTable();

        // Initialize columns for combined .magnete data
        InitializeCombinedMagneteDataTable();
    }

    /// <summary>
    /// Initializes columns for combined .txt data.
    /// </summary>
    private void InitializeCombinedTxtDataTable()
    {
        CombinedTxtDataTable.Columns.Add("№", typeof(int));
        CombinedTxtDataTable.Columns.Add("BASE-1", typeof(string));
        CombinedTxtDataTable.Columns.Add("Noise", typeof(string));
        CombinedTxtDataTable.Columns.Add("Time", typeof(string));
        CombinedTxtDataTable.Columns.Add("Time UTC", typeof(string));
    }

    /// <summary>
    /// Initializes columns for combined .magnete data.
    /// </summary>
    private void InitializeCombinedMagneteDataTable()
    {
        CombinedMagneteDataTable.Columns.Add("DATA", typeof(string));
        CombinedMagneteDataTable.Columns.Add("DATA UTC", typeof(string));
        CombinedMagneteDataTable.Columns.Add("TIME", typeof(string));
        CombinedMagneteDataTable.Columns.Add("TIME UTC", typeof(string));
        CombinedMagneteDataTable.Columns.Add("Долгота WGS", typeof(string));
        CombinedMagneteDataTable.Columns.Add("Широта WGS", typeof(string));
        CombinedMagneteDataTable.Columns.Add("Долгота UTM", typeof(double));
        CombinedMagneteDataTable.Columns.Add("Широта UTM", typeof(double));
        CombinedMagneteDataTable.Columns.Add("№1", typeof(int));
        CombinedMagneteDataTable.Columns.Add("Альтитуда", typeof(string));
        CombinedMagneteDataTable.Columns.Add("№2", typeof(int));
        CombinedMagneteDataTable.Columns.Add("Shark", typeof(string));
    }

    #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.Clear(); // Reset combined data
                }
                else
                {
                    PopulateTabControl(MagneteFilesTabControl, magneteFileData, CreateMagneteDataGrid);
                    CombinedMagneteDataTable.Clear(); // 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 (individual files)
    private DataGrid CreateTxtDataGrid(List<string[]> data)
    {
        DataGrid dataGrid = new DataGrid
        {
            AutoGenerateColumns = true,
            ItemsSource = ConvertToDataTable(data, isTxt: true).DefaultView,
            IsReadOnly = true,
            FontFamily = new System.Windows.Media.FontFamily("Consolas"),
            FontSize = 12,
            Margin = new Thickness(5)
        };
        return dataGrid;
    }

    // Create DataGrid for .magnete files (individual files)
    private DataGrid CreateMagneteDataGrid(List<string[]> data)
    {
        DataGrid dataGrid = new DataGrid
        {
            AutoGenerateColumns = true,
            ItemsSource = ConvertToDataTable(data, isTxt: false).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));
        }

        foreach (var row in data)
        {
            DataRow dr = dt.NewRow();
            if (isTxt)
            {
                dr["№"] = int.TryParse(row[0], out int number) ? number : 0;
                dr["BASE-1"] = 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["Time UTC"] = row.Length > 4 ? row[4] : 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 > 2 ? row[2] : string.Empty;
                    string fourthColumn = row.Length > 3 ? 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++;
                }
            }

            // Convert list to existing DataTable
            Dispatcher.Invoke(() =>
            {
                CombinedTxtDataTable.Clear(); // Clear existing data
                foreach (var row in combinedData)
                {
                    CombinedTxtDataTable.Rows.Add(
                        int.Parse(row[0]),
                        row[1],
                        row[2],
                        row[3],
                        row[4]
                    );
                }

                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();

            Dispatcher.Invoke(() =>
            {
                // Clear existing data
                CombinedMagneteDataTable.Clear();
            });

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

            foreach (var file in magneteFileData.Values)
            {
                foreach (var row in file)
                {
                    DataRow newRow = CombinedMagneteDataTable.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);
                        }
                    }

                    // 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");
                    }

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

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

            Dispatcher.Invoke(() =>
            {
                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);
                }
            });
        }); // <-- Added closing parentheses and brace
    } // <-- Added closing brace for CombineMagneteFilesAsync

    #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, the data is already modified in the DataTable and will reflect in the DataGrid due to binding
    }

    #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
                    CombinedTxtDataTable.Clear();
                    foreach (DataRow row in project.CombinedTxtDataTable.Rows)
                    {
                        CombinedTxtDataTable.ImportRow(row);
                    }

                    CombinedMagneteDataTable.Clear();
                    foreach (DataRow row in project.CombinedMagneteDataTable.Rows)
                    {
                        CombinedMagneteDataTable.ImportRow(row);
                    }

                    EditExportButton.IsEnabled = CombinedTxtDataTable.Rows.Count > 0 || CombinedMagneteDataTable.Rows.Count > 0;

                    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 = new GraphWindow(CombinedTxtDataTable, CombinedMagneteDataTable)
        {
            Owner = this
        };
        graphWindow.ShowDialog();

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

    #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
}

}





        <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>

// 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
{
///

/// Класс, представляющий GraphWindow с поддержкой Undo и Redo для удаления точек.
///

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;

    /// <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)
                continue;

            if (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)
                continue;

            if (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)
                continue;

            if (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)
                continue;

            if (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];
                    rowToDelete.Delete(); // Удаление строки из DataTable

                    // Удаление точки из 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);
                }
            }

            // Сброс выбранной серии и точки
            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);

        // Обновление состояния кнопок
        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);

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

}



<Grid.RowDefinitions>


</Grid.RowDefinitions>

    <!-- DataGrid для отображения и редактирования данных -->
    <DataGrid x:Name="EditDataGrid" Grid.Row="0" AutoGenerateColumns="True" 
              CanUserAddRows="True" CanUserDeleteRows="True" 
              IsReadOnly="False" 
              SelectionUnit="CellOrRowHeader" 
              SelectedCellsChanged="EditDataGrid_SelectedCellsChanged"
              FontFamily="Consolas" FontSize="12">
        <!-- Контекстное меню для вставки строк и столбцов -->
        <DataGrid.ContextMenu>
            <ContextMenu>
                <MenuItem Header="Вставить строку выше" Click="InsertRowAbove_Click"/>
                <MenuItem Header="Вставить строку ниже" Click="InsertRowBelow_Click"/>
                <Separator/>
                <MenuItem Header="Вставить столбец слева" Click="InsertColumnLeft_Click"/>
                <MenuItem Header="Вставить столбец справа" Click="InsertColumnRight_Click"/>
            </ContextMenu>
        </DataGrid.ContextMenu>
    </DataGrid>

    <!-- Панель кнопок ниже DataGrid -->
    <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,10,0,0">
        <!-- Элемент управления для выбора разделителя -->
        <ComboBox x:Name="DelimiterComboBox" Width="100" Height="30" SelectedIndex="0">
            <ComboBoxItem Content="Пробел"/>
            <ComboBoxItem Content="Табуляция"/>
            <ComboBoxItem Content="Запятая"/>
        </ComboBox>
        <Button Content="Экспортировать файл" Width="150" Height="30" Margin="10,0,0,0" Click="ExportButton_Click"/>
    </StackPanel>
</Grid>

// EditWindow.xaml.cs
using Microsoft.Win32;
using System;
using System.Data;
using System.Linq;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using ProjNet.CoordinateSystems.Transformations;
using ProjNet.CoordinateSystems;
using System.Windows.Data;

namespace TxtProcessorApp
{
public partial class EditWindow : Window
{
private DataTable _dataTable;

    // Словарь для сопоставления исходных заголовков с заголовками для экспорта
    private readonly Dictionary<string, string> exportHeaderMapping = new Dictionary<string, string>
    {
        { "No1", "№1" },
        { "No2", "№2" }
        // Добавьте другие сопоставления, если необходимо
    };

    public EditWindow(DataTable dataTable)
    {
        InitializeComponent();
        _dataTable = dataTable;
        EditDataGrid.ItemsSource = _dataTable.DefaultView;

        // Разрешаем добавление и удаление строк через интерфейс DataGrid
        EditDataGrid.CanUserAddRows = true;
        EditDataGrid.CanUserDeleteRows = true;
    }

    #region Вставка строк и столбцов

    // Вставка строки выше выбранной
    private void InsertRowAbove_Click(object sender, RoutedEventArgs e)
    {
        int rowIndex = GetSelectedRowIndex();
        if (rowIndex != -1)
        {
            _dataTable.Rows.InsertAt(_dataTable.NewRow(), rowIndex);
        }
        else
        {
            // Если ничего не выбрано, вставляем в конец
            _dataTable.Rows.Add(_dataTable.NewRow());
        }
        RefreshDataGrid();
    }

    // Вставка строки ниже выбранной
    private void InsertRowBelow_Click(object sender, RoutedEventArgs e)
    {
        int rowIndex = GetSelectedRowIndex();
        if (rowIndex != -1)
        {
            _dataTable.Rows.InsertAt(_dataTable.NewRow(), rowIndex + 1);
        }
        else
        {
            // Если ничего не выбрано, вставляем в конец
            _dataTable.Rows.Add(_dataTable.NewRow());
        }
        RefreshDataGrid();
    }

    // Вставка столбца слева от выбранного
    private void InsertColumnLeft_Click(object sender, RoutedEventArgs e)
    {
        int columnIndex = GetSelectedColumnIndex();
        InsertNewColumn(columnIndex, "NewColumn");
        RefreshDataGrid();
    }

    // Вставка столбца справа от выбранного
    private void InsertColumnRight_Click(object sender, RoutedEventArgs e)
    {
        int columnIndex = GetSelectedColumnIndex();
        InsertNewColumn(columnIndex + 1, "NewColumn");
        RefreshDataGrid();
    }

    // Добавление нового столбца в указанное место
    private void InsertNewColumn(int index, string defaultName)
    {
        // Предлагаем пользователю ввести имя нового столбца
        InputDialog inputDialog = new InputDialog("Введите имя нового столбца:", defaultName);
        if (inputDialog.ShowDialog() == true)
        {
            string columnName = inputDialog.ResponseText.Trim();
            if (string.IsNullOrEmpty(columnName))
            {
                MessageBox.Show("Имя столбца не может быть пустым.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
                return;
            }

            // Проверяем, нет ли уже столбца с таким именем
            if (_dataTable.Columns.Contains(columnName))
            {
                MessageBox.Show("Столбец с таким именем уже существует.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
                return;
            }

            // Создаём новый столбец
            DataColumn newColumn = new DataColumn(columnName, typeof(string));

            // Вставляем столбец в нужное место
            _dataTable.Columns.Add(newColumn);
            newColumn.SetOrdinal(Math.Min(index, _dataTable.Columns.Count - 1));

            // Устанавливаем значение по умолчанию для существующих строк
            foreach (DataRow row in _dataTable.Rows)
            {
                row[newColumn] = string.Empty;
            }
        }
        else
        {
            // Если пользователь отменил ввод, автоматически генерируем уникальное имя
            string autoColumnName = GenerateUniqueColumnName("NewColumn");
            DataColumn newColumn = new DataColumn(autoColumnName, typeof(string));
            _dataTable.Columns.Add(newColumn);
            newColumn.SetOrdinal(Math.Min(index, _dataTable.Columns.Count - 1));

            foreach (DataRow row in _dataTable.Rows)
            {
                row[newColumn] = string.Empty;
            }
        }
    }

    // Генерация уникального имени столбца
    private string GenerateUniqueColumnName(string baseName)
    {
        int suffix = 1;
        string newName = baseName;
        while (_dataTable.Columns.Contains(newName))
        {
            newName = $"{baseName}_{suffix}";
            suffix++;
        }
        return newName;
    }

    // Получение индекса выбранной строки
    private int GetSelectedRowIndex()
    {
        if (EditDataGrid.SelectedItem is DataRowView selectedRow)
        {
            return _dataTable.Rows.IndexOf(selectedRow.Row);
        }
        return -1;
    }

    // Получение индекса выбранного столбца
    private int GetSelectedColumnIndex()
    {
        if (EditDataGrid.SelectedCells.Count > 0)
        {
            var selectedCell = EditDataGrid.SelectedCells[0];
            var column = selectedCell.Column;
            if (column != null)
            {
                return column.DisplayIndex;
            }
        }
        return -1;
    }

    #endregion

    // Метод для обновления привязки данных к DataGrid
    private void RefreshDataGrid()
    {
        // Получаем текущий view
        var view = CollectionViewSource.GetDefaultView(EditDataGrid.ItemsSource);

        if (view != null)
        {
            view.Refresh();
        }
    }

    // Обработчик изменения выбранных ячеек (опционально)
    private void EditDataGrid_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
    {
        // Дополнительная логика при изменении выбора (если требуется)
    }

    // Обработчик кнопки Экспортировать файл
    private async void ExportButton_Click(object sender, RoutedEventArgs e)
    {
        // Выбор разделителя
        string delimiter = GetSelectedDelimiter();

        SaveFileDialog sfd = new SaveFileDialog
        {
            Filter = delimiter == "," ? "CSV файлы (*.csv)|*.csv" : "Текстовые файлы (*.txt)|*.txt",
            FileName = delimiter == "," ? "EditedCombinedFile.csv" : "EditedCombinedFile.txt"
        };

        if (sfd.ShowDialog() == true)
        {
            try
            {
                using (StreamWriter sw = new StreamWriter(sfd.FileName, false, Encoding.UTF8))
                {
                    if (delimiter == " ")
                    {
                        // Фиксированная ширина с использованием PadRight
                        ExportWithFixedWidth(sw, delimiter);
                    }
                    else
                    {
                        // Экспорт в CSV или табуляцию
                        await ExportWithDelimiterAsync(sw, delimiter);
                    }
                }

                MessageBox.Show($"Файл успешно экспортирован по адресу:\n{sfd.FileName}", "Успех", MessageBoxButton.OK, MessageBoxImage.Information);
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Ошибка при сохранении файла:\n{ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }
    }

    /// <summary>
    /// Экспорт данных с фиксированной шириной столбцов, используя PadRight.
    /// Подходит для текстовых файлов с пробелом в качестве разделителя.
    /// </summary>
    /// <param name="sw">Поток для записи.</param>
    /// <param name="delimiter">Разделитель.</param>
    private void ExportWithFixedWidth(StreamWriter sw, string delimiter)
    {
        var columns = _dataTable.Columns.Cast<DataColumn>().ToList();

        // Шаг 1: Вычисляем максимальную ширину для каждого столбца
        int[] maxColWidths = new int[columns.Count];

        for (int i = 0; i < columns.Count; i++)
        {
            int maxWidth = columns[i].ColumnName.Length;

            foreach (DataRow row in _dataTable.Rows)
            {
                string field = row[i]?.ToString() ?? string.Empty;
                if (field.Length > maxWidth)
                {
                    maxWidth = field.Length;
                }
            }

            maxColWidths[i] = maxWidth;
        }

        // Шаг 2: Запись заголовков с padding
        string headerLine = string.Join(delimiter, columns.Select((col, idx) => col.ColumnName.PadRight(maxColWidths[idx] + 2)));
        sw.WriteLine(headerLine);

        // Шаг 3: Запись данных строк с padding
        foreach (DataRow row in _dataTable.Rows)
        {
            string dataLine = string.Join(delimiter, columns.Select((col, idx) => (row[idx]?.ToString() ?? string.Empty).PadRight(maxColWidths[idx] + 2)));
            sw.WriteLine(dataLine);
        }
    }

    /// <summary>
    /// Асинхронный экспорт данных с использованием указанного разделителя (CSV или табуляция).
    /// </summary>
    /// <param name="sw">Поток для записи.</param>
    /// <param name="delimiter">Разделитель.</param>
    private async Task ExportWithDelimiterAsync(StreamWriter sw, string delimiter)
    {
        // Запись заголовков
        string[] headers = _dataTable.Columns
            .Cast<DataColumn>()
            .Select(column =>
            {
                if (exportHeaderMapping.ContainsKey(column.ColumnName))
                    return $"\"{exportHeaderMapping[column.ColumnName]}\"";
                else
                    return $"\"{column.ColumnName}\"";
            })
            .ToArray();
        await sw.WriteLineAsync(string.Join(delimiter, headers));

        // Запись данных строк
        foreach (DataRow row in _dataTable.Rows)
        {
            string[] fields = row.ItemArray.Select(field =>
            {
                string fieldStr = field?.ToString() ?? string.Empty;
                // Если поле содержит разделитель или кавычки, обрамляем его в кавычки и экранируем двойные кавычки
                if (fieldStr.Contains(delimiter) || fieldStr.Contains("\""))
                {
                    fieldStr = $"\"{fieldStr.Replace("\"", "\"\"")}\"";
                }
                return fieldStr;
            }).ToArray();
            await sw.WriteLineAsync(string.Join(delimiter, fields));
        }
    }

    /// <summary>
    /// Метод для получения выбранного разделителя.
    /// </summary>
    /// <returns>Разделитель в виде строки.</returns>
    private string GetSelectedDelimiter()
    {
        switch (DelimiterComboBox.SelectedIndex)
        {
            case 0:
                return " "; // Пробел
            case 1:
                return "\t"; // Табуляция
            case 2:
                return ","; // Запятая
            default:
                return "\t"; // По умолчанию табуляция
        }
    }



    #region Методы для объединения .magnete файлов (оригинальная логика)

    // Метод логирования расчётов и ошибок
    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)
        {
            MessageBox.Show($"Не удалось записать лог: {ex.Message}", "Ошибка логирования", MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }

    // Метод для определения UTM-зоны по долготе
    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;
    }

    // Метод для преобразования WGS84 координат в 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
}

}



<Grid.RowDefinitions>



</Grid.RowDefinitions>

    <TextBlock Text="Выберите данные:" Grid.Row="0" Margin="0,0,0,10" FontWeight="Bold" />

    <ListBox x:Name="OptionsListBox" Grid.Row="1">
        <ListBoxItem Content=".txt файлы" />
        <ListBoxItem Content=".magnete файлы" />
    </ListBox>

    <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
        <Button Content="OK" Width="75" Margin="0,0,10,0" IsDefault="True" Click="OK_Click"/>
        <Button Content="Отмена" Width="75" IsCancel="True"/>
    </StackPanel>
</Grid>

// SelectEditTypeWindow.xaml.cs
using System.Windows;
using System.Windows.Controls;

namespace TxtProcessorApp
{
public partial class SelectEditTypeWindow : Window
{
public string SelectedOption { get; private set; }

    public SelectEditTypeWindow(string prompt)
    {
        InitializeComponent();
        // Можно добавить дополнительную настройку окна при необходимости
    }

    private void OK_Click(object sender, RoutedEventArgs e)
    {
        if (OptionsListBox.SelectedItem is ListBoxItem selectedItem)
        {
            SelectedOption = selectedItem.Content.ToString();
            DialogResult = true;
        }
        else
        {
            MessageBox.Show("Пожалуйста, выберите опцию.", "Внимание", MessageBoxButton.OK, MessageBoxImage.Warning);
        }
    }
}

}



<Grid.RowDefinitions>



</Grid.RowDefinitions>

    <TextBlock x:Name="PromptTextBlock" Grid.Row="0" Text="Prompt:" Margin="0,0,0,10" TextWrapping="Wrap"/>

    <TextBox x:Name="ResponseTextBox" Grid.Row="1" Height="25"/>

    <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
        <Button Content="OK" Width="75" Margin="0,0,10,0" IsDefault="True" Click="OK_Click"/>
        <Button Content="Отмена" Width="75" IsCancel="True"/>
    </StackPanel>
</Grid>

// InputDialog.xaml.cs
using System.Windows;

namespace TxtProcessorApp
{
///

/// Логика взаимодействия для InputDialog.xaml
///

public partial class InputDialog : Window
{
public string ResponseText { get; private set; }

    public InputDialog(string question, string defaultAnswer = "")
    {
        InitializeComponent();
        PromptTextBlock.Text = question;
        ResponseTextBox.Text = defaultAnswer;
        ResponseTextBox.SelectAll();
        ResponseTextBox.Focus();
    }

    private void OK_Click(object sender, RoutedEventArgs e)
    {
        ResponseText = ResponseTextBox.Text;
        DialogResult = true;
    }
}

}

// 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; }
}
}

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

namespace TxtProcessorApp
{
///

/// Класс, представляющий действие на графике для реализации Undo и Redo.
///

public class GraphAction
{
///

/// Серия, к которой относится действие.
///

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; }
}

}

пиши полные коды только тех окон и классов которые были изменены