<|start_filename|>MSFS Kinetic Assistant/PlaneInfoResponse.cs<|end_filename|>
﻿using System;
using System.Runtime.InteropServices;

namespace MSFS_Kinetic_Assistant
{
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct PlaneInfoResponse
    {
        public double Latitude;
        public double Longitude;
        public double AltitudeAboveGround;
        public double Altitude;
        public double AbsoluteTime;
        public double PlaneHeading;
        public double PlanePitch;
        public double PlaneBank;
        public double GpsGroundSpeed;
        public double AirspeedIndicated;
        public double AirspeedTrue;
        public double VerticalSpeed;
        public double AmbientWindY;
        public double VelocityBodyX;
        public double VelocityBodyY;
        public double VelocityBodyZ;
        public double AccelerationBodyY;
        public double AccelerationBodyZ;
        public double RotationVelocityBodyX;
        public double RotationVelocityBodyY;
        public double RotationVelocityBodyZ;
        public double RotationAccelerationBodyX;
        public double RotationAccelerationBodyY;
        public double RotationAccelerationBodyZ;
        public double FoldingWindR;
    };

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct PlaneInfoCommit
    {
        public double VelocityBodyX;
        public double VelocityBodyY;
        public double VelocityBodyZ;
    };

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct PlaneInfoRotateVelocity
    {
        public double RotationVelocityBodyX;
        public double RotationVelocityBodyY;
        public double RotationVelocityBodyZ;
    };

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct PlaneInfoRotateAccel
    {
        public double RotationAccelerationBodyX;
        public double RotationAccelerationBodyY;
        public double RotationAccelerationBodyZ;
    };

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct PlaneAvionicsResponse
    {
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
        public String Title;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
        public String Type;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
        public String Model;
        public double StaticCGtoGround;
        public double SimOnGround;
        public double BrakeParkingPosition;
        public double OnAnyRunway;
        public double IsSlewActive;
        public double TotalWeight;
        public double SimRate;
        public double TailhookPosition;
        public double LaunchbarPosition;
        public double WaterRudderHandlePosition;
        public double Smoke;
        public double YokeYPosition;
        public double YokeXPosition;
        public double RudderPosition;
        public double LIGHTPANEL;
        public double LIGHTSTROBE;
        public double LIGHTLANDING;
        public double LIGHTTAXI;
        public double LIGHTBEACON;
        public double LIGHTNAV;
        public double LIGHTLOGO;
        public double LIGHTWING;
        public double LIGHTRECOGNITION;
        public double LIGHTCABIN;
        public double LIGHTGLARESHIELD;
        public double LIGHTPEDESTRAL;
        public double LIGHTPOTENTIOMETER;
    };

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct PlaneInfoLift
    {
        public double Altitude;
    };

    public struct ThermalVelocity
    {
        public double VelocityBodyY;
        public double VelocityBodyZ;
        public double RotationAccelerationBodyX;
        //public double FoldingWindR;

    };


    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct NearbyInfoResponse
    {
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
        public String Title;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
        public String Category;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8)]
        public String FlightNumber;

        public double Latitude;
        public double Longitude;
        public double Altitude;
        public double Airspeed;
        public double Verticalspeed;
        public double Heading;
        public double Pitch;
        public double Bank;
        public double SimOnGround;
        public double OnAnyRunway;
        //public double AmbientInCloud;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct TowInfoResponse
    {
        public double Latitude;
        public double Longitude;
        public double Altitude;
        public double Heading;
        public double Bank;
        public double VelocityBodyX;
        public double VelocityBodyY;
        public double VelocityBodyZ;
    };

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct TowInfoPitch
    {
        public double Heading;
        public double Pitch;
        public double Bank;
        public double VelocityBodyY;
        public double VelocityBodyZ;
    };

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct GhostCommit
    {
        public double VelocityBodyX;
        public double VelocityBodyY;
        public double VelocityBodyZ;
        //public double RotationVelocityBodyX;
        //public double RotationVelocityBodyY;
        //public double RotationVelocityBodyZ;
        public double planeHeading;
        public double planePitch;
        public double planeRoll;

    };

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct PlaneEngineData
    {
        public double TURBTHRUST1;
        public double TURBTHRUST2;
        public double TURBTHRUST3;
        public double TURBTHRUST4;
        public double ENGTORQUE1;
        public double ENGTORQUE2;
        public double ENGTORQUE3;
        public double ENGTORQUE4;
        public double ThrottleLeverPosition1;
        public double ThrottleLeverPosition2;
        public double ThrottleLeverPosition3;
        public double ThrottleLeverPosition4;
    };


    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct WeatherReport
    {
        public double AmbientAirTemperature;
        public double AmbientBarometerPressure;
        public double AmbientDensity;
        public double AmbientInCloud;
        public double AmbientPrecipState;
        public double AmbientPrecipRate;
        public double AmbientPressure;
        public double AmbientSeaLevelPressure;
        public double AmbientStandardAtmTemperature;
        public double AmbientTemperature;
        public double AmbientVisibility;
        public double AmbientWindDirection;
        public double AmbientWindVelocity;
        public double LocalDayOfYear;
        public double LocalTime;
        public double TimeOfDay;
    };

}


<|start_filename|>MSFS Kinetic Assistant/Requests.cs<|end_filename|>
﻿namespace MSFS_Kinetic_Assistant
{
    public enum Requests
    {
        PlaneInfo = 0,
        PlaneCommit = 1,
        PlaneRotationVelocity = 2,
        NearbyObjects = 3,
        TowPlane = 4,
        Airport = 5,
        SystemEvents = 6,
        TowPlaneCommit = 7,
        PlaneEngineData = 8,
        Paused = 9,
        Unpaused = 10,
        PositionChanged = 11,
        SimStart = 12,
        SimStop = 13,
        WeatherData = 14,
        PlaneAvionics = 15,
        PlaneLift = 16,
        GhostCommit = 17,
        PlaneRotateAccel = 18,
        ThermalVelocityCommit = 19,
        FoldingWindCommit = 20,
        RotationVelocityZCommit = 21,
    }

    public enum Definitions
    {
        PlaneInfo = 0,
        PlaneCommit = 1,
        PlaneRotationVelocity = 2,
        NearbyObjects = 3,
        TowPlane = 4,
        Airport = 5,
        SystemEvents = 6,
        TowPlaneCommit = 7,
        PlaneEngineData = 8,
        Paused = 9,
        Unpaused = 10,
        PositionChanged = 11,
        SimStart = 12,
        SimStop = 13,
        WeatherData = 14,
        PlaneAvionics = 15,
        PlaneLift = 16,
        GhostCommit = 17,
        PlaneRotateAccel = 18,
        ThermalVelocityCommit = 19,
        FoldingWindCommit = 20,
        RotationVelocityZCommit = 21,
    }
}


<|start_filename|>MSFS Kinetic Assistant/simVars.cs<|end_filename|>
﻿namespace MSFS_Kinetic_Assistant
{
    class SimVars
    {
        static public readonly string[] Names = new string[]
        {
            "- Control not set -",
            "LIGHT PANEL",
            "LIGHT STROBE",
            "LIGHT LANDING",
            "LIGHT TAXI",
            "LIGHT BEACON",
            "LIGHT NAV",
            "LIGHT LOGO",
            "LIGHT WING",
            "LIGHT RECOGNITION",
            "LIGHT CABIN",
            "LIGHT GLARESHIELD",
            "LIGHT PEDESTRAL",
            "LIGHT POTENTIOMETER",
        };
    }
}


