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

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










    <!-- Панель управления -->
    <StackPanel Orientation="Horizontal" DockPanel.Dock="Top" Margin="10">
        <Label Content="Выберите графики:" VerticalAlignment="Center" Margin="0,0,5,0"/>
        <!-- Используем ListBox с возможностью множественного выбора -->
        <ListBox x:Name="ChartSelector" Width="300" SelectionMode="Multiple" SelectionChanged="ChartSelector_SelectionChanged">
            <ListBoxItem Content=".txt Time UTC vs BASE-1"/>
            <ListBoxItem Content=".txt Time UTC vs Noise"/>
            <ListBoxItem Content=".magnete Долгота UTM vs Широта UTM (Альтитуда)"/>
            <ListBoxItem Content=".magnete Долгота UTM vs Широта UTM (Shark)"/>
            <ListBoxItem Content=".magnete Time UTC vs Shark"/>
        </ListBox>
        <Button x:Name="UndoButton" Content="Отменить" Width="100" Click="UndoButton_Click" IsEnabled="False" Margin="10,0,0,0"/>
        <Button x:Name="RedoButton" Content="Повторить" Width="100" Click="RedoButton_Click" IsEnabled="False" Margin="5,0,0,0"/>
        <Button Content="Сохранить" Width="100" Click="SaveButton_Click" Margin="10,0,0,0"/>
        <Button Content="Отмена" Width="100" Click="CancelButton_Click" Margin="5,0,0,0"/>
        <!-- Добавляем ToggleButton для переключения режима -->
        <ToggleButton x:Name="EditModeToggleButton" Content="Режим редактирования" Width="150" Margin="10,0,0,0" Checked="EditModeToggleButton_Checked" Unchecked="EditModeToggleButton_Unchecked"/>
    </StackPanel>

    <!-- Область для отображения графиков -->
    <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
        <!-- Используем UniformGrid для размещения графиков -->
        <UniformGrid x:Name="PlotGrid" Margin="10"/>
    </ScrollViewer>
</DockPanel>

