package com.iforce2d.balloontrackerv2;

import android.app.AlertDialog;
import android.app.Application;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.location.Location;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.support.design.widget.TabLayout;
import android.support.v4.app.ActivityCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.content.pm.ActivityInfo;
import android.util.Log;
import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;

import com.hoho.android.usbserial.driver.UsbSerialDriver;
import com.hoho.android.usbserial.driver.UsbSerialPort;
import com.hoho.android.usbserial.driver.UsbSerialProber;
import com.hoho.android.usbserial.util.SerialInputOutputManager;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;

import org.json.*;

import static java.lang.Math.atan2;
import static java.lang.Math.cos;
import static java.lang.Math.sin;
import static java.lang.Math.sqrt;

public class MainActivity extends AppCompatActivity implements
        OnMapReadyCallback,
        GoogleApiClient.OnConnectionFailedListener,
        GoogleApiClient.ConnectionCallbacks,
        LocationListener,
        SensorEventListener {

    private static final String TAG = "MyActivity";

    protected Application mApplication;

    MyPagerAdapter adapter;

    GoogleMap map;

    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
    private static UsbSerialPort sPort = null;
    protected SerialPort mSerialPort;
    protected OutputStream mOutputStream;
    private InputStream mInputStream;

    ScrollView mScrollView;
    TextView mConsoleText;
    String logContent = "";
    String jsonStr = "";

    Marker selfMarker;
    Marker balloonMarker;
    Marker cusfLandingMarker;
    Marker windSamplesLandingMarker;
    double selfAltitude = 0;

    boolean doCUSFFetch = false;

    GoogleApiClient mGoogleApiClient;

    FileOutputStream fileOutputStream = null;
    File publicStorageDirectory = null;

    private SensorManager mSensorManager;
    private final float[] mAccelerometerReading = new float[3];
    private final float[] mMagnetometerReading = new float[3];

    private final float[] mRotationMatrix = new float[9];
    private final float[] mOrientationAngles = new float[3];
    private float compassDirection = 0;

    private long lastNewDataTime = 0;
    private long lastFileWriteTime = 0;
    private long lastWindSampleTime = 0;


    WindSample[] windSamples;

    // Android sucks https://stackoverflow.com/questions/2478517/how-to-display-a-yes-no-dialog-box-on-android
    DialogInterface.OnClickListener clearWindSamplesDialogListener = new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            switch (which) {
                case DialogInterface.BUTTON_POSITIVE:
                    setupDefaultWindSamples();
                    saveWindSamplesToFile();
                    refreshData();
                    break;

                case DialogInterface.BUTTON_NEGATIVE:
                    break;
            }
        }
    };

    private SerialInputOutputManager mSerialIoManager;
    private final SerialInputOutputManager.Listener mListener =
            new SerialInputOutputManager.Listener() {
                @Override
                public void onRunError(Exception e) {
                    Log.d(TAG, "Runner stopped.");
                }

                @Override
                public void onNewData(final byte[] data) {
                    MainActivity.this.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            MainActivity.this.handleReceivedData(data);
                        }
                    });
                }
            };

    private void toast(String text) {
        Toast toast = Toast.makeText(this, text, Toast.LENGTH_LONG);
        toast.show();
    }

    private void setupDefaultWindSamples() {
        windSamples = new WindSample[50];
        windSamples[0] = new WindSample(0, 269.053);
        windSamples[1] = new WindSample(269.053, 553.914);
        windSamples[2] = new WindSample(553.914, 842.77);
        windSamples[3] = new WindSample(842.77, 1135.76);
        windSamples[4] = new WindSample(1135.76, 1433.04);
        windSamples[5] = new WindSample(1433.04, 1734.76);
        windSamples[6] = new WindSample(1734.76, 2041.09);
        windSamples[7] = new WindSample(2041.09, 2352.22);
        windSamples[8] = new WindSample(2352.22, 2668.32);
        windSamples[9] = new WindSample(2668.32, 2989.6);
        windSamples[10] = new WindSample(2989.6, 3316.28);
        windSamples[11] = new WindSample(3316.28, 3648.58);
        windSamples[12] = new WindSample(3648.58, 3986.75);
        windSamples[13] = new WindSample(3986.75, 4331.06);
        windSamples[14] = new WindSample(4331.06, 4681.78);
        windSamples[15] = new WindSample(4681.78, 5039.22);
        windSamples[16] = new WindSample(5039.22, 5403.71);
        windSamples[17] = new WindSample(5403.71, 5775.6);
        windSamples[18] = new WindSample(5775.6, 6155.27);
        windSamples[19] = new WindSample(6155.27, 6543.14);
        windSamples[20] = new WindSample(6543.14, 6939.65);
        windSamples[21] = new WindSample(6939.65, 7345.32);
        windSamples[22] = new WindSample(7345.32, 7760.66);
        windSamples[23] = new WindSample(7760.66, 8186.27);
        windSamples[24] = new WindSample(8186.27, 8622.81);
        windSamples[25] = new WindSample(8622.81, 9070.99);
        windSamples[26] = new WindSample(9070.99, 9531.62);
        windSamples[27] = new WindSample(9531.62, 10005.6);
        windSamples[28] = new WindSample(10005.6, 10493.9);
        windSamples[29] = new WindSample(10493.9, 10997.6);
        windSamples[30] = new WindSample(10997.6, 11519.8);
        windSamples[31] = new WindSample(11519.8, 12064.4);
        windSamples[32] = new WindSample(12064.4, 12633.2);
        windSamples[33] = new WindSample(12633.2, 13228.7);
        windSamples[34] = new WindSample(13228.7, 13853.4);
        windSamples[35] = new WindSample(13853.4, 14510.3);
        windSamples[36] = new WindSample(14510.3, 15203);
        windSamples[37] = new WindSample(15203, 15935.6);
        windSamples[38] = new WindSample(15935.6, 16712.9);
        windSamples[39] = new WindSample(16712.9, 17540.7);
        windSamples[40] = new WindSample(17540.7, 18426.2);
        windSamples[41] = new WindSample(18426.2, 19377.9);
        windSamples[42] = new WindSample(19377.9, 20406.5);
        windSamples[43] = new WindSample(20406.5, 21525.6);
        windSamples[44] = new WindSample(21525.6, 22752.7);
        windSamples[45] = new WindSample(22752.7, 24110.9);
        windSamples[46] = new WindSample(24110.9, 25638.4);
        windSamples[47] = new WindSample(25638.4, 27398.7);
        windSamples[48] = new WindSample(27398.7, 29459.9);
        windSamples[49] = new WindSample(29459.9, 60000);
    }

    String windSamplesFileName = "windSamples.txt";

    private void saveWindSamplesToFile() {

        try {
            File file = new File(publicStorageDirectory, windSamplesFileName);
            FileOutputStream fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(windSamples);
            fos.close();
            //Log.d(TAG, "Saved wind samples to file");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void restoreWindSamplesFromFile() {

        try {
            File file = new File(publicStorageDirectory, windSamplesFileName);
            FileInputStream fis = new FileInputStream(file);
            ObjectInputStream ois = new ObjectInputStream(fis);
            windSamples = (WindSample[]) ois.readObject();
            ois.close();
            //Log.d(TAG, "Restored wind samples from file");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void addWindSample(double alt, double x, double y) {
        for (int i = 0; i < windSamples.length; i++) {
            WindSample ws = windSamples[i];
            //Log.d(TAG, "ws.lowAlt "+ws.lowAlt);
            //Log.d(TAG, "ws.highAlt "+ws.highAlt);
            if (alt >= ws.lowAlt && alt < ws.highAlt) {
                double denom = ws.count + 1;
                ws.x = ((1 / denom) * x) + ((ws.count / denom) * ws.x);
                ws.y = ((1 / denom) * y) + ((ws.count / denom) * ws.y);
                ws.count++;
                //Log.d(TAG, "Recorded sample for "+i);
            }
        }
        saveWindSamplesToFile();
    }

    private String getWindSamplesDisplay() {
        String s = "";
        for (int i = 0; i < 40; i++) {
            long n = windSamples[i].count / 10;
            if (n > 9) n = 9;
            s += n;
        }
        return s;
    }

    class Point2d {
        public double x;
        public double y;

        Point2d() {
            x = y = 0;
        }
    }

    private Point2d calculateLandingOffsetMeters(double startingAltitude) {
        Point2d p = new Point2d();
        double secondsPerBucket = 50;
        for (int i = 0; i < windSamples.length; i++) {
            WindSample ws = windSamples[i];
            if (startingAltitude < ws.lowAlt)
                continue;
            double fractionOfBucketTraveled = 1;
            if (startingAltitude < ws.highAlt)
                fractionOfBucketTraveled = (startingAltitude - ws.lowAlt) / (ws.highAlt - ws.lowAlt);
            p.x += ws.x * secondsPerBucket * fractionOfBucketTraveled;
            p.y += ws.y * secondsPerBucket * fractionOfBucketTraveled;
        }
        return p;
    }

    // positive x is East, positive y is North
    private LatLng calculateLatLngWithMetersOffset(LatLng from, Point2d metersOffset) {
        // If your displacements aren't too great (less than a few kilometers) and you're not right at the poles,
        // use the quick and dirty estimate that 111,111 meters (111.111 km) in the y direction is 1 degree (of latitude)
        // and 111,111 * cos(latitude) meters in the x direction is 1 degree (of longitude).
        double latOffset = metersOffset.y / 111111.0f;
        double lngOffset = metersOffset.x / (111111.0f * Math.cos(from.latitude * DEGTORAD));
        return new LatLng(from.latitude + latOffset, from.longitude + lngOffset);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

//        requestPermissions(MainActivity.this,
//                new String[]{android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION},
//                1);

        if (checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
            Log.d(TAG, "Permission error. You have permission");
        } else {
            Log.d(TAG, "Permission error. You have asked for permission");
            ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION}, 1);
        }

        cusfLandingLocation = new LatLng(0, 0);
        windSamplesLandingLocation = new LatLng(0, 0);
        data_cutdown_timestamp = 0;

        lastWindSampleTime = System.currentTimeMillis();
        setupDefaultWindSamples();

        mApplication = (Application) getApplication();

        buildGoogleApiClient();

        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        setContentView(R.layout.activity_main_portrait);

        //setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        //setContentView(R.layout.activity_main_landscape);

        SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map);
        mapFragment.getMapAsync(this);

        TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout);
        tabLayout.addTab(tabLayout.newTab().setText("Location"));
        tabLayout.addTab(tabLayout.newTab().setText("Sensors"));
        tabLayout.addTab(tabLayout.newTab().setText("Tracking"));
        tabLayout.addTab(tabLayout.newTab().setText("Log"));
        tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);

        final ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
        adapter = new MyPagerAdapter(getSupportFragmentManager(), tabLayout.getTabCount());
        viewPager.setAdapter(adapter);
        viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
        tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                viewPager.setCurrentItem(tab.getPosition());
                //logConsole("Moved to tab "+tab.getPosition());
                refreshData();

                if (tab.getPosition() == 0) {
                    TabFragment1 tab1 = (TabFragment1) adapter.tab1;

                    tab1.updateLandingButton.setOnClickListener(new View.OnClickListener() {
                        public void onClick(View v) {
                            doCUSFFetch = true;
                        }
                    });
                }

                if (tab.getPosition() == 2) {
                    TabFragment3 tab3 = (TabFragment3) adapter.tab3;

                    tab3.mapTypeButton.setOnClickListener(new View.OnClickListener() {
                        public void onClick(View v) {
                            // Code here executes on main thread after user presses button
                            showMapTypeSelectorDialog();
                        }
                    });

                    tab3.followTypeButton.setOnClickListener(new View.OnClickListener() {
                        public void onClick(View v) {
                            // Code here executes on main thread after user presses button
                            showFollowTypeSelectorDialog();
                        }
                    });
                }

                if (tab.getPosition() == 3) {
                    final TabFragment4 tab4 = (TabFragment4) adapter.tab4;
                    tab4.clearWindSamplesButton.setOnClickListener(new View.OnClickListener() {
                        public void onClick(View v) {
                            AlertDialog.Builder builder = new AlertDialog.Builder(tab4.getContext());
                            builder.setMessage("Are you sure?").setPositiveButton("Yes", clearWindSamplesDialogListener)
                                    .setNegativeButton("No", clearWindSamplesDialogListener).show();
                        }
                    });
                }
            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {

            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {

            }
        });

        findUsbSerial();

        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);

        //new CheckServerReports().execute("dummy");
        new LookupCUSFPrediction().execute("dummy");

        boolean externalWritable = isExternalStorageWritable();
        boolean externalReadable = isExternalStorageReadable();
        Log.d(TAG, "External readable: " + externalReadable);
        Log.d(TAG, "External writable: " + externalWritable);

        publicStorageDirectory = getPublicStorageDir();

        restoreWindSamplesFromFile();

        for (int i = 0; i < windSamples.length; i++) {
            WindSample ws = windSamples[i];
            Log.d(TAG, "Count: " + ws.count + " x: " + ws.x + " y: " + ws.y);
        }

        Date d = new Date();

        String strDateFormatFilename = "yyyyMMdd_HHmmss";
        DateFormat dateFormatFilename = new SimpleDateFormat(strDateFormatFilename);
        String logfileName = "balloonLog_" + dateFormatFilename.format(d) + ".txt";

        File file = new File(publicStorageDirectory, logfileName);

        try {
            fileOutputStream = new FileOutputStream(file);
            String strDateFormat = "yyyy/MM/dd HH:mm:ss";
            DateFormat dateFormat = new SimpleDateFormat(strDateFormat);
            String s = dateFormat.format(d);
            fileOutputStream.write(("Log started at " + s + "\n").getBytes());
            fileOutputStream.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /* Checks if external storage is available for read and write */
    public boolean isExternalStorageWritable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            return true;
        }
        return false;
    }

    /* Checks if external storage is available to at least read */
    public boolean isExternalStorageReadable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state) ||
                Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
            return true;
        }
        return false;
    }

    public File getPublicStorageDir() {
        // Get the directory for the user's public pictures directory.
        File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "balloon");
        if (!file.mkdirs()) {
            //Log.e(TAG, "Directory not created");
        }
        Log.d(TAG, "Public storage directory: " + file.getAbsolutePath());
        return file;
    }

    @Override
    public void onMapReady(GoogleMap googleMap) {
        map = googleMap;
        map.setMapType(GoogleMap.MAP_TYPE_HYBRID);
        LatLng hamilton = new LatLng(-37.771869, 175.270158);
        LatLng northCarolina = new LatLng(35.708310, -79.925658);
        MarkerOptions selfMarkerOptions = new MarkerOptions();
        MarkerOptions balloonMarkerOptions = new MarkerOptions();
        MarkerOptions cusfLandingMarkerOptions = new MarkerOptions();
        MarkerOptions windSamplesLandingMarkerOptions = new MarkerOptions();
        balloonMarkerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE));
        cusfLandingMarkerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_MAGENTA));
        windSamplesLandingMarkerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_YELLOW));
        // order these from lowest to highest priority
        cusfLandingMarker = map.addMarker(cusfLandingMarkerOptions.position(new LatLng(0, 0)).title("CUSF"));
        windSamplesLandingMarker = map.addMarker(windSamplesLandingMarkerOptions.position(new LatLng(0, 0)).title("Wind samples"));
        selfMarker = map.addMarker(selfMarkerOptions.position(northCarolina).title("Me"));
        balloonMarker = map.addMarker(balloonMarkerOptions.position(northCarolina).title("Balloon"));
        map.moveCamera(CameraUpdateFactory.newLatLngZoom(northCarolina, 8));
    }

    private void handleReceivedData(byte[] data) {

        jsonStr += new String(data);

        int closeBracketPos = jsonStr.indexOf("}");
        if (closeBracketPos > -1) {
            String tmp = jsonStr.substring(0, closeBracketPos + 1);
            jsonStr = jsonStr.substring(closeBracketPos + 1);

            parseData(tmp);
        }

        logConsole(new String(data));

    }

    public static int countLines(String str) {
        if (str == null || str.isEmpty()) {
            return 0;
        }
        int lines = 1;
        int pos = 0;
        while ((pos = str.indexOf("\n", pos) + 1) != 0) {
            lines++;
        }
        return lines;
    }

    public void logConsole(String s) {

        //Log.d(TAG, s);

        logContent += "\n" + s;

        // Erase excessive lines
        int numLines = countLines(logContent);
        while (numLines > 30) {
            int nlPos = logContent.indexOf("\n");
            logContent = logContent.substring(nlPos + 1);
            numLines--;
        }

        if (adapter != null && adapter.tab4 != null) {
            //Log.d(TAG, "Checking adapter.tab4");
            mScrollView = ((TabFragment4) adapter.tab4).consoleScrollView;
            mConsoleText = ((TabFragment4) adapter.tab4).consoleTextView;
        }

        if (mConsoleText == null)
            return;

        mConsoleText.setText(logContent);
        //mScrollView.fullScroll(View.FOCUS_DOWN);
    }

    public void findUsbSerial() {

        logConsole("findUsbSerial");

        // Find all available drivers from attached devices.
        UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
        List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager);
        if (availableDrivers.isEmpty()) {
            logConsole("No UsbSerialDrivers");
            return;
        } else {
            logConsole("" + availableDrivers.size() + " drivers");
        }

        // Open a connection to the first available driver.
        UsbSerialDriver driver = availableDrivers.get(0);
        UsbDeviceConnection connection = manager.openDevice(driver.getDevice());
        if (connection == null) {
            logConsole("You probably need to call UsbManager.requestPermission(driver.getDevice()");
            return;
        } else
            logConsole("Connection opened");

        // Read some data! Most have just one port (port 0).
        List<UsbSerialPort> ports = driver.getPorts();
        if (ports.isEmpty()) {
            Log.d(TAG, "No ports");
            logConsole("No ports");
            return;
        } else
            logConsole("" + ports.size() + " ports");

        UsbSerialPort port = ports.get(0);
        sPort = port;

        /*try {
            port.open(connection);
            port.setParameters(9600, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
            byte buffer[] = new byte[16];
            int totalRead = 0;
            //while (totalRead < 100) {
                int numBytesRead = port.read(buffer, 1000);
                Log.d(TAG, "Read " + numBytesRead + " bytes.");
                logConsole("Read " + numBytesRead + " bytes.");
                totalRead += numBytesRead;
            //}
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                port.close();
            }
            catch (IOException e1) {
                e1.printStackTrace();
            }
        }*/

    }

    @Override
    protected void onPause() {
        super.onPause();

        mSensorManager.unregisterListener(this);

        stopIoManager();
        if (sPort != null) {
            try {
                sPort.close();
            } catch (IOException e) {
                // Ignore.
            }
            sPort = null;
        }
        finish();
    }


    @Override
    protected void onResume() {
        super.onResume();

        logConsole("Resumed, port=" + sPort);

        if (sPort == null) {
            //mTitleTextView.setText("No serial device.");
            logConsole("No serial device");
        } else {
            final UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
            UsbDeviceConnection connection = usbManager.openDevice(sPort.getDriver().getDevice());
            if (connection == null) {
                logConsole("Opening device failed");
                return;
            }
            try {
                sPort.open(connection);
                sPort.setParameters(115200, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);

                try {
                    //sendStatus();
                } catch (Exception e) {
                    StackTraceElement[] ste = e.getStackTrace();
                    for (int i = 0; i < ste.length; i++) {
                        logConsole(ste[i].toString() + "\n");
                    }
                }

            } catch (IOException e) {
                logConsole("Error opening device: " + e.getMessage());
                try {
                    sPort.close();
                } catch (IOException e2) {
// Ignore.
                }
                sPort = null;
                return;
            }
            logConsole("Serial device: " + sPort.getClass().getSimpleName());
        }

        Sensor accelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        if (accelerometer != null) {
            mSensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL, SensorManager.SENSOR_DELAY_UI);
        }

        Sensor magneticField = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
        if (magneticField != null) {
            mSensorManager.registerListener(this, magneticField, SensorManager.SENSOR_DELAY_NORMAL, SensorManager.SENSOR_DELAY_UI);
        }

        onDeviceStateChange();
    }

    private void stopIoManager() {
        if (mSerialIoManager != null) {
            logConsole("Stopping io manager ..\n");
            mSerialIoManager.stop();
            mSerialIoManager = null;
        }
    }

    private void startIoManager() {
        if (sPort != null) {
            logConsole("Starting io manager ..\n");
            mSerialIoManager = new SerialInputOutputManager(sPort, mListener);
            mExecutor.submit(mSerialIoManager);

            try {
                //sendStatus();
            } catch (Exception e) {
                StackTraceElement[] ste = e.getStackTrace();
                for (int i = 0; i < ste.length; i++) {
                    logConsole(ste[i].toString() + "\n");
                }
            }
        }
    }

    private void onDeviceStateChange() {
        stopIoManager();
        startIoManager();
    }

    private Date data_rf24_time = null;
    //private Date data_gsm_time = null;

    LatLng cusfLandingLocation;
    LatLng windSamplesLandingLocation;

    private int data_pps;
    private double data_age;
    private float data_lat;
    private float data_lon;
    private double data_hmsl;
    private double data_veld;
    private double data_gspeed;
    private double data_heading;

    private float data_cdlat;
    private float data_cdlon;
    private int data_cdtries;
    private long data_cutdown_timestamp;

    private int data_sats;
    private int data_fix;
    //private int data_csq;
    //private int data_creg;

    //private float data_gsm_lat;
    //private float data_gsm_lon;
    //private double data_gsm_hmsl;
    //private double data_gsm_veld;
    //private double data_gsm_gspeed;
    //private double data_gsm_heading;

    //private int data_gsm_sats;
    //private int data_gsm_fix;
    //private int data_gsm_csq;
    //private double data_gsm_battery;

    //private double data_ds1;
    //private double data_ds2;
    //private double data_MS5611_t;
    private double data_BMP280_t;
    //private double data_Si7021_t;
    //private double data_MPU6050_t;
    private double data_avginsidetemp_t;

    //private double data_MS5611_p;
    private double data_BMP280_p;
    private double data_avgaltitude;

    //private double data_Si7021_h;

    //private double data_MPU6050_ax;
    //private double data_MPU6050_ay;
    //private double data_MPU6050_az;
    //private double data_MPU6050_mag;

    private double data_battery;

    private double milliToMeter(int m) {
        double t = m / 100.0;
        t = Math.round(t);
        return t / 10.0;
    }

    private double hundredMilliToMeter(int m) {
        double t = m / 10000.0;
        t = Math.round(t);
        return t / 10.0;
    }

    private double mbarToMeters(double mbar) {
        double fraction = mbar / 1013.25;
        double raised = Math.pow(fraction, 0.190284);
        return 145366.45 * (1 - raised) * 0.3048;
    }

    private void parseData(String tmp) {

        try {
            JSONObject obj = new JSONObject(tmp);

            data_pps = obj.getInt("packets");
            data_age = milliToMeter(obj.getInt("dt"));

            data_lat = obj.getInt("lat") / 10000000.0f;
            data_lon = obj.getInt("lon") / 10000000.0f;
            data_hmsl = milliToMeter(obj.getInt("hmsl"));
            data_veld = milliToMeter(obj.getInt("veld"));
            data_gspeed = milliToMeter(obj.getInt("gspeed"));
            data_heading = hundredMilliToMeter(obj.getInt("heading"));

            data_sats = obj.getInt("sv");
            data_fix = obj.getInt("fix");
            //data_csq = obj.getInt("csq");
            //data_creg = obj.getInt("creg");

            //data_ds1 = obj.getDouble("ds1");
            //data_ds2 = obj.getDouble("ds2");

            //data_MS5611_t = obj.getDouble("MS5611_t");
            data_BMP280_t = obj.getDouble("BMP280_t");
            //data_Si7021_t = obj.getDouble("Si7021_t");
            //data_MPU6050_t = obj.getDouble("MPU6050_t");
            //double avginside = (data_MS5611_t + data_BMP280_t + data_Si7021_t + data_MPU6050_t) * 0.25;
            data_avginsidetemp_t = Math.round(data_BMP280_t * 100.0) / 100.0;

            //data_MS5611_p = obj.getDouble("MS5611_p");
            data_BMP280_p = obj.getDouble("BMP280_p");
            //double agvPressure = (data_MS5611_p + data_BMP280_p) * 0.5;
            data_avgaltitude = Math.round(mbarToMeters(data_BMP280_p) * 100.0) / 100.0;

            //data_Si7021_h = obj.getDouble("Si7021_h");

            //data_MPU6050_ax = obj.getDouble("MPU6050_ax");
            //data_MPU6050_ay = obj.getDouble("MPU6050_ay");
            //data_MPU6050_az = obj.getDouble("MPU6050_az");
            //double mag = sqrt(data_MPU6050_ax * data_MPU6050_ax + data_MPU6050_ay * data_MPU6050_ay + data_MPU6050_az * data_MPU6050_az);
            //data_MPU6050_mag = Math.round(mag * 100.0) / 100.0;
            data_battery = obj.getDouble("vbat");

            double old_cdlat = data_cdlat;

            data_cdlat = obj.getInt("cdlat") / 10000000.0f;
            data_cdlon = obj.getInt("cdlon") / 10000000.0f;
            data_cdtries = obj.getInt("cdtries");

            if (old_cdlat == 0 && data_cdlat != 0)
                data_cutdown_timestamp = System.currentTimeMillis();

            // only update latest time if pps indicates packets were actually received in this interval
            if (data_pps > 0) {
                data_rf24_time = new Date();
            }

            lastNewDataTime = System.currentTimeMillis();
        } catch (JSONException e) {
            // ignore
            StackTraceElement[] ste = e.getStackTrace();
            for (int i = 0; i < ste.length; i++) {
                logConsole(ste[i].toString());
            }
        }
        refreshData();
        refreshMap();
    }

    double lastBat = 0;

    private void refreshData() {

        float newestLat = 0;
        float newestLon = 0;
        double newestBat = 0;

        Date newestDate = null;
        if (data_rf24_time != null) {
            newestDate = data_rf24_time;
            newestLat = data_lat;
            newestLon = data_lon;
            newestBat = data_battery;
        }

        float uptake = 0.02f;
        newestBat = (uptake * newestBat) + ((1 - uptake) * lastBat);

        lastBat = newestBat;

        String strDateFormat = "HH:mm:ss";
        DateFormat dateFormat = new SimpleDateFormat(strDateFormat);

        String formattedRF24Date = data_rf24_time != null ? dateFormat.format(data_rf24_time) : "-";
        String formattedNewestDate = newestDate != null ? dateFormat.format(newestDate) : "-";
        String formattedCutdownTime = data_cutdown_timestamp > 0 ? dateFormat.format(data_cutdown_timestamp) : "-";

        LatLng balloonPos = new LatLng(newestLat, newestLon);
        if ( balloonMarker != null )
            balloonMarker.setPosition(balloonPos);

        Point2d descentMetersOffset = calculateLandingOffsetMeters(data_hmsl /*15000*/);
        Log.d(TAG, "offset: " + descentMetersOffset.x + ", " + descentMetersOffset.y);
        windSamplesLandingLocation = calculateLatLngWithMetersOffset(balloonPos /*new LatLng(-37.669677, 175.275923)*/, descentMetersOffset);
        if ( windSamplesLandingMarker != null )
            windSamplesLandingMarker.setPosition(windSamplesLandingLocation);

        if ( cusfLandingMarker != null )
            cusfLandingMarker.setPosition(cusfLandingLocation);


        TabFragment1 tab1 = (TabFragment1) adapter.tab1;
        TabFragment2 tab2 = (TabFragment2) adapter.tab2;
        TabFragment3 tab3 = (TabFragment3) adapter.tab3;
        TabFragment4 tab4 = (TabFragment4) adapter.tab4;

        if (tab1 != null) {
            tab1.display_rf24_time.setText("Last time: " + formattedRF24Date);
            tab1.display_pps.setText("" + data_pps);
            tab1.display_age.setText("" + data_age);

            tab1.display_location.setText("" + data_lat + ", " + data_lon);
            tab1.display_hmsl.setText("" + data_hmsl + " m");
            tab1.display_veld.setText("" + data_veld + " m/s");
            tab1.display_gspeed.setText("" + data_gspeed + " m/s");
            tab1.display_heading.setText("" + data_heading + " deg");

            tab1.display_sats.setText("" + data_sats);
            tab1.display_fix.setText("" + data_fix);
            //tab1.display_csq.setText("" + data_csq);
            //tab1.display_creg.setText("" + data_creg);

            tab1.display_cutdown_location.setText("" + data_cdlat + ", " + data_cdlon);
            tab1.display_cutdown_tries.setText("" + data_cdtries);
            tab1.display_cutdown_timestamp.setText("Cutdown time:" + formattedCutdownTime);
            //tab1.display_gsm_veld.setText("" + data_gsm_veld + " m/s");
            //tab1.display_gsm_gspeed.setText("" + data_gsm_gspeed + " m/s");
            //tab1.display_gsm_heading.setText("" + data_gsm_heading + " deg");

            //tab1.display_gsm_sats.setText("" + data_gsm_sats);
            //tab1.display_gsm_fix.setText("" + data_gsm_fix);
            //tab1.display_gsm_csq.setText("" + data_gsm_csq);

            //tab1.display_gsm_time.setText("Last time: " + formattedGSMDate);

            tab1.display_wind_values.setText(getWindSamplesDisplay());
        }

        if (tab2 != null) {
            tab2.display_newestTime.setText("Latest time: " + formattedNewestDate);
            //tab2.display_ds1.setText("" + data_ds2 + " °C");
            //tab2.display_ds2.setText("" + data_ds1 + " °C");
            //tab2.display_MS5611_t.setText("" + data_MS5611_t + " °C");
            tab2.display_BMP280_t.setText("" + data_BMP280_t + " °C");
            //tab2.display_Si7021_t.setText("" + data_Si7021_t + " °C");
            //tab2.display_MPU6050_t.setText("" + data_MPU6050_t + " °C");
            //tab2.display_avginsidetemp.setText("" + data_avginsidetemp_t + " °C");

            //tab2.display_MS5611_p.setText("" + data_MS5611_p + " mbar");
            tab2.display_BMP280_p.setText("" + data_BMP280_p + " mbar");
            tab2.display_avgaltitude.setText("" + data_avgaltitude + " m");

            //tab2.display_Si7021_h.setText("" + data_Si7021_h + " %");

            //tab2.display_MPU6050_xyz.setText("" + data_MPU6050_ax + ", " + data_MPU6050_ay + ", " + data_MPU6050_az);
            //tab2.display_MPU6050_mag.setText("" + data_MPU6050_mag + " m/s²");

            String formattedBattery = String.format("%,.2f", newestBat);
            tab2.display_battery.setText("Battery: " + formattedBattery + " V");
        }

        if (tab3 != null) {
            tab3.display_age.setText("Age: " + data_age);
        }

        updateWindSamples();

        logToFile();
    }

    public void updateWindSamples() {

        // don't update more than once per three seconds
        if (System.currentTimeMillis() < lastWindSampleTime + 3000)
            return;

        // don't write if no new info has been received
        if (lastWindSampleTime > lastNewDataTime)
            return;

        // make positive x East, and positive y North
        double x = Math.sin(data_heading * DEGTORAD) * data_gspeed;
        double y = Math.cos(data_heading * DEGTORAD) * data_gspeed;
        addWindSample(data_hmsl, x, y);

        lastWindSampleTime = System.currentTimeMillis();
    }

    public void logToFile() {
        if (fileOutputStream == null)
            return;

        // don't write more than once per five seconds
        if (System.currentTimeMillis() < lastFileWriteTime + 5000)
            return;

        // don't write if no new info has been received
        if (lastFileWriteTime > lastNewDataTime)
            return;

        String formattedBattery = String.format("%,.2f", lastBat);

        String strDateFormat = "yyyy/MM/dd HH:mm:ss";
        DateFormat dateFormat = new SimpleDateFormat(strDateFormat);

        String formattedRF24Date = "-";
        //String formattedGSMDate = "-";
        if (data_rf24_time != null) {
            formattedRF24Date = dateFormat.format(data_rf24_time);
        }

        /*if ( data_gsm_time != null ) {
            formattedGSMDate = dateFormat.format(data_gsm_time);
        }*/

        try {
            fileOutputStream.write(("" + formattedRF24Date + ",").getBytes());
            fileOutputStream.write(("" + data_pps + ",").getBytes());
            fileOutputStream.write(("" + data_age + ",").getBytes());

            fileOutputStream.write(("" + data_lat + ",").getBytes());
            fileOutputStream.write(("" + data_lon + ",").getBytes());
            fileOutputStream.write(("" + data_hmsl + ",").getBytes());
            fileOutputStream.write(("" + data_veld + ",").getBytes());
            fileOutputStream.write(("" + data_gspeed + ",").getBytes());
            fileOutputStream.write(("" + data_heading + ",").getBytes());

            fileOutputStream.write(("" + data_sats + ",").getBytes());
            fileOutputStream.write(("" + data_fix + ",").getBytes());
            //fileOutputStream.write(("" + data_csq + ",").getBytes());
            //fileOutputStream.write(("" + data_creg + ",").getBytes());

            fileOutputStream.write(("" + data_cdlat + ",").getBytes());
            fileOutputStream.write(("" + data_cdlon + ",").getBytes());
            fileOutputStream.write(("" + data_cdtries + ",").getBytes());

            fileOutputStream.write(("" + formattedBattery + ",").getBytes());
            //fileOutputStream.write(("" + data_ds1 + ",").getBytes());
            //fileOutputStream.write(("" + data_ds2 + ",").getBytes());
            //fileOutputStream.write(("" + data_MS5611_t + ",").getBytes());
            fileOutputStream.write(("" + data_BMP280_t + ",").getBytes());
            //fileOutputStream.write(("" + data_Si7021_t + ",").getBytes());
            //fileOutputStream.write(("" + data_MPU6050_t + ",").getBytes());
            //fileOutputStream.write(("" + data_Si7021_h + ",").getBytes());
            //fileOutputStream.write(("" + data_MPU6050_ax + ",").getBytes());
            //fileOutputStream.write(("" + data_MPU6050_ay + ",").getBytes());
            //fileOutputStream.write(("" + data_MPU6050_az + ",").getBytes());
            //fileOutputStream.write(("" + data_MS5611_p + ",").getBytes());
            fileOutputStream.write(("" + data_BMP280_p + ",").getBytes());

            //fileOutputStream.write(("" + formattedGSMDate + ",").getBytes());
            //fileOutputStream.write(("" + data_gsm_lat + ",").getBytes());
            //fileOutputStream.write(("" + data_gsm_lon + ",").getBytes());
            //fileOutputStream.write(("" + data_gsm_hmsl + ",").getBytes());
            //fileOutputStream.write(("" + data_gsm_veld + ",").getBytes());
            //fileOutputStream.write(("" + data_gsm_gspeed + ",").getBytes());
            //fileOutputStream.write(("" + data_gsm_heading + ",").getBytes());
            //fileOutputStream.write(("" + data_gsm_sats + ",").getBytes());
            //fileOutputStream.write(("" + data_gsm_fix + ",").getBytes());
            //fileOutputStream.write(("" + data_gsm_csq + ",").getBytes());

            fileOutputStream.write(("\n").getBytes());

            fileOutputStream.flush();

        } catch (IOException e) {
            e.printStackTrace();
        }

        lastFileWriteTime = System.currentTimeMillis();
    }

    public void refreshMap() {
        if (followType == 1) {
            map.moveCamera(CameraUpdateFactory.newLatLng(balloonMarker.getPosition()));
        } else if (followType == 2) {
            map.moveCamera(CameraUpdateFactory.newLatLng(selfMarker.getPosition()));
        } else if (followType == 3) {
            LatLngBounds.Builder builder = new LatLngBounds.Builder();
            builder.include(balloonMarker.getPosition());
            builder.include(selfMarker.getPosition());
            LatLngBounds bounds = builder.build();
            int padding = 150; // offset from edges of the map in pixels
            CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, padding);
            map.moveCamera(cu);
        } else if (followType == 4) {
            LatLngBounds.Builder builder = new LatLngBounds.Builder();
            builder.include(cusfLandingMarker.getPosition());
            builder.include(windSamplesLandingMarker.getPosition());
            LatLngBounds bounds = builder.build();
            int padding = 150; // offset from edges of the map in pixels
            CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, padding);
            map.moveCamera(cu);
        } else if (followType == 5) {
            LatLngBounds.Builder builder = new LatLngBounds.Builder();
            builder.include(selfMarker.getPosition());
            builder.include(windSamplesLandingMarker.getPosition());
            LatLngBounds bounds = builder.build();
            int padding = 150; // offset from edges of the map in pixels
            CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, padding);
            map.moveCamera(cu);
        }
    }

    protected synchronized void buildGoogleApiClient() {
        GoogleApiClient.Builder builder = new GoogleApiClient.Builder(this);
        builder.addConnectionCallbacks(this);
        builder.addOnConnectionFailedListener(this);
        builder.addApi(LocationServices.API);
        mGoogleApiClient = builder.build();
        mGoogleApiClient.connect();
    }

    @Override
    public void onConnected(Bundle connectionHint) {
        Log.d(TAG, "onConnected");
        /*mLastLocation = LocationServices.FusedLocationApi.getLastLocation(
                mGoogleApiClient);
        if (mLastLocation != null) {
            //mLatitudeText.setText(String.valueOf(mLastLocation.getLatitude()));
            //mLongitudeText.setText(String.valueOf(mLastLocation.getLongitude()));
            LatLng loc = new LatLng(mLastLocation.getLatitude(), mLastLocation.getLongitude());
            CameraPosition cameraPosition = new CameraPosition.Builder().target(loc).zoom(18).build();
            mMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
        }*/
        createLocationRequest();
    }

    protected void createLocationRequest() {
        LocationRequest mLocationRequest = new LocationRequest();
        mLocationRequest.setInterval(3000);
        mLocationRequest.setFastestInterval(1000);
        mLocationRequest.setSmallestDisplacement(1);
        mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
        if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            // TODO: Consider calling
            //    ActivityCompat#requestPermissions
            // here to request the missing permissions, and then overriding
            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
            //                                          int[] grantResults)
            // to handle the case where the user grants the permission. See the documentation
            // for ActivityCompat#requestPermissions for more details.
            return;
        }
        LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, mLocationRequest, this);
    }

    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        Log.d(TAG, "onConnectionFailed");
    }

    @Override
    public void onConnectionSuspended(int i) {
        Log.d(TAG, "onConnectionSuspended");
    }

    public void onLocationChanged(Location location) {
        Log.d(TAG, "onLocationChanged "+location.toString());

        double x = location.getLatitude();
        double y = location.getLongitude();
        LatLng loc = new LatLng(x,y);
        selfMarker.setPosition(loc);

        if ( followType > 0 )
            refreshMap();

        selfAltitude = location.getAltitude();
    }

    private static final CharSequence[] MAP_TYPE_ITEMS = {"Road Map", "Satellite", "Terrain", "Hybrid"};

    private void showMapTypeSelectorDialog() {
        // Prepare the dialog by setting up a Builder.
        final String fDialogTitle = "Select map type";
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(fDialogTitle);

        // Find the current map type to pre-check the item representing the current state.
        int checkItem = map.getMapType() - 1;

        // Add an OnClickListener to the dialog, so that the selection will be handled.
        builder.setSingleChoiceItems(
                MAP_TYPE_ITEMS,
                checkItem,
                new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int item) {
                        map.setMapType(item + 1);
                        dialog.dismiss();
                    }
                }
        );

        // Build the dialog and show it.
        AlertDialog fMapTypeDialog = builder.create();
        fMapTypeDialog.setCanceledOnTouchOutside(true);
        fMapTypeDialog.show();
    }

    private int followType = 0;
    private static final CharSequence[] FOLLOW_TYPE_ITEMS = { "Nothing", "Balloon", "Tablet", "Balloon + tablet", "Landing predictions", "Tablet + wind sample landing predictions"};

    private void showFollowTypeSelectorDialog() {
        final String fDialogTitle = "Select follow type";
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(fDialogTitle);

        // Add an OnClickListener to the dialog, so that the selection will be handled.
        builder.setSingleChoiceItems(
                FOLLOW_TYPE_ITEMS,
                followType,
                new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int item) {
                        followType = item;
                        refreshMap();
                        dialog.dismiss();
                    }
                }
        );

        // Build the dialog and show it.
        AlertDialog fMapTypeDialog = builder.create();
        fMapTypeDialog.setCanceledOnTouchOutside(true);
        fMapTypeDialog.show();
    }

    // Get readings from accelerometer and magnetometer. To simplify calculations,
    // consider storing these readings as unit vectors.
    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            System.arraycopy(event.values, 0, mAccelerometerReading,0, mAccelerometerReading.length);
            updateOrientationAngles();
        } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
            System.arraycopy(event.values, 0, mMagnetometerReading,0, mMagnetometerReading.length);
            updateOrientationAngles();
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // Do something here if sensor accuracy changes.
        // You must implement this callback in your code.
    }

    // Compute the three orientation angles based on the most recent readings from
    // the device's accelerometer and magnetometer.
    public void updateOrientationAngles() {
        // Update rotation matrix, which is needed to update orientation angles.
        SensorManager.getRotationMatrix(mRotationMatrix, null, mAccelerometerReading, mMagnetometerReading);

        // "mRotationMatrix" now has up-to-date information.

        SensorManager.getOrientation(mRotationMatrix, mOrientationAngles);

        // "mOrientationAngles" now has up-to-date information.
        //Log.d(TAG, ""+mOrientationAngles[0] / Math.PI * 180);
        refreshCompass();
    }

    private void refreshCompass() {
        TabFragment3 tab3 = (TabFragment3) adapter.tab3;
        if ( tab3 != null ) {
            float tmp = mOrientationAngles[0] * 180.f / (float)Math.PI;
            float declination = 20;
            tmp += declination;

            float delta = tmp - compassDirection;
            while ( delta > 180 )
                delta += -360;
            while ( delta < -180 )
                delta += 360;

            compassDirection += delta * 0.2f;

            while (compassDirection > 360) compassDirection -= 360;
            while (compassDirection <   0) compassDirection += 360;

            LatLng selfMarkerPos = new LatLng(0,0);
            LatLng balloonMarkerPos = new LatLng(0,0);

            if (selfMarker != null)
                selfMarkerPos = selfMarker.getPosition();
            if (balloonMarker != null)
                balloonMarkerPos = balloonMarker.getPosition();

            double bearingToTarget = geoBearing(selfMarkerPos, balloonMarkerPos);
            double distanceToTarget = geoDistance(selfMarkerPos, balloonMarkerPos);

            while (bearingToTarget > 360) bearingToTarget -= 360;
            while (bearingToTarget <   0) bearingToTarget += 360;

            tab3.display_compass.setText( ""+((int)Math.round(compassDirection))+" degrees" );
            tab3.display_bearing.setText( ""+((int)Math.round(bearingToTarget))+" degrees" );


            String distanceUnits = "m";
            if ( distanceToTarget > 10000 ) {
                distanceToTarget *= 0.001;
                distanceUnits = "km";
            }
            String formattedDistance = String.format ("%,.0f", distanceToTarget);
            tab3.display_distance.setText( "Distance: "+formattedDistance+" "+distanceUnits );


            double relativeHeight = data_hmsl - selfAltitude;
            String heightUnits = "m";
            if ( relativeHeight > 10000 ) {
                relativeHeight *= 0.001;
                heightUnits = "km";
            }
            String formattedRelativeHeight = String.format ("%,.0f", relativeHeight);
            tab3.display_relativeHeight.setText( "Height: "+formattedRelativeHeight+" "+heightUnits );


            tab3.display_arrowImage.setRotation( (float)bearingToTarget - compassDirection );
        }
    }

    public final double DEGTORAD = 0.0174532925199432957;
    public final double RADTODEG = 57.295779513082320876;

    // http://www.movable-type.co.uk/scripts/latlong.html
    double geoDistance(LatLng a, LatLng b) {
        double R = 6371000; // km
        double p1 = a.latitude * DEGTORAD;
        double p2 = b.latitude * DEGTORAD;
        double dp = (b.latitude - a.latitude) * DEGTORAD;
        double dl = (b.longitude - a.longitude) * DEGTORAD;

        double x = sin(dp/2) * sin(dp/2) + cos(p1) * cos(p2) * sin(dl/2) * sin(dl/2);
        double y = 2 * atan2(sqrt(x), sqrt(1-x));

        return R * y;
    }

    double geoBearing(LatLng a, LatLng b) {
        double y = sin(b.longitude - a.longitude) * cos(b.latitude);
        double x = cos(a.latitude)*sin(b.latitude) - sin(a.latitude)*cos(b.latitude)*cos(b.longitude - a.longitude);
        return atan2(y, x) * RADTODEG;
    }





    /*private class CheckServerReports extends AsyncTask<String, Integer, Long> {
        protected Long doInBackground(String... params) {
            int a = 2;
            while (a == 2) {

                try {
                    Thread.sleep(4000);
                }
                catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                try {
                    URL url = new URL(params[0]);
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    connection.connect();

                    InputStream stream = connection.getInputStream();

                    BufferedReader reader = new BufferedReader(new InputStreamReader(stream));

                    StringBuffer buffer = new StringBuffer();
                    String line = "";

                    while ((line = reader.readLine()) != null) {
                        buffer.append(line+"\n");
                        //Log.d("Response: ", "> " + line);
                    }

                    try {

                        JSONObject obj = new JSONObject(String.valueOf(buffer));

                        int id = obj.getInt("id");

                        Log.d(TAG, "id "+id);

                        if ( id > highestGSMReportId ) {

                            Log.d(TAG, "Doing GSM update ");

                            String timeStr = obj.getString("jst");
                            String latlon = obj.getString("latlon");
                            int battery = obj.getInt("battery");
                            data_gsm_csq = obj.getInt("signal_quality");
                            data_gsm_fix = obj.getInt("fix");
                            data_gsm_sats = obj.getInt("sats");
                            data_gsm_heading = hundredMilliToMeter(obj.getInt("heading"));
                            data_gsm_gspeed = milliToMeter(obj.getInt("speed"));
                            data_gsm_veld = milliToMeter(obj.getInt("veld"));
                            data_gsm_hmsl = milliToMeter(obj.getInt("height"));
                            String MPU6050 = obj.getString("MPU6050");
                            String MS5611 = obj.getString("MS5611");
                            String BMP280 = obj.getString("BMP280");
                            String Si7021 = obj.getString("Si7021");
                            String DS18B20 = obj.getString("DS18B20");

                            String[] latlonArray = latlon.split(",");
                            data_gsm_lat = Float.valueOf(latlonArray[0]) / 10000000.0f;
                            data_gsm_lon = Float.valueOf(latlonArray[1]) / 10000000.0f;

                            SimpleDateFormat myFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                            try {
                                data_gsm_time = myFormat.parse(timeStr);

                                Calendar cal = Calendar.getInstance();
                                cal.setTime(data_gsm_time);
                                cal.add(Calendar.HOUR_OF_DAY, 4);
                                data_gsm_time = cal.getTime();
                            } catch (ParseException e) {
                                e.printStackTrace();
                            }

                            float r1 = 3280;
                            float r2 = 9991;

                            data_gsm_battery = ((battery / 1023.0f) * 3.3) * ((r1 + r2) / r1);

                            //Log.d(TAG, "Got GSM data, battery = " + data_gsm_battery);

                            highestGSMReportId = id;
                            lastNewDataTime = System.currentTimeMillis();

                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    refreshData();
                                }
                            });
                        }
                    }
                    catch (Exception je) {
                        Log.d(TAG, je.toString());
                    }

                }
                catch (Exception e) {
                    Log.d(TAG, e.toString());
                }
            }
            return 0L;
        }

        protected void onProgressUpdate(Integer... progress) {
            //Log.d(TAG, ""+progress[0]);
        }

        protected void onPostExecute(Long result) {
            Log.d(TAG, "thread done");
        }
    }*/


    /*
    launchsite:Other
    lat:-37.6709
    lon:175.2845
    initial_alt:0
    hour:03
    min:32
    second:0
    day:04
    month:5
    year:2019
    ascent:15000
    burst:30000
    drag:5.6
    submit:Run Prediction
    */
    private class LookupCUSFPrediction extends AsyncTask<String, Integer, Long> {
        protected Long doInBackground(String... passedParams) {
            int a = 2;
            while (a == 2) {

                String uuid = "";

                try {
                    Thread.sleep(1000);
                }
                catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                if ( ! doCUSFFetch )
                    continue;

                doCUSFFetch = false;

                try {
                    String urlString = "http://predict.habhub.org/ajax.php?action=submitForm";

                    double lat = data_lat;
                    double lon = data_lon;
                    double alt = data_hmsl;

                    //lat = -37.669677;
                    //lon = 175.275923;
                    //alt = 15000;

                    Date currentDate = new Date(System.currentTimeMillis() + 300000);

                    // I fucking hate Java....
                    DateFormat dfYear = new SimpleDateFormat("yyyy");
                    dfYear.setTimeZone(TimeZone.getTimeZone("UTC"));
                    DateFormat dfMonth = new SimpleDateFormat("M");
                    dfMonth.setTimeZone(TimeZone.getTimeZone("UTC"));
                    DateFormat dfDay = new SimpleDateFormat("d");
                    dfDay.setTimeZone(TimeZone.getTimeZone("UTC"));
                    DateFormat dfHour = new SimpleDateFormat("HH");
                    dfHour.setTimeZone(TimeZone.getTimeZone("UTC"));
                    DateFormat dfMinute = new SimpleDateFormat("mm");
                    dfMinute.setTimeZone(TimeZone.getTimeZone("UTC"));

                    String tmpString = "";
                    tmpString += "&launchsite=Other";
                    tmpString += "&lat=" + lat;
                    tmpString += "&lon=" + lon;
                    tmpString += "&initial_alt=0";
                    tmpString += "&ascent="+alt;
                    tmpString += "&burst="+alt;
                    tmpString += "&drag=5.6";
                    tmpString += "&year=" + dfYear.format(currentDate);
                    tmpString += "&month=" + dfMonth.format(currentDate);
                    tmpString += "&day=" + dfDay.format(currentDate);
                    tmpString += "&hour=" + dfHour.format(currentDate);
                    tmpString += "&min=" + dfMinute.format(currentDate);
                    tmpString += "&second=0";
                    tmpString += "&submit=Run%20Prediction";

                    Log.d(TAG, tmpString);


                    // I really really fucking hate Java, look at all this cumbersome shit here just to send a POST form
                    URL url = new URL("http://predict.habhub.org/ajax.php?action=submitForm");
                    Map<String,Object> params = new LinkedHashMap<>();
                    params.put("launchsite", "Other");
                    params.put("lat", "" + lat);
                    params.put("lon", "" + lon);
                    params.put("initial_alt", "0");
                    params.put("ascent", ""+alt);
                    params.put("burst", ""+alt);
                    params.put("drag", "5.6");
                    params.put("year", "" + dfYear.format(currentDate));
                    params.put("month", "" + dfMonth.format(currentDate));
                    params.put("day", "" + dfDay.format(currentDate));
                    params.put("hour", ""+ dfHour.format(currentDate));
                    params.put("min", "" + dfMinute.format(currentDate));
                    params.put("second","0");
                    params.put("submit", "Run Prediction");

                    StringBuilder postData = new StringBuilder();
                    for (Map.Entry<String,Object> param : params.entrySet()) {
                        if (postData.length() != 0) postData.append('&');
                        postData.append(URLEncoder.encode(param.getKey(), "UTF-8"));
                        postData.append('=');
                        postData.append(URLEncoder.encode(String.valueOf(param.getValue()), "UTF-8"));
                    }
                    byte[] postDataBytes = postData.toString().getBytes("UTF-8");

                    HttpURLConnection conn = (HttpURLConnection)url.openConnection();
                    conn.setRequestMethod("POST");
                    conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                    conn.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length));
                    conn.setDoOutput(true);
                    conn.getOutputStream().write(postDataBytes);

                    BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));

                    StringBuffer buffer = new StringBuffer();
                    String line = "";

                    while ((line = reader.readLine()) != null) {
                        buffer.append(line+"\n");
                    }

                    Log.d(TAG, "Response: "+String.valueOf(buffer));

                    try {

                        JSONObject obj = new JSONObject(String.valueOf(buffer));

                        boolean valid = obj.getString("valid").equals("true");
                        if ( valid ) {
                            uuid = obj.getString("uuid");
                        }

                        Log.d(TAG, "uuid "+uuid);

                        //http://predict.habhub.org/ajax.php?action=getCSV&uuid=ffe95658521da134aa059d8f535c2fe506690f47

                        try {
                            URL url2 = new URL("http://predict.habhub.org/ajax.php?action=getCSV&uuid="+uuid);
                            Log.d(TAG, "http://predict.habhub.org/ajax.php?action=getCSV&uuid="+uuid);
                            HttpURLConnection connection2 = (HttpURLConnection) url2.openConnection();
                            connection2.connect();

                            InputStream stream2 = connection2.getInputStream();

                            BufferedReader reader2 = new BufferedReader(new InputStreamReader(stream2));

                            StringBuffer buffer2 = new StringBuffer();
                            String line2 = "";

                            while ((line2 = reader2.readLine()) != null) {
                                buffer2.append(line2+"\n");
                                //Log.d("Response: ", "> " + line);
                            }

                            //Log.d(TAG, "Response2: "+String.valueOf(buffer2));

                            try {

                                JSONArray pathPoints = new JSONArray(String.valueOf(buffer2));

                                Log.d(TAG, "Response2: "+pathPoints.toString());

                                String lastPathPoint = pathPoints.getString( pathPoints.length() - 2 );
                                Log.d(TAG, "Last point: "+lastPathPoint);

                                String[] parts = lastPathPoint.split(",");
                                cusfLandingLocation = new LatLng( Double.parseDouble( parts[1] ), Double.parseDouble( parts[2] ) );

                                runOnUiThread(new Runnable() {
                                    @Override
                                    public void run() {
                                        refreshData();
                                    }
                                });
                            }
                            catch (Exception je) {
                                Log.d(TAG, je.toString());
                            }
                        }
                        catch (Exception je) {
                            Log.d(TAG, je.toString());
                        }
                    }
                    catch (Exception je) {
                        Log.d(TAG, je.toString());
                    }

                }
                catch (Exception e) {
                    Log.d(TAG, e.toString());
                }

                try {
                    Thread.sleep(4000);
                }
                catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            return 0L;
        }

        protected void onProgressUpdate(Integer... progress) {
            //Log.d(TAG, ""+progress[0]);
        }

        protected void onPostExecute(Long result) {
            Log.d(TAG, "thread done");
        }
    }
}