<|start_filename|>MSFS Kinetic Assistant/RadarClass.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace MSFS_Kinetic_Assistant
{
    class RadarClass
    {
        public void InitRadar(Canvas RadarCanvas, double coverScale, double allowedRadarScale)
        {
            Canvas group = new Canvas();
            group.Width = 250;
            group.Height = 250;
            group.Name = "CompassRose";

            for (int i = 0; i < 36; i++)
            {
                Polyline polyline1 = new Polyline();
                polyline1.Points.Add(new Point(125, 0));
                polyline1.Points.Add(new Point(125, i % 9 == 0 ? 20 : 10));
                polyline1.Stroke = i == 0 ? Brushes.DarkRed : Brushes.Black;
                polyline1.StrokeThickness = i % 9 == 0 ? 2 : 1;

                RotateTransform rotateTransform = new RotateTransform(10 * i, 125, 125);
                polyline1.RenderTransform = rotateTransform;

                Canvas.SetLeft(polyline1, 0);
                Canvas.SetTop(polyline1, 0);
                group.Children.Add(polyline1);

                if (i % 9 == 0)
                {
                    TextBlock northLabel = new TextBlock();
                    northLabel.FontSize = 14;
                    northLabel.Margin = new Thickness(100, 20, 100, 0);
                    northLabel.Width = 50;
                    northLabel.TextAlignment = TextAlignment.Center;

                    switch (i)
                    {
                        case 0:
                            northLabel.Text = "N";
                            northLabel.Foreground = new SolidColorBrush(Colors.DarkRed);
                            break;
                        case 9:
                            northLabel.Text = "E";
                            northLabel.Foreground = new SolidColorBrush(Colors.Black);
                            break;
                        case 18:
                            northLabel.Text = "S";
                            northLabel.Foreground = new SolidColorBrush(Colors.Black);
                            break;
                        case 27:
                            northLabel.Text = "W";
                            northLabel.Foreground = new SolidColorBrush(Colors.Black);
                            break;
                    }

                    RotateTransform rotateTransform2 = new RotateTransform(10 * i, 25, 105);
                    northLabel.RenderTransform = rotateTransform2;
                    group.Children.Add(northLabel);
                }
            }

            Canvas.SetZIndex(group, 1001);
            RadarCanvas.Children.Add(group);

            Canvas group2 = new Canvas();
            group2.Width = 250;
            group2.Height = 250;
            group2.Name = "WindPointer";

            Polygon polygon = new Polygon();
            polygon.Points.Add(new Point(125, 20));
            polygon.Points.Add(new Point(145, 0));
            polygon.Points.Add(new Point(105, 0));
            polygon.Fill = Brushes.DarkBlue;

            Canvas.SetLeft(polygon, 0);
            Canvas.SetTop(polygon, 0);
            group2.Children.Add(polygon);

            TextBlock windForce = new TextBlock();
            windForce.Foreground = new SolidColorBrush(Colors.White);
            windForce.FontSize = 14;
            windForce.Margin = new Thickness(100, -2, 100, 0);
            windForce.Width = 50;
            windForce.Text = "0";
            windForce.TextAlignment = TextAlignment.Center;
            group2.Children.Add(windForce);
            Canvas.SetZIndex(group2, 1002);

            RadarCanvas.Children.Add(group2);

            Canvas group3 = new Canvas();
            group3.Width = 50;
            group3.Height = 30;
            group3.Name = "Plane";
            group3.Opacity = 0.5;
            group3.Background = new ImageBrush(new BitmapImage(new Uri("pack://application:,,,/media/gliderIcon.png")));

            Line dashV = new Line();
            Line dashH = new Line();
            dashV.Fill = dashH.Fill = new SolidColorBrush(Color.FromArgb(255, 0, 0, 255));
            dashV.Stroke = dashH.Stroke = new SolidColorBrush(Color.FromArgb(255, 0, 0, 255));
            DoubleCollection dashArray = new DoubleCollection();
            dashArray.Add(2);
            dashArray.Add(4);
            dashH.StrokeDashArray = dashV.StrokeDashArray = dashArray;
            dashH.StrokeThickness = 1;
            dashH.X1 = dashV.Y1 = 0;
            dashH.X2 = dashV.Y2 = 250;
            dashH.Y1 = dashH.Y2 = dashV.X1 = dashV.X2 = 125;

            RadarCanvas.Children.Add(dashH);
            Canvas.SetZIndex(dashH, 1003);
            RadarCanvas.Children.Add(dashV);
            Canvas.SetZIndex(dashV, 1003);


            Canvas.SetLeft(group3, 100);
            Canvas.SetTop(group3, 110);
            Canvas.SetZIndex(group3, 1003);

            RadarCanvas.Children.Add(group3);

            if (allowedRadarScale < 50)
            {
                Canvas group4 = new Canvas();
                group4.Name = "RadarCover";
                group4.Background = new ImageBrush(new BitmapImage(new Uri("pack://application:,,,/media/RadarCover.png")));
                Canvas.SetZIndex(group4, 1000);
                RadarCanvas.Children.Add(group4);
                updateRadarCover(RadarCanvas, coverScale);
            }
        }
        public void ClearRadar(Canvas RadarCanvas)
        {
            RadarCanvas.Children.Clear();
        }

        // COVER
        public void updateRadarCover(Canvas RadarCanvas, double coverScale)
        {
            foreach (var el in RadarCanvas.Children)
            {
                if (el.GetType() == typeof(Canvas) && ((Canvas)el).Name == "RadarCover")
                {
                    Canvas cover = ((Canvas)el);
                    cover.Width = 250 / coverScale;
                    cover.Height = 250 / coverScale;
                    Canvas.SetLeft(cover, -cover.Width / 2 + 125);
                    Canvas.SetTop(cover, -cover.Height / 2 + 125);

                    break;
                }
            }
        }

        // COMPASS
        public void updateCompassWind(Canvas RadarCanvas, double heading, double windDirection, double windForce)
        {
            foreach (var el in RadarCanvas.Children)
            {
                if (el.GetType() == typeof(Canvas) && ((Canvas)el).Name == "CompassRose")
                {
                    RotateTransform rotateTransform1 = new RotateTransform(-heading * 180 / Math.PI, 125, 125);
                    ((Canvas)el).RenderTransform = rotateTransform1;
                }
                else if (el.GetType() == typeof(Canvas) && ((Canvas)el).Name == "WindPointer")
                {
                    RotateTransform rotateTransform1 = new RotateTransform((-heading + windDirection) * 180 / Math.PI, 125, 125);
                    ((Canvas)el).RenderTransform = rotateTransform1;

                    foreach (var lbl in ((Canvas)el).Children)
                    {
                        if (lbl.GetType() == typeof(TextBlock))
                        {
                            ((TextBlock)lbl).Text = windForce.ToString("0");
                        }
                    }
                }
            }
        }

        // WINCH
        public void InsertWinch(Canvas RadarCanvas)
        {
            Canvas group = new Canvas();
            group.Width = 20;
            group.Height = 20;
            group.Name = "Winch";

            group.Background = new ImageBrush(new BitmapImage(new Uri("pack://application:,,,/media/winchIcon.png")));
            group.Opacity = 0.5;

            RadarCanvas.Children.Add(group);
        }

        public void RemoveWinch(Canvas RadarCanvas)
        {
            try
            {
                var forRemoving = new List<Canvas>();
                foreach (UIElement el in RadarCanvas.Children)
                {
                    if (el.GetType() == typeof(Canvas) && (((Canvas)el).Name == "Winch"))
                    {
                        forRemoving.Add((Canvas)el);
                    }
                }

                foreach (var item in forRemoving)
                    RadarCanvas.Children.Remove(item);

            }
            catch (Exception ex)
            {
                Console.WriteLine("FAILED to clear winch: " + ex.Message);
            }
        }

        public void UpdateWinch(Canvas RadarCanvas, winchDirection winchPosition, double scale)
        {
            scale = Math.Max(0.1, scale) / 125 * 1000;

            try
            {
                foreach (UIElement el in RadarCanvas.Children)
                {
                    if (el.GetType() == typeof(Canvas) && (((Canvas)el).Name == "Winch"))
                    {
                        Canvas canvas = (Canvas)el;
                        Canvas.SetLeft(canvas, 125 - 10 + winchPosition.groundDistance / scale * Math.Sin(winchPosition.heading));
                        Canvas.SetTop(canvas, 125 - 10 - winchPosition.groundDistance / scale * Math.Cos(winchPosition.heading));
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("FAILED to update winch position: " + ex.Message);
            }
        }

        // NEARBY
        public void InsertRadarNearby(Canvas RadarCanvas, SortedDictionary<Double, Button> nearbyDict, MainWindow parent)
        {
            clearRadarNearby(RadarCanvas);

            Canvas group = new Canvas();
            group.Width = 250;
            group.Height = 250;
            group.Name = "Nearbies";
            Canvas.SetZIndex(group, 1005);

            int id = 0;

            foreach (KeyValuePair<double, Button> btn in nearbyDict)
            {
                Ellipse ellipse = new Ellipse();
                ellipse.Name = "Nearby_" + btn.Value.Tag.ToString();
                ellipse.Visibility = Visibility.Collapsed;
                group.Children.Add(ellipse);

                Button label = new Button();
                label.Cursor = Cursors.Hand;
                label.Name = "NearbyLabel_" + btn.Value.Tag.ToString();
                label.Tag = btn.Value.Tag;
                label.Click += parent.attachTowCable;
                label.Content = btn.Value.Content.ToString().Split('(')[0];
                label.BorderThickness = new Thickness(0);
                label.Visibility = Visibility.Collapsed;
                label.Background = new SolidColorBrush(Colors.Transparent);
                group.Children.Add(label);

                id++;
            }

            Console.WriteLine(id + " radar nearbies loaded");
            RadarCanvas.Children.Add(group);
        }

        public void clearRadarNearby(Canvas RadarCanvas)
        {
            try
            {
                var forRemoving = new List<Canvas>();
                foreach (UIElement el in RadarCanvas.Children)
                {
                    if (el.GetType() == typeof(Canvas) && (((Canvas)el).Name == "Nearbies"))
                    {
                        forRemoving.Add((Canvas)el);
                    }
                }

                foreach (var item in forRemoving)
                    RadarCanvas.Children.Remove(item);
            }
            catch (Exception ex)
            {
                Console.WriteLine("FAILED to clear radar nearbies: " + ex.Message);
            }
        }

        public void updateRadarNearby(Canvas RadarCanvas, uint id, winchDirection planeDirection, winchPosition planePosition, bool currentTowPlane, double scale)
        {
            scale = Math.Max(0.1, scale) / 125 * 1000;

            try
            {
                foreach (UIElement canv in RadarCanvas.Children)
                {
                    if (canv.GetType() == typeof(Canvas) && ((Canvas)canv).Name == "Nearbies")
                    {
                        foreach (UIElement el in ((Canvas)canv).Children)
                        {
                            if (el.GetType() == typeof(Ellipse) && ((Ellipse)el).Name == "Nearby_" + id.ToString())
                            {
                                Ellipse circle = (Ellipse)el;

                                circle.Width = 10;
                                circle.Height = 10;
                                SolidColorBrush strokeBrush = new SolidColorBrush(currentTowPlane ? Colors.DarkRed : Colors.DarkGreen);
                                //strokeBrush.Opacity = Math.Abs(finalModifier);
                                circle.Fill = strokeBrush;
                                circle.Visibility = Visibility.Visible;
                                Canvas.SetLeft(circle, 125 - circle.Width / 2 + planeDirection.groundDistance / scale * Math.Sin(planeDirection.heading));
                                Canvas.SetTop(circle, 125 - circle.Height / 2 - planeDirection.groundDistance / scale * Math.Cos(planeDirection.heading));
                            }
                            else if (el.GetType() == typeof(Button) && ((Button)el).Name == "NearbyLabel_" + id.ToString())
                            {
                                Button label = (Button)el;

                                label.Visibility = Visibility.Visible;
                                label.Foreground = new SolidColorBrush(currentTowPlane ? Colors.DarkRed : Colors.DarkGreen);
                                label.Margin = new Thickness(125 + planeDirection.groundDistance / scale * Math.Sin(planeDirection.heading),
                                    125 - planeDirection.groundDistance / scale * Math.Cos(planeDirection.heading), 0, 0);
                            }
                        }

                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Failed to update radar nearby: " + ex.Message);
            }
        }


        // THERMALS
        public void insertRadarThermals(Canvas RadarCanvas, List<winchPosition> thermalsList, string lbl)
        {
            Canvas group = new Canvas();
            group.Width = 250;
            group.Height = 250;
            group.Name = lbl + "s";

            int id = 0;
            for (; id < thermalsList.Count; id++)
            {
                Ellipse ellipse = new Ellipse();
                ellipse.Name = lbl + "_" + id;
                group.Children.Add(ellipse);

                Label label = new Label();
                label.Name = "Label_" + lbl + "_" + id;
                label.FontSize = 12;
                label.Content = Math.Round(thermalsList[id].airspeed) + "m/s" + (thermalsList[id].alt > 1000 ? Environment.NewLine + (thermalsList[id].alt / 0.305).ToString("0") + "ft AGL" : "");
                label.Foreground = new SolidColorBrush(Colors.DarkRed);
                group.Children.Add(label);
            }

            Console.WriteLine(id + " radar " + lbl + "s loaded");
            RadarCanvas.Children.Add(group);
        }

        public void clearRadarThermals(Canvas RadarCanvas, string lbl)
        {
            try
            {
                var forRemoving = new List<Canvas>();
                foreach (UIElement el in RadarCanvas.Children)
                {
                    if (el.GetType() == typeof(Canvas) && (((Canvas)el).Name == lbl + "s"))
                    {
                        forRemoving.Add((Canvas)el);
                    }
                }

                foreach (var item in forRemoving)
                    RadarCanvas.Children.Remove(item);
            }
            catch (Exception ex)
            {
                Console.WriteLine("FAILED to clear radar thermals: " + ex.Message);
            }
        }

        public void updateRadarThermals(Canvas RadarCanvas, string id, winchDirection thermalDirection, winchPosition thermalPosition, double finalModifier, double scale)
        {
            scale = Math.Max(0.1, scale) / 125 * 1000;

            try
            {
                foreach (UIElement canv in RadarCanvas.Children)
                {
                    if (canv.GetType() == typeof(Canvas) && ((Canvas)canv).Name == id.Split('_')[0] + "s")
                    {
                        foreach (UIElement el in ((Canvas)canv).Children)
                        {
                            if (el.GetType() == typeof(Ellipse) && ((Ellipse)el).Name == id)
                            {
                                Ellipse circle = (Ellipse)el;

                                circle.Width = thermalPosition.radius * 2 / scale;
                                circle.Height = thermalPosition.radius * 2 / scale;
                                SolidColorBrush strokeBrush = new SolidColorBrush(finalModifier >= 0 ? Colors.Red : Colors.Blue);
                                strokeBrush.Opacity = Math.Abs(finalModifier >= 0 ? Math.Max(0.1, finalModifier) : Math.Min(-0.1, finalModifier));
                                circle.Fill = strokeBrush;
                                Canvas.SetLeft(circle, 125 - circle.Width / 2 + thermalDirection.groundDistance / scale * Math.Sin(thermalDirection.heading));
                                Canvas.SetTop(circle, 125 - circle.Height / 2 - thermalDirection.groundDistance / scale * Math.Cos(thermalDirection.heading));

                                //Console.WriteLine(id + " scale: " + scale + " modif: " + finalModifier);

                                //break;
                            }
                            else if (el.GetType() == typeof(Label) && ((Label)el).Name == "Label_" + id)
                            {
                                Label label = (Label)el;

                                Canvas.SetLeft(label, 110 + thermalDirection.groundDistance / scale * Math.Sin(thermalDirection.heading));
                                Canvas.SetTop(label, 115 - thermalDirection.groundDistance / scale * Math.Cos(thermalDirection.heading));

                                //break;
                            }
                        }

                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Failed to update radar thermals: " + ex.Message);
            }
        }

        // WAYPOINTS
        public string sanitizeString(string name)
        {
            if (name.Contains('|'))
                name = name.Split('|')[0];

            Regex rgx = new Regex("[^a-zA-Z0-9]");
            return rgx.Replace(name, "_");
        }
        public void insertRadarWaypoints(Canvas RadarCanvas, List<Waypoint> waypointsList)
        {
            clearRadarWaypoints(RadarCanvas);

            Canvas group = new Canvas();
            group.Width = 250;
            group.Height = 250;
            group.Name = "Waypoints";

            int id = 0;
            foreach (var wp in waypointsList)
            {
                /*Ellipse ellipse = new Ellipse();
                ellipse.Name = "Waypoint_" + wp.Name;*/
                Canvas arc = new Canvas();
                arc.Name = "Waypoint_" + sanitizeString(wp.Name) + wp.ID; // UNIQUE NAME!
                group.Children.Add(arc);

                Line leg = new Line();
                leg.Name = "Leg_" + sanitizeString(wp.Name) + wp.ID;
                group.Children.Add(leg);

                Label label = new Label();
                label.Name = "Label_" + sanitizeString(wp.Name) + wp.ID;
                label.FontSize = 10;
                string name = wp.Name;
                if (name.Contains('|'))
                {
                    string data = name.Split('|')[1].Trim();
                    name = name.Split('|')[0].Trim();
                }
                label.Content = name + Environment.NewLine + "MIN: " + wp.Elevation + "ft" + Environment.NewLine + "MAX: " + wp.Height + "ft";
                group.Children.Add(label);

                id++;
            }

            Console.WriteLine(id + " radar waypoints loaded");
            RadarCanvas.Children.Add(group);
        }

        public void clearRadarWaypoints(Canvas RadarCanvas)
        {
            try
            {
                var forRemoving = new List<Canvas>();
                foreach (UIElement el in RadarCanvas.Children)
                {
                    if (el.GetType() == typeof(Canvas) && (((Canvas)el).Name == "Waypoints"))
                    {
                        forRemoving.Add((Canvas)el);
                    }
                }

                foreach (var item in forRemoving)
                    RadarCanvas.Children.Remove(item);
            }
            catch (Exception ex)
            {
                Console.WriteLine("FAILED to clear radar waypoints: " + ex.Message);
            }
        }

        public void updateRadarWaypoints(Canvas RadarCanvas, Waypoint wp, double distance, double heading, double width, double scale, double nextBearing, double nextDistance, double altitude)
        {
            scale = Math.Max(0.1, scale) / 125 * 1000;

            try
            {
                foreach (UIElement canv in RadarCanvas.Children)
                {
                    if (canv.GetType() == typeof(Canvas) && ((Canvas)canv).Name == "Waypoints")
                    {
                        foreach (var el in ((Canvas)canv).Children)
                        {
                            double opacity = !string.IsNullOrEmpty(wp.Entered) || !string.IsNullOrEmpty(wp.Passed) ? 0.1 : 0.5; ;

                            if (el.GetType() == typeof(Canvas) && ((Canvas)el).Name == "Waypoint_" + sanitizeString(wp.Name) + wp.ID)
                            {
                                Canvas cnv = (Canvas)el;

                                cnv.Width = width / scale;
                                cnv.Height = width / 2 / scale;
                                Canvas.SetLeft(cnv, 125 - cnv.Width / 2 + distance / scale * Math.Sin(heading));
                                Canvas.SetTop(cnv, 125 - distance / scale * Math.Cos(heading));
                                cnv.Background = new ImageBrush(new BitmapImage(new Uri("pack://application:,,,/media/wp.png")));
                                cnv.Background.Opacity = opacity;

                                RotateTransform rotateTransform = new RotateTransform(nextBearing * 180 / Math.PI, cnv.Width / 2, 0);
                                cnv.RenderTransform = rotateTransform;

                                //break;
                            }
                            else if (el.GetType() == typeof(Line) && ((Line)el).Name == "Leg_" + sanitizeString(wp.Name) + wp.ID)
                            {
                                Line leg = (Line)el;

                                SolidColorBrush strokeBrush = new SolidColorBrush(Color.FromArgb(255, 0, 170, 0));
                                strokeBrush.Opacity = 0.5;
                                leg.Stroke = strokeBrush;

                                leg.X1 = 125 + distance / scale * Math.Sin(heading);
                                leg.X2 = 125 + distance / scale * Math.Sin(heading) + nextDistance / scale * Math.Sin(nextBearing); ;
                                leg.Y1 = 125 - distance / scale * Math.Cos(heading);
                                leg.Y2 = 125 - distance / scale * Math.Cos(heading) - nextDistance / scale * Math.Cos(nextBearing);

                                //break;
                            }
                            else if (el.GetType() == typeof(Label) && ((Label)el).Name == "Label_" + sanitizeString(wp.Name) + wp.ID)
                            {
                                Label label = (Label)el;
                                if (altitude == 0)
                                    label.Foreground = new SolidColorBrush(Colors.Black);
                                else
                                    label.Foreground = new SolidColorBrush(altitude > 0 ? Colors.DarkRed : Colors.DarkBlue);

                                Canvas.SetLeft(label, 125 + distance / scale * Math.Sin(heading));
                                Canvas.SetTop(label, 125 - distance / scale * Math.Cos(heading));

                                break;
                            }
                        }

                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Failed to update radar waypoints: " + ex.Message);
            }
        }
    }
}


<|start_filename|>KA-panel/kinetic-ingamepanel/html_ui/InGamePanels/KineticPanel/KineticPanel.js<|end_filename|>
class IngamePanelKineticPanel extends TemplateElement {
    constructor() {
        super(...arguments);

        this.panelActive = false;
        this.started = false;
        this.ingameUi = null;
        this.busy = false;
        this.debugEnabled = false;

        if (this.debugEnabled) {
            var self = this;
            setTimeout(() => {
                self.isDebugEnabled();
            }, 1000);
        } else {
            this.initialize();
        }
    }
    isDebugEnabled() {
        var self = this;
        if (typeof g_modDebugMgr != "undefined") {
            g_modDebugMgr.AddConsole(null);
            g_modDebugMgr.AddDebugButton("Identifier", function() {
                console.log('Identifier');
                console.log(self.instrumentIdentifier);
            });
            g_modDebugMgr.AddDebugButton("TemplateID", function() {
                console.log('TemplateID');
                console.log(self.templateID);
            });
            g_modDebugMgr.AddDebugButton("Source", function() {
                console.log('Source');
                console.log(window.document.documentElement.outerHTML);
            });
			g_modDebugMgr.AddDebugButton("close", function() {
				console.log('close');
				if (self.ingameUi) {
					console.log('ingameUi');
					self.ingameUi.closePanel();
				}
			});
            this.initialize();
        } else {
            Include.addScript("/JS/debug.js", function () {
                if (typeof g_modDebugMgr != "undefined") {
                    g_modDebugMgr.AddConsole(null);
                    g_modDebugMgr.AddDebugButton("Identifier", function() {
                        console.log('Identifier');
                        console.log(self.instrumentIdentifier);
                    });
                    g_modDebugMgr.AddDebugButton("TemplateID", function() {
                        console.log('TemplateID');
                        console.log(self.templateID);
                    });
                    g_modDebugMgr.AddDebugButton("Source", function() {
                        console.log('Source');
                        console.log(window.document.documentElement.outerHTML);
                    });
                    g_modDebugMgr.AddDebugButton("close", function() {
                        console.log('close');
                        if (self.ingameUi) {
                            console.log('ingameUi');
                            self.ingameUi.closePanel();
                        }
                    });
                    self.initialize();
                } else {
                    setTimeout(() => {
                        self.isDebugEnabled();
                    }, 2000);
                }
            });
        }
    }
    connectedCallback() {
        super.connectedCallback();

        var self = this;
        this.ingameUi = this.querySelector('ingame-ui');

        this.iframeElement = document.getElementById("KineticPanelIframe");

        this.m_MainDisplay = document.querySelector("#MainDisplay");
        this.m_MainDisplay.classList.add("hidden");

        this.m_Footer = document.querySelector("#Footer");
        this.m_Footer.classList.add("hidden");

        if (this.ingameUi) {
            this.ingameUi.addEventListener("panelActive", (e) => {
                console.log('panelActive');
                self.panelActive = true;
                if (self.iframeElement) {
                    self.iframeElement.src = 'https://www.ipify.org/';
                }
            });
            this.ingameUi.addEventListener("panelInactive", (e) => {
                console.log('panelInactive');
                self.panelActive = false;
                if (self.iframeElement) {
                    self.iframeElement.src = '';
                }
            });
            this.ingameUi.addEventListener("onResizeElement", () => {
                //self.updateImage();
            });
            this.ingameUi.addEventListener("dblclick", () => {
                if (self.m_Footer) {
                    self.m_Footer.classList.remove("hidden");
                }
			});
        }
    }
    initialize() {
        if (this.started) {
            return;
        }

        //var self = this;
        //this.m_MainDisplay = document.querySelector("#MainDisplay");
        //this.m_MainDisplay.classList.add("hidden");

        //this.m_Footer = document.querySelector("#Footer");
        //this.m_Footer.classList.add("hidden");

        //this.iframeElement = document.getElementById("KineticPanelIframe");
        //this.ingameUi = this.querySelector('ingame-ui');

        /*if (this.ingameUi) {
            this.ingameUi.addEventListener("panelActive", (e) => {
                console.log('panelActive');
                self.updateImage();
            });
            this.ingameUi.addEventListener("panelInactive", (e) => {
                console.log('panelInactive');
                self.iframeElement.src = '';
            });
            this.ingameUi.addEventListener("onResizeElement", () => {
                //self.updateImage();
            });
            this.ingameUi.addEventListener("dblclick", () => {
                if (self.m_Footer) {
                    self.m_Footer.classList.remove("hidden");
                }
            });
        }*/
        this.started = true;
    }
    disconnectedCallback() {
        super.disconnectedCallback();
    }
    updateImage() {

    }
}
window.kineticElements.define("ingamepanel-kinetic", IngamePanelKineticPanel);
checkAutoload();

<|start_filename|>KA-panel/build.bat<|end_filename|>

"%MSFS_SDK%\Tools\bin\fspackagetool.exe" "kinetic-ingamepanel\Build\kinetic-ingamepanel.xml" -nomirroring
copy /Y "kinetic-ingamepanel\Build\Packages\kinetic-ingamepanel\Build\kinetic-ingamepanel.spb" "kinetic-ingamepanel\InGamePanels"

<|start_filename|>MSFS Kinetic Assistant/Server.cs<|end_filename|>
﻿/*
 Location Bridge

 http://eartoearoak.com/software/location-bridge

 Copyright 2014-2015 <NAME>

 A simple server that provides NMEA sentences over TCP from the
 Windows location sensors.


 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, or (at your option)
 any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Timers;
using MSFS_Kinetic_Assistant.Properties;

namespace MSFS_Kinetic_Assistant
{

    // Based on http://msdn.microsoft.com/en-us/library/fx6588te.aspx

    class Client
    {
        public Socket socket { get; set; }

        public Client(Socket socket)
        {
            this.socket = socket;
        }
    }

    class Server
    {
        public const int MAX_CLIENTS = 10;

        private static byte[] warnMaxClients = Encoding.ASCII.GetBytes(
            Resources.TooManyConnections);

        private object lockGps;
        private CallbackUI callback;

        private volatile bool cancel = false;
        private Object lockClients = new Object();
        private List<Client> clients = new List<Client>();
        private IPAddress ipAddress;

        public static ManualResetEvent signal = new ManualResetEvent(false);

        public Server(object gpsLock, CallbackUI callback, string ip)
        {
            ipAddress = IPAddress.Parse(ip);
            lockGps = gpsLock;
            this.callback = callback;
            UpdateCount();

            System.Timers.Timer timerAlive = new System.Timers.Timer();
            timerAlive.Elapsed += new ElapsedEventHandler(OnTimerAlive);
            timerAlive.Interval = 1000;
            timerAlive.Enabled = true;
        }

        public void Start()
        {
            IPEndPoint localEndPoint;
            /*if (Properties.Settings.Default.LocalOnly)
                localEndPoint = new IPEndPoint(IPAddress.Loopback, 10110);
            else*/
            localEndPoint = new IPEndPoint(ipAddress, 10110);

            var socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            callback(new ServerStatus(Resources.Waiting));

            try
            {
                socketServer.Bind(localEndPoint);
                socketServer.Listen(100);

                while (!cancel)
                {
                    signal.Reset();
                    socketServer.BeginAccept(new AsyncCallback(ClientCallback),
                        socketServer);
                    signal.WaitOne();
                }
            }
            catch (SocketException e)
            {
                Console.Write("EXCEPTION: Start error " + e.Message);
                callback(new ServerStatus(ServerStatusCode.ERROR,
                    String.Format(Resources.Error0, e.SocketErrorCode.ToString())));
                if (socketServer.IsBound)
                    socketServer.Shutdown(SocketShutdown.Both);
            }
            finally
            {
                socketServer.Close();
                foreach (var client in clients.ToArray())
                    ClientRemove(client);
            }
        }

        private static string GetHostName(string address)
        {
            /*string host = address.Split(':')[0];
            string hostName = "";
            try
            {
                IPHostEntry hostEntry = Dns.GetHostEntry(host);

                hostName = hostEntry.HostName;
            }
            catch (SocketException ex)
            {
                Console.Write("EXCEPTION: Can't get host name " + ex.Message);
                hostName = host;
            }

            return "'" + hostName + "'";*/

            try
            {
                IPHostEntry entry = Dns.GetHostEntry(address);
                if (entry != null)
                {
                    return entry.HostName;
                }
            }
            catch (SocketException ex)
            {
                Console.Write("EXCEPTION: Can't get host name " + ex.Message);
            }

            return "";
        }


        private void ClientAdd(Client client)
        {
            lock (lockClients)
                clients.Add(client);
        }

        private void ClientRemove(Client client)
        {
            lock (lockClients)
                clients.Remove(client);
            var addr = GetHostName(client.socket.RemoteEndPoint.ToString());
            client.socket.Shutdown(SocketShutdown.Both);
            client.socket.Close();
            callback(new ServerStatus(String.Format(Resources._0Disconnected, addr)));
            UpdateCount();
        }

        private void ClientCallback(IAsyncResult ar)
        {
            signal.Set();

            try
            {
                var socketServer = (Socket)ar.AsyncState;
                var socketClient = socketServer.EndAccept(ar);
                var addr = GetHostName(socketClient.RemoteEndPoint.ToString());

                if (clients.Count < MAX_CLIENTS)
                {
                    var client = new Client(socketClient);
                    ClientAdd(client);
                    callback(new ServerStatus(
                        String.Format(Resources._0Connected, addr)));
                    UpdateCount();
                }
                else
                {
                    socketClient.Send(warnMaxClients);
                    socketClient.Shutdown(SocketShutdown.Both);
                    socketClient.Close();
                    callback(new ServerStatus(
                        String.Format(Resources._0RefusedTooManyConnections, addr)));
                }
            }
            catch (Exception ex)
            {
                Console.Write("EXCEPTION: Can't process callback " + ex.Message);
            }
        }

        private void Send(Client client, String data)
        {
            var byteData = Encoding.ASCII.GetBytes(data);
            try
            {
                client.socket.BeginSend(byteData, 0, byteData.Length, 0,
                                        new AsyncCallback(SendCallback), client);
            }
            catch (SocketException ex)
            {
                Console.Write("EXCEPTION: Send error " + ex.Message);
                ClientRemove(client);
            }
        }

        private void SendCallback(IAsyncResult ar)
        {
            var client = (Client)ar.AsyncState;
            try
            {
                client.socket.EndSend(ar);
            }
            catch (SocketException ex)
            {
                Console.Write("EXCEPTION: SendCallback error " + ex.Message);
                ClientRemove(client);
            }
        }

        private bool IsConnected(Socket socket)
        {
            try
            {
                return !(socket.Poll(1, SelectMode.SelectRead) && socket.Available == 0);
            }
            catch (SocketException ex)
            {
                Console.Write("EXCEPTION: IsConnected error " + ex.Message);
                return false;
            }
        }

        private void OnTimerAlive(object source, ElapsedEventArgs e)
        {
            List<Client> disconnected = new List<Client>();

            lock (lockClients)
            {
                foreach (Client client in clients)
                    if (!IsConnected(client.socket))
                        disconnected.Add(client);
                foreach (Client client in disconnected)
                    ClientRemove(client);
            }
        }

        private static string NmeaCoord(double coord, bool isLat)
        {
            string direct;

            if (isLat)
            {
                if (coord < 0)
                    direct = "S";
                else
                    direct = "N";
            }
            else
            {
                if (coord < 0)
                    direct = "W";
                else
                    direct = "E";
            }

            coord = Math.Abs(coord);

            int deg = (int)Math.Floor(coord);
            double min = (coord % 1) * 60;
            var nmea = deg.ToString() + min.ToString("00.0000000", CultureInfo.InvariantCulture) + "," + direct;
            return nmea;
        }

        private static string NmeaChecksum(string sentence)
        {
            var checksum = 0;

            for (var i = 0; i < sentence.Length; i++)
                checksum ^= sentence[i];

            return "*" + checksum.ToString("X2");
        }

        private static string NmeaFormat(Location location)
        {
            var lat = NmeaCoord(location.lat, true);
            var lon = NmeaCoord(location.lon, false);
            var alt = location.alt.ToString("F1");
            var speed = (location.speed * 1.9).ToString("F3");
            var time = location.time.ToString("HHmmss.ff");
            var date = location.time.ToString("ddMMyy");
            var ha = location.ha.ToString("0.00", CultureInfo.InvariantCulture);

            var gga = String.Format("GPGGA,{0},{1},{2},1,12,1,{3},M,,,,", time, lat, lon, alt);
            var gll = String.Format("GPGLL,{0},{1},{2},A", lat, lon, time);
            var rmc = String.Format("GPRMC,{0},A,{1},{2},{3},{4},{5},0.0,E,A", time, lat, lon, speed, ha, date);
            var gsa = "GPGSA,A,3,04,05,06,09,12,20,22,24,25,26,28,30,1,1,1,";
            var vgt = String.Format("GPVTG,{0},T,{0},M,0,N,0,K", ha);

            gga = "$" + gga + NmeaChecksum(gga) + "\r\n";
            gll = "$" + gll + NmeaChecksum(gll) + "\r\n";
            rmc = "$" + rmc + NmeaChecksum(rmc) + "\r\n";
            gsa = "$" + gsa + NmeaChecksum(gsa) + "\r\n";
            vgt = "$" + vgt + NmeaChecksum(vgt) + "\r\n";

            string responce = gga + gll + rmc + gsa + vgt;

            return responce;

            /*
+$GPGGA,101338.00,4630.9471,N,00719.523,E,1,12,1,3623.8,M,,,,*26
+$GPGLL,4630.9471,N,00719.523,E,101338.00,A*3D
+$GPRMC,101338.00,A,4630.9471,N,00719.523,E,0,33.83,250221,0.91,E*4A
$GPGSV,3,1,12,04,26,291,92,05,32,062,86,06,05,029,33,09,47,095,98*78
$GPGSV,3,2,12,12,60,224,88,20,21,240,85,22,28,311,93,24,81,037,77*78
$GPGSV,3,3,12,25,160,124,85,26,321,240,90,28,90,200,90,30,181,137,87*65
+$GPGSA,A,3,04,05,06,09,12,20,22,24,25,26,28,30,1,1,1,*02
$GPVTG,33.84,T,32.93,M,0,N,0,K*49
            */
        }

        private void UpdateCount()
        {
            callback(new ServerStatus(ServerStatusCode.CONN,
                clients.Count));
        }

        public void GpsUpdate(Location location)
        {
            lock (lockGps)
                location = new Location(location);

            lock (lockClients)
                foreach (var client in clients.ToArray())
                    Send(client, NmeaFormat(location));
        }

        public void Stop()
        {
            cancel = true;
            signal.Set();
        }
    }

    enum ServerStatusCode { OK, ERROR, CONN };

    class ServerStatus
    {
        public ServerStatusCode status { get; set; }
        public string message { get; set; }
        public int value { get; set; }

        public ServerStatus(string message)
        {
            this.status = ServerStatusCode.OK;
            this.message = message;
            this.value = 0;
        }

        public ServerStatus(ServerStatusCode status, string message)
        {
            this.status = status;
            this.message = message;
            this.value = 0;
        }

        public ServerStatus(ServerStatusCode status, int value)
        {
            this.status = status;
            this.message = null;
            this.value = value;
        }

    }

    class Location
    {
        public double lat { get; set; }
        public double lon { get; set; }
        public double alt { get; set; }
        public double speed { get; set; }
        public double ha { get; set; }
        public double va { get; set; }
        public DateTimeOffset time { get; set; }

        public Location()
        {
        }

        public Location(Location original)
        {
            lat = original.lat;
            lon = original.lon;
            alt = original.alt;
            speed = original.speed;
            ha = original.ha;
            va = original.va;
            time = original.time;
        }
    }

    delegate void CallbackUI(ServerStatus status);
}

<|start_filename|>MSFS Kinetic Assistant/kinetic-panel/html_ui/InGamePanels/ka-panel/kaPanel.js<|end_filename|>
class kaPanel extends TemplateElement {
    constructor() {
        super(...arguments);

        this.started = false;
		this.isActive = false;
        this.busy = false;
        this.front = true;
        this.url = "";
        this.zoom = 0;
        this.debugEnabled = false;
        this.onStorageReady = () => {
            if (this.debugEnabled) {
                var self = this;
                setTimeout(() => {
                    self.isDebugEnabled();
                }, 1000);
            } else {
                this.initialize();
            }
        };
    }
    isDebugEnabled() {
        var self = this;
        if (typeof g_modDebugMgr != "undefined") {
            g_modDebugMgr.AddConsole(null);
            g_modDebugMgr.AddDebugButton("Clear", function() {
                g_modDebugMgr.ClearConsole();
                console.log('Clear');
            });
            g_modDebugMgr.AddDebugButton("Reload", function() {
                console.log('Reload');
                window.document.location.reload(true);
            });
            g_modDebugMgr.AddDebugButton("Source", function() {
                console.log('Source');
                console.log(window.document.documentElement.outerHTML);
            });
            g_modDebugMgr.AddDebugButton("close", function() {
                console.log('close');
                if (self.ingameUi) {
                    console.log('ingameUi');
                    self.ingameUi.closePanel();
                }
            });
            g_modDebugMgr.AddDebugButton("min", function() {
                console.log('min');
                if (self.ingameUi && self.ingameUi.m_toolbar_listener) {
                    console.log('ingameUi');
                    self.ingameUi.m_toolbar_listener.setMinimized('KA_PANEL', true);
                }
            });
            this.initialize();
        } else {
            Include.addScript("/JS/debug.js", function () {
                if (typeof g_modDebugMgr != "undefined") {
                    g_modDebugMgr.AddConsole(null);
                    g_modDebugMgr.AddDebugButton("Clear", function() {
                        g_modDebugMgr.ClearConsole();
                        console.log('Clear');
                    });
                    g_modDebugMgr.AddDebugButton("Reload", function() {
                        console.log('Reload');
                        window.document.location.reload(true);
                    });
                    g_modDebugMgr.AddDebugButton("Source", function() {
                        console.log('Source');
                        console.log(window.document.documentElement.outerHTML);
                    });
                    g_modDebugMgr.AddDebugButton("close", function() {
                        console.log('close');
                        if (self.ingameUi) {
                            console.log('ingameUi');
                            self.ingameUi.closePanel();
                        }
                    });
                    g_modDebugMgr.AddDebugButton("min", function() {
                        console.log('min');
                        if (self.ingameUi && self.ingameUi.m_toolbar_listener) {
                            console.log('ingameUi');
                            self.ingameUi.m_toolbar_listener.setMinimized('KA_PANEL', true);
                        }
                    });
                    self.initialize();
                } else {
                    setTimeout(() => {
                        self.isDebugEnabled();
                    }, 2000);
                }
            });
        }
    }
    connectedCallback() {
        super.connectedCallback();
        document.addEventListener("dataStorageReady", this.onStorageReady);
    }
    initialize() {
        if (this.started) {
            return;
        }

        var self = this;

        this.url = "http://localhost:8212";

        this.errorElement = document.getElementById("kaPanelImageError");
        this.imageElementFront = document.getElementById("kaPanelImageFront");
        this.imageElementBack = document.getElementById("kaPanelImageBack");
        this.imageElementZoom = document.getElementById("kaPanelImageZoom");
        this.ingameUi = this.querySelector('ingame-ui');


        if (this.imageElementFront && this.imageElementBack) {
            this.imageElementFront.addEventListener("error", () => {
                self.onImageError();
            });
            this.imageElementFront.addEventListener("load", () => {
                self.onImageLoad();
            });
            this.imageElementBack.addEventListener("error", () => {
                self.onImageError();
            });
            this.imageElementBack.addEventListener("load", () => {
                self.onImageLoad();
            });
            if (this.imageElementZoom) {
				this.imageElementZoom.addEventListener('wheel', () => {
				  event.preventDefault();
				  this.zoom  += event.deltaY * 0.01;
				  self.updateImage();
				});
            }
        }

        if (this.ingameUi) {
            this.ingameUi.addEventListener("panelActive", (e) => {
				this.isActive = true;
                self.updateImage();
            });
            this.ingameUi.addEventListener("panelInactive", (e) => {
				this.isActive = false;
            });
            this.ingameUi.addEventListener("onResizeElement", () => {
                self.updateImage();
            });
            this.ingameUi.addEventListener("dblclick", () => {
            });
        }

        this.started = true;
		
		setInterval(function() {
			if (!this.isActive)
				return;
			
			self.updateImage();
		}, 10000);
    }
    disconnectedCallback() {
        document.removeEventListener("dataStorageReady", this.onStorageReady);
        super.disconnectedCallback();
    }
    onImageError() {
        var self = this;

		if (!this.isActive)
			return;
		
        if (this.front) {
            if (this.imageElementFront) {
                this.imageElementFront.setAttribute('style', '');
                this.imageElementFront.setAttribute('class', 'hidden');
            }
            this.front = false;
        } else {
            if (this.imageElementBack) {
                this.imageElementBack.setAttribute('style', '');
                this.imageElementBack.setAttribute('class', 'hidden');
            }
            this.front = true;
        }
        if (this.errorElement) {
            this.errorElement.setAttribute('class', 'show');
        }
        //this.busy = false;
        setTimeout(function() {
            self.updateImage();
        }, 250);
    }
    onImageLoad() {
        var self = this;

		if (!this.isActive)
			return;
		
        if (this.errorElement) {
            this.errorElement.setAttribute('class', '');
        }

		if (this.front) {
            if (this.imageElementFront) {
                this.imageElementFront.setAttribute('class', 'show');
                setTimeout(function(){
					this.imageElementBack.setAttribute('class', 'hidden');
				}, 0);
                //this.imageElementFront.setAttribute('style', 'z-index: 2');
                //if (this.imageElementBack) {
               //     this.imageElementBack.setAttribute('style', '');
                //}
            }
            this.front = false;
        } else {
            if (this.imageElementBack) {
                this.imageElementBack.setAttribute('class', 'show');
                setTimeout(function(){
					this.imageElementFront.setAttribute('class', 'hidden');
				}, 0);
                //this.imageElementBack.setAttribute('style', 'z-index: 2');
                //if (this.imageElementFront) {
                //    this.imageElementFront.setAttribute('style', '');
                //}
            }
            this.front = true;
        }
		
		this.front != this.front;
        //this.busy = false;
        setTimeout(function() {
            self.updateImage();
        }, 250);
    }
    updateImage() {
        if (! this.url) {
            return;
        }
		
		var url = this.url + "/?width=" + Math.max(this.imageElementFront.width, 138) + "&height=" + Math.max(this.imageElementFront.height, 155) +"&zoom="+this.zoom.toString()+"&cmd=" + Math.random();
		this.zoom = 0;
        if (this.front) {
            if (this.imageElementFront) {
                //if (! this.busy) {
                    //this.busy = true;
                    this.imageElementFront.src = url;
					//this.imageElementFront.setAttribute('class', 'hidden');
                //}
            }
        } else {
            if (this.imageElementBack) {
                //if (! this.busy) {
                    //this.busy = true;
                    this.imageElementBack.src = url;
					//this.imageElementBack.setAttribute('class', 'hidden');
                //}
            }
        }
    }
}
window.customElements.define("ingamepanel-ka", kaPanel);
checkAutoload();

<|start_filename|>MSFS Kinetic Assistant/MainWindow.xaml.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
using CTrue.FsConnect;
using Microsoft.FlightSimulator.SimConnect;
using Newtonsoft.Json;
using System.Windows.Input;
using System.IO;
using System.Windows.Controls;
using System.Reflection;
using Microsoft.Win32;
using System.Globalization;
using System.Windows.Media.Imaging;
using System.Net;
using System.Xml.Linq;
using System.Threading.Tasks;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Documents;

//-TOW RELEASE HANDLE
//=FOLDING WING LEFT PERCENT
//=FOLDING WING RIGHT PERCENT
//+WATER RUDDER HANDLE POSITION
//?CANOPY OPEN
//+TAILHOOK POSITION
//?EXIT OPEN:index


namespace MSFS_Kinetic_Assistant
{
    public partial class MainWindow : System.Windows.Window
    {
        static Mutex mutex;

        string zipDirectory = "";

        private FsConnect _fsConnect;

        private PlaneInfoResponse _planeInfoResponse;
        private PlaneInfoResponse _planeInfoResponseLast;

        private PlaneInfoCommit _planeCommit;

        private PlaneAvionicsResponse _planeAvionicsResponse;
        private PlaneAvionicsResponse _planeAvionicsResponseLast;

        private PlaneInfoRotateVelocity _planeRotateVelocity;
        private PlaneEngineData _planeEngineData;
        private PlaneInfoRotateAccel _planeRotateAccel;
        private ThermalVelocity _planeVerticalVelocity;

        private Dictionary<uint, winchPosition> _nearbyInfoResponse;
        private Dictionary<uint, winchPosition> _nearbyInfoResponseLast;
        private Dictionary<uint, winchPosition> _nearbyInfoResponsePreLast;
        private WeatherReport _weatherReport;

        // IF NOT NULL - READY TO LAUNCH
        private winchPosition _winchPosition = null;
        private winchPosition _carrierPosition = null;

        // IF NOT 0 - WINCH WORKING
        private double launchTime = 0;
        private double cableLength = 0;
        private double cableLengthPrev = 0;
        private double cableLengthPrePrev = 0;
        private double cabelTensionPrev = 0;

        // IF NOT 0 - ARRESTER CONNECTED
        private double arrestorConnectedTime = 0;

        // IF NOT 0 - CATAPULT LAUNCH IN PROCESS
        private double targedCatapultVelocity = 0;

        // IF NOT FALSE - THERMALS CALCULATION IN PROCESS
        private bool thermalsWorking = false;
        private byte thermalsDebugActive = 0;
        private GeoLocation apiThermalsLoadedPosition = null;
        double apiThermalsLoadedTime = 0;
        private List<winchPosition> thermalsList = new List<winchPosition>();
        private List<winchPosition> thermalsListAPI = new List<winchPosition>();
        private double weatherChecked = 0;
        private double windDirection = 0;
        private double windVelocity = 0;
        private double thermalFlow = 0;
        private double dayTimeModifier = 1;
        private double overcastModifier = 1;
        private int scaleRefresh = 0;

        // IF NOT 0 - TOWING ACTIVE
        private TowScanMode towScanMode = TowScanMode.Disabled;
        private double towToggledAltitude;
        public static uint TARGETMAX = 99999999;
        private uint towingTarget = TARGETMAX;
        private double lightToggled = 0;
        private KeyValuePair<uint, bool> insertedTowPlane = new KeyValuePair<uint, bool>(TARGETMAX, false);
        private double towCableLength = 0;
        private double towPrevDist = 0;
        private double towPrePrevDist = 0;
        private double towCableDesired = 40;
        private bool towPlaneInserted = false;
        private double insertTowPlanePressed = 0;
        private double AIholdInterval = 8;
        private enum TowScanMode
        {
            Disabled = 0,
            Scan = 1,
            TowSearch = 2,
            TowInsert = 3
        }
        /*
         * 1. Insert pressed
         * 2. scan enabled
         * 3. 2sec delay - searching for existing AI tow plane
         * 4. if not found, 4sec delay - creating new AI plane
         * 5. capture inserted AI plane
         * 6. 2sec delay - searching for inserted AI plane
         * 7. if not found and captured - move to the player position
        */



        bool winchAbortInitiated = false;
        bool launchpadAbortInitiated = false;
        bool arrestorsAbortInitiated = false;

        private double lastPacketReceived = 0;
        private double lostPacketRecovered = 0;

        private double absoluteTime = 0;
        private double swCurrent = 0;
        private double swLast = 0;
        private double lastFrameTiming;
        DispatcherTimer dispatcherTimer;
        DispatcherTimer launchTimer;
        DispatcherTimer nearbyTimer;
        private double lastPreparePressed = 0;
        double te_raw_ms = 0;

        string optPath;
        string plnPath;

        private bool loaded = false;
        MathClass _mathClass;
        RadarClass _radarClass;
        Tracking _trackingClass;

        Dictionary<string, double> assistantSettings;
        Dictionary<string, double> controlTimestamps;

        MediaPlayer soundPlayer = null;

        bool taskInProcess = false;

        private Server server;
        private Thread threadServer;

        // RADAR
#if DEBUG
        double allowedRadarScale = 5;
        double allowedRecords = 5;
        double allowedRecordLength = 900; // SEC
#else
        double allowedRadarScale = 50;
        double allowedRecords = 999;
        double allowedRecordLength = 36000; // SEC
#endif
        double maxRadarScale = 50;
        byte[] bitmapdata;
        double lastRadarRequest = 0;
        bool httpServerActive = false;

        public MainWindow()
        {
            mutex = new Mutex(false, "KineticAssistant");

            if (!mutex.WaitOne(TimeSpan.FromSeconds(2), false))
            {
                MessageBox.Show("KineticAssistant already launched");
                Application.Current.Shutdown();
                return;
            }

            DataContext = new SimvarsViewModel();
            _mathClass = new MathClass();
            _radarClass = new RadarClass();
            _trackingClass = new Tracking();

            assistantSettings = new Dictionary<string, double>();

            // PREPARE CONTROLS DATA
            controlTimestamps = new Dictionary<string, double>();
            foreach (var field in typeof(PlaneAvionicsResponse).GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
                if (field.FieldType == typeof(double))
                    controlTimestamps.Add(field.Name, 0);

            InitializeComponent();

            this.Title = ((AssemblyTitleAttribute)Attribute.GetCustomAttribute(Assembly.GetExecutingAssembly(), typeof(AssemblyTitleAttribute), false)).Title;
#if !DEBUG
            this.Title += "+";
#endif
            this.Title += " " + Assembly.GetExecutingAssembly().GetName().Version.ToString();
            AppName.Text = this.Title;

            // COMMUNITY CHECK
            if (Assembly.GetEntryAssembly().Location.Contains("\\Community\\"))
            {
                MessageBox.Show("You have installed Kinetic Assistant inside of Community folder, please move files outside of this folder to avoid MSFS issues", "..\\Community\\.. - Unsupported location");
                Application.Current.Shutdown();
            }

            optPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Packages\Microsoft.FlightSimulator_8wekyb3d8bbwe\LocalState\";
            if (!Directory.Exists(optPath))
                optPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\Microsoft Flight Simulator\";
            if (!Directory.Exists(optPath))
                optPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\PackagesMicrosoft.FlightDashboard_8wekyb3d8bbwe\LocalState\";

            addLogMessage("MSFS Path: " + optPath);

            addServerIPs();

            loadSettings();
            loaded = true;

            _ = CheckUpdateAsync();

            // NMEA
            if (assistantSettings.ContainsKey("nmeaServer") && assistantSettings["nmeaServer"] != 0)
            {
                ServerStart(((ComboBoxItem)nmeaServer.SelectedItem).Tag.ToString());
            }

            // HTTP
            if (assistantSettings.ContainsKey("panelServer") && assistantSettings["panelServer"] != 0)
            {
                startHTTP();
            }

            string path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + @"\kinetic-panel.zip";
            if (File.Exists(path))
            {
                TextBlock txt = makeTextBlock("", Colors.Blue);
                Hyperlink link = new Hyperlink();

                link.NavigateUri = new Uri(path);
                link.RequestNavigate += Hyperlink_RequestNavigate;
                link.Inlines.Add("Install Kinetic Panel");
                link.ToolTip = "Extract kinetic-panel folder into Community";
                link.FontSize = 14;
                txt.HorizontalAlignment = HorizontalAlignment.Center;
                txt.Inlines.Add(link);

                settingsContainer.Children.Add(txt);
            }

            // COMMON SIM VALUES REQUEST TIMER
            dispatcherTimer = new DispatcherTimer();
            dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 250);
            dispatcherTimer.Tick += new EventHandler(commonInterval);
            dispatcherTimer.Start();

            launchTimer = new DispatcherTimer();
            launchTimer.Interval = new TimeSpan(0, 0, 0, 0, 1000 / Math.Max(10, (int)(assistantSettings.ContainsKey("RequestsFrequency") ? assistantSettings["RequestsFrequency"] : 10)));
            launchTimer.Tick += new EventHandler(launchInterval);
            launchTimer.Start();

            nearbyTimer = new DispatcherTimer();
            nearbyTimer.Interval = new TimeSpan(0, 0, 0, 0, 20000 / Math.Max(10, (int)(assistantSettings.ContainsKey("RequestsFrequency") ? assistantSettings["RequestsFrequency"] : 10)));
            nearbyTimer.Tick += new EventHandler(nearbyInterval);
            nearbyTimer.Start();

            dispatcherTimer = new DispatcherTimer();
            dispatcherTimer.Interval = new TimeSpan(1, 0, 0);
            dispatcherTimer.Tick += new EventHandler(triggerCheckUpdate);
            dispatcherTimer.Start();
        }

        private void Window_PreviewLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
        {
            Application.Current.MainWindow.Topmost = assistantSettings["alwaysOnTop"] == 1;
        }
        private void commonInterval(object sender, EventArgs e)
        {
            if (validConnection())
            {
                _fsConnect.RequestData(Requests.PlaneAvionics, Definitions.PlaneAvionics);

                if (_winchPosition == null && _carrierPosition == null && targedCatapultVelocity == 0 && !thermalsWorking && towingTarget == TARGETMAX && !taskInProcess)
                {
                    lastPacketReceived += 0.25;
                    _fsConnect.RequestData(Requests.PlaneInfo, Definitions.PlaneInfo);

                    // PACKET LOST??
                    if (lastPacketReceived >= 5 && taskInProcess)
                    {
                        Console.WriteLine("Packet lost, reconnect (" + lastPacketReceived + "s)");
                        toggleQuickConnect(false);
                        toggleQuickConnect(true);
                    }
                }

                if (towScanMode > TowScanMode.Disabled && towingTarget == TARGETMAX && !_trackingClass.ghostPlayerActive())
                    _fsConnect.RequestData(Requests.NearbyObjects, Definitions.NearbyObjects, (uint)(1000 * Math.Min(allowedRadarScale, (assistantSettings.ContainsKey("RadarScale") ? assistantSettings["RadarScale"] : allowedRadarScale))), getTowObjectType());

                // RADAR PANEL
                if (assistantSettings.ContainsKey("panelServer") && assistantSettings["panelServer"] != 0 && (absoluteTime - lastRadarRequest) <= 0.25)
                {
                    RadarContainer.Background = new SolidColorBrush(Colors.White);
                    generateRadarBitmap();
                }
                else if (absoluteTime - lastRadarRequest > 1 && absoluteTime - lastRadarRequest < 2) // RESTORE TRANSPARENCY
                {
                    RadarContainer.Background = new SolidColorBrush(Colors.Transparent);
                }
            }
        }

        public void generateRadarBitmap()
        {
            Rect bounds = new Rect(1, 1, 275, 250);
            double dpi = 96d;

            RenderTargetBitmap rtb = new RenderTargetBitmap((int)bounds.Width, (int)bounds.Height, dpi, dpi, PixelFormats.Pbgra32);

            DrawingVisual dv = new DrawingVisual();
            using (DrawingContext dc = dv.RenderOpen())
            {
                VisualBrush vb = new VisualBrush(RadarContainer);
                dc.DrawRectangle(vb, null, new Rect(new Point(), bounds.Size));
            }

            rtb.Render(dv);

            MemoryStream ms = new MemoryStream();
            PngBitmapEncoder encoder = new PngBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(rtb));
            encoder.Save(ms);
            bitmapdata = ms.ToArray();

            //rtbString = Convert.ToBase64String(bitmapdata);

            //Console.WriteLine("Radar generate");
        }

        private void launchInterval(object sender, EventArgs e)
        {
            if (validConnection())
            {
                if (_winchPosition != null || _carrierPosition != null || targedCatapultVelocity != 0 || thermalsWorking || towingTarget != TARGETMAX || taskInProcess)
                {
                    lastPacketReceived += 1 / (double)Math.Max(10, (int)(assistantSettings.ContainsKey("RequestsFrequency") ? assistantSettings["RequestsFrequency"] : 10));
                    _fsConnect.RequestData(Requests.PlaneInfo, Definitions.PlaneInfo);

                    // PACKET LOST??
                    if (lastPacketReceived >= 5 && taskInProcess)
                    {
                        Console.WriteLine("Packet lost, reconnect (" + lastPacketReceived + "s)");
                        toggleQuickConnect(false);
                        toggleQuickConnect(true);
                    }
                }

                if (towScanMode > TowScanMode.Disabled && (towingTarget != TARGETMAX || _trackingClass.ghostPlayerActive()))
                    _fsConnect.RequestData(Requests.NearbyObjects, Definitions.NearbyObjects, (uint)(1000 * Math.Min(allowedRadarScale, assistantSettings["RadarScale"])), getTowObjectType());

                if (assistantSettings.ContainsKey("towType") && assistantSettings["towType"] == 2 && _planeAvionicsResponse.SimOnGround == 100 && _planeInfoResponse.GpsGroundSpeed < 5 &&
                    (launchTime == 0 || launchTime - absoluteTime > 0) && (_nearbyInfoResponse == null || !_nearbyInfoResponse.ContainsKey(towingTarget)) &&
                    _planeEngineData.ThrottleLeverPosition1 < 10 && _planeEngineData.ThrottleLeverPosition2 < 10)
                {
                    levelUpGlider(true);
                }
            }
        }

        private SIMCONNECT_SIMOBJECT_TYPE getTowObjectType()
        {
            switch ((int)assistantSettings["towType"])
            {
                case 1:
                    return SIMCONNECT_SIMOBJECT_TYPE.AIRCRAFT;
                case 2:
                    return SIMCONNECT_SIMOBJECT_TYPE.AIRCRAFT;
                case 3:
                    return SIMCONNECT_SIMOBJECT_TYPE.GROUND;
                case 4:
                    return SIMCONNECT_SIMOBJECT_TYPE.BOAT;
                case 5:
                    return SIMCONNECT_SIMOBJECT_TYPE.HELICOPTER;
                default:
                    return SIMCONNECT_SIMOBJECT_TYPE.ALL;
            }

        }

        private void toggleQuickConnect(bool newStatus)
        {
            if (newStatus)
            {
                try
                {
                    _fsConnect = new FsConnect();
                    _fsConnect.Connect("Kinetic Assistant", "127.0.0.1", 500, SimConnectProtocol.Ipv4);
                }
                catch (Exception ex)
                {
                    addLogMessage(ex.Message, 2);
                    return;
                }

                try
                {
                    _fsConnect.FsDataReceived += HandleReceivedFsData;
                    //_fsConnect.AirportDataReceived += HandleReceivedAirports;
                    _fsConnect.ObjectAddremoveEventReceived += HandleReceivedSystemEvent;
                    //_fsConnect.SystemEventReceived += HandleReceivedSystemEvent;

                    Console.WriteLine("Initializing data definitions");
                    InitializeDataDefinitions(_fsConnect);

                    _fsConnect.RequestData(Requests.PlaneInfo, Definitions.PlaneInfo);
                    //_fsConnect.RequestData(Requests.PlaneCommit, Definitions.PlaneCommit);
                    //_fsConnect.RequestFacilitiesList(Requests.Airport);
                }
                catch (Exception ex) { }

                _planeInfoResponse.AbsoluteTime = 0;
                _planeInfoResponseLast.AbsoluteTime = 0;

                lastPacketReceived = 0;
                lostPacketRecovered = 0;

                ClearRecords(null, null);
            }
            else
            {
                try
                {
                    _fsConnect.Disconnect();
                    _fsConnect.Dispose();
                }
                catch (Exception ex)
                {
                    addLogMessage(ex.Message, 2);
                }

                _fsConnect = null;
            }
        }

        private void toggleConnect(object sender, RoutedEventArgs e)
        {
            Console.WriteLine("toggleConnect");

            if (!validConnection())
            {
                Application.Current.Dispatcher.Invoke(() => _radarClass.InitRadar(RadarCanvas, assistantSettings["RadarScale"] / maxRadarScale, allowedRadarScale));

                try
                {
                    _fsConnect = new FsConnect();
                    _fsConnect.Connect("Kinetic Assistant", "127.0.0.1", 500, SimConnectProtocol.Ipv4);
                }
                catch (Exception ex)
                {
                    addLogMessage(ex.Message, 2);
                    return;
                }

                try
                {
                    _fsConnect.FsDataReceived += HandleReceivedFsData;
                    //_fsConnect.AirportDataReceived += HandleReceivedAirports;
                    _fsConnect.ObjectAddremoveEventReceived += HandleReceivedSystemEvent;
                    //_fsConnect.SystemEventReceived += HandleReceivedSystemEvent;

                    Console.WriteLine("Initializing data definitions");
                    InitializeDataDefinitions(_fsConnect);

                    _fsConnect.RequestData(Requests.PlaneInfo, Definitions.PlaneInfo);
                    //_fsConnect.RequestData(Requests.PlaneCommit, Definitions.PlaneCommit);
                    //_fsConnect.RequestFacilitiesList(Requests.Airport);

                    addLogMessage("Kinetic Assistant connected");

                    Application.Current.Dispatcher.Invoke(() => playSound("true"));
                    changeButtonStatus(false, connectButton, true, "DISCONNECT");
                    changeButtonStatus(true, launchPrepareButton, true);
                    changeButtonStatus(true, hookPrepareButton, true);
                    changeButtonStatus(true, catapultlaunchButton, true);
                    changeButtonStatus(true, thermalsToggleButton, true, "Enable thermals");
                    changeButtonStatus(true, towToggleButton, true);
                    changeButtonStatus(true, towConnectButton, false);
                    changeButtonStatus(true, towInsertButton, true);
                }
                catch (Exception ex) { }

                RadarScale.Maximum = maxRadarScale;
                lastPacketReceived = 0;
                lostPacketRecovered = 0;
                _planeInfoResponse.AbsoluteTime = 0;
                _planeInfoResponseLast.AbsoluteTime = 0;

            }
            else
            {
                //Application.Current.Dispatcher.Invoke(() => _radarClass.ClearRadar(RadarCanvas));

                if (towScanMode > TowScanMode.Disabled)
                {
                    toggleScanning(null, null);
                }

                try
                {
                    addLogMessage("Kinetic Assistant disconnected");
                    //Application.Current.Dispatcher.Invoke(() => playSound("error"));
                    _fsConnect.Disconnect();
                    _fsConnect.Dispose();
                }
                catch (Exception ex)
                {
                    addLogMessage(ex.Message, 2);
                }

                _fsConnect = null;

                Application.Current.Dispatcher.Invoke(() => abortLaunch());

                changeButtonStatus(true, connectButton, true, "CONNECT");
                changeButtonStatus(false, launchPrepareButton, false);
                changeButtonStatus(false, hookPrepareButton, false);
                changeButtonStatus(false, catapultlaunchButton, false);
                changeButtonStatus(false, thermalsToggleButton, false, "Disable thermals");
                changeButtonStatus(true, towToggleButton, false);
                changeButtonStatus(true, towInsertButton, false);

                Console.WriteLine("Disconnected");
            }
        }

        private bool validConnection()
        {
            if (_fsConnect == null/* || !_fsConnect.Connected*/)
                return false;
            else
                return true;
        }

        private bool restrictionsPassed()
        {
            if (assistantSettings["realisticRestrictions"] == 0 || _planeAvionicsResponse.BrakeParkingPosition != 100 && (_planeAvionicsResponse.SimOnGround == 100 || _planeInfoResponse.AltitudeAboveGround <= _planeAvionicsResponse.StaticCGtoGround * 1.25))
                return true;

            return false;
        }


        // WINCH START
        private void toggleLaunchPrepare(object sender, RoutedEventArgs e)
        {
            Console.WriteLine("toggleLaunchPrepare");

            if (!validConnection())
            {
                Console.WriteLine("connection lost");
                Application.Current.Dispatcher.Invoke(() => abortLaunch());
            }
            else
            {
                _planeAvionicsResponse.WaterRudderHandlePosition = 0;
                _planeAvionicsResponseLast.WaterRudderHandlePosition = 0;

                if (_winchPosition == null) // PREPARE TO LAUNCH
                {
                    if (restrictionsPassed())
                    {
                        _planeAvionicsResponse.WaterRudderHandlePosition = 50;
                        _planeAvionicsResponseLast.WaterRudderHandlePosition = 50;

                        lastPreparePressed = absoluteTime;
                        addLogMessage("Creating winch");

                        cableLength = Math.Max(100, assistantSettings["stringLength"]);
                        _winchPosition = _mathClass.getWinchPosition(_planeInfoResponse, cableLength - 10);

                        Application.Current.Dispatcher.Invoke(() => _radarClass.InsertWinch(RadarCanvas));
                        Application.Current.Dispatcher.Invoke(() => setRadarScale(cableLength / 1000 * 1.2));

                        Console.WriteLine($"Current location: {_planeInfoResponse.Latitude * 180 / Math.PI} {_planeInfoResponse.Longitude * 180 / Math.PI}");
                        Console.WriteLine($"Winch location: {_winchPosition.location.Latitude * 180 / Math.PI} {_winchPosition.location.Longitude * 180 / Math.PI}");
                        Console.WriteLine($"Bearing: {_planeInfoResponse.PlaneHeading * 180 / Math.PI}deg Distance: {cableLength / 1000}km");

                        //showMessage("Winch cable connected - disengage parking brakes to launch", _fsConnect);
                        Application.Current.Dispatcher.Invoke(() => changeButtonStatus(false, launchPrepareButton, true, "Release winch cable"));
                        launchTime = absoluteTime + 5.001;
                        showMessage("Launch in " + Math.Floor(launchTime - absoluteTime) + " seconds", _fsConnect);
                        Application.Current.Dispatcher.Invoke(() => playSound("true"));
                    }
                    else
                    {
                        showMessage("Winch can be activated only on the ground and with brakes disengaged", _fsConnect);
                        Application.Current.Dispatcher.Invoke(() => playSound("error"));
                    }
                }
                else // ABORT LAUNCH
                {
                    showMessage("Winch cable released. Gained altitude: " + (_planeInfoResponse.Altitude - _winchPosition.alt).ToString("0.0") + " meters", _fsConnect);
                    Application.Current.Dispatcher.Invoke(() => playSound("false"));
                    Application.Current.Dispatcher.Invoke(() => abortLaunch());
                }

                commitAvionicsData();
            }
        }

        private void abortLaunch()
        {
            Console.WriteLine("abortLaunch");

            Application.Current.Dispatcher.Invoke(() => changeButtonStatus(_fsConnect != null ? true : false, launchPrepareButton, _fsConnect != null ? true : false, "Attach winch cable"));

            if (validConnection() && launchTime != 0 && _winchPosition != null)
            {
                winchAbortInitiated = true;
            }
            else
            {
                _winchPosition = null;
                Application.Current.Dispatcher.Invoke(() => _radarClass.RemoveWinch(RadarCanvas));
                launchTime = 0;
                cableLength = 0;

                _planeAvionicsResponse.WaterRudderHandlePosition = 0;
                _planeAvionicsResponseLast.WaterRudderHandlePosition = 0;

                if (validConnection())
                    commitAvionicsData();
            }
        }

        private void processLaunch()
        {
            bool applyForces;

            if (winchAbortInitiated)
            {
                _winchPosition = null;
                Application.Current.Dispatcher.Invoke(() => _radarClass.RemoveWinch(RadarCanvas));
                launchTime = 0;
                cableLength = 0;
                winchAbortInitiated = false;
                applyForces = true;

                _planeAvionicsResponse.WaterRudderHandlePosition = 0;
                _planeAvionicsResponseLast.WaterRudderHandlePosition = 0;
                commitAvionicsData();
            }
            else
            {
                // GET ANGLE TO WINCH POSITION
                winchDirection _winchDirection = _mathClass.getForceDirection(_winchPosition, _planeInfoResponse);
                double targetVelocity = 0.514 * Math.Max(10, assistantSettings["targetSpeed"]);
                double bodyAcceleration = 0;

                if (thermalsDebugActive > 0 || httpServerActive)
                    Application.Current.Dispatcher.Invoke(() => _radarClass.UpdateWinch(RadarCanvas, _winchDirection, Math.Min(maxRadarScale, assistantSettings["RadarScale"])));

                // GET DRAFT CABLE TENSION
                double accelerationLimit = (assistantSettings["realisticFailures"] == 1 ? 6 : 30) * 9.81;
                double cableTension = _mathClass.getCableTension(cableLength, Math.Max(1, assistantSettings["elasticExtension"]), _winchDirection);

                // SHORTEN THE STRING
                if (launchTime != 0 && launchTime - absoluteTime < 0 && cableLength > 10)
                {
                    double pitchCompensation = 1;// Math.Pow(Math.Abs(Math.Cos(_winchDirection.climbAngle)), 1.25);
                    double tensionMultiplier = 1.5 - Math.Pow(cableTension, 2);

                    double timePassed = -launchTime + absoluteTime;
                    double StartTime = Math.Max(assistantSettings["winchSpeedUp"], 3);

                    if (timePassed < StartTime) // SMOOTH START
                    {
                        cableLength -= timePassed / StartTime * tensionMultiplier * pitchCompensation * targetVelocity * lastFrameTiming;
                    }
                    else
                    {
                        cableLength -= tensionMultiplier * pitchCompensation * targetVelocity * lastFrameTiming;
                    }

                }

                // GET FINAL CABLE TENSION
                cableTension = 0.2 * cabelTensionPrev + 0.4 * cableTension + 0.4 * _mathClass.getCableTension(cableLength, Math.Max(1, assistantSettings["elasticExtension"]), _winchDirection);

                cabelTensionPrev = cableTension;

                // LEVEL UP GLIDER BEFORE LAUNCH
                levelUpGlider();

                if (cableTension > 0)
                {
                    bodyAcceleration = _mathClass.getBodyVelocity(_winchDirection, _planeCommit, cableTension, accelerationLimit, -cableLength, -cableLengthPrev, -cableLengthPrePrev, lastFrameTiming);
                }

                applyForces = applyWinchForces(bodyAcceleration, accelerationLimit, _winchDirection, targetVelocity, "Winch", assistantSettings["connectionPoint"]);

                cableLengthPrePrev = cableLengthPrev;
                cableLengthPrev = cableLength;
            }

            if (applyForces)
            {
                if (!double.IsNaN(_planeCommit.VelocityBodyX) && !double.IsNaN(_planeCommit.VelocityBodyY) && !double.IsNaN(_planeCommit.VelocityBodyZ))
                {
                    try
                    {
                        _fsConnect.UpdateData(Definitions.PlaneCommit, _planeCommit);
                    }
                    catch (Exception ex)
                    {
                        addLogMessage(ex.Message);
                    }
                }
            }
        }

        public void levelUpGlider(bool control = false)
        {
            if (_planeAvionicsResponse.SimOnGround == 100 && _planeInfoResponse.GpsGroundSpeed < 5)
            {
                _planeRotateVelocity.RotationVelocityBodyX = 0;
                _planeRotateVelocity.RotationVelocityBodyY = _planeAvionicsResponse.RudderPosition / 200 * lastFrameTiming;
                _planeRotateVelocity.RotationVelocityBodyZ = -Math.Sin(_planeInfoResponse.PlaneBank) * Math.Pow(Math.Abs(Math.Sin(_planeInfoResponse.PlaneBank)), 0.5);

                //Console.WriteLine($"Leveling {_planeRotateVelocity.RotationVelocityBodyZ:F5}");

                if (!double.IsNaN(_planeRotateVelocity.RotationVelocityBodyX) && !double.IsNaN(_planeRotateVelocity.RotationVelocityBodyY) && !double.IsNaN(_planeRotateVelocity.RotationVelocityBodyZ))
                {
                    try
                    {
                        _fsConnect.UpdateData(Definitions.PlaneRotationVelocity, _planeRotateVelocity);
                    }
                    catch (Exception ex)
                    {
                        addLogMessage(ex.Message);
                    }
                }

                if (control)
                {
                    double pushSpeed = 0.5 * _planeInfoResponse.GpsGroundSpeed + 0.5 * Math.Min(2, Math.Max(-2, -_planeAvionicsResponse.YokeYPosition / 25));
                    _planeCommit.VelocityBodyX = 0;
                    _planeCommit.VelocityBodyY = _planeInfoResponse.VelocityBodyY + _planeInfoResponse.AccelerationBodyY * lastFrameTiming * lastFrameTiming;
                    _planeCommit.VelocityBodyZ = _planeInfoResponse.AccelerationBodyZ * lastFrameTiming + pushSpeed;

                    if (!double.IsNaN(_planeCommit.VelocityBodyX) && !double.IsNaN(_planeCommit.VelocityBodyY) && !double.IsNaN(_planeCommit.VelocityBodyZ))
                    {
                        try
                        {
                            _fsConnect.UpdateData(Definitions.PlaneCommit, _planeCommit);
                        }
                        catch (Exception ex)
                        {
                            addLogMessage(ex.Message);
                        }
                    }
                }
            }
        }

        private bool applyWinchForces(double bodyAcceleration, double accelerationLimit, winchDirection _winchDirection, double targetVelocity, string type, double connectionPoint)
        {
            if (double.IsNaN(bodyAcceleration) || lastFrameTiming == 0)
            {
                return false;
            }

            // RANDOM FAILURE
            if (assistantSettings["randomFailures"] > 0 && Math.Abs(bodyAcceleration) > 0)
            {
                double rnd = new Random().NextDouble();
                double thrs = Math.Pow(10, assistantSettings["randomFailures"]) * lastFrameTiming * Math.Abs(bodyAcceleration) / accelerationLimit / 1000;
                //Console.WriteLine(rnd + " / " + thrs);

                if (rnd < thrs)
                {
                    bodyAcceleration = 1.1 * accelerationLimit;
                }
            }


            if (type == "Tow" && (towCableLength > towCableDesired * (connectionPoint == 2 ? 0.2 : 1) || towCableLength < towCableDesired * (connectionPoint == 2 ? 0.2 : 1) - 1))
            {
                Console.WriteLine("Shortening tow rope from " + towCableLength + " to " + towCableDesired * (connectionPoint == 2 ? 0.2 : 1));

                towCableLength -= lastFrameTiming * (towCableLength > towCableDesired * (connectionPoint == 2 ? 0.2 : 1) ? 1 : -1);
                towPrevDist -= lastFrameTiming * (towCableLength > towCableDesired * (connectionPoint == 2 ? 0.2 : 1) ? 1 : -1);
                towPrePrevDist -= lastFrameTiming * (towCableLength > towCableDesired * (connectionPoint == 2 ? 0.2 : 1) ? 1 : -1);
            }

            //Console.WriteLine($"{type}: {bodyAcceleration / 9.81:F2}g {(type == "Winch" ? cableLength : towCableLength):F2}m / {_winchDirection.distance:F2}m h{(_winchDirection.heading * 180 / Math.PI):F0}deg p{(_winchDirection.pitch * 180 / Math.PI):F0}deg");

            double angleHLimit = (_planeAvionicsResponse.SimOnGround == 0 ? 89 : 179) * Math.PI / 180;
            double angleVLimit = 69 * Math.PI / 180;

            // WHAT IS THIS? SKIP!
            if (Math.Abs(bodyAcceleration) > 1000 * accelerationLimit || (insertTowPlanePressed != 0 && _planeInfoResponse.AbsoluteTime - insertTowPlanePressed < AIholdInterval))
            {
                Console.WriteLine("SKIP ITERATION: " + bodyAcceleration + " / " + accelerationLimit);
                return false;
            }
            // FAIL
            else if (Math.Abs(bodyAcceleration) > accelerationLimit || Math.Abs(_winchDirection.heading) > angleHLimit || Math.Abs(_winchDirection.pitch) > angleVLimit)
            {
                if (type == "Winch")
                {
                    showMessage(bodyAcceleration > accelerationLimit ?
                        type + " cable failure" :
                        type + " cable released. Gained altitude: " + (_planeInfoResponse.Altitude - _winchPosition.alt).ToString("0.0") + " meters", _fsConnect);

                    Application.Current.Dispatcher.Invoke(() => playSound("false"));
                    winchAbortInitiated = true;
                    Application.Current.Dispatcher.Invoke(() => abortLaunch());
                }
                else if (type == "Tow" && towingTarget != TARGETMAX)
                {
                    toggleTowCable(towingTarget);
                }
            }
            else
            {
                _planeCommit.VelocityBodyX += _winchDirection.localForceDirection.X * bodyAcceleration * lastFrameTiming;
                _mathClass.restrictAirspeed(_planeCommit.VelocityBodyX, targetVelocity, lastFrameTiming);
                _planeCommit.VelocityBodyY += (_planeInfoResponse.AccelerationBodyY + _winchDirection.localForceDirection.Y * bodyAcceleration) * lastFrameTiming;
                _mathClass.restrictAirspeed(_planeCommit.VelocityBodyY, targetVelocity, lastFrameTiming);
                _planeCommit.VelocityBodyZ += (_planeInfoResponse.AccelerationBodyZ + _winchDirection.localForceDirection.Z * bodyAcceleration) * lastFrameTiming;
                _mathClass.restrictAirspeed(_planeCommit.VelocityBodyZ, targetVelocity, lastFrameTiming);

                // APPLY ROTATION FORCES
                //double degreeThershold = (_planeAvionicsResponse.SimOnGround == 0 ? 20 : 1) * Math.PI / 180;
                double accelThreshold = 9.81 / (_planeAvionicsResponse.SimOnGround == 0 ? 1000 : 50);

                if (bodyAcceleration > accelThreshold /*&& (Math.Abs(_winchDirection.pitch) > degreeThershold || Math.Abs(_winchDirection.heading) > degreeThershold)*/)
                {
                    // CG CONNECTION
                    if (connectionPoint == 0)
                    {
                        _winchDirection.pitch += Math.PI/6;
                        while (_winchDirection.pitch > Math.PI) { _winchDirection.pitch -= 2 * Math.PI; }
                        while (_winchDirection.pitch < -Math.PI) { _winchDirection.pitch += 2 * Math.PI; }

                        double rotationForce = 10 * (bodyAcceleration - accelThreshold) / accelerationLimit;

                        double sinHeading = Math.Sign(_winchDirection.heading) * Math.Pow(Math.Abs(Math.Sin(_winchDirection.heading)), 1.5);
                        double sinPitch = Math.Sign(_winchDirection.pitch) * Math.Pow(Math.Abs(Math.Sin(_winchDirection.pitch)), 1.5);

                        //Console.WriteLine("Math.Sign(_winchDirection.pitch)" + Math.Sign(_winchDirection.pitch) + " Math.Sin(_winchDirection.pitch / 2):" + Math.Sin(_winchDirection.pitch / 2) + " Math.Pow(Math.Sin(_winchDirection.pitch / 2), 1.5):" + Math.Pow(Math.Sin(_winchDirection.pitch / 2), 1.5));
                        //Console.WriteLine("_planeRotateVelocity.RotationVelocityBodyX:" + _planeRotateVelocity.RotationVelocityBodyX + " rotationForce:" + rotationForce + " rotationForce:" + rotationForce + " sinPitch:" + sinPitch + " lastFrameTiming:" + lastFrameTiming);

                        //Console.WriteLine($"rotationForce {rotationForce:F2} Pitch {_winchDirection.pitch:F2} Heading {_winchDirection.heading:F2}");

                        if (_planeAvionicsResponse.SimOnGround == 100)
                        {
                            _planeRotateVelocity.RotationVelocityBodyX = -rotationForce * sinPitch / 10 * lastFrameTiming;
                            _planeRotateVelocity.RotationVelocityBodyY = 10 * rotationForce * sinHeading * lastFrameTiming;
                            _planeRotateVelocity.RotationVelocityBodyZ = lastFrameTiming * rotationForce * sinHeading * lastFrameTiming;

                            if (!double.IsNaN(_planeRotateVelocity.RotationVelocityBodyX) && Math.Abs(_planeRotateVelocity.RotationVelocityBodyX) < 1000 &&
                                !double.IsNaN(_planeRotateVelocity.RotationVelocityBodyY) && Math.Abs(_planeRotateVelocity.RotationVelocityBodyY) < 1000 &&
                                !double.IsNaN(_planeRotateVelocity.RotationVelocityBodyZ) && Math.Abs(_planeRotateVelocity.RotationVelocityBodyZ) < 1000)
                            {
                                try
                                {
                                    _fsConnect.UpdateData(Definitions.PlaneRotationVelocity, _planeRotateVelocity);
                                }
                                catch (Exception ex)
                                {
                                    addLogMessage(ex.Message);
                                }
                            }
                        }
                        else
                        {
                            _planeRotateAccel.RotationAccelerationBodyX += -100 * rotationForce * sinPitch;
                            _planeRotateAccel.RotationAccelerationBodyY += 20 * rotationForce * sinHeading;
                            _planeRotateAccel.RotationAccelerationBodyZ += 100 * rotationForce * sinHeading;

                            //Console.WriteLine($"Pitch {_planeRotateAccel.RotationAccelerationBodyX:F2} Heading {_planeRotateAccel.RotationAccelerationBodyY:F2}");

                            if (!double.IsNaN(_planeRotateAccel.RotationAccelerationBodyX) && Math.Abs(_planeRotateAccel.RotationAccelerationBodyX) < 1000 &&
                                !double.IsNaN(_planeRotateAccel.RotationAccelerationBodyY) && Math.Abs(_planeRotateAccel.RotationAccelerationBodyY) < 1000 &&
                                !double.IsNaN(_planeRotateAccel.RotationAccelerationBodyZ) && Math.Abs(_planeRotateAccel.RotationAccelerationBodyZ) < 1000)
                            {
                                try
                                {
                                    _fsConnect.UpdateData(Definitions.PlaneRotateAccel, _planeRotateAccel);
                                }
                                catch (Exception ex)
                                {
                                    addLogMessage(ex.Message);
                                }
                            }
                        }
                    }
                    // NOSE/TAIL CONNECTION
                    else
                    {
                        if (connectionPoint == 2) // 
                        {
                            _winchDirection.heading += Math.PI;
                            while (_winchDirection.heading > Math.PI) { _winchDirection.heading -= 2 * Math.PI; }
                            while (_winchDirection.heading < -Math.PI) { _winchDirection.heading += 2 * Math.PI; }
                        }

                        double rotationForce = 10 * (bodyAcceleration - accelThreshold) / accelerationLimit;

                        double sinHeading = Math.Sign(_winchDirection.heading) * Math.Pow(Math.Abs(Math.Sin(_winchDirection.heading)), 1.5);
                        double sinPitch = Math.Sign(_winchDirection.pitch) * Math.Pow(Math.Abs(Math.Sin(_winchDirection.pitch)), 1.5);

                        //Console.WriteLine("Math.Sign(_winchDirection.pitch)" + Math.Sign(_winchDirection.pitch) + " Math.Sin(_winchDirection.pitch / 2):" + Math.Sin(_winchDirection.pitch / 2) + " Math.Pow(Math.Sin(_winchDirection.pitch / 2), 1.5):" + Math.Pow(Math.Sin(_winchDirection.pitch / 2), 1.5));
                        //Console.WriteLine("_planeRotateVelocity.RotationVelocityBodyX:" + _planeRotateVelocity.RotationVelocityBodyX + " rotationForce:" + rotationForce + " rotationForce:" + rotationForce + " sinPitch:" + sinPitch + " lastFrameTiming:" + lastFrameTiming);

                        //Console.WriteLine($"rotationForce {rotationForce:F2} Pitch {_winchDirection.pitch:F2} Heading {_winchDirection.heading:F2}");

                        if (_planeAvionicsResponse.SimOnGround == 100)
                        {
                            _planeRotateVelocity.RotationVelocityBodyX = -rotationForce * sinPitch / 10 * lastFrameTiming;
                            _planeRotateVelocity.RotationVelocityBodyY = 25 * rotationForce * sinHeading * lastFrameTiming;
                            _planeRotateVelocity.RotationVelocityBodyZ = lastFrameTiming * rotationForce * sinHeading * lastFrameTiming;

                            if (!double.IsNaN(_planeRotateVelocity.RotationVelocityBodyX) && Math.Abs(_planeRotateVelocity.RotationVelocityBodyX) < 1000 &&
                                !double.IsNaN(_planeRotateVelocity.RotationVelocityBodyY) && Math.Abs(_planeRotateVelocity.RotationVelocityBodyY) < 1000 &&
                                !double.IsNaN(_planeRotateVelocity.RotationVelocityBodyZ) && Math.Abs(_planeRotateVelocity.RotationVelocityBodyZ) < 1000)
                            {
                                try
                                {
                                    _fsConnect.UpdateData(Definitions.PlaneRotationVelocity, _planeRotateVelocity);
                                }
                                catch (Exception ex)
                                {
                                    addLogMessage(ex.Message);
                                }
                            }
                        }
                        else
                        {
                            _planeRotateAccel.RotationAccelerationBodyX += - 250 * rotationForce * sinPitch;
                            _planeRotateAccel.RotationAccelerationBodyY += 250 * rotationForce * sinHeading;
                            _planeRotateAccel.RotationAccelerationBodyZ += 10 * rotationForce * sinHeading;

                            //Console.WriteLine($"Pitch {_planeRotateAccel.RotationAccelerationBodyX:F2} Heading {_planeRotateAccel.RotationAccelerationBodyY:F2}");

                            if (!double.IsNaN(_planeRotateAccel.RotationAccelerationBodyX) && Math.Abs(_planeRotateAccel.RotationAccelerationBodyX) < 1000 &&
                                !double.IsNaN(_planeRotateAccel.RotationAccelerationBodyY) && Math.Abs(_planeRotateAccel.RotationAccelerationBodyY) < 1000 &&
                                !double.IsNaN(_planeRotateAccel.RotationAccelerationBodyZ) && Math.Abs(_planeRotateAccel.RotationAccelerationBodyZ) < 1000)
                            {
                                try
                                {
                                    _fsConnect.UpdateData(Definitions.PlaneRotateAccel, _planeRotateAccel);
                                }
                                catch (Exception ex)
                                {
                                    addLogMessage(ex.Message);
                                }
                            }
                        }
                    }
                }

                return true;
            }

            return false;
        }
        // WINCH END

        // TOW START
        private void toggleScanning(object sender, RoutedEventArgs e)
        {
            Console.WriteLine("toggleScanning");

            _nearbyInfoResponse = new Dictionary<uint, winchPosition>();
            _nearbyInfoResponseLast = new Dictionary<uint, winchPosition>();
            _nearbyInfoResponsePreLast = new Dictionary<uint, winchPosition>();

            if (!validConnection())
            {
                Console.WriteLine("connection lost");
            }
            else
            {
                if (towScanMode == TowScanMode.Disabled) // START SEARCH
                {
                    towScanMode = TowScanMode.Scan;
                    Application.Current.Dispatcher.Invoke(() => changeButtonStatus(false, towToggleButton, true, "STOP"));
                    Application.Current.Dispatcher.Invoke(() => changeButtonStatus(true, towConnectButton, true, "Attach To Closest"));

                    Application.Current.Dispatcher.Invoke(() => toggleSidebarWindow(true));
                }
                else // STOP SEARCH
                {
                    towScanMode = TowScanMode.Disabled;
                    if (towingTarget != TARGETMAX)
                    {
                        toggleTowCable(towingTarget);
                    }

                    Application.Current.Dispatcher.Invoke(() => _radarClass.clearRadarNearby(RadarCanvas));

                    _nearbyInfoResponse = new Dictionary<uint, winchPosition>();

                    Application.Current.Dispatcher.Invoke(() => nearbyObjects.Children.Clear());
                    Application.Current.Dispatcher.Invoke(() => farObjects.Children.Clear());
                    Application.Current.Dispatcher.Invoke(() => currentTarget.Children.Clear());
                    Application.Current.Dispatcher.Invoke(() => changeButtonStatus(true, towToggleButton, true, "SEARCH"));
                    Application.Current.Dispatcher.Invoke(() => changeButtonStatus(true, towConnectButton, false, "Attach To Closest"));

                    //toggleSidebarWindow(false);
                }
            }
        }

        private void toggleTowClosest(object sender, EventArgs e)
        {
            attachTowCable(null, new EventArgs());
        }

        private void aiTowPlane(object sender, EventArgs e)
        {
            if (validConnection())
            {
                if (restrictionsPassed())
                {
                    if (towPlaneTrack.Items.Count > 0)
                    {
                        string trackName = ((ComboBoxItem)(towPlaneTrack.SelectedItem)).Tag.ToString();
                        if (!string.IsNullOrEmpty(trackName) && File.Exists(trackName))
                        {
                            Console.WriteLine("Tow plane inserting");
                            ClearRecords(null, null);
                            changePlayStatus(false);
                            beforeLoadTrackRecord(1);
                            loadTrackRecord(trackName, true);
                            towPlaneInserted = true;
                            Console.WriteLine("Tow plane inserted");
                        }
                    }
                    else
                    {
                        showMessage("No flight records found in TOWPLANES folder", _fsConnect);
                    }
                }
                else
                {
                    showMessage("Tow plane can be inserted only on the ground and with brakes disengaged", _fsConnect);
                }
            }
        }

        private void assignTowPlane(uint id, winchPosition pos)
        {
            winchDirection direction = _mathClass.getForceDirection(pos, _planeInfoResponse);

            double menuValue = 0.2;
            towCableDesired = 40;
            double cableLength = towCableDesired;

            Console.WriteLine("assignTowPlane in " + direction.distance + "m");

            //towSearchRadius.Text = menuValue.ToString(".0");
            towCableDesired *= 2;
            cableLength = towCableDesired - 10;
            //teleportTowPlane(id);
            //return;

            if (RadarScale.Value < menuValue)
            {
                setRadarScale(menuValue);
            }

            insertedTowPlane = new KeyValuePair<uint, bool>(id, true);
            toggleTowCable(id, pos, cableLength);
            //assignFlightPlan(id);

            //showMessage("AI tow plane assigned", _fsConnect);
        }

        private void teleportTowPlane(uint id)
        {
            GeoLocation newPlaneLocation = _mathClass.FindPointAtDistanceFrom(new GeoLocation(_planeInfoResponse.Latitude, _planeInfoResponse.Longitude), _planeInfoResponse.PlaneHeading, 0.08);

            TowInfoResponse towInfo = new TowInfoResponse();
            towInfo.Altitude = _planeInfoResponse.Altitude;
            towInfo.Latitude = newPlaneLocation.Latitude;
            towInfo.Longitude = newPlaneLocation.Longitude;
            towInfo.Heading = _planeInfoResponse.PlaneHeading;
            towInfo.Bank = 0;
            towInfo.VelocityBodyX = towInfo.VelocityBodyY = towInfo.VelocityBodyZ = 0;

            assignFlightPlan(id);

            Console.WriteLine("Tow plane teleported");

            if (!double.IsNaN(towInfo.Altitude) && !double.IsNaN(towInfo.Latitude) && !double.IsNaN(towInfo.Longitude) && !double.IsNaN(towInfo.Heading) && !double.IsNaN(towInfo.Bank))
            {
                try
                {
                    _fsConnect.UpdateData(Definitions.TowPlane, towInfo, id);
                }
                catch (Exception ex)
                {
                    addLogMessage(ex.Message);
                }
            }
        }

        private void teleportGhostPlane(GhostPlane gp, double progress = 0)
        {
            if (gp.TrackPoints.Count > 0)
            {
                int i = 0;
                foreach (TrackPoint point in gp.TrackPoints)
                {
                    if (point.Timer >= progress)
                        break;

                    i++;
                }

                if (i < gp.TrackPoints.Count)
                {

                    TowInfoResponse towInfo = new TowInfoResponse();
                    towInfo.Altitude = gp.TrackPoints[i].Elevation;
                    towInfo.Latitude = gp.TrackPoints[i].Location.Latitude;
                    towInfo.Longitude = gp.TrackPoints[i].Location.Longitude;
                    towInfo.Heading = gp.TrackPoints[i].Heading * Math.PI / 180;
                    towInfo.Bank = gp.TrackPoints[i].Roll * Math.PI / 180;
                    towInfo.VelocityBodyX = towInfo.VelocityBodyY = towInfo.VelocityBodyZ = 0;

                    if (!double.IsNaN(towInfo.Altitude) && !double.IsNaN(towInfo.Latitude) && !double.IsNaN(towInfo.Longitude) && !double.IsNaN(towInfo.Heading) && !double.IsNaN(towInfo.Bank))
                    {
                        try
                        {
                            _fsConnect.UpdateData(Definitions.TowPlane, towInfo, gp.ID);
                        }
                        catch (Exception ex)
                        {
                            addLogMessage(ex.Message);
                        }
                    }
                }
            }
        }

        private void assignFlightPlan(uint id)
        {
            if (plnPath != "")
                _fsConnect.AISetAircraftFlightPlan(id, plnPath, Definitions.TowPlane);
        }

        private void nearbyInterval(object sender, EventArgs e)
        {
            if (validConnection())
            {
                if (threadServer != null && server != null)
                {
                    Location loc = new Location();
                    loc.lat = _planeInfoResponse.Latitude * 180 / Math.PI;
                    loc.lon = _planeInfoResponse.Longitude * 180 / Math.PI;
                    loc.speed = _planeInfoResponse.GpsGroundSpeed;
                    loc.alt = _planeInfoResponse.Altitude;
                    loc.time = DateTime.UtcNow;
                    loc.ha = _planeInfoResponse.PlaneHeading * 180 / Math.PI;
                    //loc.va

                    server.GpsUpdate(loc);
                }

                if (towScanMode > TowScanMode.Disabled /*&& _nearbyInfoResponse.Count > 0*/)
                {
                    if (insertTowPlanePressed != 0 && (_planeInfoResponse.AbsoluteTime - insertTowPlanePressed) > AIholdInterval)
                        insertTowPlanePressed = 0;

                    if (insertTowPlanePressed != 0)
                        Console.WriteLine("towingTarget: " + towingTarget + " time: " + (_planeInfoResponse.AbsoluteTime - insertTowPlanePressed) + " towScanMode: " + towScanMode + " insertedTowPlane: " + insertedTowPlane.ToString());

                    bool abort = false;
                    nearbyObjects.Children.Clear();
                    farObjects.Children.Clear();
                    currentTarget.Children.Clear();

                    SortedDictionary<double, Button> nearbyDict = new SortedDictionary<double, Button>();
                    SortedDictionary<double, Button> farDict = new SortedDictionary<double, Button>();
                    KeyValuePair<double, Button> curr = new KeyValuePair<double, Button>();

                    try
                    {
                        KeyValuePair<uint, winchPosition> tempPos = new KeyValuePair<uint, winchPosition>();
                        KeyValuePair<double, KeyValuePair<uint, winchPosition>> closestAI = new KeyValuePair<double, KeyValuePair<uint, winchPosition>>(0, tempPos);

                        foreach (KeyValuePair<uint, winchPosition> obj in new Dictionary<uint, winchPosition>(_nearbyInfoResponse))
                        {
                            if (obj.Key.ToString() == "1")
                                continue;

                            // FILTER OUT CATEGORIES
                            if (getTowObjectType() == SIMCONNECT_SIMOBJECT_TYPE.ALL)
                            {
                                switch (obj.Value.category.ToLower())
                                {
                                    case "airplane":
                                    case "groundvehicle":
                                    case "staticobject":
                                    case "human":
                                    case "boat":
                                    case "":
                                        continue;
                                }
                            }

                            winchDirection dir = _mathClass.getForceDirection(obj.Value, _planeInfoResponse);

                            Button label = new Button();
                            label.Tag = obj.Key.ToString();
                            label.FontSize = 10;
                            label.Background = new SolidColorBrush(Colors.Transparent);

                            string title = _trackingClass.possiblyGetPlaneName(obj.Key, obj.Value.title.Replace("_", " "));
                            Console.WriteLine("title: " + title);
                            label.Content = title + " (" + dir.distance.ToString(".0m") + ")";

                            if (dir.distance < 2)
                            {
                                // SKIP PLAYER
                            }
                            // AVAILABLE OBJECTS + CURRENT TARGET
                            else if (dir.distance <= 1000 * Math.Min(allowedRadarScale, assistantSettings["RadarScale"]) || obj.Key == towingTarget)
                            {
                                label.Foreground = new SolidColorBrush(obj.Key == towingTarget ? Colors.DarkRed : Colors.DarkGreen);
                                label.BorderBrush = new SolidColorBrush(obj.Key == towingTarget ? Colors.DarkRed : Colors.DarkGreen);
                                label.Margin = new Thickness(5, 5, 5, 5);
                                label.Height = 26;
                                label.Click += attachTowCable;
                                label.Cursor = Cursors.Hand;

                                // IS IT OUR AI TOWPLANE?
                                if ((towScanMode >= TowScanMode.TowSearch || obj.Key == insertedTowPlane.Key) && towingTarget == TARGETMAX && obj.Value.title == "Tow Plane")
                                {
                                    if ((_nearbyInfoResponse.ContainsKey(obj.Key) || _nearbyInfoResponseLast.ContainsKey(obj.Key) || _nearbyInfoResponsePreLast.ContainsKey(obj.Key)) && restrictionsPassed())
                                    {
                                        addLogMessage("Towplane found");
                                        assignTowPlane(obj.Key, obj.Value);
                                    }
                                }

                                if (obj.Key == towingTarget)
                                {
                                    currentTarget.Children.Add(label);
                                    curr = new KeyValuePair<double, Button>(dir.distance, label);
                                }
                                else
                                {
                                    if (!nearbyDict.ContainsKey(dir.distance))
                                    {
                                        nearbyDict.Add(dir.distance, label);
                                    }
                                }
                            }
                            else
                            {
                                label.Margin = new Thickness(5, 1, 5, 1);
                                label.IsEnabled = false;

                                if (!farDict.ContainsKey(dir.distance))
                                {
                                    farDict.Add(dir.distance, label);
                                }
                            }

                            if (_nearbyInfoResponseLast.ContainsKey(obj.Key)) { _nearbyInfoResponsePreLast[obj.Key] = _nearbyInfoResponseLast[obj.Key]; }
                            if (_nearbyInfoResponse.ContainsKey(obj.Key)) { _nearbyInfoResponseLast[obj.Key] = _nearbyInfoResponse[obj.Key]; }

                            // CHECK CURRENT TARGET EXISTANCE
                            if (towingTarget != TARGETMAX &&
                                !_nearbyInfoResponse.ContainsKey(towingTarget) && !_nearbyInfoResponseLast.ContainsKey(towingTarget) && !_nearbyInfoResponsePreLast.ContainsKey(towingTarget))
                                abort = true;

                            if (abort)
                            {
                                addLogMessage("Tow plane lost");
                                toggleTowCable(towingTarget);
                            }
                        }

                        // AI TOW NOT FOUND - INSERT NEW ONE
                        /*if (_planeInfoResponse.AbsoluteTime - insertTowPlanePressed > 2 && towScanMode == TowScanMode.TowInsert)
                        {
                            if (plnPath != "")
                            {
                                //createFlightPlan();
                                insertTowPlane();

                                // CONTINUE SEARCH
                                towScanMode = TowScanMode.TowSearch;
                            }
                        }
                        // AI TOW INSERTED BUT NOT FOUND - TRY TO TELEPORT IT
                        else */
                        if (towingTarget == TARGETMAX && (_planeInfoResponse.AbsoluteTime - insertTowPlanePressed) > 6 && towScanMode >= TowScanMode.TowSearch && insertedTowPlane.Key != TARGETMAX && !insertedTowPlane.Value)
                        {
                            Console.WriteLine("Teleporting tow plane");
                            teleportTowPlane(insertedTowPlane.Key);
                        }

                        _nearbyInfoResponse = new Dictionary<uint, winchPosition>();

                        foreach (KeyValuePair<Double, Button> btn in nearbyDict)
                            nearbyObjects.Children.Add(btn.Value);

                        foreach (KeyValuePair<Double, Button> btn in farDict)
                            farObjects.Children.Add(btn.Value);

                        if (curr.Key != 0)
                        {
                            nearbyDict.Add(curr.Key, curr.Value);
                        }

                        if (thermalsDebugActive > 0 || httpServerActive)
                            Application.Current.Dispatcher.Invoke(() => _radarClass.InsertRadarNearby(RadarCanvas, nearbyDict, this));

                    }
                    catch (Exception ex)
                    {
                        addLogMessage(ex.Message);
                    }


                    // SET REPLAY SLIDERS
                    try
                    {
                        foreach (GhostPlane ghostPlane in _trackingClass.ghostPlanes)
                        {
                            if (ghostPlane.Progress != 0 && ghostPlane.ID != 99999999)
                            {
                                foreach (StackPanel group in GhostsList.Children)
                                    if (group.Tag.ToString() == ghostPlane.ID.ToString())
                                    {
                                        foreach (var el in group.Children)
                                            if (el.GetType() == typeof(Slider))
                                            {
                                                ((Slider)el).Value = ghostPlane.Progress;
                                                break;
                                            }

                                        break;
                                    }
                            }
                        }
                    }
                    catch { }
                }
            }
        }

        public void attachTowCable(object sender, EventArgs e)
        {
            uint newTarget = TARGETMAX;

            if (sender == null)
            {
                if (towingTarget != TARGETMAX)
                    newTarget = towingTarget;
                else if (nearbyObjects.Children.Count > 0)
                    uint.TryParse(((Button)nearbyObjects.Children[0]).Tag.ToString(), out newTarget);
            }
            else
                uint.TryParse(((Button)sender).Tag.ToString(), out newTarget);

            toggleTowCable(newTarget);
        }

        private void toggleTowCable(uint id, winchPosition position = null, double length = 0)
        {
            _planeAvionicsResponse.WaterRudderHandlePosition = 0;
            _planeAvionicsResponseLast.WaterRudderHandlePosition = 0;

            if (towingTarget == id)
            {
                _planeAvionicsResponse.WaterRudderHandlePosition = 0;
                _planeAvionicsResponseLast.WaterRudderHandlePosition = 0;

                towingTarget = TARGETMAX;
                towCableLength = 0;
                insertedTowPlane = new KeyValuePair<uint, bool>(TARGETMAX, false);

                showMessage("Tow rope released", _fsConnect);
                Application.Current.Dispatcher.Invoke(() => playSound("false"));
                Application.Current.Dispatcher.Invoke(() => changeButtonStatus(true, towConnectButton, true, "Attach To Closest"));
            }
            else if (position == null && !_nearbyInfoResponse.ContainsKey(id))
            {
                showMessage("Object " + id + " not in the list (yet?)", _fsConnect);
            }
            else if (restrictionsPassed())
            {
                if (assistantSettings.ContainsKey("towType") && assistantSettings["towType"] == 1)
                {
                    _planeAvionicsResponse.WaterRudderHandlePosition = 100;
                    _planeAvionicsResponseLast.WaterRudderHandlePosition = 100;
                }

                towingTarget = id;
                lightToggled = absoluteTime;

                // FINISH SEARCH
                towScanMode = TowScanMode.Scan;
                //insertedTowPlane = new KeyValuePair<uint, bool>(id, true);

                if (position == null && _nearbyInfoResponse.ContainsKey(id)) { position = _nearbyInfoResponse[id]; }

                if (position != null)
                {
                    winchDirection direction = _mathClass.getForceDirection(position, _planeInfoResponse);
                    towCableLength = length > 0 ? length + 10 : direction.distance + 10;
                    towPrevDist = towCableLength;
                    towPrePrevDist = towCableLength;

                    showMessage("Tow rope connected to #" + id, _fsConnect);
                    Application.Current.Dispatcher.Invoke(() => playSound("false"));
                    Application.Current.Dispatcher.Invoke(() => changeButtonStatus(false, towConnectButton, true, "Release Tow Rope"));
                }
                else
                {
                    showMessage("Failed to start towing", _fsConnect);
                }
            }
            else
            {
                showMessage("Tow rope can be connected only on the ground and with brakes disengaged", _fsConnect);
            }

            Application.Current.Dispatcher.Invoke(() => nearbyInterval(new object(), new EventArgs()));

            if (validConnection())
                commitAvionicsData();
        }

        private void processTowing()
        {
            //Console.WriteLine(Environment.NewLine + "processTowing");

            if (_nearbyInfoResponse.ContainsKey(towingTarget))
            {
                // BLINK LIGHTS
                if (towingTarget != insertedTowPlane.Key && absoluteTime - lightToggled > 2.5)
                {
                    lightToggled = absoluteTime;
                    _planeAvionicsResponse.LIGHTLANDING = _planeAvionicsResponse.LIGHTLANDING == 100 ? 0 : 100;
                    //_planeAvionicsResponse.LIGHTTAXI = _planeAvionicsResponse.LIGHTLANDING == 100 ? 0 : 100;
                    commitAvionicsData();
                }

                double bodyAcceleration = 0;

                // GET ANGLE TO TUG POSITION
                winchPosition winchPosition = _nearbyInfoResponse[towingTarget];
                winchDirection winchDirection = _mathClass.getForceDirection(winchPosition, _planeInfoResponse);

                // SET DESIRED ROPE LENGTH
                towCableDesired = Math.Max(/*_planeAvionicsResponse.SimOnGround != 100 && _planeAvionicsResponse.OnAnyRunway != 100 ? 80 : 40*/80, winchPosition.airspeed);

                // LEVEL UP GLIDER BEFORE LAUNCH
                levelUpGlider();

                // GET FINAL CABLE TENSION
                double accelerationLimit = (assistantSettings["realisticFailures"] == 1 ? 8 : 40) * 9.81;
                if (_planeAvionicsResponse.SimOnGround == 100)
                {
                    accelerationLimit /= 2;
                }
                double cableTension = _mathClass.getCableTension(towCableLength, Math.Max(1, assistantSettings["elasticExtension"]), winchDirection);
                double targetVelocity = winchPosition.airspeed > 25 ? winchPosition.airspeed * 2 : 50;

                if (cableTension > 0)
                {
                    bodyAcceleration = _mathClass.getBodyVelocity(winchDirection, _planeCommit, cableTension, accelerationLimit, winchDirection.distance, towPrevDist, towPrePrevDist, lastFrameTiming);
                }

                if (!double.IsNaN(cableTension) && lastFrameTiming != 0 && applyWinchForces(bodyAcceleration, accelerationLimit, winchDirection, targetVelocity, "Tow", assistantSettings["towConnectionPoint"]) &&
                   !double.IsNaN(_planeCommit.VelocityBodyX) && !double.IsNaN(_planeCommit.VelocityBodyY) && !double.IsNaN(_planeCommit.VelocityBodyZ))
                {
                    try
                    {
                        _fsConnect.UpdateData(Definitions.PlaneCommit, _planeCommit);
                    }
                    catch (Exception ex)
                    {
                        addLogMessage(ex.Message);
                    }
                }

                towPrePrevDist = towPrevDist;
                towPrevDist = winchDirection.distance;
            }
        }

        // TOW END


        // LAUNCHPAD START
        private void toggleLaunchpadPrepare(object sender, RoutedEventArgs e)
        {
            Console.WriteLine("toggleLaunchpadPrepare");

            if (!validConnection())
            {
                Console.WriteLine("connection lost");
                Application.Current.Dispatcher.Invoke(() => abortLaunchpad());
            }
            else
            {
                if (targedCatapultVelocity == 0) // PREPARE TO LAUNCH
                {
                    if (_planeAvionicsResponse.BrakeParkingPosition == 100 && (_planeAvionicsResponse.SimOnGround == 100 || _planeInfoResponse.AltitudeAboveGround <= _planeAvionicsResponse.StaticCGtoGround * 1.25))
                    {
                        targedCatapultVelocity = 0.9 * 0.514 * assistantSettings["catapultTargetSpeed"];
                        showMessage("Launchpad connected - disengage parking brakes to launch", _fsConnect);
                        Application.Current.Dispatcher.Invoke(() => playSound("true"));
                        Application.Current.Dispatcher.Invoke(() => changeButtonStatus(false, catapultlaunchButton, true, "Abort launch"));
                    }
                    else
                    {
                        showMessage("Engage parking brakes first, then connect launchpad", _fsConnect);
                        Application.Current.Dispatcher.Invoke(() => playSound("error"));
                    }
                }
                else // ABORT LAUNCH
                {
                    showMessage("Launch aborted", _fsConnect);
                    Application.Current.Dispatcher.Invoke(() => playSound("false"));
                    Application.Current.Dispatcher.Invoke(() => abortLaunchpad());
                }
            }
        }
        private void abortLaunchpad()
        {
            Console.WriteLine("abortLaunchpad");
            Application.Current.Dispatcher.Invoke(() => changeButtonStatus(_fsConnect != null ? true : false, catapultlaunchButton, true, "Ready to launch"));

            if (validConnection() && targedCatapultVelocity != 0)
            {
                launchpadAbortInitiated = true;
            }
            else
            {
                _planeAvionicsResponse.LaunchbarPosition = 0;
                targedCatapultVelocity = 0;

                if (validConnection())
                    commitAvionicsData();
            }
        }
        private void processLaunchpad()
        {
            if (launchpadAbortInitiated)
            {
                _planeAvionicsResponse.LaunchbarPosition = 0;
                commitAvionicsData();
                targedCatapultVelocity = 0;
                launchpadAbortInitiated = false;
            }
            else if (_planeAvionicsResponse.BrakeParkingPosition == 0)
            {
                // ANIMATE LAUNCHPAD
                if (_planeAvionicsResponse.LaunchbarPosition != 100) { _planeAvionicsResponse.LaunchbarPosition = 100; commitAvionicsData(); }

                // ENCREASE SPEED
                if (_planeCommit.VelocityBodyZ < 0.514 * assistantSettings["catapultTargetSpeed"])
                {
                    double diff = lastFrameTiming * (0.514 * assistantSettings["catapultTargetSpeed"] - targedCatapultVelocity);
                    targedCatapultVelocity -= diff;
                    _planeCommit.VelocityBodyZ += diff;

                    if (_planeCommit.VelocityBodyZ >= 0.9 * 0.514 * assistantSettings["catapultTargetSpeed"] || _planeAvionicsResponse.SimOnGround != 100)
                        abortLaunchpad();

                    Console.WriteLine("Launchpad acceleration: " + diff / lastFrameTiming);

                    // LIMIT ROTATION VELOCITY
                    _planeRotateVelocity.RotationVelocityBodyX *= lastFrameTiming;
                    _planeRotateVelocity.RotationVelocityBodyY *= lastFrameTiming;
                    _planeRotateVelocity.RotationVelocityBodyZ *= lastFrameTiming;

                    if (!double.IsNaN(_planeRotateVelocity.RotationVelocityBodyX) && !double.IsNaN(_planeRotateVelocity.RotationVelocityBodyY) && !double.IsNaN(_planeRotateVelocity.RotationVelocityBodyZ))
                    {
                        try
                        {
                            _fsConnect.UpdateData(Definitions.PlaneRotationVelocity, _planeRotateVelocity);
                        }
                        catch (Exception ex)
                        {
                            addLogMessage(ex.Message);
                        }
                    }
                }
            }
            // STICK TO THE POSITION
            else
            {
                double accel = _planeCommit.VelocityBodyZ - _planeInfoResponseLast.VelocityBodyZ;
                _planeCommit.VelocityBodyZ -= _planeCommit.VelocityBodyZ * lastFrameTiming + accel;
            }

            if (!double.IsNaN(_planeCommit.VelocityBodyX) && !double.IsNaN(_planeCommit.VelocityBodyY) && !double.IsNaN(_planeCommit.VelocityBodyZ))
            {
                try
                {
                    _fsConnect.UpdateData(Definitions.PlaneCommit, _planeCommit);
                }
                catch (Exception ex)
                {
                    addLogMessage(ex.Message);
                }
            }
        }
        // LAUNCHPAD END

        // LANDING START
        private void toggleLandingPrepare(object sender, RoutedEventArgs e)
        {
            Console.WriteLine("toggleLandingPrepare");

            if (!validConnection())
            {
                Console.WriteLine("connection lost");
            }
            else if (_carrierPosition == null)
            {
                _carrierPosition = new winchPosition();
                arrestorConnectedTime = 0;
                showMessage("Tailhook deployed", _fsConnect);
                Application.Current.Dispatcher.Invoke(() => playSound("true"));
                Application.Current.Dispatcher.Invoke(() => changeButtonStatus(false, hookPrepareButton, true, "Retract tailhook"));
            }
            else
            {
                arrestorsAbortInitiated = true;
            }
        }
        private void processLanding()
        {
            if (arrestorsAbortInitiated)
            {
                // RESET HOOK
                _planeAvionicsResponse.TailhookPosition = 0;
                commitAvionicsData();

                arrestorsAbortInitiated = false;
                _carrierPosition = null;
                arrestorConnectedTime = 0;
                //showMessage("Tailhook retracted", _fsConnect);
                Application.Current.Dispatcher.Invoke(() => changeButtonStatus(_fsConnect != null ? true : false, hookPrepareButton, true, "Deploy tailhook"));
            }
            else
            {
                // ANIMATE HOOK
                if (_planeAvionicsResponse.TailhookPosition < 100)
                {
                    _planeAvionicsResponse.TailhookPosition = Math.Min(_planeAvionicsResponse.TailhookPosition + 50 * Math.Min(0.1, lastFrameTiming), 100);
                    commitAvionicsData();
                }

                // SET CONTACT POINT
                if (_planeCommit.VelocityBodyZ > 20 && _carrierPosition.alt == 0 && _planeAvionicsResponse.TailhookPosition == 100 &&
                    (_planeAvionicsResponse.SimOnGround == 100 || _planeInfoResponse.AltitudeAboveGround <= _planeAvionicsResponse.StaticCGtoGround * 1.25))
                {
                    _carrierPosition.alt = _planeInfoResponse.Altitude - _planeAvionicsResponse.StaticCGtoGround;
                    _carrierPosition.location = new GeoLocation(_planeInfoResponseLast.Latitude, _planeInfoResponseLast.Longitude);
                    arrestorConnectedTime = absoluteTime;

                    Console.WriteLine($"Current location: {_planeInfoResponse.Latitude * 180 / Math.PI} {_planeInfoResponse.Longitude * 180 / Math.PI}");
                    Console.WriteLine($"String location: {_carrierPosition.location.Latitude * 180 / Math.PI} {_carrierPosition.location.Longitude * 180 / Math.PI}");
                }

                if (_carrierPosition.alt != 0)
                {
                    double timeLeft = 1.33 * assistantSettings["arrestorFullStopTime"] - (absoluteTime - arrestorConnectedTime);

                    // GET ANGLE TO STRING POSITION
                    winchDirection _carrierDirection = _mathClass.getForceDirection(_carrierPosition, _planeInfoResponse);

                    double accel = _planeCommit.VelocityBodyZ - _planeInfoResponseLast.VelocityBodyZ;

                    double cableTension = 0;
                    double tensionLimit = 5 * 9.81;
                    // ARREST IN PROCESS - FIND OUT TENSION
                    cableTension = 1.2 * (_planeCommit.VelocityBodyZ * lastFrameTiming / (timeLeft / 1.5) + (accel > 0 ? accel : 0));
                    cableTension = Math.Min(tensionLimit * lastFrameTiming, cableTension);

                    //Console.WriteLine("TAIL HOOK " + (_planeInfoResponse.AltitudeAboveGround - _planeInfoResponse.StaticCGtoGround));
                    Console.WriteLine($"String: cableTension{cableTension / lastFrameTiming:F2} timeLeft{timeLeft:F2} {_carrierDirection.distance:F2}m h{(_carrierDirection.heading * 180 / Math.PI):F0}deg p{(_carrierDirection.pitch * 180 / Math.PI):F0}deg");

                    if (timeLeft <= 0 || assistantSettings["realisticFailures"] == 1 && (cableTension / lastFrameTiming > 100 || _carrierDirection.distance > 105))
                    {
                        showMessage(
                            timeLeft <= 0 ? "Arresting cable released. Distance: " + _carrierDirection.distance.ToString("0.0") + " meters" :
                            "Arresting cable failure. Distance " + _carrierDirection.distance.ToString("0.0") + " meters", _fsConnect);
                        Application.Current.Dispatcher.Invoke(() => playSound("false"));
                        arrestorsAbortInitiated = true;
                        _planeAvionicsResponse.TailhookPosition = 50;
                        commitAvionicsData();
                    }
                    else if (!double.IsNaN(cableTension) && lastFrameTiming != 0 && cableTension != 0 && _carrierDirection.localForceDirection.Norm > 0)
                    {
                        // LIMIT ROTATION VELOCITY
                        _planeRotateVelocity.RotationVelocityBodyX *= lastFrameTiming;
                        _planeRotateVelocity.RotationVelocityBodyY *= lastFrameTiming;
                        _planeRotateVelocity.RotationVelocityBodyZ *= lastFrameTiming;

                        if (!double.IsNaN(_planeRotateVelocity.RotationVelocityBodyX) && !double.IsNaN(_planeRotateVelocity.RotationVelocityBodyY) && !double.IsNaN(_planeRotateVelocity.RotationVelocityBodyZ))
                        {
                            try
                            {
                                _fsConnect.UpdateData(Definitions.PlaneRotationVelocity, _planeRotateVelocity);
                            }
                            catch (Exception ex)
                            {
                                addLogMessage(ex.Message);
                            }
                        }

                        _planeCommit.VelocityBodyX += _carrierDirection.localForceDirection.X * cableTension;
                        _planeCommit.VelocityBodyY += _carrierDirection.localForceDirection.Y * cableTension;
                        _planeCommit.VelocityBodyZ += _carrierDirection.localForceDirection.Z * cableTension;

                        // REMOVE VERTICAL VELOCITY
                        if (_planeCommit.VelocityBodyY > 0.1)
                            _planeCommit.VelocityBodyY = -0.01;
                    }
                }
            }


            if (!double.IsNaN(_planeCommit.VelocityBodyX) && !double.IsNaN(_planeCommit.VelocityBodyY) && !double.IsNaN(_planeCommit.VelocityBodyZ))
            {
                try
                {
                    _fsConnect.UpdateData(Definitions.PlaneCommit, _planeCommit);
                }
                catch (Exception ex)
                {
                    addLogMessage(ex.Message);
                }
            }
        }
        // LANDING END

        // THERMALS START
        private void thermalsLoadMap(object sender, RoutedEventArgs e)
        {
            OpenFileDialog openFileDialog = new OpenFileDialog();
            openFileDialog.Filter = "Iittle Navmap Userpoints (*.csv)|*.csv";
            openFileDialog.FilterIndex = 2;
            openFileDialog.RestoreDirectory = true;
            if (openFileDialog.ShowDialog() == true)
            {
                string content = File.ReadAllText(openFileDialog.FileName);
                insertThermals(content);

                thermalsClearButton.Content = thermalsList.Count + thermalsListAPI.Count > 0 ? "Remove " + (thermalsList.Count + thermalsListAPI.Count).ToString() + " thermals" : "No thermals loaded";
            }
        }

        private void thermalsClear(object sender, RoutedEventArgs e)
        {
            thermalsList = new List<winchPosition>();
            Application.Current.Dispatcher.Invoke(() => _radarClass.clearRadarThermals(RadarCanvas, "Thermal"));
            thermalsListAPI = new List<winchPosition>();
            Application.Current.Dispatcher.Invoke(() => _radarClass.clearRadarThermals(RadarCanvas, "ThermalAPI"));
            Application.Current.Dispatcher.Invoke(() => thermalsClearButton.Content = "No thermals loaded");
        }

        private void enableApiThermals(Button btn, bool enable, bool force = false)
        {

            Application.Current.Dispatcher.Invoke(() => changeButtonStatus(!enable, btn, true, enable ? "Disable API thermals" : "Enable API thermals"));

            if (!enable)
            {
                if (force || (!assistantSettings.ContainsKey("APIthermalsAutoload") || assistantSettings["APIthermalsAutoload"] == 0))
                {
                    Console.WriteLine("dsiableApiThermals");
                    thermalsListAPI = new List<winchPosition>();
                    apiThermalsLoadedPosition = null;
                    apiThermalsLoadedTime = 0;
                    Application.Current.Dispatcher.Invoke(() => thermalsClearButton.Content = thermalsList.Count + thermalsListAPI.Count > 0 ? "Remove " + (thermalsList.Count + thermalsListAPI.Count).ToString() + " thermals" : "No thermals loaded");

                    // DISABLE THERMALS
                    if (thermalsWorking && thermalsList.Count == 0)
                    {
                        toggleThermals(null, null);
                    }
                }
            }
            else
            {
                Console.WriteLine("enableApiThermals");
            }
        }

        private void loadThermalsApiData()
        {
            if ((assistantSettings["APIthermalsAutoload"] == 1) && _planeInfoResponse.Latitude != 0 && _planeInfoResponse.Longitude != 0)
            {
                thermalsListAPI = new List<winchPosition>();
                apiThermalsLoadedPosition = new GeoLocation(_planeInfoResponse.Latitude, _planeInfoResponse.Longitude);
                apiThermalsLoadedTime = _planeInfoResponse.AbsoluteTime;

                // S W N E
                double[] bounds = getThermalsBounds(new GeoLocation(_planeInfoResponse.Latitude, _planeInfoResponse.Longitude), true);

                if ((bounds[0] != 0 || bounds[1] != 0 || bounds[2] != 0 || bounds[3] != 0) && bounds[0] != bounds[2] && bounds[1] != bounds[3])
                {
                    if (assistantSettings.ContainsKey("APIthermalsAutoload") && assistantSettings["APIthermalsAutoload"] == 1)
                    {
                        string url = "https://thermal.kk7.ch/api/hotspots/csv/all/" + bounds[0].ToString().Replace(',', '.') + "," + bounds[1].ToString().Replace(',', '.') + "," + bounds[2].ToString().Replace(',', '.') + "," + bounds[3].ToString().Replace(',', '.');
                        Console.WriteLine(url);

                        try
                        {
                            var lWebClient = new WebClient();
                            string respond = lWebClient.DownloadString(url);

                            if (respond.Length > 150)
                            {
                                insertApiThermals(respond, bounds);
                            }
                        }
                        catch (Exception ex)
                        {
                            MessageBox.Show("Unable to load thermals from URL" + Environment.NewLine + url + Environment.NewLine + "error: " + ex.Message);
                        }
                    }

                    if (assistantSettings.ContainsKey("APIthermalsAutoload") && assistantSettings["APIthermalsAutoload"] == 1 && Directory.Exists("openaip"))
                    {
                        try
                        {

                            var files = Directory.EnumerateFiles("openaip", "*_hot.aip", SearchOption.TopDirectoryOnly);
                            foreach (var file in files)
                            {
                                string xml = File.ReadAllText(file);

                                if (!string.IsNullOrEmpty(xml))
                                {
                                    //Console.WriteLine("Parsing " + file);

                                    string csv = "";
                                    foreach (XElement hotspot in XElement.Parse(xml).Element("HOTSPOTS").Elements("HOTSPOT"))
                                    {
                                        XElement geolocation = hotspot.Element("GEOLOCATION");
                                        string lat = geolocation.Element("LAT").Value;
                                        string lng = geolocation.Element("LON").Value;
                                        string reliability = hotspot.Element("RELIABILITY").Value;

                                        if (!string.IsNullOrEmpty(lat) && !string.IsNullOrEmpty(lng) && !string.IsNullOrEmpty(reliability))
                                        {
                                            csv += lat + "," + lng + ",1," + reliability.Replace("0.", "") + Environment.NewLine;
                                        }
                                    }

                                    insertApiThermals(csv, bounds);
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            MessageBox.Show("Unable to load thermals from local files. error: " + ex.Message);
                        }
                    }
                }
                else
                {
                    MessageBox.Show("Failed to request hotspots data");
                }

                if (thermalsListAPI.Count > 0)
                {
                    Application.Current.Dispatcher.Invoke(() => _radarClass.insertRadarThermals(RadarCanvas, thermalsListAPI, "ThermalAPI"));
                    showMessage(thermalsListAPI.Count + " hotspots loaded from API sources", _fsConnect);
                    Application.Current.Dispatcher.Invoke(() => APICsvExportButton.Visibility = Visibility.Visible);
                }
                else
                {
                    showMessage("No hotspots for this area from API sources", _fsConnect);
                    Application.Current.Dispatcher.Invoke(() => APICsvExportButton.Visibility = Visibility.Collapsed);
                }
            }

            Application.Current.Dispatcher.Invoke(() => thermalsClearButton.Content = thermalsList.Count + thermalsListAPI.Count > 0 ? "Remove " + (thermalsList.Count + thermalsListAPI.Count).ToString() + " thermals" : "No thermals loaded");
        }

        private double[] getThermalsBounds(GeoLocation coords, bool returnDegree)
        {
            double lat = coords.Latitude * 180 / Math.PI;
            double lon = coords.Longitude * 180 / Math.PI;
            int lonCount = (int)Math.Round(1 / Math.Cos(coords.Latitude)) - 1;

            // S W N E
            double s = Math.Floor(lat - 0.0000001);
            double w = Math.Floor(lon - lonCount - 0.0000001);
            double n = Math.Ceiling(lat + 0.0000001);
            double e = Math.Ceiling(lon + lonCount + 0.0000001);
            return new double[] {
                s * (!returnDegree ? Math.PI / 180 : 1),
                w * (!returnDegree ? Math.PI / 180 : 1),
                n * (!returnDegree ? Math.PI / 180 : 1),
                e * (!returnDegree ? Math.PI / 180 : 1)
            };
        }

        private bool coordinateInsideBounds(GeoLocation apiThermalsLoadedPosition, double[] bounds)
        {
            if (apiThermalsLoadedPosition.Latitude < bounds[0] || apiThermalsLoadedPosition.Latitude > bounds[2] || apiThermalsLoadedPosition.Longitude < bounds[1] || apiThermalsLoadedPosition.Longitude > bounds[3])
            {
                return false;
            }

            return true;
        }

        private void insertThermals(string content)
        {
            //List<winchPosition> radarThermals = new List<winchPosition>();
            Application.Current.Dispatcher.Invoke(() => _radarClass.clearRadarThermals(RadarCanvas, "Thermal"));

            foreach (string line in content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None))
            { // Location,,thermal,40.17048,-111.96616,5000,11.95587,10,,,250,2021-01-04T23:32:47.770,
                if (!String.IsNullOrWhiteSpace(line))
                {
                    string[] data = line.Split(',');
                    if (data.Length > 7 && data[2].ToLower().Trim() == "thermal")
                    {
                        double radius = 0.5;
                        double strength = 10;

                        if (double.TryParse(data[3].Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out double lat) &&
                            double.TryParse(data[4].Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out double lng) &&
                            double.TryParse(data[5].Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out double alt) &&
                            (data[7].Contains(" ") && double.TryParse(data[7].Split(' ')[0].Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out radius) && double.TryParse(data[7].Split(' ')[1].Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out strength) ||
                            double.TryParse(data[7].Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out radius)))
                        {
                            winchPosition _thermalPosition = new winchPosition(new GeoLocation(lat / 180 * Math.PI, lng / 180 * Math.PI), 0.305 * alt, 1852 * radius, 0.75 * strength / 1.9 * 5);
                            thermalsList.Add(_thermalPosition);
                            //radarThermals.Add(_thermalPosition);
                        }
                        else
                        {
                            MessageBox.Show("Userpoint record numbers format is incorrect");
                        }
                    }
                    else
                    {
                        MessageBox.Show("Userpoint record does not have enough parameters");
                    }
                }
            }

            if (thermalsList.Count > 0)
                Application.Current.Dispatcher.Invoke(() => _radarClass.insertRadarThermals(RadarCanvas, thermalsList, "Thermal"));
        }

        private void insertApiThermals(string content, double[] bounds)
        {
            Random rand = new Random();
            Application.Current.Dispatcher.Invoke(() => _radarClass.clearRadarThermals(RadarCanvas, "ThermalAPI"));

            foreach (string line in content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None))
            { // Location,,thermal,40.17048,-111.96616,5000,11.95587,10,,,250,2021-01-04T23:32:47.770,
                if (!String.IsNullOrWhiteSpace(line) && !line.Contains("latitude"))
                {
                    string[] data = line.Split(',');
                    if (data.Length >= 4)
                    {
                        double radius = 0.5;
                        double strength = 25; // KNOTS

                        if (double.TryParse(data[0], NumberStyles.Any, CultureInfo.InvariantCulture, out double lat) &&
                            double.TryParse(data[1], NumberStyles.Any, CultureInfo.InvariantCulture, out double lng) &&
                            double.TryParse(data[2], NumberStyles.Any, CultureInfo.InvariantCulture, out double alt) &&
                            double.TryParse(data[3], NumberStyles.Any, CultureInfo.InvariantCulture, out double probability)
                        )
                        {
                            if (!coordinateInsideBounds(new GeoLocation(lat, lng), bounds))
                            {
                                continue;
                            }

                            double modifier = probability / 100;
                            lat = lat / 180 * Math.PI;
                            lng = lng / 180 * Math.PI;
                            radius *= 1852 * modifier * (1 + 0.5 * rand.NextDouble());
                            // CHECK SIBLINGS
                            if (thermalsListAPI.Count > 0)
                            {
                                bool cnt = false;

                                foreach (winchPosition therm in thermalsListAPI)
                                {
                                    double distance = Math.Abs(_mathClass.findDistanceBetweenPoints(lat, lng, therm.location.Latitude, therm.location.Longitude));

                                    if ((distance - radius - therm.radius) < 10000 * (modifier * therm.airspeed / strength))
                                    {
                                        cnt = true;
                                        break;
                                    }
                                }

                                if (cnt)
                                {
                                    continue;
                                }
                            }


                            winchPosition _thermalPosition = new winchPosition(new GeoLocation(lat, lng), 0, radius, 0.75 * strength / 1.9 * modifier);
                            thermalsListAPI.Add(_thermalPosition);
                        }
                        else
                        {
                            MessageBox.Show("Userpoint record numbers format is incorrect");
                        }
                    }
                    else
                    {
                        MessageBox.Show("Userpoint record does not have enough parameters");
                    }
                }
            }
        }
        private void APICsvExport(object sender, RoutedEventArgs e)
        {
            if (thermalsListAPI.Count > 0)
            {
                string csv = "";

                foreach (winchPosition thermal in thermalsListAPI)
                {
                    csv += "Location,,thermal," + (thermal.location.Latitude * 180 / Math.PI).ToString(CultureInfo.InvariantCulture) + "," + (thermal.location.Longitude * 180 / Math.PI).ToString(CultureInfo.InvariantCulture) + ",0,1," + (thermal.radius / 1852).ToString("0.0", CultureInfo.InvariantCulture) + " " + (thermal.airspeed * 1.9 / 4).ToString("0.0", CultureInfo.InvariantCulture) + ",,,250,2020-01-01T00:00:00.000," + Environment.NewLine;
                }

                SaveFileDialog sfd = new SaveFileDialog();

                sfd.Filter = "LittleNavMap userpoints (*.csv)|*.csv";
                sfd.FilterIndex = 1;

                if (sfd.ShowDialog() == true)
                {
                    try
                    {
                        File.WriteAllText(sfd.FileName, csv);
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show("Failed to save API data. Error: " + ex.Message);
                    }
                }
            }
        }
        private void toggleThermals(object sender, RoutedEventArgs e)
        {
            Console.WriteLine("toggleThermals");

            if (thermalsWorking)
            {
                thermalsWorking = false;
                //showMessage("Thermals disabled", _fsConnect);
                Application.Current.Dispatcher.Invoke(() => playSound("false"));
                Application.Current.Dispatcher.Invoke(() => changeButtonStatus(true, thermalsToggleButton, true, "Enable thermals"));
            }
            else if (!validConnection())
            {
                Console.WriteLine("connection lost");
            }
            else
            {
                if (thermalsList.Count == 0 && thermalsListAPI.Count == 0)
                {
                    Application.Current.Dispatcher.Invoke(() => thermalsClearButton.Content = "No thermal maps loaded");
                }

                if (lastFrameTiming > 0)
                {
                    Console.WriteLine("REquesting weather " + lastFrameTiming);
                    _fsConnect.RequestData(Requests.WeatherData, Definitions.WeatherData);
                }

                thermalsWorking = true;
                //showMessage("Thermals enabled", _fsConnect);
                Application.Current.Dispatcher.Invoke(() => playSound("true"));
                Application.Current.Dispatcher.Invoke(() => changeButtonStatus(false, thermalsToggleButton, true, "Disable thermals"));
                Application.Current.Dispatcher.Invoke(() => toggleSidebarWindow(true));
            }
        }
        private void processThermals(List<winchPosition> list, bool API = false)
        {
            // WIND MODIFIERS
            double windModifier = 0;
            double thermalLeaning = 0;

            if (assistantSettings.ContainsKey("thermalsType") && assistantSettings["thermalsType"] >= 1)
            {
                windModifier = Math.Pow(Math.Min(Math.Max(0, windVelocity / 50), 1), 0.5);
                thermalLeaning = _planeInfoResponse.AltitudeAboveGround * windModifier;
            }

            int id = 0;
            foreach (winchPosition thermal in list)
            {
                double height = 0;
                double acAltitude = 0;
                double thermalRadius = thermal.radius;
                double finalModifier = 0.1;

                // DRIFT
                if (assistantSettings.ContainsKey("thermalsType") && assistantSettings["thermalsType"] >= 2)
                {
                    thermal.location = _mathClass.FindPointAtDistanceFrom(thermal.location, windDirection - Math.PI, windVelocity / 1000 / 4 * lastFrameTiming);
                }

                GeoLocation thermalCenter = new GeoLocation(thermal.location.Latitude, thermal.location.Longitude);

                // AGL
                acAltitude = _planeInfoResponse.AltitudeAboveGround;
                if (thermal.alt >= 1000)
                {
                    height = thermal.alt;
                }
                // MSL
                else
                {
                    height = Math.Max(1000, assistantSettings["thermalsHeight"] * 0.305) + thermal.alt - (_planeInfoResponse.Altitude - _planeInfoResponse.AltitudeAboveGround);
                }

                thermalRadius *= 1 + windModifier;

                if (thermalRadius > 0)
                {
                    // LEANING
                    if (thermalLeaning > 10)
                    {
                        thermalCenter = _mathClass.FindPointAtDistanceFrom(thermalCenter, windDirection - Math.PI, thermalLeaning / 1000);
                    }

                    winchPosition thermalTemp = new winchPosition(thermalCenter, height, thermalRadius);
                    thermalTemp.alt = _planeInfoResponse.Altitude - 10 * thermalTemp.radius;
                    winchDirection _thermalDirection = _mathClass.getForceDirection(thermalTemp, _planeInfoResponse);

                    if (_thermalDirection.groundDistance < thermalTemp.radius)
                    {
                        //Console.WriteLine("Thermal leaning: " + thermalLeaning.ToString("0.0") + " width scale: " + (1 + windModifier));

                        // DISTANCE TO THE CENTER
                        double horizontalModifier = Math.Pow((thermalTemp.radius - _thermalDirection.groundDistance) / thermalTemp.radius, 0.25);

                        // DISTANCE TO THE TOP
                        double verticalModifier = acAltitude < height ?
                            (height - acAltitude < 500 ? (height - acAltitude) / 500 : 1) : // UNDER INVERSION
                            Math.Max(-1, (height - acAltitude) / 500); // ABOVE INVERSION

                        if (verticalModifier > 0)
                        {
                            // ATTITUDE
                            double pitchBankModifier = Math.Abs(Math.Cos(_planeInfoResponse.PlaneBank)) * Math.Abs(Math.Cos(_planeInfoResponse.PlanePitch)); // ROTATION

                            // AIRSPEED
                            double airspeedModifier = _planeInfoResponse.AirspeedIndicated < 150 ? Math.Pow(Math.Max(0, 1 - _planeInfoResponse.AirspeedIndicated / 150), 0.5) : 0; // OVERSPEED
                            airspeedModifier *= _planeInfoResponse.AirspeedIndicated > 20 ? Math.Pow(Math.Max(0, (_planeInfoResponse.AirspeedIndicated - 20) / 130), 0.5) : 0; // STALL
                            airspeedModifier = Math.Min(1, 3 * airspeedModifier); // POW COMPENSATION

                            double ambientModifier = (1 - windModifier) * overcastModifier * dayTimeModifier;

                            finalModifier = horizontalModifier * verticalModifier * ambientModifier;

                            // WIND INDICATION
                            thermalFlow += thermal.airspeed * finalModifier;

                            finalModifier *= pitchBankModifier * airspeedModifier;
                            double liftAmount = -_thermalDirection.localForceDirection.Y * thermal.airspeed * finalModifier;// + Math.Abs(_planeInfoResponse.AmbientWindY);

                            // COMPARE WITH TE
                            liftAmount = liftAmount - Math.Max(0, te_raw_ms);

                            if (liftAmount > 0)
                            {
                                // PUSH OUT FROM THE THERMAL
                                double assimetricalLift = (1 - Math.Pow(horizontalModifier, 0.05)) *
                                    Math.Sin(_thermalDirection.heading) * Math.Cos(_planeInfoResponse.PlaneBank) *
                                    thermal.airspeed;
                                if (assimetricalLift > 0.2 || assimetricalLift < -0.2)
                                {
                                    assimetricalLift -= assimetricalLift > 0 ? 0.2 : -0.2;
                                    //Console.WriteLine("assimetricalLift: " + assimetricalLift + " heading: " + Math.Sin(_thermalDirection.heading));
                                    _fsConnect.UpdateData(Definitions.RotationVelocityZCommit, _planeInfoResponse.RotationAccelerationBodyZ + 25 * assimetricalLift);
                                }

                                try
                                {
                                    //Console.WriteLine("LiftY: " + liftAmount + " thermalFlow: " + thermalFlow + " TE: " + te_raw_ms);

                                    if (_planeInfoResponse.FoldingWindR == 0)
                                    {
                                        _planeVerticalVelocity.VelocityBodyY = _planeInfoResponse.VelocityBodyY + (_planeInfoResponse.AccelerationBodyY + liftAmount) * lastFrameTiming;
                                        _planeVerticalVelocity.VelocityBodyZ = _planeInfoResponse.VelocityBodyZ + (_planeInfoResponse.AccelerationBodyZ + liftAmount / 2) * lastFrameTiming;
                                        _planeVerticalVelocity.RotationAccelerationBodyX = _planeInfoResponse.RotationAccelerationBodyX + 
                                            liftAmount * Math.Max(0, Math.Min(0.34, 0.17 - (_planeInfoResponse.PlanePitch + _planeInfoResponse.RotationVelocityBodyX)));

                                        if (!double.IsNaN(_planeVerticalVelocity.VelocityBodyY) && !double.IsNaN(_planeVerticalVelocity.VelocityBodyZ) && !double.IsNaN(_planeVerticalVelocity.RotationAccelerationBodyX))
                                            _fsConnect.UpdateData(Definitions.ThermalVelocityCommit, _planeVerticalVelocity);
                                    }
                                    else
                                    {
                                        _fsConnect.UpdateData(Definitions.FoldingWindCommit, liftAmount);
                                    }

                                    /*string bar = "||||||||||||||||||||||||||||||||||||||||||||||||||";
                                    Application.Current.Dispatcher.Invoke(() => thermalsDebugData.Height = 90);
                                    Application.Current.Dispatcher.Invoke(() => messagesLogScroll.Height = 0);
                                    Application.Current.Dispatcher.Invoke(() => thermalsDebugData.Children.Add(makeTextBlock("To center:  " + bar.Substring(0, (int)(horizontalModifier * 100 / 4)), Colors.Black, 12)));
                                    Application.Current.Dispatcher.Invoke(() => thermalsDebugData.Children.Add(makeTextBlock("To top:     " + bar.Substring(0, (int)(verticalModifier * 100 / 4)), (height - acAltitude) > 0 ? Colors.Black : Colors.DarkRed, 12)));
                                    Application.Current.Dispatcher.Invoke(() => thermalsDebugData.Children.Add(makeTextBlock("Pitch/bank: " + bar.Substring(0, (int)(pitchBankModifier * 100 / 4)), Colors.Black, 12)));
                                    Application.Current.Dispatcher.Invoke(() => thermalsDebugData.Children.Add(makeTextBlock("Velocity:   " + bar.Substring(0, (int)(airspeedModifier * 100 / 4)), Colors.Black, 12)));
                                    Application.Current.Dispatcher.Invoke(() => thermalsDebugData.Children.Add(makeTextBlock("Ambient:    " + bar.Substring(0, (int)(ambientModifier * 100 / 4)) + Environment.NewLine + Environment.NewLine, ambientModifier > 0 ? Colors.Black : Colors.DarkRed, 12)));
                                    Application.Current.Dispatcher.Invoke(() => thermalsDebugData.Children.Add(makeTextBlock("Total lift: " + bar.Substring(0, (int)(finalModifier * 100 / 4)) + Environment.NewLine + Environment.NewLine, finalModifier > 0 ? Colors.Black : Colors.DarkRed, 12)));*/
                                }
                                catch (Exception ex)
                                {
                                    Console.WriteLine(ex.Message);
                                }

                                /*try
                                {
                                    PlaneInfoLift planeInfoLift = new PlaneInfoLift();
                                    planeInfoLift.Altitude = _planeInfoResponse.Altitude + liftAmount * lastFrameTiming;
                                    _fsConnect.UpdateData(Definitions.PlaneLift, planeInfoLift);
                                }
                                catch (Exception ex)
                                {
                                    Console.WriteLine(ex.Message);
                                }*/
                            }
                        }
                        else
                        {
                            finalModifier = verticalModifier;
                        }
                    }

                    if ((thermalsDebugActive > 0 || httpServerActive) && (!assistantSettings.ContainsKey("RadarScale") || scaleRefresh > 0 || _thermalDirection.groundDistance / 1.25 < Math.Min(allowedRadarScale, assistantSettings["RadarScale"]) * 1000 + thermalTemp.radius))
                    {
                        Application.Current.Dispatcher.Invoke(() => _radarClass.updateRadarThermals(RadarCanvas, (!API ? "Thermal_" : "ThermalAPI_") + id, _thermalDirection, thermalTemp, finalModifier, Math.Min(maxRadarScale, assistantSettings["RadarScale"])));
                    }
                }

                id++;
            }

            if (scaleRefresh > 0)
                scaleRefresh--;
        }
        // THERMALS END

        // DATA EXCHANGE
        private void HandleReceivedFsData(object sender, FsDataReceivedEventArgs e)
        {
            if (validConnection())
            {
                try
                {
                    // PLANE INFO
                    if (e.RequestId == (uint)Requests.PlaneInfo)
                    {
                        //Console.WriteLine(JsonConvert.SerializeObject(e.Data, Formatting.Indented));
                        _planeInfoResponseLast = _planeInfoResponse;
                        _planeInfoResponse = (PlaneInfoResponse)(e.Data);

                        swCurrent = _planeInfoResponse.AbsoluteTime;

                        // PAUSE?
                        if (_planeAvionicsResponse.SimOnGround != 100 &&
                            (_planeInfoResponse.AirspeedIndicated == _planeInfoResponseLast.AirspeedIndicated &&
                            _planeInfoResponse.GpsGroundSpeed == _planeInfoResponseLast.GpsGroundSpeed &&
                            _planeInfoResponse.Altitude == _planeInfoResponseLast.Altitude &&
                            _planeInfoResponse.PlaneBank == _planeInfoResponseLast.PlaneBank &&
                            _planeInfoResponse.PlanePitch == _planeInfoResponseLast.PlanePitch))
                        {
                            lastFrameTiming = 0;
                        }
                        else
                        {
                            lastFrameTiming = Math.Max(0, swCurrent - swLast);
                        }

                        absoluteTime += lastFrameTiming;

                        weatherChecked += lastFrameTiming;
                        if (lastFrameTiming > 0 && validConnection() && weatherChecked >= 10)
                        {
                            Console.WriteLine("Requesting weather " + lastFrameTiming);
                            _fsConnect.RequestData(Requests.WeatherData, Definitions.WeatherData);
                            _fsConnect.RequestData(Requests.PlaneEngineData, Definitions.PlaneEngineData);

                            weatherChecked = 0;
                        }

                        _planeRotateVelocity = new PlaneInfoRotateVelocity();
                        _planeRotateVelocity.RotationVelocityBodyX = _planeInfoResponse.RotationVelocityBodyX;
                        _planeRotateVelocity.RotationVelocityBodyY = _planeInfoResponse.RotationVelocityBodyY;
                        _planeRotateVelocity.RotationVelocityBodyZ = _planeInfoResponse.RotationVelocityBodyZ;

                        _planeRotateAccel = new PlaneInfoRotateAccel();
                        _planeRotateAccel.RotationAccelerationBodyX = _planeInfoResponse.RotationAccelerationBodyX;
                        _planeRotateAccel.RotationAccelerationBodyY = _planeInfoResponse.RotationAccelerationBodyY;
                        _planeRotateAccel.RotationAccelerationBodyZ = _planeInfoResponse.RotationAccelerationBodyZ;

                        _planeCommit = new PlaneInfoCommit();
                        _planeCommit.VelocityBodyX = _planeInfoResponse.VelocityBodyX;
                        _planeCommit.VelocityBodyY = _planeInfoResponse.VelocityBodyY;
                        _planeCommit.VelocityBodyZ = _planeInfoResponse.VelocityBodyZ;

                        Application.Current.Dispatcher.Invoke(() => VerticalWindPos.Height = 125 - Math.Min(Math.Max(0, (_planeInfoResponse.AmbientWindY + thermalFlow) * 1.94) * 6.25, 125));
                        Application.Current.Dispatcher.Invoke(() => VerticalWindNeg.Height = 125 - Math.Min(Math.Abs(Math.Min(0, (_planeInfoResponse.AmbientWindY + thermalFlow) * 1.94) * 6.25), 125));

                        // UPDATE KK7 DATA
                        if (assistantSettings.ContainsKey("APIthermalsAutoload") && assistantSettings["APIthermalsAutoload"] == 1 &&
                            thermalsWorking && _planeInfoResponse.Latitude != 0 && _planeInfoResponse.Longitude != 0 && _planeInfoResponse.AbsoluteTime > 20 + apiThermalsLoadedTime)
                        {
                            if (apiThermalsLoadedPosition == null)
                            {
                                loadThermalsApiData();
                            }
                            else
                            {
                                double[] bounds = getThermalsBounds(apiThermalsLoadedPosition, false);
                                // S W N E
                                if (!coordinateInsideBounds(new GeoLocation(_planeInfoResponse.Latitude, _planeInfoResponse.Longitude), bounds))
                                {
                                    addLogMessage($"Plane outside of loaded thermals area: {_planeInfoResponse.Latitude:F5} {_planeInfoResponse.Longitude:F5} - {bounds[0]:F5} {bounds[1]:F5} {bounds[2]:F5} {bounds[3]:F5} ");
                                    loadThermalsApiData();
                                }
                            }
                        }

                        if (thermalsDebugActive > 0 || httpServerActive)
                            Application.Current.Dispatcher.Invoke(() => _radarClass.updateCompassWind(RadarCanvas, _planeInfoResponse.PlaneHeading, _weatherReport.AmbientWindDirection, _weatherReport.AmbientWindVelocity * 1.94384));

                        // PAST COMMIT
                        if (lastPacketReceived != 0 /*1000 * lastFrameTiming > interval * 0.75*/ && lastFrameTiming > 0.001 && lastFrameTiming <= 1.0 && _planeAvionicsResponse.IsSlewActive != 100)
                        {
                            // CALCULATE TOTAL ENERGY
                            double vertical_speed = (_planeInfoResponse.Altitude - _planeInfoResponseLast.Altitude) / lastFrameTiming;
                            double te_compensation = (Math.Pow(_planeInfoResponse.AirspeedIndicated, 2) - Math.Pow(_planeInfoResponseLast.AirspeedIndicated, 2)) / (2 * lastFrameTiming * 9.80665);
                            double newTE = (vertical_speed + te_compensation) * _planeInfoResponse.AirspeedIndicated / _planeInfoResponse.AirspeedTrue;
                            if (newTE < 20 && newTE > -20)
                            {
                                te_raw_ms = Math.Max(-10, Math.Min(10, (1 - 5 * lastFrameTiming) * te_raw_ms + 5 * lastFrameTiming * newTE));
                                //Console.WriteLine("vertical_speed: " + vertical_speed + " te_compensation: " + te_compensation + " newTE: " + newTE + " te_raw_ms: " + te_raw_ms);
                            }

                            // TRACK RECORDING
                            if (_trackingClass.trackRecording != null && _trackingClass.recordingCounter > 0)
                            {
                                // FREE VERSION
                                if (allowedRecordLength < _trackingClass.recordingCounter)
                                {
                                    Application.Current.Dispatcher.Invoke(() => setTrackRecording(false));
                                }
                                else
                                {
                                    _trackingClass.captureTrackPoint(_planeInfoResponse, _planeAvionicsResponse, absoluteTime, 0.5);
                                    _trackingClass.lastRecordingCounter = _trackingClass.recordingCounter;
                                    _trackingClass.recordingCounter += Math.Max(0, lastFrameTiming);
                                }
                            }

                            // APPLY PHYSICS IS LAUNCH ACTIVE
                            if (lastFrameTiming != 0 && _winchPosition != null)
                            {
                                processLaunch();
                            }
                            // ARRESTING PHYSICS
                            if (_carrierPosition != null)
                            {
                                processLanding();
                            }
                            // LAUNCHPAD PHYSICS
                            if (targedCatapultVelocity != 0)
                            {
                                processLaunchpad();
                            }
                            // THERMALS PHYSICS
                            thermalFlow = 0;
                            if (thermalsWorking)
                            {
                                //Application.Current.Dispatcher.Invoke(() => thermalsDebugData.Children.Clear());
                                //Application.Current.Dispatcher.Invoke(() => thermalsDebugData.Height = 0);
                                //Application.Current.Dispatcher.Invoke(() => messagesLogScroll.Height = 90);

                                if (_planeInfoResponse.AirspeedIndicated < 100)
                                {
                                    processThermals(thermalsList);
                                    processThermals(thermalsListAPI, true);
                                }
                            }
                        }
                        else
                        {
                            Console.WriteLine("Frame skip, lastFrameTiming: " + lastFrameTiming);

                            if (lastFrameTiming > 0 && lastFrameTiming < 50 && _trackingClass.recordingCounter > 0)
                            {
                                _trackingClass.lastRecordingCounter = _trackingClass.recordingCounter;
                                _trackingClass.recordingCounter += Math.Max(0, lastFrameTiming);
                            }
                        }

                        swLast = swCurrent;
                        lastPacketReceived = 0;
                    }
                    // PLANE AVIONICS
                    else if (e.RequestId == (uint)Requests.PlaneAvionics)
                    {
                        //Console.WriteLine(JsonConvert.SerializeObject(e.Data, Formatting.Indented));
                        _planeAvionicsResponseLast = _planeAvionicsResponse;
                        _planeAvionicsResponse = (PlaneAvionicsResponse)(e.Data);

                        trackControlChanges();
                    }
                    // NEARBY DATA
                    else if (e.RequestId == (uint)Requests.NearbyObjects)
                    {
                        //Console.WriteLine(e.ObjectID);
                        //Console.WriteLine(JsonConvert.SerializeObject(e.Data, Formatting.Indented));
                        NearbyInfoResponse response = (NearbyInfoResponse)e.Data;

                        if (_nearbyInfoResponseLast.ContainsKey(e.ObjectID)) { _nearbyInfoResponsePreLast[e.ObjectID] = _nearbyInfoResponseLast[e.ObjectID]; }
                        if (_nearbyInfoResponse.ContainsKey(e.ObjectID)) { _nearbyInfoResponseLast[e.ObjectID] = _nearbyInfoResponse[e.ObjectID]; }
                        winchPosition nearbyPosition = new winchPosition(new GeoLocation(response.Latitude, response.Longitude), response.Altitude, 0, response.Airspeed, response.FlightNumber == "9999" || insertedTowPlane.Key == e.ObjectID ? "Tow Plane" : response.Title, response.Category);
                        _nearbyInfoResponse[e.ObjectID] = nearbyPosition;

                        // TOWING IN PROCESS
                        if (towingTarget == e.ObjectID)
                        {
                            processTowing();
                        }

                        // RADAR
                        if (thermalsDebugActive > 0 || httpServerActive)
                            Application.Current.Dispatcher.Invoke(() => _radarClass.updateRadarNearby(RadarCanvas, e.ObjectID, _mathClass.getForceDirection(nearbyPosition, _planeInfoResponse), nearbyPosition, towingTarget == e.ObjectID, Math.Min(maxRadarScale, assistantSettings["RadarScale"])));

                        // PLAY RECORDING
                        if (_trackingClass.ghostPlayerActive(e.ObjectID))
                        {
                            GhostCommit towCommit = new GhostCommit();
                            towCommit = _trackingClass.updateGhostPlayer(e.ObjectID, response, towCommit, _fsConnect, _mathClass, absoluteTime);
                            if (_trackingClass.message.Key != 0)
                            {
                                switch (_trackingClass.message.Value)
                                {
                                    case "RELEASE":
                                        if (towingTarget == _trackingClass.message.Key)
                                            attachTowCable(null, null);
                                        break;
                                    case "REMOVE":
                                        Application.Current.Dispatcher.Invoke(() => removeRecordSlider(_trackingClass.message.Key));
                                        break;
                                    default:
                                        showMessage(_trackingClass.message.Value, _fsConnect);
                                        break;
                                }

                                _trackingClass.message = new KeyValuePair<uint, string>();
                            }

                            if (//!double.IsNaN(towCommit.RotationVelocityBodyX) && !double.IsNaN(towCommit.RotationVelocityBodyY) && !double.IsNaN(towCommit.RotationVelocityBodyZ) &&
                                !double.IsNaN(towCommit.planeHeading) && !double.IsNaN(towCommit.planePitch) && !double.IsNaN(towCommit.planeRoll) &&
                                !double.IsNaN(towCommit.VelocityBodyX) && !double.IsNaN(towCommit.VelocityBodyZ) && !double.IsNaN(towCommit.VelocityBodyZ))
                            {
                                try
                                {
                                    //Console.WriteLine($"RotationVelocityBodyX: {towCommit.RotationVelocityBodyX:F4} RotationVelocityBodyY: {towCommit.RotationVelocityBodyY:F4} RotationVelocityBodyZ: {towCommit.RotationVelocityBodyZ:F4}");
                                    //Console.WriteLine($"VelocityBodyZ: {towCommit.VelocityBodyZ:F4}");
                                    _fsConnect.UpdateData(Definitions.GhostCommit, towCommit, e.ObjectID);

                                }
                                catch (Exception ex)
                                {
                                    addLogMessage(ex.Message);
                                }
                            }
                        }
                        else if (response.FlightNumber == "9999" || insertedTowPlane.Key == e.ObjectID)
                        {
                            bool AIhold = insertTowPlanePressed != 0 && _planeInfoResponse.AbsoluteTime - insertTowPlanePressed < AIholdInterval;
                            // HOLD INSERTED PLANE FOR A WHILE
                            if (AIhold)
                            {
                                Console.WriteLine("Holding AI tow plane #" + e.ObjectID);
                                TowInfoResponse towInfo = new TowInfoResponse();
                                towInfo.Altitude = response.Altitude;
                                towInfo.Latitude = response.Latitude;
                                towInfo.Longitude = response.Longitude;
                                towInfo.Heading = response.Heading;
                                towInfo.Bank = 0;
                                towInfo.VelocityBodyX = 0;
                                towInfo.VelocityBodyY = 0;
                                towInfo.VelocityBodyZ = 0;

                                if (!double.IsNaN(towInfo.Altitude) && !double.IsNaN(towInfo.Latitude) && !double.IsNaN(towInfo.Longitude) && !double.IsNaN(towInfo.Heading) && !double.IsNaN(towInfo.Bank) && !double.IsNaN(towInfo.VelocityBodyZ))
                                {
                                    Console.WriteLine("Leveling AI tow plane #" + e.ObjectID);
                                    try
                                    {
                                        _fsConnect.UpdateData(Definitions.TowPlane, towInfo, e.ObjectID);
                                    }
                                    catch (Exception ex)
                                    {
                                        addLogMessage(ex.Message);
                                    }
                                }
                            }
                            // LEVEL UP TAXIING AI TOW PLANE
                            else if (Math.Abs(response.Bank) > 1 * Math.PI / 180 && response.SimOnGround == 100)
                            {
                                TowInfoPitch towCommit = new TowInfoPitch();
                                towCommit.Bank = response.Bank - Math.Sign(response.Bank) * lastFrameTiming / 50;
                                towCommit.Heading = response.Heading;
                                towCommit.Pitch = response.Pitch;
                                towCommit.VelocityBodyY = response.Verticalspeed * Math.Cos(response.Bank);
                                towCommit.VelocityBodyZ = response.Airspeed;

                                if (!double.IsNaN(towCommit.Heading) && !double.IsNaN(towCommit.Pitch) && !double.IsNaN(towCommit.Bank) && !double.IsNaN(towCommit.VelocityBodyY) &&
                                    !double.IsNaN(towCommit.VelocityBodyZ))
                                {
                                    try
                                    {
                                        _fsConnect.UpdateData(Definitions.TowPlaneCommit, towCommit, e.ObjectID);
                                    }
                                    catch (Exception ex)
                                    {
                                        addLogMessage(ex.Message);
                                    }
                                }
                            }
                        }
                    }
                    // ENGINE DATA
                    else if (e.RequestId == (uint)Requests.PlaneEngineData)
                    {
                        //Console.WriteLine(e.ObjectID);
                        //Console.WriteLine(JsonConvert.SerializeObject(e.Data, Formatting.Indented));
                        _planeEngineData = (PlaneEngineData)e.Data;
                    }

                    // WEATHER DATA
                    else if (e.RequestId == (uint)Requests.WeatherData)
                    {
                        //Console.WriteLine(e.ObjectID);
                        //Console.WriteLine(JsonConvert.SerializeObject(e.Data, Formatting.Indented));
                        _weatherReport = (WeatherReport)e.Data;

                        // WIND
                        if (_weatherReport.AmbientWindDirection - windDirection > Math.PI)
                        {
                            _weatherReport.AmbientWindDirection -= 2 * Math.PI;
                        }
                        else if (_weatherReport.AmbientWindDirection - windDirection < -Math.PI)
                        {
                            _weatherReport.AmbientWindDirection += 2 * Math.PI;
                        }

                        windDirection = 0.9 * (windDirection + 2 * Math.PI) + 0.1 * (_weatherReport.AmbientWindDirection + 2 * Math.PI);
                        windVelocity = 0.9 * windVelocity + 0.1 * _weatherReport.AmbientWindVelocity;

                        while (windDirection > Math.PI) { windDirection -= 2 * Math.PI; }
                        while (windDirection < -Math.PI) { windDirection += 2 * Math.PI; }

                        // WEATHER
                        switch (_weatherReport.AmbientPrecipState)
                        {
                            case 4: // RAIN
                            case 8: // SNOW
                                overcastModifier = 0.5 * overcastModifier + 0.1 * (1 + 4 * Math.Max(0, 1 - _weatherReport.AmbientPrecipRate / 100));
                                break;
                            case 2: // DRY
                            default:
                                overcastModifier = 0.9 * overcastModifier + 0.1;
                                break;
                        }

                        // DAYTIME
                        switch (_weatherReport.TimeOfDay)
                        {
                            case 0: // DAWN
                                dayTimeModifier = 0.95 * dayTimeModifier + 0.025;
                                break;
                            case 1: // DAY
                                dayTimeModifier = 0.9 * dayTimeModifier + 0.1;
                                break;
                            case 2: // DUSK
                            case 3: // NIGHT
                                dayTimeModifier = 0.95 * dayTimeModifier + 0.01;
                                break;
                        }

                        Console.WriteLine($"windDirection: {windDirection:F2} windVelocity: {windVelocity:F2} dayTimeModifier: {dayTimeModifier:F2} overcastModifier: {overcastModifier:F}");
                    }
                    else
                    {
                        Console.WriteLine("Unknown request ID " + (uint)e.RequestId + " received (type " + e.Data.GetType() + ")");
                    }
                }
                catch (Exception ex)
                {
                    addLogMessage("Could not handle received FS data: " + ex.Message);
                }
            }
        }

        // 23-01-2021 THIS REQUEST IS BROKEN
        /*private void HandleReceivedAirports(object sender, AirportDataReceivedEventArgs e)
        {
            try
            {
                //Console.WriteLine(JsonConvert.SerializeObject(e.Data, Formatting.Indented));
                if (airportsArray == null)
                {
                    airportsArray = (Object[])e.Data;
                }
                else
                {
                    airportsArray = airportsArray.Concat((Object[])e.Data).ToArray();
                }

                Console.WriteLine("Airports data received: " + airportsArray.Count());
            }
            catch (Exception ex)
            {
                Console.WriteLine("Could not handle received AP data: " + ex.Message);
            }
        }*/

        private void HandleReceivedSystemEvent(object sender, ObjectAddremoveEventReceivedEventArgs e)
        {
            try
            {
                Console.WriteLine("System EVENT data received ObjectID: " + e.ObjectID + " RequestId: " + e.RequestId + " respondID: " + e.Data);

                switch (e.RequestId)
                {
                    case (uint)Definitions.SystemEvents:

                        GhostPlane gp = _trackingClass.tryCaptureGhostPlane((uint)e.Data, absoluteTime);
                        if (gp.TrackPoints != null && gp.TrackPoints.Count > 0)
                        {
                            //_fsConnect.AIReleaseControl(insertedTowPlane.Key, Requests.TowPlane);
                            Application.Current.Dispatcher.Invoke(() => addRecordSlider(gp));
                            teleportGhostPlane(gp);
                        }

                        if (towScanMode == TowScanMode.TowSearch && insertedTowPlane.Key == TARGETMAX)
                        {
                            insertedTowPlane = new KeyValuePair<uint, bool>((uint)e.Data, false);
                            //towScanMode = TowScanMode.Scan;
                            Console.WriteLine("Tow plane number " + insertedTowPlane);

                            if (towingTarget == TARGETMAX && towScanMode >= TowScanMode.TowSearch)
                            {
                                Console.WriteLine("Teleporting tow plane");
                                _fsConnect.AIReleaseControl(insertedTowPlane.Key, Requests.TowPlane);
                                teleportTowPlane(insertedTowPlane.Key);
                            }
                        }
                        break;
                }
            }
            catch (Exception ex)
            {
                addLogMessage("Could not handle received EVENT data: " + ex.Message);
            }
        }


        private void trackControlChanges()
        {
            if (_planeAvionicsResponse.TotalWeight != 0)
            {
                foreach (var field in typeof(PlaneAvionicsResponse).GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
                    if (controlTimestamps.ContainsKey(field.Name) && field.FieldType == typeof(double) && field.Name.StartsWith("LIGHT") &&
                        (double)field.GetValue(_planeAvionicsResponse) != (double)field.GetValue(_planeAvionicsResponseLast))
                    {
                        if (absoluteTime - controlTimestamps[field.Name] < 1.0)
                        {
                            Console.WriteLine(field.Name + " toggle detected");

                            // FIND FUNCTION TO TOGGLE
                            Application.Current.Dispatcher.Invoke(() => runControlFunction(field.Name));

                            controlTimestamps[field.Name] = 0;
                        }
                        else
                        {
                            controlTimestamps[field.Name] = absoluteTime;
                        }
                    }

                // CAPTURE WINCH/TOW TRIGGER
                if (_planeAvionicsResponse.WaterRudderHandlePosition >= 0 && _planeAvionicsResponse.WaterRudderHandlePosition != _planeAvionicsResponseLast.WaterRudderHandlePosition)
                {
                    if (_planeAvionicsResponse.WaterRudderHandlePosition < 50)
                    {
                        if (_winchPosition != null) // WINCH RELEASE
                        {
                            Console.WriteLine("Winch release detected");
                            Application.Current.Dispatcher.Invoke(() => toggleLaunchPrepare(null, null));
                        }

                        if (towingTarget != TARGETMAX) // TOW RELEASE
                        {
                            Console.WriteLine("Tow release detected");
                            Application.Current.Dispatcher.Invoke(() => toggleTowCable(towingTarget));
                        }
                    }

                    if (_planeAvionicsResponse.WaterRudderHandlePosition == 50) // WINCH CONNECT
                    {
                        Console.WriteLine("Winch connect detected");
                        Application.Current.Dispatcher.Invoke(() => toggleLaunchPrepare(null, null));
                    }
                    else if (_planeAvionicsResponse.WaterRudderHandlePosition == 100) // TOW INSERT
                    {
                        Console.WriteLine("Tow connect detected");
                        Application.Current.Dispatcher.Invoke(() => aiTowPlane(null, null));
                    }
                }
            }
        }

        private void runControlFunction(string fieldName)
        {
            foreach (ComboBox tb in FindLogicalChildren<ComboBox>(window))
            {
                if (tb.Tag != null)
                {
                    try
                    {
                        if (tb.SelectedItem != null && tb.SelectedItem.ToString().Replace(" ", "") == fieldName)
                        {
                            Console.WriteLine("Triggering function " + tb.Tag.ToString());
                            Type type = Application.Current.MainWindow.GetType();
                            MethodInfo method = type.GetMethod(tb.Tag.ToString(), BindingFlags.NonPublic | BindingFlags.Instance);
                            if (method != null)
                            {
                                method.Invoke(Application.Current.MainWindow, new object[] { null, null });
                            }
                            else
                            {
                                addLogMessage("Function " + tb.Tag.ToString() + " can't be triggered");
                            }
                        }
                    }
                    catch { }
                }
            }
        }

        private void InitializeDataDefinitions(FsConnect fsConnect)
        {
            Console.WriteLine("InitializeDataDefinitions");

            // PLANE INFO
            List<SimProperty> definition = new List<SimProperty>();
            definition.Add(new SimProperty(FsSimVar.PlaneLatitude, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.PlaneLongitude, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.PlaneAltitudeAboveGround, FsUnit.Meter, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.PlaneAltitude, FsUnit.Meter, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.AbsoluteTime, FsUnit.Second, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.PlaneHeading, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.PlanePitch, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.PlaneBank, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.GpsGroundSpeed, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.AirspeedIndicated, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.AirspeedTrue, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.Verticalspeed, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.AmbientWindY, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.VelocityBodyX, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.VelocityBodyY, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.VelocityBodyZ, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.AccelerationBodyY, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.AccelerationBodyZ, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.RotationVelocityBodyX, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.RotationVelocityBodyY, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.RotationVelocityBodyZ, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.RotationAccelerationBodyX, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.RotationAccelerationBodyY, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.RotationAccelerationBodyZ, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            definition.Add(new SimProperty(FsSimVar.FoldingWindR, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<PlaneInfoResponse>(Definitions.PlaneInfo, definition);

            // VELOCITY COMMIT
            List<SimProperty> cDefinition = new List<SimProperty>();
            cDefinition.Add(new SimProperty(FsSimVar.VelocityBodyX, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            cDefinition.Add(new SimProperty(FsSimVar.VelocityBodyY, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            cDefinition.Add(new SimProperty(FsSimVar.VelocityBodyZ, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<PlaneInfoCommit>(Definitions.PlaneCommit, cDefinition);

            List<SimProperty> vDefinition = new List<SimProperty>();
            vDefinition.Add(new SimProperty(FsSimVar.VelocityBodyY, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            vDefinition.Add(new SimProperty(FsSimVar.VelocityBodyZ, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            vDefinition.Add(new SimProperty(FsSimVar.RotationAccelerationBodyX, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            //vDefinition.Add(new SimProperty(FsSimVar.FoldingWindR, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<ThermalVelocity>(Definitions.ThermalVelocityCommit, vDefinition);

            List<SimProperty> fDefinition = new List<SimProperty>();
            fDefinition.Add(new SimProperty(FsSimVar.FoldingWindR, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<double>(Definitions.FoldingWindCommit, fDefinition);

            List<SimProperty> rxDefinition = new List<SimProperty>();
            rxDefinition.Add(new SimProperty(FsSimVar.RotationAccelerationBodyZ, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<double>(Definitions.RotationVelocityZCommit, rxDefinition);

            // AVIONICS
            List<SimProperty> aDefinition = new List<SimProperty>();
            aDefinition.Add(new SimProperty(FsSimVar.Title, FsUnit.None, SIMCONNECT_DATATYPE.STRING256));
            aDefinition.Add(new SimProperty(FsSimVar.Type, FsUnit.None, SIMCONNECT_DATATYPE.STRING256));
            aDefinition.Add(new SimProperty(FsSimVar.Model, FsUnit.None, SIMCONNECT_DATATYPE.STRING256));
            aDefinition.Add(new SimProperty(FsSimVar.StaticCGtoGround, FsUnit.Meter, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.SimOnGround, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.BrakeParkingPosition, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.OnAnyRunway, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.IsSlewActive, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.TotalWeight, FsUnit.Pounds, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.SimRate, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.TailhookPosition, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LaunchbarPosition, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.WaterRudderHandlePosition, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.Smoke, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.YokeYPosition, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.YokeXPosition, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.RudderPosition, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));

            aDefinition.Add(new SimProperty(FsSimVar.LIGHTPANEL, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LIGHTSTROBE, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LIGHTLANDING, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LIGHTTAXI, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LIGHTBEACON, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LIGHTNAV, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LIGHTLOGO, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LIGHTWING, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LIGHTRECOGNITION, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LIGHTCABIN, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LIGHTGLARESHIELD, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LIGHTPEDESTRAL, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            aDefinition.Add(new SimProperty(FsSimVar.LIGHTPOTENTIOMETER, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<PlaneAvionicsResponse>(Definitions.PlaneAvionics, aDefinition);

            List<SimProperty> lDefinition = new List<SimProperty>();
            lDefinition.Add(new SimProperty(FsSimVar.PlaneAltitude, FsUnit.Meter, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<PlaneInfoLift>(Definitions.PlaneLift, lDefinition);

            List<SimProperty> rDefinition = new List<SimProperty>();
            rDefinition.Add(new SimProperty(FsSimVar.RotationVelocityBodyX, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            rDefinition.Add(new SimProperty(FsSimVar.RotationVelocityBodyY, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            rDefinition.Add(new SimProperty(FsSimVar.RotationVelocityBodyZ, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<PlaneInfoRotateVelocity>(Definitions.PlaneRotationVelocity, rDefinition);

            List<SimProperty> raDefinition = new List<SimProperty>();
            raDefinition.Add(new SimProperty(FsSimVar.RotationAccelerationBodyX, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            raDefinition.Add(new SimProperty(FsSimVar.RotationAccelerationBodyY, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            raDefinition.Add(new SimProperty(FsSimVar.RotationAccelerationBodyZ, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<PlaneInfoRotateAccel>(Definitions.PlaneRotateAccel, raDefinition);

            List<SimProperty> nDefinition = new List<SimProperty>();
            nDefinition.Add(new SimProperty(FsSimVar.Title, FsUnit.None, SIMCONNECT_DATATYPE.STRING256));
            nDefinition.Add(new SimProperty(FsSimVar.Category, FsUnit.None, SIMCONNECT_DATATYPE.STRING256));
            nDefinition.Add(new SimProperty(FsSimVar.ATCFLIGHTNUMBER, FsUnit.None, SIMCONNECT_DATATYPE.STRING8));
            nDefinition.Add(new SimProperty(FsSimVar.PlaneLatitude, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            nDefinition.Add(new SimProperty(FsSimVar.PlaneLongitude, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            nDefinition.Add(new SimProperty(FsSimVar.PlaneAltitude, FsUnit.Meter, SIMCONNECT_DATATYPE.FLOAT64));
            nDefinition.Add(new SimProperty(FsSimVar.AirspeedTrue, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            nDefinition.Add(new SimProperty(FsSimVar.Verticalspeed, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            nDefinition.Add(new SimProperty(FsSimVar.PlaneHeading, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            nDefinition.Add(new SimProperty(FsSimVar.PlanePitch, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            nDefinition.Add(new SimProperty(FsSimVar.PlaneBank, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            nDefinition.Add(new SimProperty(FsSimVar.SimOnGround, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            nDefinition.Add(new SimProperty(FsSimVar.OnAnyRunway, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            //nDefinition.Add(new SimProperty(FsSimVar.AmbientInCloud, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<NearbyInfoResponse>(Definitions.NearbyObjects, nDefinition);

            List<SimProperty> tDefinition = new List<SimProperty>();
            tDefinition.Add(new SimProperty(FsSimVar.PlaneLatitude, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            tDefinition.Add(new SimProperty(FsSimVar.PlaneLongitude, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            tDefinition.Add(new SimProperty(FsSimVar.PlaneAltitude, FsUnit.Meter, SIMCONNECT_DATATYPE.FLOAT64));
            tDefinition.Add(new SimProperty(FsSimVar.PlaneHeading, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            tDefinition.Add(new SimProperty(FsSimVar.PlaneBank, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            tDefinition.Add(new SimProperty(FsSimVar.VelocityBodyX, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            tDefinition.Add(new SimProperty(FsSimVar.VelocityBodyY, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            tDefinition.Add(new SimProperty(FsSimVar.VelocityBodyZ, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<TowInfoResponse>(Definitions.TowPlane, tDefinition);

            List<SimProperty> pDefinition = new List<SimProperty>();
            pDefinition.Add(new SimProperty(FsSimVar.PlaneHeading, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            pDefinition.Add(new SimProperty(FsSimVar.PlanePitch, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            pDefinition.Add(new SimProperty(FsSimVar.PlaneBank, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            pDefinition.Add(new SimProperty(FsSimVar.VelocityBodyY, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            pDefinition.Add(new SimProperty(FsSimVar.VelocityBodyZ, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<TowInfoPitch>(Definitions.TowPlaneCommit, pDefinition);

            List<SimProperty> gDefinition = new List<SimProperty>();
            gDefinition.Add(new SimProperty(FsSimVar.VelocityBodyX, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            gDefinition.Add(new SimProperty(FsSimVar.VelocityBodyY, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            gDefinition.Add(new SimProperty(FsSimVar.VelocityBodyZ, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            //gDefinition.Add(new SimProperty(FsSimVar.RotationVelocityBodyX, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            //gDefinition.Add(new SimProperty(FsSimVar.RotationVelocityBodyY, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            //gDefinition.Add(new SimProperty(FsSimVar.RotationVelocityBodyZ, FsUnit.RadianPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            gDefinition.Add(new SimProperty(FsSimVar.PlaneHeading, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            gDefinition.Add(new SimProperty(FsSimVar.PlanePitch, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            gDefinition.Add(new SimProperty(FsSimVar.PlaneBank, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<GhostCommit>(Definitions.GhostCommit, gDefinition);

            List<SimProperty> eDefinition = new List<SimProperty>();
            eDefinition.Add(new SimProperty(FsSimVar.ENGTORQUE1, FsUnit.FootPounds, SIMCONNECT_DATATYPE.FLOAT64));
            eDefinition.Add(new SimProperty(FsSimVar.ENGTORQUE2, FsUnit.FootPounds, SIMCONNECT_DATATYPE.FLOAT64));
            eDefinition.Add(new SimProperty(FsSimVar.ENGTORQUE3, FsUnit.FootPounds, SIMCONNECT_DATATYPE.FLOAT64));
            eDefinition.Add(new SimProperty(FsSimVar.ENGTORQUE4, FsUnit.FootPounds, SIMCONNECT_DATATYPE.FLOAT64));
            eDefinition.Add(new SimProperty(FsSimVar.TURBTHRUST1, FsUnit.FootPounds, SIMCONNECT_DATATYPE.FLOAT64));
            eDefinition.Add(new SimProperty(FsSimVar.TURBTHRUST2, FsUnit.FootPounds, SIMCONNECT_DATATYPE.FLOAT64));
            eDefinition.Add(new SimProperty(FsSimVar.TURBTHRUST3, FsUnit.FootPounds, SIMCONNECT_DATATYPE.FLOAT64));
            eDefinition.Add(new SimProperty(FsSimVar.TURBTHRUST4, FsUnit.FootPounds, SIMCONNECT_DATATYPE.FLOAT64));
            eDefinition.Add(new SimProperty(FsSimVar.ThrottleLeverPosition1, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            eDefinition.Add(new SimProperty(FsSimVar.ThrottleLeverPosition2, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            eDefinition.Add(new SimProperty(FsSimVar.ThrottleLeverPosition3, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            eDefinition.Add(new SimProperty(FsSimVar.ThrottleLeverPosition4, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));

            fsConnect.RegisterDataDefinition<PlaneEngineData>(Definitions.PlaneEngineData, eDefinition);

            List<SimProperty> wDefinition = new List<SimProperty>();
            wDefinition.Add(new SimProperty(FsSimVar.AmbientAirTemperature, FsUnit.Celsius, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.AmbientBarometerPressure, FsUnit.Millibars, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.AmbientDensity, FsUnit.SlugsPerCubiFeet, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.AmbientInCloud, FsUnit.Percent, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.AmbientPrecipState, FsUnit.Meter, SIMCONNECT_DATATYPE.FLOAT64));//  2 = None 4 = Rain 8 = Snow
            wDefinition.Add(new SimProperty(FsSimVar.AmbientPrecipRate, FsUnit.Meter, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.AmbientPressure, FsUnit.Millibars, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.AmbientSeaLevelPressure, FsUnit.Millibars, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.AmbientStandardAtmTemperature, FsUnit.Celsius, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.AmbientTemperature, FsUnit.Celsius, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.AmbientVisibility, FsUnit.Meter, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.AmbientWindDirection, FsUnit.Radians, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.AmbientWindVelocity, FsUnit.MeterPerSecond, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.LocalDayOfYear, FsUnit.Meter, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.LocalTime, FsUnit.Second, SIMCONNECT_DATATYPE.FLOAT64));
            wDefinition.Add(new SimProperty(FsSimVar.TimeOfDay, FsUnit.Meter, SIMCONNECT_DATATYPE.FLOAT64));// 0 = Dawn      1 = Day     2 = Dusk    3 = Night



            fsConnect.RegisterDataDefinition<WeatherReport>(Definitions.WeatherData, wDefinition);
        }

        private void changeButtonStatus(bool active, Button btn, bool? enabled = null, string text = "")
        {
            if (active)
            {
                btn.BorderBrush = new SolidColorBrush(Colors.DarkGreen);
                btn.Foreground = new SolidColorBrush(Colors.DarkGreen);
            }
            else
            {
                btn.BorderBrush = new SolidColorBrush(Colors.DarkRed);
                btn.Foreground = new SolidColorBrush(Colors.DarkRed);
            }

            if (!string.IsNullOrEmpty(text))
                btn.Content = text;

            if (enabled != null)
                btn.IsEnabled = enabled == true;
        }

        private static IEnumerable<T> FindLogicalChildren<T>(DependencyObject depObj) where T : DependencyObject
        {
            if (depObj != null)
            {
                foreach (object rawChild in LogicalTreeHelper.GetChildren(depObj))
                {
                    if (rawChild is DependencyObject)
                    {
                        DependencyObject child = (DependencyObject)rawChild;
                        if (child is T)
                        {
                            yield return (T)child;
                        }

                        foreach (T childOfChild in FindLogicalChildren<T>(child))
                        {
                            yield return childOfChild;
                        }
                    }
                }
            }
        }

        public void commitAvionicsData()
        {
            try
            {
                if (!double.IsNaN(_planeAvionicsResponse.WaterRudderHandlePosition) && !double.IsNaN(_planeAvionicsResponse.TotalWeight) && !double.IsNaN(_planeAvionicsResponse.Smoke) &&
                    !double.IsNaN(_planeAvionicsResponse.SimRate) && !double.IsNaN(_planeAvionicsResponse.BrakeParkingPosition) && !double.IsNaN(_planeAvionicsResponse.IsSlewActive) &&
                    !double.IsNaN(_planeAvionicsResponse.LaunchbarPosition) && !double.IsNaN(_planeAvionicsResponse.OnAnyRunway) && !double.IsNaN(_planeAvionicsResponse.SimOnGround))
                    _fsConnect.UpdateData(Definitions.PlaneAvionics, _planeAvionicsResponse);
            }
            catch (Exception ex)
            {
                addLogMessage(ex.Message);
            }
        }

        // SETTINGS STUFF
        private void processSettings(bool write = true)
        {
            foreach (ComboBox tb in FindLogicalChildren<ComboBox>(Application.Current.MainWindow))
            {
                if (tb.Name != null && tb.Name != "towPlaneModel")
                {
                    if (write && assistantSettings.ContainsKey(tb.Name))
                    {
                        ComboBox cb = (ComboBox)window.FindName(tb.Name);

                        if (cb.IsEditable == true)
                            cb.Text = assistantSettings[tb.Name].ToString();
                        else if (cb.Items.Count > (int)assistantSettings[tb.Name])
                        {
                            cb.SelectedIndex = (int)assistantSettings[tb.Name];
                            Console.WriteLine(tb.Name + " " + assistantSettings[tb.Name].ToString() + "(" + cb.Items.Count + ")");
                        }
                        else
                        {
                            assistantSettings[tb.Name] = 0;
                        }
                    }
                    else if (!write)
                    {
                        double value;
                        if (tb.IsEditable == true)
                            double.TryParse(tb.Text, out value);
                        else
                            value = tb.SelectedIndex;

                        if (!assistantSettings.ContainsKey(tb.Name))
                            assistantSettings.Add(tb.Name, value);
                        else
                            assistantSettings[tb.Name] = value;
                    }
                }
            }

            foreach (CheckBox tb in FindLogicalChildren<CheckBox>(Application.Current.MainWindow))
                if (tb.Name != null)
                {
                    if (write && assistantSettings.ContainsKey(tb.Name))
                    {
                        ((CheckBox)window.FindName(tb.Name)).IsChecked = assistantSettings[tb.Name] != 0;
                    }
                    else if (!write)
                    {
                        if (!assistantSettings.ContainsKey(tb.Name))
                            assistantSettings.Add(tb.Name, tb.IsChecked == true ? 1 : 0);
                        else
                            assistantSettings[tb.Name] = tb.IsChecked == true ? 1 : 0;
                    }
                }

            foreach (Slider tb in FindLogicalChildren<Slider>(Application.Current.MainWindow))
                if (tb.Name != null)
                {
                    if (write && assistantSettings.ContainsKey(tb.Name))
                    {
                        if (tb.Name == "RadarScale")
                        {
                            assistantSettings[tb.Name] = Math.Min(maxRadarScale, assistantSettings[tb.Name]);
                        }

                        ((Slider)window.FindName(tb.Name)).Value = assistantSettings[tb.Name];
                    }
                    else if (!write)
                    {
                        if (!assistantSettings.ContainsKey(tb.Name))
                            assistantSettings.Add(tb.Name, tb.Value);
                        else
                            assistantSettings[tb.Name] = tb.Value;
                    }
                }

            foreach (TextBox tb in FindLogicalChildren<TextBox>(Application.Current.MainWindow))
                if (tb.Name != null)
                {
                    if (write)
                    //if (write && assistantSettings.ContainsKey(tb.Name + "_"))
                    {
                        foreach (var val in assistantSettings)
                        {
                            if (val.Key.StartsWith(tb.Name + "_"))
                            {
                                tb.Text = val.Key.Replace(tb.Name + "_", "");
                            }
                        }
                    }
                    else if (!write)
                    {
                        if (!assistantSettings.ContainsKey(tb.Name + "_" + tb.Text))
                            assistantSettings.Add(tb.Name + "_" + tb.Text, 0);
                        else
                            assistantSettings[tb.Name + "_" + tb.Text] = 0;
                    }
                }
        }

        private void loadSettings()
        {
            if (File.Exists("assistantSettings.json"))
            {
                try
                {
                    JsonConvert.PopulateObject(File.ReadAllText("assistantSettings.json"), assistantSettings);
                    processSettings(true);

                    WindowBackground.Opacity = assistantSettings.ContainsKey("transparentBackground") && assistantSettings["transparentBackground"] == 1 ? 0.2 : 1;
                    Application.Current.MainWindow.Topmost = assistantSettings.ContainsKey("alwaysOnTop") && assistantSettings["alwaysOnTop"] == 1;
                    OnTopIcon.Content = getIconImage(alwaysOnTop.IsChecked == true, (OnTopIcon.Tag.ToString()));
                    TransparentIcon.Content = getIconImage(transparentBackground.IsChecked == true, (TransparentIcon.Tag.ToString()));

                    enableApiThermals(APIthermalsButton, assistantSettings.ContainsKey("APIthermalsAutoload") && assistantSettings["APIthermalsAutoload"] == 1);
                    //toggleSidebarWindow(assistantSettings.ContainsKey("sidebarActive") && assistantSettings["sidebarActive"] == 1);

                    TowPlaneTrackContainer.Visibility = assistantSettings.ContainsKey("towType") && assistantSettings["towType"] == 1 ? Visibility.Visible : Visibility.Collapsed;
                    towInsertContainer.Visibility = assistantSettings.ContainsKey("towType") && assistantSettings["towType"] == 1 ? Visibility.Visible : Visibility.Collapsed;

                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }
            }

            // LOAD TOW PLANE RECORDS
            if (Directory.Exists(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + "\\TOWPLANES\\"))
            {
                int counter = 0;
                var files = Directory.EnumerateFiles("TOWPLANES", "*.gpx", SearchOption.TopDirectoryOnly);
                foreach (var file in files)
                {
                    ComboBoxItem item = new ComboBoxItem();

                    if (counter < allowedRecords)
                    {
                        item.Content = Path.GetFileNameWithoutExtension(file);
                        item.Tag = file;
                    }
                    else
                    {
                        item.Content = "KineticAssistant+";
                        item.Tag = "";
                        item.IsEnabled = false;
                    }

                    towPlaneTrack.Items.Add(item);

                    counter++;
                }

                if (towPlaneTrack.Items.Count > 0)
                    towPlaneTrack.SelectedIndex = 0;
            }
        }

        private void saveSettings(object sender, RoutedEventArgs e)
        {
            try
            {
                if (loaded)
                {
                    processSettings(false);
                    File.WriteAllText("assistantSettings.json", JsonConvert.SerializeObject(assistantSettings));

                    // ADDITIONAL ACTIONS
                    if (sender != null && sender.GetType() == typeof(CheckBox))
                    {
                        if (((CheckBox)sender).Name == "transparentBackground")
                        {
                            WindowBackground.Opacity = ((CheckBox)sender).IsChecked == true ? 0.2 : 1;
                            TransparentIcon.Content = getIconImage(((CheckBox)sender).IsChecked == true, (TransparentIcon.Tag.ToString()));
                        }
                        else if (((CheckBox)sender).Name == "alwaysOnTop")
                        {
                            Application.Current.MainWindow.Topmost = ((CheckBox)sender).IsChecked == true;
                            OnTopIcon.Content = getIconImage(((CheckBox)sender).IsChecked == true, (OnTopIcon.Tag.ToString()));
                        }
                        else if (((CheckBox)sender).Name == "APIthermalsAutoload")
                        {
                            enableApiThermals(APIthermalsButton, ((CheckBox)sender).IsChecked == true);

                            if (((CheckBox)sender).IsChecked != true)
                            {
                                APICsvExportButton.Visibility = Visibility.Collapsed;
                            }
                        }
                        else if (((CheckBox)sender).Name == "sidebarActive")
                        {
                            toggleSidebarWindow(((CheckBox)sender).IsChecked == true);
                        }
                    }
                    else if (sender != null && sender.GetType() == typeof(ComboBox))
                    {
                        if (((ComboBox)sender).Name == "nmeaServer")
                        {
                            if (((ComboBox)sender).SelectedIndex != 0)
                            {
                                ServerStop();
                                ServerStart(((ComboBoxItem)((ComboBox)sender).SelectedItem).Tag.ToString());
                            }
                            else
                            {
                                ServerStop();
                            }
                        }
                        if (((ComboBox)sender).Name == "panelServer")
                        {
                            if (((ComboBox)sender).SelectedIndex != 0 && !httpServerActive)
                            {
                                startHTTP();
                            }
                        }
                        else if (((ComboBox)sender).Name == "aircraftType" || ((ComboBox)sender).Name == "preferredServer" || ((ComboBox)sender).Name == "showPrivate")
                        {
                            //loadEventsList(null, null);
                        }
                        else if (((ComboBox)sender).Name == "towType")
                        {
                            TowPlaneTrackContainer.Visibility = Visibility.Collapsed;
                            towInsertContainer.Visibility = Visibility.Collapsed;
                            pushPicture.Visibility = Visibility.Collapsed;

                            switch (((ComboBox)sender).SelectedIndex)
                            {
                                case 0: // PLAYER
                                    break;
                                case 1: // TRACK
                                    TowPlaneTrackContainer.Visibility = Visibility.Visible;
                                    towInsertContainer.Visibility = Visibility.Visible;
                                    break;
                                case 2: // PUSH
                                    pushPicture.Visibility = Visibility.Visible;
                                    break;
                                case 3: // GROUND
                                    break;
                                case 4: // BOAT
                                    break;
                                case 5: // HELI
                                    break;
                            }
                        }
                    }
                    else if (sender != null && sender.GetType() == typeof(Slider))
                    {
                        if (((Slider)sender).Name == "RequestsFrequency")
                        {
                            launchTimer.Interval = new TimeSpan(0, 0, 0, 0, 1000 / Math.Max(10, (int)assistantSettings["RequestsFrequency"]));
                            nearbyTimer.Interval = new TimeSpan(0, 0, 0, 0, 20000 / Math.Max(10, (int)(assistantSettings.ContainsKey("RequestsFrequency") ? assistantSettings["RequestsFrequency"] : 10)));
                        }
                        else if (((Slider)sender).Name == "RadarScale")
                        {
                            if (allowedRadarScale < 50)
                                _radarClass.updateRadarCover(RadarCanvas, assistantSettings["RadarScale"] / maxRadarScale);
                            scaleRefresh = 2;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        private void Hyperlink_RequestNavigate(object sender,
                                               System.Windows.Navigation.RequestNavigateEventArgs e)
        {
            string link = e.Uri.ToString().Replace(@"file:///", "");
            if (e.Uri.ToString().Contains("msfs.touching.cloud"))
            {
                resetMissedUpdates(null, null);
            }

            System.Diagnostics.Process.Start(e.Uri.ToString().Contains("//") ? e.Uri.AbsoluteUri : e.Uri.ToString());
        }

        private void toggleControlOptions(object sender, RoutedEventArgs e)
        {
            foreach (var element in ((StackPanel)((Button)sender).Parent).Children)
            {
                if (element.GetType() == typeof(ComboBox))
                {
                    ((ComboBox)element).Visibility = ((ComboBox)element).Visibility == Visibility.Collapsed ? Visibility.Visible : Visibility.Collapsed;
                    break;
                }
                else if (element.GetType() == typeof(Button))
                    ((Button)element).Visibility = ((Button)element).Visibility == Visibility.Collapsed ? Visibility.Visible : Visibility.Collapsed;
            }
        }
        private void showMessage(string text, FsConnect _fsConnect)
        {
            if (assistantSettings["displayTips"] == 1)
            {
                try
                {
                    _fsConnect.SetText(text, 1);
                }
                catch { }
            }

            addLogMessage(text);
        }

        private void addLogMessage(string text, int clr = 0)
        {
            if (System.Windows.Application.Current != null)
            {
                Color color = Colors.Black;
                if (clr == 1) { color = Colors.DarkOrange; }
                else if (clr == 2) { color = Colors.DarkRed; }

                Application.Current.Dispatcher.Invoke(() => messagesLog.Children.Insert(0, makeTextBlock(DateTime.UtcNow.ToString("u") + ": " + text, color, 10, TextWrapping.Wrap)));
            }
            Console.WriteLine(text);
        }

        private void playSound(string soundName)
        {
            if (assistantSettings["playSounds"] == 1)
            {
                if (soundPlayer != null)
                {
                    soundPlayer.Stop();
                }

                soundPlayer = new MediaPlayer();
                soundPlayer.Volume = assistantSettings["soundsVolume"] / 100;
                soundPlayer.Open(new Uri("pack://siteoforigin:,,,/" + soundName + ".wav"));
                soundPlayer.Play();
            }
        }

        private TextBlock makeTextBlock(string text, Color color, int size = 0, TextWrapping wrap = TextWrapping.NoWrap)
        {
            TextBlock textblock = new TextBlock();
            textblock.Text = text;
            textblock.Foreground = new SolidColorBrush(color);
            textblock.FontFamily = new FontFamily("Consolas");
            textblock.TextWrapping = wrap;
            if (size > 0)
                textblock.FontSize = size;

            return textblock;
        }

        // UI

        private void Window_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton == MouseButton.Left)
                this.DragMove();
        }

        private void OnTopIconClick(object sender, EventArgs e)
        {
            alwaysOnTop.IsChecked = !alwaysOnTop.IsChecked;
            saveSettings(alwaysOnTop, null);
        }
        private void TransparentIconClick(object sender, EventArgs e)
        {
            transparentBackground.IsChecked = !transparentBackground.IsChecked;
            saveSettings(transparentBackground, null);
        }
        private void toggleHotspotsAutoload(object sender, RoutedEventArgs e)
        {
            APIthermalsAutoload.IsChecked = !APIthermalsAutoload.IsChecked;
            saveSettings(APIthermalsAutoload, null);
        }

        private void toggleSidebar(object sender, RoutedEventArgs e)
        {
            //sidebarActive.IsChecked = !sidebarActive.IsChecked;
            //saveSettings(sidebarActive, null);
            toggleSidebarWindow();
        }
        private void toggleMainbar(object sender, RoutedEventArgs e)
        {
            toggleMainbarWindow(thermalsDebugActive == 2);
        }


        private void toggleSidebarWindow(bool? state = null)
        {
            if (state == true || state == null && toggleSidebarButton.Text == ">")
            {
                toggleSidebarButton.Text = "<";
                thermalsDebugActive = 1;
                SidebarColumn.Width = GridLength.Auto;
                window.Width = 550;
            }
            else
            {
                toggleSidebarButton.Text = ">";
                thermalsDebugActive = 0;
                SidebarColumn.Width = new GridLength(0);
                window.Width = 275;
            }
        }
        private void toggleMainbarWindow(bool state)
        {
            if (state)
            {
                toggleMainButton.Text = ">";
                thermalsDebugActive = 1;
                MaincontentColumn.Width = GridLength.Auto;
                window.Width = 550;
            }
            else
            {
                toggleMainButton.Text = "<";
                thermalsDebugActive = 2;
                MaincontentColumn.Width = new GridLength(0);
                window.Width = 275;
            }
        }

        private void MinimizeIconClick(object sender, EventArgs e)
        {
            WindowState = WindowState.Minimized;
        }
        private void closeIconClick(object sender, EventArgs e)
        {
            if (!validConnection() || MessageBox.Show("Connection currently is active. You sure to close application?", "Warning", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
            {
                if (validConnection())
                {
                    toggleConnect(null, null);
                }

                ServerStop();

                mutex.ReleaseMutex();
                Application.Current.Shutdown();
            }
        }

        private Image getIconImage(bool state, string names)
        {
            Image img = new Image();
            img.Source = new BitmapImage(new Uri("media/" + names.Split('|')[state ? 1 : 0], UriKind.Relative));

            return img;
        }

        // UPDATES START
        private void setNewsLabel(string counter)
        {
            if (counter != "0")
            {
                Application.Current.Dispatcher.Invoke(() => newsLink.Text = "(" + counter + ")");
                Application.Current.Dispatcher.Invoke(() => SettingsTab.Background = new SolidColorBrush(Color.FromArgb(10, 255, 0, 0)));
            }
            else
            {
                Application.Current.Dispatcher.Invoke(() => newsLink.Text = "");
                Application.Current.Dispatcher.Invoke(() => SettingsTab.Background = new SolidColorBrush(Colors.Transparent));
            }
        }
        private void resetMissedUpdates(object sender, EventArgs e)
        {
            Application.Current.Dispatcher.Invoke(() => newLastRead.Text = DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds.ToString());
            saveSettings(newLastRead, null);
            setNewsLabel("0");
        }
        private void triggerCheckUpdate(object sender, EventArgs e)
        {
            _ = CheckUpdateAsync();
        }

        private async Task CheckUpdateAsync()
        {
            //string pubVer = Assembly.GetExecutingAssembly().GetName().Version.ToString();

            {
                var client = new HttpClient();
                if (!assistantSettings.TryGetValue("newLastRead", out double date))
                    date = 0;
#if DEBUG
                string data = await client.GetStringAsync("https://msfs.touching.cloud/kineticassistant/?last_read=" + date + "&build=DEBUG");
#else
                string data = await client.GetStringAsync("https://msfs.touching.cloud/kineticassistant/?last_read=" + date + "&build=RELEASE");
#endif
                // GET NEWS COUNT
                Regex regexNews = new Regex(@"news=(\d+)");
                Match matchNews = regexNews.Match(data);
                if (matchNews.Groups.Count >= 2)
                {
                    setNewsLabel(matchNews.Groups[1].ToString());
                }
            }
        }
        // UPDATES END

        // TCP SERVER START
        private void addServerIPs()
        {
            try
            {
                String strHostName = Dns.GetHostName();
                IPHostEntry iphostentry = Dns.GetHostByName(strHostName);
                foreach (IPAddress ipaddress in iphostentry.AddressList)
                {
                    ComboBoxItem option = new ComboBoxItem();
                    option.Tag = ipaddress.ToString();
                    option.Content = "XCSoar server: " + option.Tag;
                    nmeaServer.Items.Add(option);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        private void ServerStart(string ip = "127.0.0.1")
        {
            addLogMessage("NMEA Server: START");
            if (threadServer == null)
            {
                try
                {
                    server = new Server(true, new CallbackUI(ServerStatus), ip);
                    threadServer = new Thread(new ThreadStart(server.Start));
                    threadServer.Start();
                }
                catch (Exception ex)
                {
                    addLogMessage("Can't start NMEA server: " + ex.Message);
                }

                while (!threadServer.IsAlive) ;
            }
        }

        private void ServerStop()
        {
            addLogMessage("NMEA Server: STOP");
            if (threadServer != null)
            {
                try
                {
                    server.Stop();
                    threadServer.Join(1000);
                    threadServer = null;
                }
                catch (Exception ex)
                {
                    addLogMessage("Can't stop NMEA server: " + ex.Message);
                }
            }
        }
        private void ServerStatus(ServerStatus status)
        {
            switch (status.status)
            {
                case ServerStatusCode.OK:
                    addLogMessage("NMEA Server: OK");
                    break;
                case ServerStatusCode.ERROR:
                    addLogMessage("NMEA Server: ERROR");
                    ServerStop();
                    break;
                case ServerStatusCode.CONN:
                    addLogMessage("NMEA Server: " + status.value);
                    break;
            }
        }
        // TCP SERVER ENDS

        // TRACKING START
        public void StartRecord(object sender, EventArgs e)
        {
            if (_trackingClass.trackRecording == null)
            {
                setTrackRecording(true);
            }
            else
            {
                setTrackRecording(false);
                _trackingClass.trackRecording = null;
            }

        }

        public void setTrackRecording(bool enable, bool save = true)
        {
            if (validConnection())
            {
                if (enable)
                {
                    Application.Current.Dispatcher.Invoke(() => changeButtonStatus(false, StartRecordButton));
                    _trackingClass.trackRecording = new List<TrackPoint>();
                    _trackingClass.lastTrackCapture = 0;
                    _trackingClass.recordingCounter = 0.0001;

                    if (save)
                        showMessage("Track recording started", _fsConnect);
                }
                else
                {
                    Application.Current.Dispatcher.Invoke(() => changeButtonStatus(true, StartRecordButton));

                    if (save)
                    {
                        zipDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + @"\RECORDINGS\";
                        if (!Directory.Exists(zipDirectory))
                        {
                            try
                            {
                                Directory.CreateDirectory(zipDirectory);
                            }
                            catch
                            {
                                MessageBox.Show("Can't create recordings directory");
                                return;
                            }
                        }
                        string filename = _planeAvionicsResponse.Title + " - " + DateTime.UtcNow.ToString("s").Replace(":", "-") + ".gpx";
                        KeyValuePair<double, string> trackContentAligned = _trackingClass.buildTrackFile(AppName.Text, "", _planeInfoResponse, _planeAvionicsResponse, _mathClass, "recorded_track_" + filename, true);
                        _trackingClass.saveTrackfile(trackContentAligned.Value, zipDirectory, "recorded_track_" + filename);

                        showMessage("Recorded track saved, length: " + _trackingClass.recordingCounter.ToString("0") + " seconds", _fsConnect);
                    }

                    //_trackingClass.trackRecording = null;
                    _trackingClass.recordingCounter = 0;
                    _trackingClass.lastRecordingCounter = 0;

                }
            }
        }

        public void LoadRecord(object sender, EventArgs e)
        {
            if (validConnection())
            {
                beforeLoadTrackRecord(0);

                OpenFileDialog sfd = new OpenFileDialog();
                sfd.Multiselect = true;

                sfd.Filter = "Flight track (*.gpx)|*.gpx";
                sfd.FilterIndex = 1;

                if (sfd.ShowDialog() == true)
                {
                    int i = 0;
                    foreach (String file in sfd.FileNames)
                    {
                        if (File.Exists(file))
                        {
                            Task.Delay(i * 500).ContinueWith(t => loadTrackRecord(file, true));
                        }
                        i++;
                    }
                }
                else
                {
                    addLogMessage("File not found");
                }
            }
        }

        public void beforeLoadTrackRecord(int mode)
        {
            if (mode == 1)
            {
                towType.SelectedIndex = 1;
                if (RadarScale.Value < 0.2)
                {
                    setRadarScale(0.2);
                }
            }
            else if (assistantSettings["RadarScale"] < 5)
            {
                setRadarScale(5);
            }

            _trackingClass.ghostTeleport = new winchPosition();
            if (mode == 1 || mode == 0 && assistantSettings.ContainsKey("ghostTeleport") && assistantSettings["ghostTeleport"] != 0)
            {
                _trackingClass.ghostTeleport.radius = _planeInfoResponse.PlaneHeading;
                _trackingClass.ghostTeleport.alt = _planeInfoResponse.Altitude;
                _trackingClass.ghostTeleport.location = _mathClass.FindPointAtDistanceFrom(new GeoLocation(_planeInfoResponse.Latitude, _planeInfoResponse.Longitude), _planeInfoResponse.PlaneHeading, 0.075);
            }
        }

        public void loadTrackRecord(string path, bool start = true)
        {
            GhostPlane gp = _trackingClass.parseTrackFile(path, _mathClass, allowedRecordLength);
            if (gp.TrackPoints.Count > 0)
            {
                if (start)
                {
                    double distance = _mathClass.findDistanceBetweenPoints(_planeInfoResponse.Latitude, _planeInfoResponse.Longitude, gp.TrackPoints[0].Location.Latitude, gp.TrackPoints[0].Location.Longitude);
                    if (distance < 5000)
                    {
                        SIMCONNECT_DATA_INITPOSITION position = new SIMCONNECT_DATA_INITPOSITION();
                        position.Altitude = gp.TrackPoints[0].Elevation;
                        position.Latitude = gp.TrackPoints[0].Location.Latitude * 180 / Math.PI;
                        position.Longitude = gp.TrackPoints[0].Location.Longitude * 180 / Math.PI;
                        //position.Airspeed = 0;
                        position.Heading = gp.TrackPoints[0].Heading * 180 / Math.PI;
                        position.Pitch = gp.TrackPoints[0].Pitch * 180 / Math.PI;
                        position.Bank = gp.TrackPoints[0].Roll * 180 / Math.PI;
                        position.Airspeed = 0;
                        position.OnGround = 0;// (uint)(gp.TrackPoints[0].AltitudeAboveGround < 5 ? 1 : 0);

                        _fsConnect.CreateNonATCAircraft(position, gp.Name, Requests.TowPlane);

                        Console.WriteLine("AI plane inserted altitude: " + position.Altitude);
                    }
                    else
                    {
                        showMessage($"Failed to insert AI plane - you are {distance / 1000:F1}km away from ghost position", _fsConnect);
                    }
                }
            }
            else
            {
                addLogMessage("Failed to parse GPX file");
            }
        }

        public void addRecordSlider(GhostPlane gp)
        {
            StackPanel group = new StackPanel();
            group.Name = "Record_" + gp.ID;
            group.Tag = gp.ID;
            group.Margin = new Thickness(0, 0, 0, 10);

            group.Children.Add(makeTextBlock(gp.Name + "(" + gp.Length.ToString("0") + "s)", Colors.Black, 12, TextWrapping.Wrap));

            Slider slider = new Slider();
            slider.Tag = gp.ID;
            slider.Minimum = 0;
            slider.Maximum = gp.Length;
            slider.TickFrequency = 60;
            slider.AutoToolTipPlacement = System.Windows.Controls.Primitives.AutoToolTipPlacement.TopLeft;
            slider.AutoToolTipPrecision = 0;
            slider.AddHandler(MouseLeftButtonUpEvent, new MouseButtonEventHandler(changeGhostPlaneProgress), true);
            group.Children.Add(slider);

            GhostsList.Children.Add(group);

            if (towPlaneInserted)
            {
                if (PlayRecordsButton.Content.ToString() == "PLAY")
                    PlayRecords(PlayRecordsButton, null);

                //towingTarget = gp.ID;
                towScanMode = TowScanMode.TowInsert;
                insertedTowPlane = new KeyValuePair<uint, bool>(gp.ID, true);
                towPlaneInserted = false;
            }
        }

        public void removeRecordSlider(uint ID)
        {
            _trackingClass.clearRecord(_fsConnect, ID);

            for (int i = GhostsList.Children.Count - 1; i >= 0; i--)
            {
                if (GhostsList.Children[i].GetType() == typeof(StackPanel) && ((StackPanel)GhostsList.Children[i]).Tag.ToString() == ID.ToString())
                {
                    GhostsList.Children.Remove((StackPanel)GhostsList.Children[i]);
                }
            }

            if (GhostsList.Children.Count == 0 && towScanMode != TowScanMode.Disabled)
            {
                toggleScanning(null, null);

                if (PlayRecordsButton.Content.ToString() == "STOP")
                    PlayRecords(PlayRecordsButton, null);
            }
        }
        public void changeGhostPlaneProgress(object sender, MouseButtonEventArgs e)
        {
            Slider slider = (Slider)sender;
            int ID = int.Parse(slider.Tag.ToString());
            Console.WriteLine("Progress update of " + ID + ": " + slider.Value);
            int index = _trackingClass.ghostPlanes.FindIndex(m => m.ID == ID);
            if (index >= 0)
            {
                GhostPlane gp = _trackingClass.ghostPlanes[index];
                gp.Progress = slider.Value;

                _trackingClass.ghostPlanes[index] = gp;
                teleportGhostPlane(_trackingClass.ghostPlanes[index], slider.Value);
            }
        }

        public void stopRecords()
        {
            int index;
            while ((index = _trackingClass.ghostPlanes.FindIndex(m => m.Progress != 0)) >= 0)
            {
                GhostPlane gp = _trackingClass.ghostPlanes[index];
                gp.Progress = 0;
                _trackingClass.ghostPlanes[index] = gp;
                teleportGhostPlane(_trackingClass.ghostPlanes[index], 0);
            }
        }

        public void PlayRecords(object sender, EventArgs e)
        {
            Button btn = (Button)sender;
            if (btn.Content.ToString() == "PLAY")
            {
                changePlayStatus(true);
            }
            else
            {
                changePlayStatus(false);
            }

        }

        public void changePlayStatus(bool newStatus)
        {
            if (newStatus)
            {
                Application.Current.Dispatcher.Invoke(() => changeButtonStatus(false, PlayRecordsButton, true, "STOP"));
                if (towScanMode == TowScanMode.Disabled)
                    toggleScanning(null, null);

                _trackingClass.playRecords(absoluteTime);
            }
            else
            {
                Application.Current.Dispatcher.Invoke(() => changeButtonStatus(true, PlayRecordsButton, true, "PLAY"));
                stopRecords();
            }

        }

        public void ClearRecords(object sender, EventArgs e)
        {
            _trackingClass.clearRecords(_fsConnect);
            Application.Current.Dispatcher.Invoke(() => GhostsList.Children.Clear());
        }
        // TRACKING ENDS

        // HTTP START
        public async void startHTTP()
        {
            await listenHTTP();
            generateRadarBitmap();
        }

        async Task listenHTTP()
        {
            // WEB
            httpServerActive = true;

            ServicePointManager.DefaultConnectionLimit = 500;
            ServicePointManager.Expect100Continue = false;
            ServicePointManager.MaxServicePoints = 500;

            HttpListener listener = new HttpListener();
            listener.Prefixes.Add("http://localhost:8212/");
            listener.Start();

            await Task.Run(async () => //This code runs on a new thread, control is returned to the caller on the UI thread.
            {
                while (true)
                {
                    try
                    {
                        var t = listener.GetContextAsync();
                        HttpListenerContext context = t.Result;
                        HttpListenerRequest request = context.Request;
                        // Obtain a response object.
                        HttpListenerResponse response = context.Response;
                        response.Headers.Add("content-type", "image/png");
                        //response.Headers.Add("content-type", "text/html");
                        response.Headers.Add("vary", "Accept-Encoding");
                        response.Headers.Add("Cache-Control", "no-cache");


                        byte[] buffer = await Task.Run(() => GetRadarImage(request.QueryString.Get("zoom")));
                        //string responseString = await Task.Run(() => getRadarString());
                        //byte[] buffer = System.Text.Encoding.UTF8.GetBytes("<!doctype html><html><body><img class=\"radarImage\" src=\"" + responseString + "\"  style=\"width: 100%; height: 100%; object-fit: contain; position: absolute;\" /></body></html>");

                        // Construct a response. 

                        if (buffer != null && buffer.Length > 0)
                        {
                            // Get a response stream and write the response to it.
                            response.ContentLength64 = buffer.Length;
                            System.IO.Stream output = response.OutputStream;
                            output.Write(buffer, 0, buffer.Length);
                            output.Close();
                            //Console.WriteLine("Output " + DateTime.Now.ToString() + " zoom: " + request.QueryString.Get("zoom"));
                        }
                        else
                        {
                            //Console.WriteLine("Empty " + DateTime.Now.ToString());
                        }
                    }
                    catch (Exception ex)
                    {
                        httpServerActive = false;

                        if (assistantSettings.ContainsKey("panelServer") && assistantSettings["panelServer"] != 0)
                        {
                            startHTTP();
                        }

                        addLogMessage(ex.Message, 2);
                        break;
                    }
                }
            });
        }

        private async Task<byte[]> GetRadarImage(string zoom)
        {
            if (bitmapdata != null && bitmapdata.Length > 0 && !string.IsNullOrEmpty(zoom) && double.TryParse(zoom, NumberStyles.Any, CultureInfo.InvariantCulture, out double zoomResult))
            {
                Application.Current.Dispatcher.Invoke(() => updateRadarScale(zoomResult));
            }

            lastRadarRequest = absoluteTime;
            return bitmapdata;
        }

        void updateRadarScale(double zoomResult)
        {
            if (zoomResult != 0)
            {
                RadarScale.Value = Math.Min(maxRadarScale, RadarScale.Value + zoomResult);
            }
        }
        // HTTP ENDS

        void setRadarScale(double zoomResult)
        {
            if (zoomResult != 0)
            {
                RadarScale.Value = Math.Min(maxRadarScale, zoomResult);
            }
        }

        void radarZoom(object sender, MouseWheelEventArgs e)
        {
            RadarScale.Value = Math.Min(maxRadarScale, RadarScale.Value - 0.01 * e.Delta);
        }

        void copyLog(object sender, MouseButtonEventArgs e)
        {
            string text = "";

            messagesLog.Children.Insert(0, makeTextBlock(DateTime.UtcNow.ToString("u") + ": log copied to clipboard", Colors.Black, 10, TextWrapping.Wrap));

            foreach (TextBlock txt in messagesLog.Children)
            {
                text += txt.Text + Environment.NewLine;
            }

            Clipboard.SetText(text);
        }
    }
}

<|start_filename|>MSFS Kinetic Assistant/MathClass.cs<|end_filename|>
﻿using Accord.Math;
using System;

namespace MSFS_Kinetic_Assistant
{
    class MathClass
    {
        public winchPosition getWinchPosition(PlaneInfoResponse _planeInfoResponse, double _stringLength)
        {
            winchPosition _winchPosition = new winchPosition();
            _winchPosition.location = FindPointAtDistanceFrom(new GeoLocation(_planeInfoResponse.Latitude, _planeInfoResponse.Longitude), _planeInfoResponse.PlaneHeading, _stringLength / 1000);
            _winchPosition.alt = _planeInfoResponse.Altitude + 2;

            return _winchPosition;
        }

        public winchDirection getForceDirection(winchPosition _winchPosition, PlaneInfoResponse _planeInfoResponse)
        {
            winchDirection _winchDirection = new winchDirection();

            double globalX = (_winchPosition.location.Longitude - _planeInfoResponse.Longitude) * Math.Cos(_winchPosition.location.Latitude) * 6378137;
            double globalY = _winchPosition.alt - _planeInfoResponse.Altitude;
            double globalZ = (_winchPosition.location.Latitude - _planeInfoResponse.Latitude) * 180 / Math.PI * 111694;
            Vector3 globalToWinch = new Vector3((float)globalX, (float)globalY, (float)globalZ);
            Vector3 globalToWinchNorm = globalToWinch;
            globalToWinchNorm.Normalize();

            _winchDirection.climbAngle = Math.Abs(Math.Asin(globalToWinchNorm.Y));

            Matrix3x3 attitude = Matrix3x3.CreateFromYawPitchRoll((float)_planeInfoResponse.PlaneHeading, (float)_planeInfoResponse.PlanePitch, (float)_planeInfoResponse.PlaneBank);
            _winchDirection.localForceDirection = Matrix3x3.Multiply(attitude.Inverse(), globalToWinchNorm);
            _winchDirection.localForceDirection.Normalize();

            _winchDirection.heading = -_planeInfoResponse.PlaneHeading + findBearingToPoint(_planeInfoResponse.Latitude, _planeInfoResponse.Longitude, _winchPosition.location.Latitude, _winchPosition.location.Longitude);
            _winchDirection.pitch = Math.Asin(_winchDirection.localForceDirection.Y);
            _winchDirection.distance = (double)(globalToWinch.Norm);
            _winchDirection.groundDistance = findDistanceBetweenPoints(_planeInfoResponse.Latitude, _planeInfoResponse.Longitude, _winchPosition.location.Latitude, _winchPosition.location.Longitude);
            globalToWinch.Y = 0;

            if (_winchDirection.heading > Math.PI) { _winchDirection.heading -= 2 * Math.PI; }
            if (_winchDirection.heading < -Math.PI) { _winchDirection.heading += 2 * Math.PI; }
            if (_winchDirection.pitch > Math.PI) { _winchDirection.pitch -= 2 * Math.PI; }
            if (_winchDirection.pitch < -Math.PI) { _winchDirection.pitch += 2 * Math.PI; }

            return _winchDirection;
        }

        public GeoLocation FindPointAtDistanceFrom(GeoLocation startPoint, double initialBearingRadians, double distanceKilometres)
        {
            const double radiusEarthKilometres = 6371.01;
            double distRatio = distanceKilometres / radiusEarthKilometres;
            double distRatioSine = Math.Sin(distRatio);
            double distRatioCosine = Math.Cos(distRatio);

            double startLatCos = Math.Cos(startPoint.Latitude);
            double startLatSin = Math.Sin(startPoint.Latitude);

            double endLatRads = Math.Asin((startLatSin * distRatioCosine) + (startLatCos * distRatioSine * Math.Cos(initialBearingRadians)));

            double endLonRads = startPoint.Longitude
                + Math.Atan2(
                    Math.Sin(initialBearingRadians) * distRatioSine * startLatCos,
                    distRatioCosine - startLatSin * Math.Sin(endLatRads));

            return new GeoLocation(endLatRads, endLonRads);
        }
        public GeoLocation RotatePointFrom(GeoLocation startPoint, double initialBearingRadians, GeoLocation endPoint)
        {
            double dist = findDistanceBetweenPoints(startPoint.Latitude, startPoint.Longitude, endPoint.Latitude, endPoint.Longitude);
            double bearing = findBearingToPoint(startPoint.Latitude, startPoint.Longitude, endPoint.Latitude, endPoint.Longitude);

            return FindPointAtDistanceFrom(startPoint, bearing + initialBearingRadians, dist / 1000);
        }
        public double findDistanceBetweenPoints(double rlat1, double rlon1, double rlat2, double rlon2)
        {
            double rtheta = rlon1 - rlon2;
            double dist = Math.Sin(rlat1) * Math.Sin(rlat2) + Math.Cos(rlat1) *
                Math.Cos(rlat2) * Math.Cos(rtheta);
            dist = Math.Acos(dist);
            dist = dist * 180 / Math.PI;
            dist = dist * 60 * 1852;

            return dist;
        }

        public double findBearingToPoint(double rlat1, double rlon1, double rlat2, double rlon2)
        {
            double rtheta = rlon2 - rlon1;
            double dPhi = Math.Log(
                Math.Tan(rlat2 / 2 + Math.PI / 4) / Math.Tan(rlat1 / 2 + Math.PI / 4));
            if (Math.Abs(rtheta) > Math.PI)
                rtheta = rtheta > 0 ? -(2 * Math.PI - rtheta) : (2 * Math.PI + rtheta);
            return Math.Atan2(rtheta, dPhi) % Math.PI;
        }

        public double restrictAirspeed(double speed, double _targetSpeed, double lastFrameTiming)
        {
            if (Math.Abs(speed) > _targetSpeed)
                speed -= speed * Math.Sign(speed) * Math.Pow((Math.Abs(speed) - _targetSpeed) / _targetSpeed, 2) * lastFrameTiming;

            return speed;
        }

        public double getCableTension(double cableLength, double elasticExtension, winchDirection _winchDirection)
        {
            double cableTension = 0;
            double dumpingLength = Math.Max(5.0, cableLength * elasticExtension / 100);

            // CALCULATION TENSION MODIFIER
            if (cableLength < _winchDirection.distance)
            {
                cableTension = (_winchDirection.distance - cableLength) / dumpingLength;
            }

            return cableTension;
        }

        public double getBodyVelocity(winchDirection _winchDirection, PlaneInfoCommit _planeCommit, double cableTension, double accelerationLimit, double cableLength, double cableLengthPrev, double cableLengthPrePrev, double lastFrameTiming)
        {
            double baseAcceleration = 5 * 9.81;

            if (Math.Abs(cableLength - cableLengthPrev) > 10) { cableLengthPrev = cableLength; }
            if (Math.Abs(cableLengthPrev - cableLengthPrePrev) > 10) { cableLengthPrePrev = cableLengthPrev; }
            double lengthDiff = (cableLength - cableLengthPrev) * 0.5 + (cableLengthPrev - cableLengthPrePrev) * 0.5;
            double cableSpeed = lengthDiff / lastFrameTiming * cableTension;
            if (cableTension > 1 && lengthDiff > 0)
                cableSpeed += baseAcceleration * Math.Pow(cableTension - 1, 2);

            if (cableSpeed < 2 * accelerationLimit && cableSpeed > accelerationLimit)
            {
                cableSpeed = Math.Min(cableSpeed, 0.99 * accelerationLimit);
            }

            Vector3 planeMotion = new Vector3((float)(_planeCommit.VelocityBodyX * Math.Cos(_winchDirection.heading) * Math.Cos(_winchDirection.pitch)),
                (float)(_planeCommit.VelocityBodyY * Math.Sin(_winchDirection.heading)),
                (float)(_planeCommit.VelocityBodyZ * Math.Cos(_winchDirection.heading) * Math.Sin(_winchDirection.pitch)));

            double appliedVelocity = cableSpeed - planeMotion.Norm;

            //Console.WriteLine($"getBodyVelocity: {cableTension:F4} {appliedVelocity:F4}m/s  lengthDiff{lengthDiff:F4}m cableSpeed{cableSpeed:F4}");
            //Console.WriteLine($"cabC: {cableLength:F4}m cabP{cableLengthPrev:F4}m  cabPP{cableLengthPrePrev:F4}m");

            return appliedVelocity > 0 ? appliedVelocity : 0;
        }
    }

    public class winchPosition
    {
        public winchPosition() { }
        public winchPosition(GeoLocation Location, double Altitude, double Radius = 0, double Airspeed = 0, string Title = "", string Category = "")
        {
            location = Location;
            alt = Altitude;
            radius = Radius;
            airspeed = Airspeed;
            title = Title;
            category = Category;
        }

        public GeoLocation location { get; set; }
        public double alt { get; set; }
        public double radius { get; set; }
        public double airspeed { get; set; }
        public string title { get; set; }
        public string category { get; set; }
    }

    public class winchDirection
    {
        public double heading { get; set; }
        public double pitch { get; set; }
        public double distance { get; set; }
        public double groundDistance { get; set; }
        public double climbAngle { get; set; }
        public Vector3 localForceDirection { get; set; }
    }

    public class GeoLocation
    {
        public GeoLocation(double latitude, double longitude)
        {
            Latitude = latitude;
            Longitude = longitude;
        }

        public double Latitude { get; set; }
        public double Longitude { get; set; }
    }
    public class Waypoint
    {
        public Waypoint(int id, GeoLocation loc, string name, string entered, string passed, double fine, double weight, string wind, double altitude, double elevation, double height, double width, bool bottomMeasurement, bool topMeasurement)
        {
            ID = id;
            Location = loc;
            Name = name;
            Entered = entered;
            Passed = passed;
            Fine = fine;
            Weight = weight;
            Wind = wind;
            Altitude = altitude;
            Elevation = elevation;
            Height = height;
            Width = width;
            BottomMeasurement = bottomMeasurement;
            TopMeasurement = topMeasurement;
        }

        public int ID { get; set; }
        public GeoLocation Location { get; set; }
        public string Name { get; set; }
        public string Entered { get; set; }
        public string Passed { get; set; }
        public double Fine { get; set; }
        public double Weight { get; set; }
        public string Wind { get; set; }
        public double Altitude { get; set; }
        public double Elevation { get; set; }
        public double Height { get; set; }
        public double Width { get; set; }
        public bool BottomMeasurement { get; set; }
        public bool TopMeasurement { get; set; }
    }
}


<|start_filename|>MSFS Kinetic Assistant/Properties/Resources.Designer.cs<|end_filename|>
﻿//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace MSFS_Kinetic_Assistant.Properties {
    using System;
    
    
    /// <summary>
    ///   A strongly-typed resource class, for looking up localized strings, etc.
    /// </summary>
    // This class was auto-generated by the StronglyTypedResourceBuilder
    // class via a tool like ResGen or Visual Studio.
    // To add or remove a member, edit your .ResX file then rerun ResGen
    // with the /str option, or rebuild your VS project.
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    internal class Resources {
        
        private static global::System.Resources.ResourceManager resourceMan;
        
        private static global::System.Globalization.CultureInfo resourceCulture;
        
        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        internal Resources() {
        }
        
        /// <summary>
        ///   Returns the cached ResourceManager instance used by this class.
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Resources.ResourceManager ResourceManager {
            get {
                if (object.ReferenceEquals(resourceMan, null)) {
                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MSFS_Kinetic_Assistant.Properties.Resources", typeof(Resources).Assembly);
                    resourceMan = temp;
                }
                return resourceMan;
            }
        }
        
        /// <summary>
        ///   Overrides the current thread's CurrentUICulture property for all
        ///   resource lookups using this strongly typed resource class.
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Globalization.CultureInfo Culture {
            get {
                return resourceCulture;
            }
            set {
                resourceCulture = value;
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to {0} connected.
        /// </summary>
        internal static string _0Connected {
            get {
                return ResourceManager.GetString("_0Connected", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to {0} disconnected.
        /// </summary>
        internal static string _0Disconnected {
            get {
                return ResourceManager.GetString("_0Disconnected", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to {0} refused (too many connections).
        /// </summary>
        internal static string _0RefusedTooManyConnections {
            get {
                return ResourceManager.GetString("_0RefusedTooManyConnections", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to Check failed.
        /// </summary>
        internal static string CheckFailed {
            get {
                return ResourceManager.GetString("CheckFailed", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to Checking for update....
        /// </summary>
        internal static string CheckingForUpdate {
            get {
                return ResourceManager.GetString("CheckingForUpdate", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to Connection failed.
        /// </summary>
        internal static string ConnectionFailed {
            get {
                return ResourceManager.GetString("ConnectionFailed", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to error {0}.
        /// </summary>
        internal static string Error0 {
            get {
                return ResourceManager.GetString("Error0", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to Exit.
        /// </summary>
        internal static string Exit {
            get {
                return ResourceManager.GetString("Exit", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to GPS: .
        /// </summary>
        internal static string GPS_ {
            get {
                return ResourceManager.GetString("GPS_", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to Server: .
        /// </summary>
        internal static string Server_ {
            get {
                return ResourceManager.GetString("Server_", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to Start.
        /// </summary>
        internal static string Start {
            get {
                return ResourceManager.GetString("Start", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to Started.
        /// </summary>
        internal static string Started {
            get {
                return ResourceManager.GetString("Started", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to Starting.
        /// </summary>
        internal static string Starting {
            get {
                return ResourceManager.GetString("Starting", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to Stop.
        /// </summary>
        internal static string Stop {
            get {
                return ResourceManager.GetString("Stop", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to Stopped.
        /// </summary>
        internal static string Stopped {
            get {
                return ResourceManager.GetString("Stopped", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to Stopping.
        /// </summary>
        internal static string Stopping {
            get {
                return ResourceManager.GetString("Stopping", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to Too many connections.
        /// </summary>
        internal static string TooManyConnections {
            get {
                return ResourceManager.GetString("TooManyConnections", resourceCulture);
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to waiting.
        /// </summary>
        internal static string Waiting {
            get {
                return ResourceManager.GetString("Waiting", resourceCulture);
            }
        }
    }
}


<|start_filename|>MSFS Kinetic Assistant/SimvarsViewModel.cs<|end_filename|>
﻿namespace MSFS_Kinetic_Assistant
{
    public class SimvarsViewModel
    {
        public string[] aSimvarNames
        {
            get { return SimVars.Names; }
            private set { }
        }
    }
}


<|start_filename|>MSFS Kinetic Assistant/Tracking.cs<|end_filename|>
﻿using CTrue.FsConnect;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Xml.Linq;

namespace MSFS_Kinetic_Assistant
{
    class Tracking
    {
        public double lastTrackCapture = 0;
        public List<TrackPoint> trackRecording = null;
        public List<TrackPoint> trackRecorded = null;
        public double recordingCounter = 0;
        public double lastRecordingCounter = 0;
        const uint TARGETMAX = 99999999;

        //public List<TrackPoint> trackPlaying = null;
        //public double trackPlayingTimer = 0;
        //public string trackPlayingModel = "";
        //public GeoLocation trackPlayingCoords = null;

        public KeyValuePair<uint, string> message = new KeyValuePair<uint, string>();
        public string lastMessage = "";

        public List<GhostPlane> ghostPlanes = new List<GhostPlane>();
        public winchPosition ghostTeleport = new winchPosition();
        public List<string> competitiorsList = new List<string>();

        public short normalizeAngle(double rad)
        {
            short deg = (short)((rad * 180 / Math.PI) % 360);

            if (deg < 0)
                deg += 360;
            else if (deg >= 360)
                deg -= 360;

            return deg;
        }
        public double normalizeRadAngle(double angle)
        {
            if (angle < 0) { angle += 2 * Math.PI; }
            return angle;
        }

        public double zeroeRadAngle(double angle)
        {
            angle %= 2 * Math.PI;

            if (angle > Math.PI) { angle -= 2 * Math.PI; }
            else if (angle < -Math.PI) { angle += 2 * Math.PI; }

            return angle;
        }


        public void captureTrackPoint(PlaneInfoResponse _planeInfoResponse, PlaneAvionicsResponse _planeAvionicsResponse, double absoluteTime, double baseInterval = 0.5)
        {
            double trackCaptureInterval = Math.Abs(_planeInfoResponse.AirspeedIndicated) > baseInterval ?
                Math.Max(baseInterval, Math.Abs(_planeInfoResponse.AirspeedIndicated) / 30 - 1) :
                baseInterval * 5;

            if (absoluteTime - lastTrackCapture >= trackCaptureInterval)
            {
                lastTrackCapture = absoluteTime;
                trackRecording.Add(new TrackPoint(new GeoLocation(_planeInfoResponse.Latitude, _planeInfoResponse.Longitude), _planeInfoResponse.Altitude, (int)_planeInfoResponse.AltitudeAboveGround,
                    _planeInfoResponse.VelocityBodyZ, _planeInfoResponse.AirspeedIndicated, normalizeAngle(_planeInfoResponse.PlaneHeading), normalizeAngle(_planeInfoResponse.PlanePitch), normalizeAngle(_planeInfoResponse.PlaneBank),
                    packLights(_planeAvionicsResponse), packAvionics(_planeAvionicsResponse), DateTime.UtcNow, recordingCounter));

                Console.WriteLine("Track capture: " + recordingCounter);
            }
        }

        public KeyValuePair<double, string> buildTrackFile(string appName, string nickName, PlaneInfoResponse _planeInfoResponse, PlaneAvionicsResponse _planeAvionicsResponse, MathClass _mathClass, string filename, bool timeAligned = false)
        {
            string str = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><gpx creator=\"" + appName + "\" version=\"1.0\"><trk><name>" + _planeAvionicsResponse.Title + " - " + nickName + "</name><desc></desc><trkseg>";
            TrackPoint prev = new TrackPoint(new GeoLocation(0, 0), 0, 0, 0, 0, 0, 0, 0, 0, 0, DateTime.Now, 0);
            double distance = 0;

            foreach (TrackPoint trackPoint in trackRecording)
            {
                if (prev.Elevation != 0)
                {
                    double flatDistance = _mathClass.findDistanceBetweenPoints(prev.Location.Latitude, prev.Location.Longitude, trackPoint.Location.Latitude, trackPoint.Location.Longitude);
                    distance += flatDistance / Math.Cos((prev.Elevation - trackPoint.Elevation) / flatDistance);
                }

                DateTime timestamp = !timeAligned ? trackPoint.Time : (new DateTime(2000, 1, 1)).AddSeconds(trackPoint.Timer);

                str += Environment.NewLine + "<trkpt lon=\"" + (trackPoint.Location.Longitude * 180 / Math.PI).ToString(CultureInfo.InvariantCulture) + "\" lat=\"" + (trackPoint.Location.Latitude * 180 / Math.PI).ToString(CultureInfo.InvariantCulture) + "\">" +
                    Environment.NewLine + " <ele>" + trackPoint.Elevation.ToString(CultureInfo.InvariantCulture) + "</ele>" +
                    Environment.NewLine + " <agl>" + trackPoint.AltitudeAboveGround.ToString() + "</agl>" +
                    Environment.NewLine + " <velocity>" + trackPoint.Velocity.ToString(CultureInfo.InvariantCulture) + "</velocity>" +
                    Environment.NewLine + " <airspeed>" + trackPoint.Airspeed.ToString(CultureInfo.InvariantCulture) + "</airspeed>" +
                    Environment.NewLine + " <heading>" + trackPoint.Heading + "</heading>" +
                    Environment.NewLine + " <pitch>" + trackPoint.Pitch + "</pitch>" +
                    Environment.NewLine + " <roll>" + trackPoint.Roll + "</roll>" +
                    Environment.NewLine + " <lights>" + trackPoint.Lights.ToString() + "</lights>" +
                    Environment.NewLine + " <avionics>" + trackPoint.Avionics.ToString() + "</avionics>" +
                    Environment.NewLine + " <time>" + timestamp.ToString("O") + "Z" + "</time>" +
                Environment.NewLine + "</trkpt>";

                prev = trackPoint;
            }

            str += "</trkseg></trk></gpx>";

            if (trackRecording.Count <= 1)
            {
                //addLogMessage("Track data damaged!", 2);
            }


            return new KeyValuePair<double, string>(distance, str);
        }

        public GhostPlane parseTrackFile(string file, MathClass _mathClass, double allowedRecordLength)
        {
            GhostPlane ghostPlane = new GhostPlane();

            Console.WriteLine("Parsing " + file);
            XElement trackpointsXml = XElement.Load(file);
            if (trackpointsXml != null)
            {
                ghostPlane.TrackPoints = new List<TrackPoint>();
                ghostPlane.ID = TARGETMAX;

                if (trackpointsXml.Descendants("name").First() != null)
                {
                    string[] title = trackpointsXml.Descendants("name").First().Value.Split(new string[] { " - " }, StringSplitOptions.None);
                    ghostPlane.Name = title[0].Trim();
                    ghostPlane.Username = title.Length > 1 ? title[1] : "";
                }
                else
                {
                    ghostPlane.Name = "DA40-NG Asobo";
                    ghostPlane.Username = "";
                }

                double counter = 0;
                DateTime lastTimer = new DateTime();

                double latitudeOffset = 0;
                double longitudeOffset = 0;
                double headingOffset = 0;
                double altitudeOffset = 0;
                GeoLocation root = new GeoLocation(0, 0);

                foreach (var trackPoint in trackpointsXml.Descendants("trkseg").First().Elements("trkpt"))
                {
                    try
                    {
                        // FREE VERSION
                        if (counter > allowedRecordLength)
                            break;

                        double.TryParse(trackPoint.Attribute("lat").Value, NumberStyles.Any, CultureInfo.InvariantCulture, out double lat);
                        double.TryParse(trackPoint.Attribute("lon").Value, NumberStyles.Any, CultureInfo.InvariantCulture, out double lon);
                        lat *= Math.PI / 180;
                        lon *= Math.PI / 180;

                        if (counter == 0)
                            root = new GeoLocation(lat, lon);

                        double ele = 1000;
                        double.TryParse(trackPoint.Element("ele").Value, NumberStyles.Any, CultureInfo.InvariantCulture, out ele);
                        DateTime time = DateTime.Parse(trackPoint.Element("time").Value);

                        int agl = 10;
                        if (trackPoint.Element("agl") != null)
                            int.TryParse(trackPoint.Element("agl").Value, NumberStyles.Any, CultureInfo.InvariantCulture, out agl);

                        double velocity = 0;
                        if (trackPoint.Element("velocity") != null)
                            double.TryParse(trackPoint.Element("velocity").Value, NumberStyles.Any, CultureInfo.InvariantCulture, out velocity);

                        double airspeed = 0;
                        if (trackPoint.Element("airspeed") != null)
                            double.TryParse(trackPoint.Element("airspeed").Value, NumberStyles.Any, CultureInfo.InvariantCulture, out velocity);

                        short heading = 0;
                        if (trackPoint.Element("heading") != null)
                            short.TryParse(trackPoint.Element("heading").Value, NumberStyles.Any, CultureInfo.InvariantCulture, out heading);

                        short pitch = 0;
                        if (trackPoint.Element("pitch") != null)
                            short.TryParse(trackPoint.Element("pitch").Value, NumberStyles.Any, CultureInfo.InvariantCulture, out pitch);

                        short roll = 0;
                        if (trackPoint.Element("roll") != null)
                            short.TryParse(trackPoint.Element("roll").Value, NumberStyles.Any, CultureInfo.InvariantCulture, out roll);

                        int lights = 0;
                        if (trackPoint.Element("lights") != null)
                            int.TryParse(trackPoint.Element("lights").Value, NumberStyles.Any, CultureInfo.InvariantCulture, out lights);

                        int avionics = 0;
                        if (trackPoint.Element("avionics") != null)
                            int.TryParse(trackPoint.Element("avionics").Value, NumberStyles.Any, CultureInfo.InvariantCulture, out avionics);

                        if (counter != 0)
                            counter += (time - lastTimer).TotalSeconds;

                        GeoLocation loc = new GeoLocation(lat, lon);

                        // START OFFSETS
                        if (ghostTeleport.alt != 0 && altitudeOffset == 0)
                        {
                            //Console.WriteLine(JsonConvert.SerializeObject(ghostTeleport, Formatting.Indented));
                            latitudeOffset = ghostTeleport.location.Latitude - loc.Latitude;
                            longitudeOffset = ghostTeleport.location.Longitude - loc.Longitude;
                            headingOffset = (ghostTeleport.radius * 180 / Math.PI) - heading;
                            altitudeOffset = ghostTeleport.alt - ele;
                            Console.WriteLine("Offsets: lat" + latitudeOffset + " lon" + longitudeOffset + " head" + headingOffset + " alt" + altitudeOffset);
                        }
                        // CONTINUE OFFSETS
                        if (ghostTeleport.alt != 0 && ghostPlane.TrackPoints.Count > 0 && _mathClass.findDistanceBetweenPoints(ghostTeleport.location.Latitude, ghostTeleport.location.Longitude, root.Latitude, root.Longitude) > 100)
                        {
                            loc = _mathClass.RotatePointFrom(root, headingOffset * Math.PI / 180, loc);
                        }

                        loc.Latitude += latitudeOffset;
                        loc.Longitude += longitudeOffset;

                        heading += (short)headingOffset;
                        ele += altitudeOffset;

                        ghostPlane.TrackPoints.Add(new TrackPoint(loc, ele, agl, velocity, airspeed,
                            (short)(heading > 180 ? heading - 360 : heading), (short)(pitch > 180 ? pitch - 360 : pitch), (short)(roll > 180 ? roll - 360 : roll),
                            lights, avionics, time, counter, trackPoint.Element("message") != null ? trackPoint.Element("message").Value : ""));

                        if (counter == 0)
                        {
                            counter += 0.0001;
                        }

                        lastTimer = time;
                    }
                    catch { }
                }

                ghostPlane.Length = counter;

                if (ghostPlane.TrackPoints.Count > 0)
                {
                    Console.WriteLine(ghostPlane.TrackPoints.Count + " track points loaded");
                    ghostPlanes.Add(ghostPlane);
                }
            }
            else
            {
                MessageBox.Show("GPX file is invalid");
            }

            //trackPlaying = null;
            return ghostPlane;
        }

        public int packLights(PlaneAvionicsResponse _planeAvionicsResponse)
        {
            bool[] myBools = new bool[] {
                _planeAvionicsResponse.LIGHTBEACON == 100,
                _planeAvionicsResponse.LIGHTCABIN == 100,
                _planeAvionicsResponse.LIGHTGLARESHIELD == 100,
                _planeAvionicsResponse.LIGHTLANDING == 100,
                _planeAvionicsResponse.LIGHTLOGO == 100,
                _planeAvionicsResponse.LIGHTNAV == 100,
                _planeAvionicsResponse.LIGHTPANEL == 100,
                _planeAvionicsResponse.LIGHTPEDESTRAL == 100,
                _planeAvionicsResponse.LIGHTPOTENTIOMETER == 100,
                _planeAvionicsResponse.LIGHTRECOGNITION == 100,
                _planeAvionicsResponse.LIGHTSTROBE == 100,
                _planeAvionicsResponse.LIGHTTAXI == 100,
                _planeAvionicsResponse.LIGHTWING == 100,
            };

            byte[] byteArray = myBools.Select(b => (byte)(b ? 1 : 0)).ToArray();
            int lights = BitConverter.ToInt32(byteArray, 0);

            return lights;
        }
        public int packAvionics(PlaneAvionicsResponse _planeAvionicsResponse)
        {
            return 0;
        }
        public GhostPlane tryCaptureGhostPlane(uint ID, double absoluteTime)
        {
            int index = ghostPlanes.FindIndex(m => m.ID == TARGETMAX);
            if (index >= 0)
            {
                GhostPlane gp = ghostPlanes[index];
                gp.ID = ID;
                //gp.Progress = 0.00001;
                //gp.LastTrackPlayed = absoluteTime;

                ghostPlanes[index] = gp;

                return ghostPlanes[index];
            }

            return new GhostPlane();
        }
        public GhostPlane getGhostPlane(uint ID)
        {
            int index = ghostPlanes.FindIndex(m => m.ID == ID);
            if (index >= 0)
            {
                return ghostPlanes[index];
            }

            return new GhostPlane();
        }
        public string possiblyGetPlaneName(uint ID, string name)
        {
            if (ghostPlanes.Count > 0 && ID > 1)
            {
                GhostPlane gp = getGhostPlane(ID);
                if (!string.IsNullOrEmpty(gp.Username))
                {
                    return gp.Username;
                }
            }

            return name;
        }
        public void playRecords(double absoluteTime)
        {
            int index;
            while ((index = ghostPlanes.FindIndex(m => m.Progress == 0)) >= 0)
            {
                GhostPlane gp = ghostPlanes[index];
                gp.Progress = 0.00001;
                gp.LastTrackPlayed = absoluteTime;

                ghostPlanes[index] = gp;
            }
        }

        public void clearRecords(FsConnect _fsConnect)
        {
            foreach (GhostPlane ghostPlane in ghostPlanes)
            {
                if (ghostPlane.ID != TARGETMAX)
                {
                    _fsConnect.RemoveObject(ghostPlane.ID, Requests.TowPlane);
                }
            }
            ghostPlanes = new List<GhostPlane>();
        }

        public void clearRecord(FsConnect _fsConnect, uint ID)
        {
            for (int i = ghostPlanes.Count - 1; i >= 0; i--)
            {
                if (ID == ghostPlanes[i].ID)
                {
                    _fsConnect.RemoveObject(ghostPlanes[i].ID, Requests.TowPlane);
                    ghostPlanes.RemoveAt(i);
                }
            }
        }

        public void saveTrackfile(string str, string zipDirectory, string filename, string log = "")
        {
            try
            {
                string path = zipDirectory + filename;
                Console.WriteLine("Saving to " + path);

                if (!Directory.Exists(zipDirectory))
                {
                    try
                    {
                        Directory.CreateDirectory(zipDirectory);
                    }
                    catch
                    {
                        MessageBox.Show("Can't create task directory");
                    }
                }

                File.WriteAllText(path, str);

                if (!string.IsNullOrEmpty(log))
                {
                    File.WriteAllText(path.Replace(".gpx", ".log"), log);
                }

                Console.WriteLine("Track file saved");
            }
            catch
            {
            }
        }
        public GhostCommit updateGhostPlayer(uint ID, NearbyInfoResponse response, GhostCommit towCommit, FsConnect _fsConnect, MathClass _mathClass, double absoluteTime)
        {
            int index = 0;
            double deltaTime = 0;

            foreach (GhostPlane ghostPlane in ghostPlanes)
            {
                if (ghostPlane.ID == ID && ghostPlane.Progress > 0 && ghostPlane.TrackPoints.Count > 0)
                {
                    deltaTime = absoluteTime - ghostPlane.LastTrackPlayed;

                    TrackPoint prev = new TrackPoint();
                    TrackPoint curr = new TrackPoint();
                    TrackPoint next = new TrackPoint();

                    bool found = false;
                    foreach (var point in ghostPlane.TrackPoints)
                    {
                        if (!found)
                            curr = point;
                        else
                        {
                            next = point;
                            break;
                        }

                        if (prev.Timer < (ghostPlane.Progress + deltaTime) && point.Timer >= (ghostPlane.Progress + deltaTime))
                        {
                            found = true;

                            if (!string.IsNullOrEmpty(curr.Message) && lastMessage != curr.Message)
                            {
                                message = new KeyValuePair<uint, string>(ghostPlane.ID, point.Message);
                                lastMessage = point.Message;
                            }
                        }
                        else
                            prev = point;
                    }

                    if (found && prev.Location != null && curr.Location != null && next.Location != null)
                    {
                        try
                        {
                            double progress = ((ghostPlane.Progress + deltaTime) - prev.Timer) / (curr.Timer - prev.Timer);
                            double timeLeft = curr.Timer - (ghostPlane.Progress + deltaTime);

                            //double distancePrev = _mathClass.findDistanceBetweenPoints(response.Latitude, response.Longitude, prev.Location.Latitude, prev.Location.Longitude);
                            double distanceCurr = _mathClass.findDistanceBetweenPoints(response.Latitude, response.Longitude, curr.Location.Latitude, curr.Location.Longitude);
                            //double distanceNext = _mathClass.findDistanceBetweenPoints(response.Latitude, response.Longitude, next.Location.Latitude, next.Location.Longitude);

                            /*if (distanceCurr / timeLeft > 1.25 * curr.Velocity)
                            {
                                distanceCurr = 1.25 * curr.Velocity * timeLeft;
                                Console.WriteLine($"TOO FAR FROM NEXT POINT: {distanceCurr:F6}");
                            }*/


                            double bearing = normalizeRadAngle(_mathClass.findBearingToPoint(response.Latitude, response.Longitude, next.Location.Latitude, next.Location.Longitude));
                            response.Heading = normalizeRadAngle(response.Heading);
                            bearing = zeroeRadAngle(bearing - response.Heading);
                            double newHeading = zeroeRadAngle(response.Heading + (absoluteTime - ghostPlane.LastTrackPlayed) * bearing);
                            //towCommit.RotationVelocityBodyY = Math.Sin(newHeading - response.Heading);
                            towCommit.planeHeading = newHeading;
                            //Console.WriteLine($"Tracking heading: {response.Heading:F4} bearing: {bearing:F4} newHeading: {newHeading:F4}");

                            double requiredBank = ((1 - progress) * (prev.Roll * Math.PI / 180 - response.Bank) + progress * (curr.Roll * Math.PI / 180 - response.Bank)) / 2;
                            //towCommit.RotationVelocityBodyZ = Math.Sin(requiredBank - response.Bank);
                            towCommit.planeRoll = requiredBank;
                            double requiredPitch = ((1 - progress) * (prev.Pitch * Math.PI / 180 - response.Pitch) + progress * (prev.Pitch * Math.PI / 180 - response.Pitch)) / 2;
                            //towCommit.RotationVelocityBodyX = Math.Sin(requiredPitch - response.Pitch);
                            towCommit.planePitch = requiredPitch;

                            //Console.WriteLine($"RotationVelocityBodyX: {towCommit.RotationVelocityBodyX:F4} RotationVelocityBodyY: {towCommit.RotationVelocityBodyY:F4} RotationVelocityBodyZ: {towCommit.RotationVelocityBodyZ:F4}");
                            //towCommit.Heading = newHeading;

                            towCommit.VelocityBodyX = 0;
                            towCommit.VelocityBodyY = ((1 - progress) * (prev.Elevation - response.Altitude) + progress * (curr.Elevation - response.Altitude)) / 2;
                            towCommit.VelocityBodyZ = (0.9 * curr.Velocity + 0.1 * distanceCurr / Math.Max(1, timeLeft)) * (distanceCurr < 10 ? distanceCurr / 10 : 1);
                            
                            // TAXIING
                            if (towCommit.VelocityBodyZ < 1 && towCommit.VelocityBodyZ > -1)
                            {
                                towCommit.VelocityBodyZ = Math.Sign(towCommit.VelocityBodyZ) * Math.Pow(Math.Abs(towCommit.VelocityBodyZ), 4);
                            }


                            if (towCommit.VelocityBodyY > 10)
                            {
                                TowInfoResponse towInfo = new TowInfoResponse();
                                towInfo.Altitude = response.Altitude + towCommit.VelocityBodyY;
                                towInfo.Latitude = response.Latitude;
                                towInfo.Longitude = response.Longitude;
                                towInfo.Heading = response.Heading;
                                towInfo.Bank = response.Bank;

                                if (!double.IsNaN(towInfo.Altitude) && !double.IsNaN(towInfo.Latitude) && !double.IsNaN(towInfo.Longitude) && !double.IsNaN(towInfo.Heading) && !double.IsNaN(towInfo.Bank))
                                {
                                    try
                                    {
                                        Console.WriteLine("Sinking, TELEPORT!");
                                        _fsConnect.UpdateData(Definitions.TowPlane, towInfo, ID);
                                    }
                                    catch (Exception ex)
                                    {
                                    }
                                }
                            }

                            //Console.WriteLine("Tracking animation " + (ghostPlane.Progress + deltaTime) + " h" + towCommit.VelocityBodyZ + " v" + towCommit.VelocityBodyY + " d" + distanceCurr);
                        }
                        catch { }
                    }
                    else if (curr.Location != null && next.Location == null)
                    {
                        message = new KeyValuePair<uint, string>(ghostPlane.ID, "REMOVE");
                    }

                    break;
                }

                index++;
            }


            if (index < ghostPlanes.Count)
            {
                GhostPlane gp = ghostPlanes[index];
                gp.LastTrackPlayed = absoluteTime;
                gp.Progress += deltaTime;

                ghostPlanes[index] = gp;
            }
            else
            {

            }


            return towCommit;
        }

        public void stopGhostPlayer(uint ID)
        {
            GhostPlane gp = ghostPlanes.Find(x => x.ID == ID);
            gp.Progress = 0;
        }

        public bool ghostPlayerActive(uint ID = TARGETMAX)
        {
            foreach (GhostPlane ghostPlane in ghostPlanes)
            {
                if (ghostPlane.Progress != 0 && (ID == TARGETMAX || ghostPlane.ID == ID))
                {
                    return true;
                }
            }

            return false;
        }
    }

    public struct GhostPlane
    {
        public GhostPlane(string name, string username, uint id, double length, double progress, double lastTrackPlayed, List<TrackPoint> trackPoints)
        {
            Name = name;
            Username = username;
            ID = id;
            Length = length;
            Progress = progress;
            LastTrackPlayed = lastTrackPlayed;
            TrackPoints = trackPoints;
        }
        public string Name;
        public string Username;
        public uint ID;
        public double Length;
        public double Progress;
        public double LastTrackPlayed;
        public List<TrackPoint> TrackPoints;
    }

    public struct TrackPoint
    {
        public TrackPoint(GeoLocation location, double elevation, int altitudeAboveGround, double velocity, double airspeed, short heading, short pitch, short roll, int lights, int avionics, DateTime time, double timer, string message = "")
        {
            Location = location;
            Elevation = elevation;
            AltitudeAboveGround = altitudeAboveGround;
            Velocity = velocity;
            Airspeed = airspeed;
            Heading = heading;
            Pitch = pitch;
            Roll = roll;
            Lights = lights;
            Avionics = avionics;
            Time = time;
            Timer = timer;
            Message = message;
        }
        public GeoLocation Location { get; set; }
        public double Elevation { get; set; }
        public double AltitudeAboveGround { get; set; }
        public double Velocity { get; set; }
        public double Airspeed { get; set; }
        public short Heading { get; set; }
        public short Pitch { get; set; }
        public short Roll { get; set; }
        public int Lights { get; set; }
        public int Avionics { get; set; }
        public DateTime Time { get; set; }
        public double Timer { get; set; }
        public string Message { get; set; }
    }

}