// GraphWindow.xaml.cs
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
using OxyPlot.Annotations;
using OxyPlot.Wpf;
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
{
///

/// Окно для отображения и взаимодействия с графиками.
///

public partial class GraphWindow : Window
{
// Словарь для хранения фабрик PlotInfo
private readonly Dictionary<string, Func> plotModelFactories = new();

    // Хранит текущие отображаемые графики
    private List<PlotInfo> currentPlotInfos = new();

    private readonly Stack<IUndoRedoAction> undoStack = new();
    private readonly Stack<IUndoRedoAction> redoStack = new();

    private ScreenPoint? selectionStartPoint;
    private RectangleAnnotation selectionRectangle;

    private readonly List<(ScatterSeries Series, ScatterPoint Point, int Index, DataRow DataRow, List<DataRow> DataRows)> selectedPoints = new();

    private readonly Dictionary<PlotModel, List<PointAnnotation>> selectedPointAnnotations = new();

    private readonly DataTable txtDataCopy;
    private readonly DataTable magneteDataCopy;

    public DataTable TxtDataCopy => txtDataCopy;
    public DataTable MagneteDataCopy => magneteDataCopy;

    public GraphWindow(DataTable txtData, DataTable magneteData)
    {
        InitializeComponent();

        txtDataCopy = txtData.Copy();
        magneteDataCopy = magneteData.Copy();

        InitializePlotModelFactories();

        ChartSelector.SelectionChanged -= ChartSelector_SelectionChanged;
        ChartSelector.SelectedItems.Add(ChartSelector.Items[0]);
        ChartSelector.SelectedItems.Add(ChartSelector.Items[1]);
        ChartSelector.SelectionChanged += ChartSelector_SelectionChanged;

        UpdatePlots();
        UpdateUndoRedoButtons();

        Loaded += (s, e) => Keyboard.Focus(this);
    }

    /// <summary>
    /// Инициализирует фабрики для создания графиков.
    /// </summary>
    private void InitializePlotModelFactories()
    {
        plotModelFactories[".txt Time UTC vs BASE-1"] = () => CreateTxtPlotModel("BASE-1");
        plotModelFactories[".txt Time UTC vs Noise"] = () => CreateTxtPlotModel("Noise");
        plotModelFactories[".magnete Долгота UTM vs Широта UTM (Альтитуда)"] = () => CreateMagnetePlotModel("Альтитуда");
        plotModelFactories[".magnete Долгота UTM vs Широта UTM (Shark)"] = () => CreateMagnetePlotModel("Shark");
        plotModelFactories[".magnete Time UTC vs Shark"] = CreateMagneteTimeSharkPlotModel;
    }

    /// <summary>
    /// Класс для хранения информации о графике.
    /// </summary>
    private class PlotInfo
    {
        public PlotModel Model { get; set; }
        public Dictionary<ScatterSeries, List<DataRow>> SeriesDataRows { get; set; } = new();
    }

    private PlotInfo CreateTxtPlotModel(string valueColumn)
    {
        string title = $".txt Time UTC vs {valueColumn}";
        var model = new PlotModel { Title = title };
        var xAxis = new DateTimeAxis
        {
            Position = AxisPosition.Bottom,
            StringFormat = "HH:mm:ss",
            Title = "Time UTC",
            IntervalType = DateTimeIntervalType.Hours,
            MinorIntervalType = DateTimeIntervalType.Minutes
        };
        var yAxis = new LinearAxis
        {
            Position = AxisPosition.Left,
            Title = valueColumn
        };
        model.Axes.Add(xAxis);
        model.Axes.Add(yAxis);

        var series = new ScatterSeries
        {
            Title = valueColumn,
            MarkerType = MarkerType.Circle,
            MarkerSize = 5,
            TrackerFormatString = $"Time UTC: {{2:HH:mm:ss}}\n{valueColumn}: {{4}}"
        };

        var dataRows = new List<DataRow>();

        foreach (DataRow row in txtDataCopy.Rows)
        {
            if (row.RowState == DataRowState.Deleted)
                continue;

            if (DateTime.TryParse(row["Time UTC"].ToString(), out DateTime timeUtc) &&
                double.TryParse(row[valueColumn].ToString(), out double value))
            {
                double timeInDouble = DateTimeAxis.ToDouble(timeUtc);
                series.Points.Add(new ScatterPoint(timeInDouble, value));
                dataRows.Add(row);
            }
        }

        model.Series.Add(series);

        var plotInfo = new PlotInfo { Model = model };
        plotInfo.SeriesDataRows[series] = dataRows;
        return plotInfo;
    }

    private PlotInfo CreateMagnetePlotModel(string valueColumn)
    {
        string title = $".magnete Долгота UTM vs Широта UTM ({valueColumn})";
        var model = new PlotModel { Title = title };
        var xAxis = new LinearAxis
        {
            Position = AxisPosition.Bottom,
            Title = "Долгота UTM"
        };
        var yAxis = new LinearAxis
        {
            Position = AxisPosition.Left,
            Title = "Широта UTM"
        };
        model.Axes.Add(xAxis);
        model.Axes.Add(yAxis);

        var colorAxis = new LinearColorAxis
        {
            Position = AxisPosition.Right,
            Palette = OxyPalettes.Jet(500),
            Title = valueColumn
        };
        model.Axes.Add(colorAxis);

        var series = new ScatterSeries
        {
            Title = valueColumn,
            MarkerType = MarkerType.Circle,
            MarkerSize = 5,
            TrackerFormatString = $"Долгота UTM: {{2}}\nШирота UTM: {{4}}\n{valueColumn}: {{6}}"
        };

        var dataRows = new List<DataRow>();

        foreach (DataRow row in magneteDataCopy.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[valueColumn].ToString(), out double value))
            {
                series.Points.Add(new ScatterPoint(longitudeUTM, latitudeUTM, 5, value));
                dataRows.Add(row);
            }
        }

        model.Series.Add(series);

        var plotInfo = new PlotInfo { Model = model };
        plotInfo.SeriesDataRows[series] = dataRows;

        SetEqualAxisScaling(xAxis, yAxis, dataRows, "Долгота UTM", "Широта UTM");

        return plotInfo;
    }

    private PlotInfo CreateMagneteTimeSharkPlotModel()
    {
        string title = ".magnete Time UTC vs Shark";
        var model = new PlotModel { Title = title };
        var xAxis = new DateTimeAxis
        {
            Position = AxisPosition.Bottom,
            StringFormat = "HH:mm:ss",
            Title = "Time UTC",
            IntervalType = DateTimeIntervalType.Hours,
            MinorIntervalType = DateTimeIntervalType.Minutes
        };
        var yAxis = new LinearAxis
        {
            Position = AxisPosition.Left,
            Title = "Shark"
        };
        model.Axes.Add(xAxis);
        model.Axes.Add(yAxis);

        var series = new ScatterSeries
        {
            Title = "Shark",
            MarkerType = MarkerType.Circle,
            MarkerSize = 5,
            TrackerFormatString = $"Time UTC: {{2:HH:mm:ss}}\nShark: {{4}}"
        };

        var dataRows = new List<DataRow>();

        foreach (DataRow row in magneteDataCopy.Rows)
        {
            if (row.RowState == DataRowState.Deleted)
                continue;

            if (DateTime.TryParse(row["TIME UTC"].ToString(), out DateTime timeUtc) &&
                double.TryParse(row["Shark"].ToString(), out double shark))
            {
                double timeInDouble = DateTimeAxis.ToDouble(timeUtc);
                series.Points.Add(new ScatterPoint(timeInDouble, shark));
                dataRows.Add(row);
            }
        }

        model.Series.Add(series);

        var plotInfo = new PlotInfo { Model = model };
        plotInfo.SeriesDataRows[series] = dataRows;
        return plotInfo;
    }

    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 maxRange = Math.Max(maxX - minX, maxY - minY);
            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 = yAxis.MajorStep = maxRange / 10;
        }
        catch (Exception ex)
        {
            MessageBox.Show($"Ошибка при установке масштаба осей: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }

    private void ChartSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        UpdatePlots();
    }

    private void UpdatePlots()
    {
        PlotGrid.Children.Clear();
        currentPlotInfos.Clear();
        selectedPointAnnotations.Clear();
        selectedPoints.Clear();

        var selectedTitles = ChartSelector.SelectedItems.Cast<ListBoxItem>().Select(item => item.Content.ToString()).ToList();

        if (selectedTitles.Count == 0)
            return;

        int columns = (int)Math.Ceiling(Math.Sqrt(selectedTitles.Count));
        int rows = (int)Math.Ceiling((double)selectedTitles.Count / columns);

        PlotGrid.Rows = rows;
        PlotGrid.Columns = columns;

        foreach (var title in selectedTitles)
        {
            if (plotModelFactories.TryGetValue(title, out var factory))
            {
                var plotInfo = factory();
                currentPlotInfos.Add(plotInfo);

                var plotView = new PlotView
                {
                    Model = plotInfo.Model,
                    Margin = new Thickness(5),
                    Focusable = true
                };

                var controller = new PlotController();

                if (EditModeToggleButton.IsChecked == true)
                {
                    controller.UnbindAll();
                }

                plotView.Controller = controller;

                plotView.MouseDown += PlotView_MouseDown;
                plotView.MouseMove += PlotView_MouseMove;
                plotView.MouseUp += PlotView_MouseUp;
                plotView.KeyDown += PlotView_KeyDown;

                selectedPointAnnotations[plotInfo.Model] = new List<PointAnnotation>();

                PlotGrid.Children.Add(plotView);
            }
        }
    }

    private void PlotView_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (sender is not PlotView plotView)
            return;

        plotView.Focus();

        if (EditModeToggleButton.IsChecked == true && e.LeftButton == MouseButtonState.Pressed)
        {
            var mousePos = e.GetPosition(plotView);
            var xAxis = plotView.Model.Axes.FirstOrDefault(a => a.IsHorizontal());
            var yAxis = plotView.Model.Axes.FirstOrDefault(a => a.IsVertical());

            if (xAxis == null || yAxis == null)
                return;

            // Преобразуем экранные координаты в данные
            double xData = xAxis.InverseTransform(mousePos.X);
            double yData = yAxis.InverseTransform(mousePos.Y);
            selectionStartPoint = new ScreenPoint(xData, yData);

            selectionRectangle = new RectangleAnnotation
            {
                Stroke = OxyColors.Red,
                StrokeThickness = 2,
                Fill = OxyColor.FromAColor(60, OxyColors.Orange),
                Layer = AnnotationLayer.AboveSeries,
                MinimumX = xData,
                MaximumX = xData,
                MinimumY = yData,
                MaximumY = yData
            };
            plotView.Model.Annotations.Add(selectionRectangle);

            plotView.CaptureMouse();
            e.Handled = true;
        }
    }

    private void PlotView_MouseMove(object sender, MouseEventArgs e)
    {
        if (sender is not PlotView plotView || selectionStartPoint == null || selectionRectangle == null)
            return;

        if (EditModeToggleButton.IsChecked != true)
            return;

        var mousePos = e.GetPosition(plotView);
        var xAxis = plotView.Model.Axes.FirstOrDefault(a => a.IsHorizontal());
        var yAxis = plotView.Model.Axes.FirstOrDefault(a => a.IsVertical());

        if (xAxis == null || yAxis == null)
            return;

        // Преобразуем текущие экранные координаты в данные
        double currentX = xAxis.InverseTransform(mousePos.X);
        double currentY = yAxis.InverseTransform(mousePos.Y);

        selectionRectangle.MinimumX = Math.Min(selectionStartPoint.Value.X, currentX);
        selectionRectangle.MaximumX = Math.Max(selectionStartPoint.Value.X, currentX);
        selectionRectangle.MinimumY = Math.Min(selectionStartPoint.Value.Y, currentY);
        selectionRectangle.MaximumY = Math.Max(selectionStartPoint.Value.Y, currentY);

        plotView.Model.InvalidatePlot(false);
        e.Handled = true;
    }

    private void PlotView_MouseUp(object sender, MouseButtonEventArgs e)
    {
        if (sender is not PlotView plotView || selectionStartPoint == null || selectionRectangle == null)
            return;

        if (EditModeToggleButton.IsChecked == true && e.LeftButton == MouseButtonState.Released)
        {
            var mousePos = e.GetPosition(plotView);
            var xAxis = plotView.Model.Axes.FirstOrDefault(a => a.IsHorizontal());
            var yAxis = plotView.Model.Axes.FirstOrDefault(a => a.IsVertical());

            if (xAxis != null && yAxis != null)
            {
                double xCurrent = xAxis.InverseTransform(mousePos.X);
                double yCurrent = yAxis.InverseTransform(mousePos.Y);

                double minX = Math.Min(selectionStartPoint.Value.X, xCurrent);
                double maxX = Math.Max(selectionStartPoint.Value.X, xCurrent);
                double minY = Math.Min(selectionStartPoint.Value.Y, yCurrent);
                double maxY = Math.Max(selectionStartPoint.Value.Y, yCurrent);

                selectedPoints.Clear();

                var annotations = selectedPointAnnotations[plotView.Model];
                foreach (var annotation in annotations)
                {
                    plotView.Model.Annotations.Remove(annotation);
                }
                annotations.Clear();

                var plotInfo = currentPlotInfos.FirstOrDefault(pi => pi.Model == plotView.Model);
                if (plotInfo != null)
                {
                    foreach (var series in plotView.Model.Series.OfType<ScatterSeries>())
                    {
                        var dataRows = plotInfo.SeriesDataRows[series];

                        for (int i = 0; i < series.Points.Count; i++)
                        {
                            var point = series.Points[i];
                            if (point.X >= minX && point.X <= maxX && point.Y >= minY && point.Y <= maxY)
                            {
                                selectedPoints.Add((series, point, i, dataRows[i], dataRows));

                                var pointAnnotation = new PointAnnotation
                                {
                                    X = point.X,
                                    Y = point.Y,
                                    Shape = MarkerType.Circle,
                                    Fill = OxyColors.Red,
                                    Stroke = OxyColors.Transparent,
                                    Size = 7
                                };
                                plotView.Model.Annotations.Add(pointAnnotation);
                                annotations.Add(pointAnnotation);
                            }
                        }
                    }
                }

                plotView.Model.Annotations.Remove(selectionRectangle);
                selectionRectangle = null;
                selectionStartPoint = null;

                plotView.ReleaseMouseCapture();
                plotView.Model.InvalidatePlot(false);
                e.Handled = true;
            }
        }
    }


    private void PlotView_KeyDown(object sender, KeyEventArgs e)
    {
        if (EditModeToggleButton.IsChecked == true && e.Key == Key.Delete && selectedPoints.Any())
        {
            DeleteSelectedPoints();
            e.Handled = true;
        }
    }

    private void DeleteSelectedPoints()
    {
        if (selectedPoints.Any())
        {
            var result = MessageBox.Show($"Вы уверены, что хотите удалить выбранные точки ({selectedPoints.Count})?", "Подтверждение удаления", MessageBoxButton.YesNo, MessageBoxImage.Question);
            if (result == MessageBoxResult.Yes)
            {
                var seriesGroups = selectedPoints.GroupBy(sp => sp.Series);

                foreach (var group in seriesGroups)
                {
                    var scatterSeries = group.Key;
                    var pointsToRemove = group.OrderByDescending(sp => sp.Index).ToList();

                    foreach (var sp in pointsToRemove)
                    {
                        sp.DataRow.Delete();
                        scatterSeries.Points.RemoveAt(sp.Index);
                        sp.DataRows.RemoveAt(sp.Index);
                    }

                    var yAxis = scatterSeries.PlotModel.Axes.FirstOrDefault(a => a.IsVertical());
                    if (yAxis != null)
                    {
                        yAxis.Minimum = Double.NaN;
                        yAxis.Maximum = Double.NaN;
                    }

                    undoStack.Push(new MultiPointGraphAction(pointsToRemove));
                    redoStack.Clear();

                    scatterSeries.PlotModel.InvalidatePlot(true);
                }

                foreach (var kvp in selectedPointAnnotations)
                {
                    var model = kvp.Key;
                    var annotations = kvp.Value;
                    foreach (var annotation in annotations)
                    {
                        model.Annotations.Remove(annotation);
                    }
                    annotations.Clear();
                }

                UpdateUndoRedoButtons();
            }

            selectedPoints.Clear();
        }
    }

    private void UndoButton_Click(object sender, RoutedEventArgs e)
    {
        if (undoStack.Any())
        {
            var action = undoStack.Pop();
            action.Undo();

            redoStack.Push(action);

            if (action is GraphAction graphAction)
            {
                graphAction.Series.PlotModel.InvalidatePlot(true);
            }
            else if (action is MultiPointGraphAction multiPointAction)
            {
                multiPointAction.InvalidatePlots();
            }

            UpdateUndoRedoButtons();
        }
    }

    private void RedoButton_Click(object sender, RoutedEventArgs e)
    {
        if (redoStack.Any())
        {
            var action = redoStack.Pop();
            action.Redo();

            undoStack.Push(action);

            if (action is GraphAction graphAction)
            {
                graphAction.Series.PlotModel.InvalidatePlot(true);
            }
            else if (action is MultiPointGraphAction multiPointAction)
            {
                multiPointAction.InvalidatePlots();
            }

            UpdateUndoRedoButtons();
        }
    }

    private void SaveButton_Click(object sender, RoutedEventArgs e)
    {
        var result = MessageBox.Show("Вы уверены, что хотите сохранить изменения?", "Подтверждение сохранения", MessageBoxButton.YesNo, MessageBoxImage.Question);
        if (result == MessageBoxResult.Yes)
        {
            txtDataCopy.AcceptChanges();
            magneteDataCopy.AcceptChanges();
            DialogResult = true;
        }
    }

    private void CancelButton_Click(object sender, RoutedEventArgs e)
    {
        var result = MessageBox.Show("Вы уверены, что хотите отменить все изменения?", "Подтверждение отмены", MessageBoxButton.YesNo, MessageBoxImage.Question);
        if (result == MessageBoxResult.Yes)
        {
            txtDataCopy.RejectChanges();
            magneteDataCopy.RejectChanges();
            DialogResult = false;
        }
    }

    private void ResetZoomMenuItem_Click(object sender, RoutedEventArgs e)
    {
        foreach (var plotView in PlotGrid.Children.OfType<PlotView>())
        {
            if (plotView.Model != null)
            {
                foreach (var axis in plotView.Model.Axes)
                {
                    axis.Reset();
                }
                plotView.Model.InvalidatePlot(false);
            }
        }
    }

    private void ResetYZoomMenuItem_Click(object sender, RoutedEventArgs e)
    {
        foreach (var plotView in PlotGrid.Children.OfType<PlotView>())
        {
            if (plotView.Model != null)
            {
                foreach (var axis in plotView.Model.Axes.Where(a => a.IsVertical()))
                {
                    axis.Reset();
                }
                plotView.Model.InvalidatePlot(false);
            }
        }
    }

    private void ResetXZoomMenuItem_Click(object sender, RoutedEventArgs e)
    {
        foreach (var plotView in PlotGrid.Children.OfType<PlotView>())
        {
            if (plotView.Model != null)
            {
                foreach (var axis in plotView.Model.Axes.Where(a => a.IsHorizontal()))
                {
                    axis.Reset();
                }
                plotView.Model.InvalidatePlot(false);
            }
        }
    }

    private void UpdateUndoRedoButtons()
    {
        UndoButton.IsEnabled = undoStack.Any();
        RedoButton.IsEnabled = redoStack.Any();
    }

    private void EditModeToggleButton_Checked(object sender, RoutedEventArgs e)
    {
        UpdatePlots();
    }

    private void EditModeToggleButton_Unchecked(object sender, RoutedEventArgs e)
    {
        UpdatePlots();
    }

    // Интерфейс для действий Undo/Redo
    private interface IUndoRedoAction
    {
        void Undo();
        void Redo();
    }

    // Класс для действий с множественными точками
    private class MultiPointGraphAction : IUndoRedoAction
    {
        private readonly List<(ScatterSeries Series, ScatterPoint Point, int Index, DataRow DataRow, List<DataRow> DataRows)> points;

        public MultiPointGraphAction(List<(ScatterSeries Series, ScatterPoint Point, int Index, DataRow DataRow, List<DataRow> DataRows)> points)
        {
            this.points = points;
        }

        public void Undo()
        {
            var grouped = points.GroupBy(p => p.Series);
            foreach (var group in grouped)
            {
                var series = group.Key;

                foreach (var sp in group.OrderBy(p => p.Index))
                {
                    if (sp.DataRow.RowState == DataRowState.Deleted)
                        sp.DataRow.RejectChanges();

                    series.Points.Insert(sp.Index, sp.Point);
                    sp.DataRows.Insert(sp.Index, sp.DataRow);
                }

                var yAxis = series.PlotModel.Axes.FirstOrDefault(a => a.IsVertical());
                if (yAxis != null)
                {
                    yAxis.Minimum = Double.NaN;
                    yAxis.Maximum = Double.NaN;
                }

                series.PlotModel.InvalidatePlot(true);
            }
        }

        public void Redo()
        {
            var grouped = points.GroupBy(p => p.Series);
            foreach (var group in grouped)
            {
                var series = group.Key;

                foreach (var sp in group.OrderByDescending(p => p.Index))
                {
                    sp.DataRow.Delete();
                    series.Points.RemoveAt(sp.Index);
                    sp.DataRows.RemoveAt(sp.Index);
                }

                var yAxis = series.PlotModel.Axes.FirstOrDefault(a => a.IsVertical());
                if (yAxis != null)
                {
                    yAxis.Minimum = Double.NaN;
                    yAxis.Maximum = Double.NaN;
                }

                series.PlotModel.InvalidatePlot(true);
            }
        }

        public void InvalidatePlots()
        {
            var seriesSet = points.Select(p => p.Series).Distinct();
            foreach (var series in seriesSet)
            {
                series.PlotModel.InvalidatePlot(true);
            }
        }
    }
}

}

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

namespace TxtProcessorApp
{
///

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

public class GraphAction : IUndoRedoAction
{
public ScatterSeries Series { get; }
public ScatterPoint Point { get; }
public int PointIndex { get; }
public DataRow DataRow { get; }

    public GraphAction(ScatterSeries series, ScatterPoint point, int pointIndex, DataRow dataRow)
    {
        Series = series;
        Point = point;
        PointIndex = pointIndex;
        DataRow = dataRow;
    }

    public void Undo()
    {
        // Восстанавливаем строку данных, если она была удалена
        if (DataRow.RowState == DataRowState.Deleted)
            DataRow.RejectChanges();

        // Вставляем точку обратно в серию
        Series.Points.Insert(PointIndex, Point);

        // Обновляем оси для перерасчета их масштабов

    }

    public void Redo()
    {
        // Удаляем точку из серии
        Series.Points.RemoveAt(PointIndex);

        // Удаляем строку данных
        if (DataRow.RowState != DataRowState.Deleted)
            DataRow.Delete();

        // Обновляем оси для перерасчета их масштабов

    }

    private void ResetAxisLimits(ScatterSeries series)
    {
        var yAxis = series.PlotModel.Axes.FirstOrDefault(a => a.IsVertical());
        if (yAxis != null)
        {
            yAxis.Minimum = double.NaN;
            yAxis.Maximum = double.NaN;
        }

        series.PlotModel.InvalidatePlot(true);
    }
}

}

// UndoRedoManager.cs
using System.Collections.Generic;

namespace TxtProcessorApp
{
///

/// Класс для управления операциями Undo и Redo.
///

public class UndoRedoManager
{
private readonly Stack undoStack = new();
private readonly Stack redoStack = new();
private readonly int maxStackSize;

    public UndoRedoManager(int maxStackSize = 100)
    {
        this.maxStackSize = maxStackSize;
    }

    public void ExecuteAction(IUndoRedoAction action)
    {
        undoStack.Push(action);
        if (undoStack.Count > maxStackSize)
        {
            var tempStack = new Stack<IUndoRedoAction>(undoStack.Reverse());
            tempStack.Pop(); // Удаляем самое старое действие
            undoStack.Clear();
            foreach (var act in tempStack)
                undoStack.Push(act);
        }
        redoStack.Clear();
    }

    public void Undo()
    {
        if (CanUndo)
        {
            var action = undoStack.Pop();
            action.Undo();
            redoStack.Push(action);
        }
    }

    public void Redo()
    {
        if (CanRedo)
        {
            var action = redoStack.Pop();
            action.Redo();
            undoStack.Push(action);
        }
    }

    public bool CanUndo => undoStack.Count > 0;
    public bool CanRedo => redoStack.Count > 0;

    public void Clear()
    {
        undoStack.Clear();
        redoStack.Clear();
    }
}

/// <summary>
/// Интерфейс для действий, поддерживающих Undo и Redo.
/// </summary>
public interface IUndoRedoAction
{
    void Undo();
    void Redo();
}

}

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

namespace TxtProcessorApp
{
///

/// Класс для хранения данных проекта.
///

public class ProjectData
{
public Dictionary<string, List<string[]>> TxtFileData { get; set; } = new();
public Dictionary<string, List<string[]>> MagneteFileData { get; set; } = new();
public DataTable CombinedTxtDataTable { get; set; } = new();
public DataTable CombinedMagneteDataTable { get; set; } = new();
}
}


<Window.Resources>


</Window.Resources>









        <!-- Пункт меню "Загрузить" -->
        <MenuItem Header="_Загрузить">
            <MenuItem Header="Загрузить .txt файлы" Click="LoadFiles_Click" Tag="txt" />
            <MenuItem Header="Загрузить .magnete файлы" Click="LoadFiles_Click" Tag="magnete" />
        </MenuItem>

        <!-- Пункт меню "Операции с файлами" -->
        <MenuItem Header="_Операции с файлами">
            <MenuItem Header="Сформировать файл" Click="CombineFiles_Click" />
            <MenuItem Header="Редактировать и Экспортировать" Click="EditAndExport_Click" IsEnabled="False" x:Name="EditExportButton" />
        </MenuItem>

        <!-- Пункт меню "Графики" -->
        <MenuItem Header="_Графики" Click="ShowGraphs_Click" />
    </Menu>

    <!-- Строка состояния -->
    <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.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="5"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

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

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

            <!-- Объединённые .magnete данные -->
            <GroupBox Header="Объединённые .magnete Файлы" Grid.Column="2" Margin="5,10,0,10">
                <DataGrid x:Name="CombinedMagneteDataGrid" 
                          AutoGenerateColumns="True" 
                          ItemsSource="{Binding CombinedMagneteDataTable.DefaultView}" 
                          Style="{StaticResource CommonDataGridStyle}"/>
            </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;

namespace TxtProcessorApp
{
///

/// Класс MainWindow.xaml.cs для приложения TxtProcessorApp
///

public partial class MainWindow : Window, INotifyPropertyChanged
{
// Реализация интерфейса INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;

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

    // Словари для хранения данных из файлов
    private Dictionary<string, List<string[]>> txtFileData = new Dictionary<string, List<string[]>>();
    private Dictionary<string, List<string[]>> magneteFileData = new Dictionary<string, List<string[]>>();

    // Поля для объединенных DataTable
    private DataTable combinedTxtDataTable = new DataTable();
    private DataTable combinedMagneteDataTable = new DataTable();

    // Свойства для привязки
    public DataTable CombinedTxtDataTable
    {
        get => combinedTxtDataTable;
        set
        {
            combinedTxtDataTable = value;
            OnPropertyChanged(nameof(CombinedTxtDataTable));
        }
    }

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

    /// <summary>
    /// Конструктор MainWindow
    /// </summary>
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = this; // Установка контекста данных

        // Инициализация столбцов для объединенных .txt данных
        InitializeCombinedTxtDataTable();

        // Инициализация столбцов для объединенных .magnete данных
        InitializeCombinedMagneteDataTable();
    }

    /// <summary>
    /// Инициализирует столбцы для объединенных .txt данных
    /// </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>
    /// Инициализирует столбцы для объединенных .magnete данных
    /// </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 Обработчики Загрузки Файлов

    /// <summary>
    /// Обработчик для загрузки .txt и .magnete файлов через меню
    /// </summary>
    private async void LoadFiles_Click(object sender, RoutedEventArgs e)
    {
        if (sender is MenuItem menuItem && menuItem.Tag is string fileType)
        {
            await LoadFilesAsync(fileType);
        }
    }

    /// <summary>
    /// Универсальный метод для загрузки файлов
    /// </summary>
    /// <param name="fileType">Тип файла: "txt" или "magnete"</param>
    private async Task LoadFilesAsync(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);
            }

            // Обновление соответствующего TabControl
            if (fileType == "txt")
            {
                PopulateTabControl(FilesTabControl, txtFileData, CreateTxtDataGrid);
                CombinedTxtDataTable.Clear(); // Сброс объединенных данных
            }
            else
            {
                PopulateTabControl(MagneteFilesTabControl, magneteFileData, CreateMagneteDataGrid);
                CombinedMagneteDataTable.Clear(); // Сброс объединенных данных
            }

            // Обновление состояния кнопки "Редактировать и Экспортировать"
            EditExportButton.IsEnabled = false;

            // Обновление строки состояния
            StatusTextBlock.Text = $"Загружено {(fileType == "txt" ? txtFileData.Count : magneteFileData.Count)} {(fileType == "txt" ? ".txt" : ".magnete")} файлов.";
        }
    }

    /// <summary>
    /// Асинхронный метод для обработки загруженного файла
    /// </summary>
    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(); // Пропустить первые 11 строк
                }

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

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

                    if (columns.Length == 0)
                        continue;

                    // Проверка первого столбца
                    if (columns[0].ToLower() == "nan" || string.IsNullOrWhiteSpace(columns[0]))
                        continue;

                    // Добавить столбцы в зависимости от типа файла
                    if (fileType == "txt")
                    {
                        // Для .txt пропустить первый столбец
                        processedLines.Add(columns.Skip(1).ToArray());
                    }
                    else
                    {
                        // Для .magnete добавить все столбцы
                        processedLines.Add(columns);
                    }
                }

                // Добавить обработанные данные в словарь
                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));
            }
        });
    }

    /// <summary>
    /// Генерация уникального имени файла
    /// </summary>
    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;
    }

    /// <summary>
    /// Метод для заполнения TabControl данными файлов
    /// </summary>
    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);
            }
        });
    }

    /// <summary>
    /// Создание DataGrid для .txt файлов
    /// </summary>
    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;
    }

    /// <summary>
    /// Создание DataGrid для .magnete файлов
    /// </summary>
    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;
    }

    /// <summary>
    /// Универсальный метод для преобразования данных в DataTable
    /// </summary>
    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
        {
            // Определение столбцов для .magnete, предполагая фиксированное количество столбцов
            dt.Columns.Add("Time", typeof(string));
            dt.Columns.Add("Широта WGS", typeof(string));
            dt.Columns.Add("Долгота WGS", typeof(string));
            dt.Columns.Add("Альтитуда", typeof(string));
            dt.Columns.Add("Shark", 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["Time"] = row.Length > 0 ? row[0] : string.Empty;
                dr["Широта WGS"] = row.Length > 1 ? row[1] : string.Empty;
                dr["Долгота WGS"] = row.Length > 2 ? row[2] : string.Empty;
                dr["Альтитуда"] = row.Length > 3 ? row[3] : string.Empty;
                dr["Shark"] = row.Length > 4 ? row[4] : string.Empty;
                // Заполните другие столбцы при необходимости
            }
            dt.Rows.Add(dr);
        }

        return dt;
    }

    #endregion

    #region Обработчики Объединения Файлов

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

        // Диалог для выбора типа файлов для объединения
        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);
            }
        }
    }

    /// <summary>
    /// Асинхронный метод для объединения .txt файлов
    /// </summary>
    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++;
                }
            }

            // Преобразование списка в существующую DataTable
            Dispatcher.Invoke(() =>
            {
                CombinedTxtDataTable.Clear(); // Очистка существующих данных
                foreach (var row in combinedData)
                {
                    CombinedTxtDataTable.Rows.Add(
                        int.Parse(row[0]),
                        row[1],
                        row[2],
                        row[3],
                        row[4]
                    );
                }

                // Включение кнопки "Редактировать и Экспортировать" при наличии данных
                EditExportButton.IsEnabled = CombinedTxtDataTable.Rows.Count > 0 || CombinedMagneteDataTable.Rows.Count > 0;

                // Обновление строки состояния
                StatusTextBlock.Text = $"Объединены .txt файлы. Строк: {CombinedTxtDataTable.Rows.Count}.";
            });
        });
    }

    /// <summary>
    /// Асинхронный метод для объединения .magnete файлов
    /// </summary>
    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);
            int sequentialNumber = 1;
            List<string> errorMessages = new List<string>();
            List<DataRow> newRows = new List<DataRow>();

            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='{latitude}') в строке: {string.Join("\t", row)}. Текст ошибки: {ex.Message}.";
                            errorMessages.Add(error);
                            LogCalculation(error);
                        }
                    }

                    // Обработка столбца "Stan"
                    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");
                    }

                    newRows.Add(newRow);
                    sequentialNumber++;

                    // Логирование расчетов для каждой строки
                    string logMessage = $"TIME: '{stanValue}', ComputedDate: '{newRow["DATA"]}', ComputedTime: '{newRow["TIME UTC"]}'";
                    LogCalculation(logMessage);
                }
            }

            Dispatcher.Invoke(() =>
            {
                // Добавляем все новые строки в DataTable на UI-потоке
                foreach (var newRow in newRows)
                {
                    CombinedMagneteDataTable.Rows.Add(newRow);
                }

                // Включение кнопки "Редактировать и Экспортировать" при наличии данных
                EditExportButton.IsEnabled = CombinedTxtDataTable.Rows.Count > 0 || CombinedMagneteDataTable.Rows.Count > 0;

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

    #endregion

    #region Обработчики Редактирования и Экспорта

    /// <summary>
    /// Обработчик для редактирования и экспорта через меню
    /// </summary>
    private void EditAndExport_Click(object sender, RoutedEventArgs e)
    {
        if ((CombinedTxtDataTable == null || CombinedTxtDataTable.Rows.Count == 0) &&
            (CombinedMagneteDataTable == null || CombinedMagneteDataTable.Rows.Count == 0))
        {
            MessageBox.Show("Нет объединённых данных для редактирования.", "Информация", MessageBoxButton.OK, MessageBoxImage.Information);
            return;
        }

        // Создание и открытие диалогового окна для выбора типа данных для редактирования
        SelectEditTypeWindow selectWindow = new SelectEditTypeWindow("Выберите данные для редактирования:");
        selectWindow.Owner = this;

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

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

    /// <summary>
    /// Метод для открытия окна редактирования
    /// </summary>
    private void OpenEditWindow(DataTable dataTable, string windowTitle)
    {
        EditWindow editWindow = new EditWindow(dataTable)
        {
            Title = windowTitle,
            Owner = this
        };
        editWindow.ShowDialog();

        // После редактирования данные уже изменены в DataTable и отобразятся в DataGrid благодаря привязке
    }

    #endregion

    #region Обработчики Сохранения и Загрузки Проекта

    /// <summary>
    /// Обработчик для сохранения проекта через меню
    /// </summary>
    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
            {
                // Асинхронная сериализация объекта в 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);
            }
        }
    }

    /// <summary>
    /// Обработчик для загрузки проекта через меню
    /// </summary>
    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)
                {
                    // Загрузка данных .txt
                    this.txtFileData = project.TxtFileData ?? new Dictionary<string, List<string[]>>();
                    PopulateTabControl(FilesTabControl, txtFileData, CreateTxtDataGrid);

                    // Загрузка данных .magnete
                    this.magneteFileData = project.MagneteFileData ?? new Dictionary<string, List<string[]>>();
                    PopulateTabControl(MagneteFilesTabControl, magneteFileData, CreateMagneteDataGrid);

                    // Загрузка объединенных DataTable
                    CombinedTxtDataTable.Clear();
                    foreach (DataRow row in project.CombinedTxtDataTable.Rows)
                    {
                        CombinedTxtDataTable.ImportRow(row);
                    }

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

                    // Применяем AcceptChanges, чтобы очистить статус изменений
                    CombinedTxtDataTable.AcceptChanges();
                    CombinedMagneteDataTable.AcceptChanges();

                    // Обновляем состояние кнопки "Редактировать и Экспортировать"
                    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 Обработчики Для Графиков

    /// <summary>
    /// Обработчик для отображения графиков через меню
    /// </summary>
    private void ShowGraphs_Click(object sender, RoutedEventArgs e)
    {
        if ((CombinedTxtDataTable == null || CombinedTxtDataTable.Rows.Count == 0) &&
            (CombinedMagneteDataTable == null || CombinedMagneteDataTable.Rows.Count == 0))
        {
            MessageBox.Show("Нет объединённых данных для построения графиков.", "Информация", MessageBoxButton.OK, MessageBoxImage.Information);
            return;
        }

        // Создаем копии DataTable для передачи в GraphWindow
        DataTable txtDataCopy = CombinedTxtDataTable.Copy();
        DataTable magneteDataCopy = CombinedMagneteDataTable.Copy();

        GraphWindow graphWindow = new GraphWindow(txtDataCopy, magneteDataCopy)
        {
            Owner = this
        };
        bool? dialogResult = graphWindow.ShowDialog();

        if (dialogResult == true)
        {
            // Пользователь нажал "Сохранить", применяем изменения
            // Обновляем Combined DataTables с копиями из GraphWindow
            CombinedTxtDataTable = graphWindow.TxtDataCopy;
            CombinedMagneteDataTable = graphWindow.MagneteDataCopy;

            // Применяем изменения
            CombinedTxtDataTable.AcceptChanges();
            CombinedMagneteDataTable.AcceptChanges();

            // Обновляем привязки
            OnPropertyChanged(nameof(CombinedTxtDataTable));
            OnPropertyChanged(nameof(CombinedMagneteDataTable));

            // Обновление строки состояния
            StatusTextBlock.Text = $"Данные графиков обновлены после сохранения изменений.";
        }
        else
        {
            // Пользователь отменил изменения
            StatusTextBlock.Text = $"Изменения на графиках были отменены.";
        }
    }

    #endregion

    #region Обработчики Меню

    /// <summary>
    /// Обработчик для выхода из приложения через меню
    /// </summary>
    private void Exit_Click(object sender, RoutedEventArgs e)
    {
        this.Close();
    }

    /// <summary>
    /// Обработчик для пункта "О программе" через меню
    /// </summary>
    private void About_Click(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Txt Processor App\nВерсия 1.0\n© 2023 Ваша Компания", "О программе", MessageBoxButton.OK, MessageBoxImage.Information);
    }

    #endregion

    #region Модели Данных

    /// <summary>
    /// Класс для хранения данных проекта
    /// </summary>
    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; }
    }

    #endregion

    #region Helper Methods для Преобразований Координат

    /// <summary>
    /// Метод для логирования расчетов и ошибок
    /// </summary>
    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));
        }
    }

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

    /// <summary>
    /// Метод для преобразования координат WGS84 в UTM
    /// </summary>
    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 Новый Обработчик для "Новый проект"

    /// <summary>
    /// Обработчик для создания нового пустого проекта
    /// </summary>
    private void NewProject_Click(object sender, RoutedEventArgs e)
    {
        var result = MessageBox.Show("Вы уверены, что хотите создать новый проект? Все несохраненные данные будут потеряны.", "Новый проект", MessageBoxButton.YesNo, MessageBoxImage.Question);
        if (result == MessageBoxResult.Yes)
        {
            // Очистка словарей с данными файлов
            txtFileData.Clear();
            magneteFileData.Clear();

            // Очистка TabControl для .txt файлов
            FilesTabControl.Items.Clear();

            // Очистка TabControl для .magnete файлов
            MagneteFilesTabControl.Items.Clear();

            // Очистка объединенных DataTable
            CombinedTxtDataTable.Clear();
            CombinedMagneteDataTable.Clear();

            // Сброс AcceptChanges
            CombinedTxtDataTable.AcceptChanges();
            CombinedMagneteDataTable.AcceptChanges();

            // Отключение кнопки "Редактировать и Экспортировать"
            EditExportButton.IsEnabled = false;

            // Обновление строки состояния
            StatusTextBlock.Text = "Создан новый проект.";
        }
    }

    #endregion
}

}

<!-- EditWindow.xaml -->
<Window x:Class="TxtProcessorApp.EditWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Редактирование данных" Height="600" Width="900">
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </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">
            <!-- Кнопки Undo и Redo -->
            <Button x:Name="UndoButton" Content="Отменить" Width="100" Height="30" Margin="0,0,10,0" Click="UndoButton_Click" IsEnabled="False"/>
            <Button x:Name="RedoButton" Content="Повторить" Width="100" Height="30" Margin="0,0,10,0" Click="RedoButton_Click" IsEnabled="False"/>

            <!-- Кнопка Применить -->
            <Button x:Name="ApplyButton" Content="Применить" Width="100" Height="30" Margin="0,0,10,0" Click="ApplyButton_Click"/>
            <Button x:Name="CancelButton" Content="Отмена" Width="100" Height="30" Margin="0,0,10,0" Click="CancelButton_Click"/>

            <!-- Элемент управления для выбора разделителя -->
            <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>
</Window>


// 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 System.Collections.Generic;
using System.Windows.Data;
using System.Threading.Tasks;

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

        // Стек для операций Undo/Redo
        private readonly Stack<IUndoRedoAction> undoStack = new();
        private readonly Stack<IUndoRedoAction> redoStack = new();

        public EditWindow(DataTable dataTable)
        {
            InitializeComponent();

            // Создаём копию исходного DataTable
            _originalDataTable = dataTable;
            _dataTableCopy = dataTable.Copy();

            EditDataGrid.ItemsSource = _dataTableCopy.DefaultView;

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

            // Подписываемся на события изменений данных
            _dataTableCopy.RowChanged += DataTable_RowChanged;
            _dataTableCopy.RowDeleting += DataTable_RowDeleting; // Используем RowDeleting вместо RowDeleted
            _dataTableCopy.ColumnChanged += DataTable_ColumnChanged;

            UpdateUndoRedoButtons();
        }

        #region Обработчики изменений данных

        private void DataTable_RowChanged(object sender, DataRowChangeEventArgs e)
        {
            if (e.Action == DataRowAction.Add)
            {
                var action = new DataRowUndoRedoAction(e.Row, DataRowAction.Add, _dataTableCopy);
                undoStack.Push(action);
                redoStack.Clear();
                UpdateUndoRedoButtons();
            }
        }

        private void DataTable_RowDeleting(object sender, DataRowChangeEventArgs e)
        {
            var action = new DataRowUndoRedoAction(e.Row, DataRowAction.Delete, _dataTableCopy);
            undoStack.Push(action);
            redoStack.Clear();
            UpdateUndoRedoButtons();
        }

        private void DataTable_ColumnChanged(object sender, DataColumnChangeEventArgs e)
        {
            object oldValue;

            if (e.Row.HasVersion(DataRowVersion.Original))
            {
                oldValue = e.Row[e.Column, DataRowVersion.Original];
            }
            else if (e.Row.HasVersion(DataRowVersion.Current))
            {
                oldValue = e.Row[e.Column, DataRowVersion.Current];
            }
            else
            {
                oldValue = null; // Or set a default value if appropriate
            }

            var action = new DataCellChangeAction(e.Row, e.Column, oldValue, e.ProposedValue);
            undoStack.Push(action);
            redoStack.Clear();
            UpdateUndoRedoButtons();
        }


        #endregion

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

        // Вставка строки выше выбранной
        private void InsertRowAbove_Click(object sender, RoutedEventArgs e)
        {
            int rowIndex = GetSelectedRowIndex();
            if (rowIndex != -1)
            {
                var newRow = _dataTableCopy.NewRow();
                _dataTableCopy.Rows.InsertAt(newRow, rowIndex);
                var action = new DataRowUndoRedoAction(newRow, DataRowAction.Add, _dataTableCopy);
                undoStack.Push(action);
                redoStack.Clear();
                UpdateUndoRedoButtons();
            }
            else
            {
                var newRow = _dataTableCopy.NewRow();
                _dataTableCopy.Rows.Add(newRow);
                var action = new DataRowUndoRedoAction(newRow, DataRowAction.Add, _dataTableCopy);
                undoStack.Push(action);
                redoStack.Clear();
                UpdateUndoRedoButtons();
            }
            RefreshDataGrid();
        }

        // Вставка строки ниже выбранной
        private void InsertRowBelow_Click(object sender, RoutedEventArgs e)
        {
            int rowIndex = GetSelectedRowIndex();
            if (rowIndex != -1)
            {
                var newRow = _dataTableCopy.NewRow();
                _dataTableCopy.Rows.InsertAt(newRow, rowIndex + 1);
                var action = new DataRowUndoRedoAction(newRow, DataRowAction.Add, _dataTableCopy);
                undoStack.Push(action);
                redoStack.Clear();
                UpdateUndoRedoButtons();
            }
            else
            {
                var newRow = _dataTableCopy.NewRow();
                _dataTableCopy.Rows.Add(newRow);
                var action = new DataRowUndoRedoAction(newRow, DataRowAction.Add, _dataTableCopy);
                undoStack.Push(action);
                redoStack.Clear();
                UpdateUndoRedoButtons();
            }
            RefreshDataGrid();
        }

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

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

        private void InsertNewColumn(int index)
        {
            string columnName = GenerateUniqueColumnName("NewColumn");
            DataColumn newColumn = new DataColumn(columnName, typeof(string));
            _dataTableCopy.Columns.Add(newColumn);
            newColumn.SetOrdinal(Math.Min(index, _dataTableCopy.Columns.Count - 1));

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

            var action = new DataColumnAction(newColumn, DataColumnActionType.Add, _dataTableCopy);
            undoStack.Push(action);
            redoStack.Clear();
            UpdateUndoRedoButtons();
        }

        private string GenerateUniqueColumnName(string baseName)
        {
            int suffix = 1;
            string newName = baseName;
            while (_dataTableCopy.Columns.Contains(newName))
            {
                newName = $"{baseName}_{suffix}";
                suffix++;
            }
            return newName;
        }

        private int GetSelectedRowIndex()
        {
            if (EditDataGrid.SelectedItem is DataRowView selectedRow)
            {
                return _dataTableCopy.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

        private void RefreshDataGrid()
        {
            var view = CollectionViewSource.GetDefaultView(EditDataGrid.ItemsSource);
            if (view != null)
            {
                view.Refresh();
            }
        }

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

        #region Экспорт данных

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

        private async Task ExportWithDelimiterAsync(StreamWriter sw, string delimiter)
        {
            string[] headers = _dataTableCopy.Columns
                .Cast<DataColumn>()
                .Select(column => $"\"{column.ColumnName}\"")
                .ToArray();
            await sw.WriteLineAsync(string.Join(delimiter, headers));

            foreach (DataRow row in _dataTableCopy.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));
            }
        }

        private string GetSelectedDelimiter()
        {
            switch (DelimiterComboBox.SelectedIndex)
            {
                case 0:
                    return " "; // Пробел
                case 1:
                    return "\t"; // Табуляция
                case 2:
                    return ","; // Запятая
                default:
                    return "\t"; // По умолчанию табуляция
            }
        }

        #endregion

        #region Undo/Redo Functionality

        private void UndoButton_Click(object sender, RoutedEventArgs e)
        {
            if (undoStack.Any())
            {
                var action = undoStack.Pop();
                action.Undo();
                redoStack.Push(action);
                UpdateUndoRedoButtons();
                RefreshDataGrid();
            }
        }


        private void RedoButton_Click(object sender, RoutedEventArgs e)
        {
            if (redoStack.Any())
            {
                var action = redoStack.Pop();
                action.Redo();
                undoStack.Push(action);
                UpdateUndoRedoButtons();
            }
        }

        private void UpdateUndoRedoButtons()
        {
            UndoButton.IsEnabled = undoStack.Any();
            RedoButton.IsEnabled = redoStack.Any();
        }

        #endregion

        #region Кнопки Применить и Отмена

        private void ApplyButton_Click(object sender, RoutedEventArgs e)
        {
            // Применяем изменения к исходному DataTable
            _originalDataTable.Clear();
            foreach (DataRow row in _dataTableCopy.Rows)
            {
                _originalDataTable.ImportRow(row);
            }
            _originalDataTable.AcceptChanges();

            // Закрываем окно после применения изменений
            this.DialogResult = true;
            this.Close();
        }

        private void CancelButton_Click(object sender, RoutedEventArgs e)
        {
            // Закрываем окно без сохранения изменений
            this.DialogResult = false;
            this.Close();
        }

        #endregion

        #region Undo/Redo Actions

        private interface IUndoRedoAction
        {
            void Undo();
            void Redo();
        }

        private class DataRowUndoRedoAction : IUndoRedoAction
        {
            private readonly DataTable _table;
            private readonly DataRowAction _actionType;
            private readonly object[] _data;
            private readonly int _rowIndex;

            public DataRowUndoRedoAction(DataRow row, DataRowAction actionType, DataTable table)
            {
                _table = table;
                _actionType = actionType;
                _data = row.ItemArray.Clone() as object[];
                _rowIndex = _table.Rows.IndexOf(row);
            }

            public void Undo()
            {
                if (_actionType == DataRowAction.Add)
                {
                    // Undo adding the row: remove it
                    _table.Rows.RemoveAt(_rowIndex);
                }
                else if (_actionType == DataRowAction.Delete)
                {
                    // Undo deleting the row: insert it back
                    DataRow newRow = _table.NewRow();
                    newRow.ItemArray = _data;
                    _table.Rows.InsertAt(newRow, _rowIndex);
                }
            }

            public void Redo()
            {
                if (_actionType == DataRowAction.Add)
                {
                    // Redo adding the row: insert it back
                    DataRow newRow = _table.NewRow();
                    newRow.ItemArray = _data;
                    _table.Rows.InsertAt(newRow, _rowIndex);
                }
                else if (_actionType == DataRowAction.Delete)
                {
                    // Redo deleting the row: remove it
                    _table.Rows.RemoveAt(_rowIndex);
                }
            }
        }

        private class DataCellChangeAction : IUndoRedoAction
        {
            private readonly DataRow _row;
            private readonly DataColumn _column;
            private readonly object _oldValue;
            private readonly object _newValue;

            public DataCellChangeAction(DataRow row, DataColumn column, object oldValue, object newValue)
            {
                _row = row;
                _column = column;
                _oldValue = oldValue;
                _newValue = newValue;
            }

            public void Undo()
            {
                _row[_column] = _oldValue ?? DBNull.Value;
            }

            public void Redo()
            {
                _row[_column] = _newValue ?? DBNull.Value;
            }
        }

        private enum DataColumnActionType
        {
            Add,
            Remove
        }

        private class DataColumnAction : IUndoRedoAction
        {
            private readonly DataColumn _column;
            private readonly DataColumnActionType _actionType;
            private readonly DataTable _table;

            public DataColumnAction(DataColumn column, DataColumnActionType actionType, DataTable table)
            {
                _column = column;
                _actionType = actionType;
                _table = table;
            }

            public void Undo()
            {
                if (_actionType == DataColumnActionType.Add)
                {
                    _table.Columns.Remove(_column);
                }
                else if (_actionType == DataColumnActionType.Remove)
                {
                    _table.Columns.Add(_column);
                }
            }

            public void Redo()
            {
                if (_actionType == DataColumnActionType.Add)
                {
                    _table.Columns.Add(_column);
                }
                else if (_actionType == DataColumnActionType.Remove)
                {
                    _table.Columns.Remove(_column);
                }
            }
        }

        #endregion
    }
}


<!-- InputDialog.xaml -->
<Window x:Class="TxtProcessorApp.InputDialog"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Ввод" Height="150" Width="400" WindowStartupLocation="CenterOwner" ResizeMode="NoResize">
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </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>
</Window>

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

namespace TxtProcessorApp
{
    /// <summary>
    /// Логика взаимодействия для InputDialog.xaml
    /// </summary>
    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;
        }
    }
}

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