__author__ = 'Todd.Hay'

# -------------------------------------------------------------------------------
# Name:        DistanceFished.py
# Purpose:     Algorithms for calculating the distance fished for the Trawl Survey
#
# Author:      Todd.Hay
# Email:       Todd.Hay@noaa.gov
#
# Created:     March 2, 2017
# License:     MIT
#-------------------------------------------------------------------------------
import logging
import math
import arrow
import pandas as pd
import numpy as np
import os
import psycopg2.tz

from py.trawl_analyzer.CommonFunctions import CommonFunctions

from ctypes import cdll, c_int, c_double, Structure, POINTER, ARRAY, cast



class LatLon(Structure):
    _fields_ = [("latitude", c_double), ("longitude", c_double)]


class RangeBearing(Structure):
    _fields_ = [("range", c_double), ("bearing", c_double)]


geodll = cdll.LoadLibrary(r"py\trawl_analyzer\GeographicLib.dll")
geodll.sum_test.restype = c_double
geodll.get_lat_lon.restype = POINTER(LatLon)
geodll.get_range_bearing.restype = POINTER(RangeBearing)



# Constants
N_PER_M = 0.000539957            # Nautical Miles Per Meter
SPEED_CHANGE_PCT = 0.04          # Percent Speed Change Threshold

# Technique Types
TECHNIQUES = ["ITI R/B", "ITI $IIGLL", "ITI R/B + Trig", "Catenary", "Vessel + Trig", "GCD + Trig"]
                # "Range Ext + Trig", "Range Ext + Cat + Trig",


class DistanceFished:
    """
    The Distance Fished class performs all of the distance fished calculations for the Trawl Survey.  Algorithms
    typically have two components:  Pre Haulback and Post Haulback.
    """
    def __init__(self):
        super().__init__()
        self._functions = CommonFunctions()
        self._is_depth_valid = False

    def calculate_all_distances_fished(self, tracklines, scope, span, df_depth, df_headrope):
        """
        Method to calculate the distance fished.  We have five techniques from which to choose:

        ---------------------------------------------------------------------------------------------------------
        | Technique     |   PreHaulback                                 | PostHaulback     |   Result           |
        ---------------------------------------------------------------------------------------------------------
        |   A.          |   Vessel + ITI Range / Bearing running mean .....................|  Gear Track        |
        |   B.          |   Vessel + ITI Range / Bearing running mean   | Trig Method      |  Gear Track        |
        |   C.          |   Extrapolated Range from Slope/Distance      | Trig Method      |  Gear Track        |
        |   D.          |   ITI $IIGLL running mean smoothed track line ...................|  Gear Track        |
        |   E.          |   Vessel Great Circle Distance                | Trig Method      |  Distance Fished   |
        |   F.          |   Extrapolated Range from Slope/Distance      | Catenary + Trig  |  Gear Track        |
        ---------------------------------------------------------------------------------------------------------

        So Technique E is never actually directly called, but the distance is just automatically calculated for each
         technique.

        :param tracklines: dict - dictionary containing the tracklines (vessel, iti $iigll, iti range/bearing)
        :param scope: float - scope in meters of the tow line that was let out
        :param span: int - size of window for calculating the running means of the gear track lines
        :param df_depth: pandas DataFrame - contains the depth used for some of the methods
        :param df_headrope: pandas DataFrame - contains the Headrope height.  This combine with the Depth gives the total depth
        :return:
        """

        logging.info(f"starting distance fished calculations")

        gear_tracklines = dict()

        # Gather the waypoint times to use to mask the vessel dataframe, df_vessel
        if "waypoints" not in tracklines["vessel"]:
            logging.info(f"waypoints are missing for some reason, returning from calculating distance fished")
            return None

        waypoints = tracklines["vessel"]["waypoints"]
        start_haul = arrow.get(waypoints.loc["Start Haul", "best_datetime"]).isoformat() \
            if waypoints.loc["Start Haul", "best_datetime"] is not None and \
               waypoints.loc["Start Haul", "best_datetime"] is not pd.NaT else \
                arrow.get(waypoints.loc["Start Haul", "datetime"]).isoformat()
        set_doors = arrow.get(waypoints.loc["Set Doors", "best_datetime"]).isoformat() \
            if waypoints.loc["Set Doors", "best_datetime"] is not None and \
                waypoints.loc["Set Doors", "best_datetime"] is not pd.NaT else \
                arrow.get(waypoints.loc["Set Doors", "datetime"]).isoformat()
        doors_fully_out = arrow.get(waypoints.loc["Doors Fully Out", "best_datetime"]).isoformat() \
            if waypoints.loc["Doors Fully Out", "best_datetime"] is not None and \
               waypoints.loc["Doors Fully Out", "best_datetime"] is not pd.NaT else \
                arrow.get(waypoints.loc["Doors Fully Out", "datetime"]).isoformat()
        begin_tow = arrow.get(waypoints.loc["Begin Tow", "best_datetime"]).isoformat() \
            if waypoints.loc["Begin Tow", "best_datetime"] is not None and \
               waypoints.loc["Begin Tow", "best_datetime"] is not pd.NaT else \
                arrow.get(waypoints.loc["Begin Tow", "datetime"]).isoformat()
        start_haulback = arrow.get(waypoints.loc["Start Haulback", "best_datetime"]).isoformat() \
            if waypoints.loc["Start Haulback", "best_datetime"] is not None and \
               waypoints.loc["Start Haulback", "best_datetime"] is not pd.NaT else \
                arrow.get(waypoints.loc["Start Haulback", "datetime"]).isoformat()
        net_off_bottom = arrow.get(waypoints.loc["Net Off Bottom", "best_datetime"]).isoformat() \
            if waypoints.loc["Net Off Bottom", "best_datetime"] is not None and \
               waypoints.loc["Net Off Bottom", "best_datetime"] is not pd.NaT else \
                arrow.get(waypoints.loc["Net Off Bottom", "datetime"]).isoformat()
        doors_at_surface = arrow.get(waypoints.loc["Doors At Surface", "best_datetime"]).isoformat() \
            if waypoints.loc["Doors At Surface", "best_datetime"] is not None and \
               waypoints.loc["Doors At Surface", "best_datetime"] is not pd.NaT else \
                arrow.get(waypoints.loc["Doors At Surface", "datetime"]).isoformat()
        end_of_haul = arrow.get(waypoints.loc["End Of Haul", "best_datetime"]).isoformat() \
            if waypoints.loc["End Of Haul", "best_datetime"] is not None and \
               waypoints.loc["End Of Haul", "best_datetime"] is not pd.NaT else \
                arrow.get(waypoints.loc["End Of Haul", "datetime"]).isoformat()

        """
        Make waypoint adjustments. 
         
        This, in particular, is needed for the catenary line calculation.  For instance, if the begin_tow is adjusted,
        the doors_fully_out time could occur after the begin_tow waypoint, but the doors_fully_out waypoint is 
        a phase change for the catenary line calculation, so it needs to be updated in this case.
        
        """
        if doors_fully_out > begin_tow:
            doors_fully_out = begin_tow

            # TODO Todd Hay - Update the Events Data + Mpl_map points

        logging.info(f"distance fished waypoints:"
                     f"\n\tstart_haul\t\t\t{start_haul}"
                     f"\n\tset_doors\t\t\t{set_doors}"
                     f"\n\tdoors_fully_out\t\t{doors_fully_out}"
                     f"\n\tbegin_tow\t\t\t{begin_tow}"
                     f"\n\tstart_haulback\t\t{start_haulback}"
                     f"\n\tnet_off_bottom\t\t{net_off_bottom}"
                     f"\n\tdoors_at_surface\t{doors_at_surface}")

        # Get and prepare the vessel dataframe
        if "dataframe" not in tracklines["vessel"]:
            logging.error("Vessel trackline was not loaded successfully, stopping calculation")
            return

        df_vessel = tracklines["vessel"]["dataframe"]                                   # Used for Trig Method
        df_vessel.drop_duplicates(subset=['times'], keep="first", inplace=True)         # Required as headings are at 4 Hz

        # TODO Todd Hay - Is this bearing correction even correct?  I'm not convinced !!!!!!! ******* XXXXXX
        df_vessel["bearing"] = df_vessel.apply(
            lambda x: (x["track made good"] - 180)  # Calculate the net bearing from vessel heading
            if (x["track made good"] - 180) >= 0 else x["track made good"] + 180, axis=1)  # Create Bearing

        logging.info(f"\tVESSEL track data, in distance fished, start/end: {df_vessel.iloc[0]} > {df_vessel.iloc[-1]}")

        # Mask out invalid values
        vessel_mask = ((~df_vessel["latitude_invalid"]) & (~df_vessel["longitude_invalid"]))
        df_vessel = df_vessel.loc[vessel_mask]

        logging.info(f"\tVESSEL track data, in distance fished, start/end: {df_vessel.iloc[0]} > {df_vessel.iloc[-1]}")

        logging.info(f"df_vessel size before depth merger: {len(df_vessel)}")
        logging.info(f"df_vessel: {df_vessel.iloc[0]}")
        logging.info(f"df_vessel: {df_vessel.iloc[-1]}")
        logging.info(f"before depth merger: start df_vessel: {df_vessel['times'].iloc[0]} >>> end df_vessel: {df_vessel['times'].iloc[-1]}")
        logging.info(f"before depth merger: start df_depth: {df_depth['times'].iloc[0]} >>> end df_depth: {df_depth['times'].iloc[-1]}")


        if isinstance(df_depth, pd.DataFrame) and not df_depth.empty:
            self._is_depth_valid, df_vessel = self.create_vessel_depth_dataframe(df_vessel=df_vessel, df_depth=df_depth,
                                                                      begin_tow=begin_tow,
                                                                      net_off_bottom=net_off_bottom,
                                                                      doors_at_surface=doors_at_surface)
            df_vessel.drop_duplicates(subset=['times'], keep="first", inplace=True)         #

        else:
            logging.info(f"depth is invalid ...")
            logging.info(f"df_depth = {df_depth}")

        logging.info(f"after depth merger: start df_vessel: {df_vessel['times'].iloc[0]} >>> end df_vessel: {df_vessel['times'].iloc[-1]}")

        logging.info(f"begin_tow: {begin_tow} >>> net_off_bottom: {net_off_bottom}")
        logging.info(f"vessel points size: {len(df_vessel)}")

        padding = math.floor(span / 2)

        logging.info(f"scope: {scope}")
        logging.info(f"span: {span}")
        logging.info(f"padding: {padding}")

        # Iterate through each of the techniques, creating the gear trackline and calculating the distance fished for it
        for technique in TECHNIQUES:

            logging.info(f"\n\nTechnique: {technique}")

            # If scope does not exist for certain techniques, then we need to skip as it is a prerequisite
            if scope is None and ("trig" in technique.lower() or "catenary" in technique.lower()):
                logging.info(f"Scope is null, so cannot continue with the {technique} technique.")
                continue

            df_gear = None
            df_pre = None
            distance = None
            speed = None

            # DEPTH NOT REQUIRED
            if technique in ["ITI R/B", "ITI $IIGLL"]:

                if technique.lower() in tracklines:
                    df_trackline = tracklines[technique.lower()]["dataframe"]

                    # Mask out invalid values
                    if technique == "ITI R/B":
                        mask = ((~df_trackline["range_invalid"]) & (~df_trackline["bearing_invalid"]))
                    elif technique == "ITI $IIGLL":
                        mask = ((~df_trackline["latitude_invalid"]) & (~df_trackline["longitude_invalid"]))
                    df_trackline = df_trackline.loc[mask]

                    df_gear = self.create_gearline_by_smoothed_line(df_trackline=df_trackline, span=span)

                    # Calculate the Pre Haulback Speed
                    df_gear.drop_duplicates(subset=["times"], keep="first", inplace=True)
                    mask = (df_gear["times"] >= begin_tow) & (df_gear["times"] < start_haulback)
                    df_pre = df_gear.loc[mask].copy(deep=True)

            # DEPTH NOT REQUIRED
            elif technique == "ITI R/B + Trig":
                if "iti r/b" in tracklines:
                    df_trackline = tracklines["iti r/b"]["dataframe"].copy(deep=True)

                    # Mask out invalid values
                    mask = ((~df_trackline["range_invalid"]) & (~df_trackline["bearing_invalid"]))
                    df_trackline = df_trackline.loc[mask]

                    df_pre = self.create_prehaulback_gearline_by_smoothed_line(df_trackline=df_trackline,
                                                                           doors_fully_out=doors_fully_out,
                                                                           begin_tow=begin_tow,
                                                                           start_haulback=start_haulback,
                                                                           span=span,
                                                                           padding=padding)

            # DEPTH REQUIRED
            elif technique == "Catenary":

                logging.info(f"is_depth_valid = {self._is_depth_valid}")
                if not self._is_depth_valid:
                    continue

                df_gear = self.create_gearline_by_catenary_prior_bearing(df_vessel=df_vessel,
                                                                               start_haul=start_haul,
                                                                               doors_fully_out=doors_fully_out,
                                                                               begin_tow=begin_tow,
                                                                               start_haulback=start_haulback,
                                                                               net_off_bottom=net_off_bottom,
                                                                               doors_at_surface=doors_at_surface,
                                                                               end_of_haul=end_of_haul,
                                                                               padding=padding,
                                                                               scope=scope,
                                                                               span=span)

                # Calculate the Pre Haulback Speed
                # if isinstance(df_gear, pd.DataFrame) and not df_gear.empty:
                #     df_pre = df_gear.loc[(df_gear["times"] >= begin_tow) & (df_gear["times"] <= start_haulback)].copy(deep=True)
                # else:
                #     continue

            # DEPTH NOT REQUIRED
            elif technique == "Vessel + Trig":

                df_pre = self.create_prehaulback_gearline_by_smoothed_vessel(df_vessel=df_vessel,
                                                                             start_haul=start_haul,
                                                                             start_haulback=start_haulback,
                                                                             end_of_haul=end_of_haul,
                                                                             span=span)

            # DEPTH NOT REQUIRED
            elif technique == "GCD + Trig":

                df_pre = self.create_prehaulback_gearline_by_gcd(df_vessel=df_vessel,
                                                                 begin_tow=begin_tow,
                                                                 start_haulback=start_haulback)

            # DEPTH REQUIRED
            if "Trig" in technique:

                if not self._is_depth_valid:
                    continue

                # Create df_gear + calculate Pre Haulback Speed
                if isinstance(df_pre, pd.DataFrame) and not df_pre.empty:

                    df_trig, df_post = self.create_tow_dataframes(df_vessel=df_vessel,
                                                                  df_depth=df_depth,
                                                                  doors_fully_out=doors_fully_out,
                                                                  start_haulback=start_haulback,
                                                                  net_off_bottom=net_off_bottom,
                                                                  doors_at_surface=doors_at_surface,
                                                                  end_of_haul=end_of_haul,
                                                                  padding=padding)

                    df_post = self.create_posthaulback_gearline_by_trig(df_trig=df_trig,
                                                                        df_post=df_post,
                                                                        scope=scope,
                                                                        span=span,
                                                                        padding=padding)

                    if df_post is None:
                        continue

                    # Merge Pre + Post Haulback into df_gear
                    df_gear = df_pre.append(df_post)

                    # Drop duplicates that occur due to the padding at the end of df_pre and beginning of df_post
                    df_gear.drop_duplicates(subset=["times"], keep="first", inplace=True)

                else:

                    continue

            # Clean Gear Trackline, Transfer Waypoints to it, and Calculate Distances
            if isinstance(df_gear, pd.DataFrame) and not df_gear.empty:

                # Drop rows where gear_lat and gear_lon are NA
                df_gear = df_gear.dropna(subset=["gear_lat", "gear_lon"])

                logging.info(f"size of df_gear: {len(df_gear)}")

                # Transfer the Waypoints to the Gear Trackline
                df_gear, gear_waypoints = self.transfer_waypoints_to_gearline(df_vessel=df_vessel,
                                                                              df_gear=df_gear,
                                                                              begin_tow=begin_tow,
                                                                              start_haulback=start_haulback,
                                                                              net_off_bottom=net_off_bottom)


                logging.info(f"size of df_gear after transferring waypoints: {len(df_gear)}")

                # Calculate the segment distances between all of the points in df_gear from Begin Tow to Net Off Bottom
                pre_mask = (df_gear["times"] >= begin_tow) & (df_gear["times"] <= start_haulback)
                post_mask = (df_gear["times"] >= start_haulback) & (df_gear["times"] <= net_off_bottom)
                df_dist = df_gear.copy(deep=True)
                df_dist = df_dist.rename(columns={"gear_lat": "lat1", "gear_lon": "lon1"})
                df_dist["lat2"] = df_dist["lat1"].shift()
                df_dist["lon2"] = df_dist["lon1"].shift()
                df_dist["dist"] = df_dist.apply(lambda x: self._functions.get_distance(x["lat1"], x["lon1"],
                                                                                       x["lat2"], x["lon2"]), axis=1)

                distance_pre_M = df_dist.loc[pre_mask, "dist"].sum()
                distance_post_M = df_dist.loc[post_mask, "dist"].sum()
                distance_M = distance_pre_M + distance_post_M

                logging.info(f"Distances Fished:\n\tpre: {float(distance_pre_M):.5} M"
                             f"\n\tpost: {float(distance_post_M):.5} M\n\ttotal: {float(distance_M):.5} M")

                distance_pre_N = N_PER_M * distance_pre_M
                time_diff = (arrow.get(start_haulback) - arrow.get(begin_tow)).total_seconds() / 3600
                speed = distance_pre_N / time_diff

                logging.info(f"Pre-Haulback Speed: {speed} kts")

                # Convert from numpy.float64 to regular python floats
                distance_pre_M = float(distance_pre_M) if distance_pre_M else None
                distance_post_M = float(distance_post_M) if distance_post_M else None
                distance_M = float(distance_M) if distance_M else None
                speed = float(speed) if speed else None

                # Do a final mask on df_gear to only have points from start_haul > end_of_haul
                mask = (df_gear["times"] >= start_haul) & (df_gear["times"] <= end_of_haul)
                df_gear = df_gear.loc[mask]

                # Add the track line to our dictionary of all track lines
                gear_tracklines[technique] = {"dataframe": df_gear, "distance_M": distance_M,
                                              "distance_pre_M": distance_pre_M, "distance_post_M": distance_post_M,
                                              "speed": speed, "waypoints": gear_waypoints,
                                              "start_haul": start_haul, "end_of_haul": end_of_haul}

        return gear_tracklines

    def transfer_waypoints_to_gearline(self, df_vessel, df_gear, begin_tow, start_haulback, net_off_bottom):
        """
        Method to plot the gear waypoints on the newly generated gear trackline
        :param df_gear: pandas DataFrame of the gear trackline
        :param begin_tow: arrow date/time
        :param start_haulback: arrow date/time
        :param net_off_bottom: arrow date/time
        :return:
        """
        # logging.info(f"times: BT: {begin_tow}, SH: {start_haulback}, NOB: {net_off_bottom}")

        # Find waypoints whose times are already actualized as points on the df_gear trackline
        tow_waypoints = [begin_tow, start_haulback, net_off_bottom]
        df_waypoints = df_gear.loc[df_gear["times"].isin(tow_waypoints)]

        logging.info(f"VESSEL:\n\tstart time: {df_vessel.iloc[1].loc['times']}\n\tend time: {df_vessel.iloc[-1].loc['times']}")
        logging.info(f"GEAR:\n\tstart time: {df_gear.iloc[1].loc['times']}\n\tend time: {df_gear.iloc[-1].loc['times']}")

        logging.info(f"tow_waypoints: {tow_waypoints}")
        logging.info(f"isin exactly found waypoints: {df_waypoints.loc[:,['times', 'gear_lat', 'gear_lon']]}")

        time_map = {"Begin Tow": begin_tow,
                    "Start Haulback": start_haulback,
                    "Net Off Bottom": net_off_bottom}

        waypoints = dict()
        for idx, wp in df_waypoints.iterrows():
            point_type = None
            for k, v in time_map.items():
                if arrow.get(wp['times']).isoformat() == v:
                    point_type = k
                    break
            if point_type:
                logging.info(f"Transferring waypoint:  Existing point on gear trackline: {point_type}")

                tow_waypoints.remove(time_map[k])
                point_dict = dict()
                point_dict["type"] = point_type
                point_dict["datetime"] = wp["times"]
                point_dict["gear_lat"] = wp["gear_lat"]
                point_dict["gear_lon"] = wp["gear_lon"]
                waypoints[point_type] = point_dict

        # Find placement positions in df_gear for the remaining waypoints whose times are not already in df_gear
        # logging.info(f'df_gear times: {df_gear["times"]}')
        tow_waypoints = [arrow.get(x).datetime for x in tow_waypoints]
        logging.info(f"tow_waypoints in datetime format: {tow_waypoints}")
        idx = df_gear["times"].searchsorted(tow_waypoints)
        logging.info(f"searchsorted interpolated waypoints: {idx}")

        if len(idx) > 0:
            for i, x in enumerate(idx):
                logging.info(f"Transferring waypoint:  Interpolating: {x}")
                if x >= len(df_gear):
                    # Past the end of the line, skipping adding the waypoint
                    continue

                logging.info(f"\t\t{df_gear.iloc[x].index.tolist()[0]} >>> {df_gear['times'].iloc[x-1]} >>> {df_gear['times'].iloc[x]}")

                lat1 = df_gear["gear_lat"].iloc[x - 1]
                lon1 = df_gear["gear_lon"].iloc[x - 1]
                lat2 = df_gear["gear_lat"].iloc[x]
                lon2 = df_gear["gear_lon"].iloc[x]

                # Get the time difference
                end_time = arrow.get(df_gear["times"].iloc[x])
                time_diff = (end_time - arrow.get(df_gear["times"].iloc[x - 1])).total_seconds()

                # Find the distance and bearing between the before and after df_gear points
                dist, bearing = self._functions.get_distance_bearing(lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2)

                # Calculate the range to the waypoint that is going to be added to df_gear
                wp_time = arrow.get(tow_waypoints[i])
                ratio = (end_time - wp_time).total_seconds() / time_diff
                wp_range = ratio * dist

                # Calculate the lat/lon for the new df_gear point
                wp_lat, wp_lon = self._functions.get_lat_lon(lat1=lat1, lon1=lon1, range=wp_range, bearing=bearing)

                # logging.info(f"\t\tNew waypoint info:  time= {tow_waypoints[i]}, lat= {wp_lat}, lon= {wp_lon}")

                # Plot it
                point_type = None
                for k, v in time_map.items():
                    if wp_time.isoformat() == v:
                        point_type = k
                        break
                if point_type:
                    logging.info(f"\t\tPoint type to transfer: {point_type}")
                    point_dict = dict()
                    point_dict["type"] = point_type
                    point_dict["datetime"] = wp_time.datetime
                    point_dict["gear_lat"] = wp_lat
                    point_dict["gear_lon"] = wp_lon
                    waypoints[point_type] = point_dict

                    # logging.info(f"point_type: {point_type} >>> waypoint: {point_dict}")

                    # Add this newly interpolated point to the df_gear track line
                    # TODO Todd Hay - Hardcoded the temporal offset

                    offset = wp_time.utcoffset()
                    offset = offset.days * 1440 + offset.seconds / 60
                    logging.info(f"offset = {offset}")

                    """
                    Had to adjust the tzinfo to use a psycopg2.tz.FixedOffsetTimezone otherwise it would fail with a 
                    typical arrow-based timezone that was converted to a standard datetime object via .datetime
                    Reference - https://github.com/crsmithdev/arrow/issues/313
                                http://initd.org/psycopg/docs/tz.html
                    """
                    dfdict = {"times": wp_time.replace(tzinfo=psycopg2.tz.FixedOffsetTimezone(offset=offset)).datetime,
                              "gear_lat": wp_lat, "gear_lon": wp_lon}
                    df_gear = df_gear.append(dfdict, ignore_index=True)

            # Reset order the index - need to convert to pandas datetime format and do some timezone conversions to get it to work
            df_gear["times"] = pd.to_datetime(df_gear["times"], utc=True)
            df_gear.set_index("times", drop=False, inplace=True)
            logging.info(f"tzinfo: {df_gear.index.tzinfo}")
            if df_gear.index.tzinfo:
                df_gear.index = df_gear.index.tz_convert("US/Pacific")
            else:
                df_gear.index = df_gear.index.tz_localize("UTC").tz_convert("US/Pacific")
            df_gear.sort_index(inplace=True)
            df_gear.drop(["times"], axis=1, inplace=True)
            df_gear.reset_index(inplace=True)

        return df_gear, waypoints

    def create_vessel_depth_dataframe(self, df_vessel, df_depth, begin_tow, net_off_bottom, doors_at_surface):
        """
        Method to merge the vessel and depth dataframes.  With a solid depth curve, this is easy. However if the
        depth curve stopped abruptly (we see this sometimes with the SBE39) data, then we need to get an average
        of the available depth on bottom and then extrapolate the rest of the curve from that.
        :param df_vessel:
        :param df_depth:
        :return:
        """
        depth_exists = False

        if isinstance(df_depth, pd.DataFrame) and not df_depth.empty:

            depth_mask = (~df_depth["invalid"])
            df_depth = df_depth.loc[depth_mask]

            df_depth = df_depth.rename(columns={"values": "depth"})               # Rename the depth column
            df_depth.drop(["invalid", "id"], axis=1, inplace=True)                # Drop extraneuous df_depth columns

            logging.info(f"size of df_depth: {len(df_depth)}")
            logging.info(f"df_depth col: {df_depth.columns.values}")

            # Round both the depth and vessel dataframes to the nearest second, otherwise they won't merge on microseconds
            df_depth["times"] = df_depth["times"].dt.round('1s')
            df_vessel["times"] = df_vessel["times"].dt.round('1s')
            # s = 200
            # e = s+10
            # logging.info(f"df_depth:\n{df_depth.loc[s:e, 'times']}")
            # s = 142
            # e = s+10
            # logging.info(f"df_vessel:\n{df_vessel.loc[s:e, 'times']}")

            df_vessel = pd.merge(df_vessel, df_depth, how="left", on="times")  # Add depth to vessel

            last_time = arrow.get(df_depth["times"].iloc[-1]).isoformat()
            logging.info(f"last_time = {last_time}, net_off_bottom = {net_off_bottom}")

            if last_time < net_off_bottom:
                # Full depth curve does not exist, piece together an average depth on bottom

                logging.info(f"\n\tbegin_tow: {begin_tow}"
                             f"\n\tnet_off_bottom: {net_off_bottom}")

                s = 0
                e = s+10
                logging.info(f"df_depth beginning\n{df_depth.loc[s:e, 'times']}")

                s = len(df_depth) - 10
                e = s+10
                logging.info(f"df_depth ending\n{df_depth.loc[s:e, 'times']}")

                mask = (df_depth["times"] >= begin_tow) & (df_depth["times"] <= net_off_bottom)
                df_depth_test = df_depth.loc[mask]
                logging.info(f"df_depth size within time on bottom: {len(df_depth_test)}")

                if len(df_depth_test) >= 5:
                    avg_depth = df_depth_test.loc[:, "depth"].mean()

                    # Apply the average bottom depth to the remaining of the on bottom curve
                    mask = (df_vessel["times"] > last_time) & (df_vessel["times"] <= net_off_bottom)
                    df_vessel["depth"].loc[mask] = avg_depth

                    # Apply a linear depth for the remainder of the graph
                    mask = (df_vessel["times"] > net_off_bottom) & (df_vessel["times"] <= doors_at_surface)
                    num_pts = len(df_vessel.loc[mask])
                    rate = avg_depth / num_pts
                    df_vessel["depth"].loc[mask] = [avg_depth - (i+1) * rate for i in range(num_pts)]

                    # path = os.path.expanduser("~/Desktop/df_vessel_depth.csv")
                    # df_vessel.loc[:, ['times', 'latitude', 'longitude', 'track made good', 'depth']].to_csv(path)

                    depth_exists = True
                    df_vessel = df_vessel.dropna(subset=["depth"])  # Drop rows where column values are NA

            else:

                depth_exists = True
                df_vessel = df_vessel.dropna(subset=["depth"])  # Drop rows where column values are NA

        return depth_exists, df_vessel

    def create_tow_dataframes(self, df_vessel, df_depth, doors_fully_out, start_haulback, net_off_bottom, doors_at_surface, end_of_haul, padding):
        """
        Method to create the df_trig and df_post data frames
        :param df_vessel:
        :param start_haulback:
        :param net_off_bottom:
        :param doors_at_surface:
        :param padding:
        :return:
        """

        # Add in the df_depth data if it is valid
        df_depth["times"] = df_depth["times"].dt.round('1s')
        df_vessel["times"] = df_vessel["times"].dt.round('1s')

        if self._is_depth_valid:
            df_vessel = pd.merge(df_vessel, df_depth, how="left", on="times")  # Add depth to vessel
            df_vessel = df_vessel.dropna(subset=["depth"])  # Drop rows where column values are NA
            logging.info(f"vessel and depth successfully merged")

        # Create the df_trig dataframe - from start_haulback to end_of_haul
        trig_method_mask = (df_vessel["times"] >= start_haulback) & (df_vessel["times"] <= end_of_haul)
        df_trig = df_vessel.loc[trig_method_mask]
        logging.info(f"df_trig size: {len(df_trig)}")

        if len(df_trig) > 0:
            start = df_vessel.index.get_loc(df_trig.head(1).index.tolist()[0]) - padding
            end = df_vessel.index.get_loc(df_trig.tail(1).index.tolist()[0])
            df_trig = df_vessel.iloc[start:end].copy(deep=True)
        else:
            logging.info(f"df_trig is zero, skipping masking")

        # Define df_post elements
        post_haulback_mask = (df_vessel["times"] >= start_haulback) & (df_vessel["times"] <= end_of_haul)
        df_post = df_vessel.loc[post_haulback_mask]

        if len(df_post) > 0:
            start = df_vessel.index.get_loc(df_post.head(1).index.tolist()[0]) - padding
            end = df_vessel.index.get_loc(df_post.tail(1).index.tolist()[0])
            df_post = df_vessel.iloc[start:end].copy(deep=True)
        else:
            logging.info(f"df_post is zero, skipping masking")

        return df_trig, df_post

    def calculate_prehaulback_speed(self, technique, df_pre, begin_tow, start_haulback):
        """
        Method to calculate the speed of the prehaulback period, found from df_pre
        :param df_pre:
        :return:
        """
        speed = None
        if technique == "GCD + Trig":
            if len(df_pre) == 2:
                lat1 = df_pre.iloc[0]["gear_lat"]
                lon1 = df_pre.iloc[0]["gear_lon"]
                lat2 = df_pre.iloc[1]["gear_lat"]
                lon2 = df_pre.iloc[1]["gear_lon"]
                distance, bearing = self._functions.get_distance_bearing(lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2)
                distance_N = N_PER_M * distance
                time_diff = (arrow.get(df_pre.iloc[1]["times"]) - arrow.get(df_pre.iloc[0]["times"])).total_seconds() / 3600
                speed = distance_N / time_diff

        else:
            mask = (df_pre["times"] >= begin_tow) & (df_pre["times"] <= start_haulback)
            df_dist_pre = df_pre.copy(deep=True)
            df_dist_pre = df_dist_pre.rename(columns={"gear_lat": "lat1", "gear_lon": "lon1"})
            df_dist_pre["lat2"] = df_dist_pre["lat1"].shift()
            df_dist_pre["lon2"] = df_dist_pre["lon1"].shift()
            df_dist_pre.loc[mask, "dist"] = df_dist_pre.loc[mask].apply(
                lambda x: self._functions.get_distance(x["lat1"], x["lon1"], x["lat2"], x["lon2"]), axis=1)
            distance = df_dist_pre["dist"].sum()
            distance_N = N_PER_M * distance

            time_diff = (arrow.get(start_haulback) - arrow.get(begin_tow)).total_seconds() / 3600
            speed = distance_N / time_diff

        logging.info(f"Gear PreHaulback Distance, BT > SH: {distance_N:.2} NM")
        logging.info(f"Gear PreHaulback Speed, BT > SH: {speed:.2} kts")

        return speed

    def create_prehaulback_gearline_by_smoothed_vessel(self, df_vessel, start_haul, start_haulback, end_of_haul, span):
        """
        Method to create the df_pre gear trackline from a smoothed vessel trackline
        :param df_vessel:
        :param start_haul:
        :param start_haulback:
        :param end_of_haul:
        :param span:
        :return:
        """
        df_pre = df_vessel.copy(deep=True)
        df_pre.drop_duplicates(subset=['times'], keep="first", inplace=True)  # Required, headings are at 4 Hz
        df_pre.loc[:, "gear_lat"] = df_pre.loc[:, "latitude"]
        df_pre.loc[:, "gear_lon"] = df_pre.loc[:, "longitude"]
        df_pre.loc[:, ["gear_lat", "gear_lon"]] = \
            df_pre.loc[:, ["gear_lat", "gear_lon"]].rolling(window=span, center=True).mean()
        mask = (df_pre["times"] < start_haulback)
        df_pre = df_pre.loc[mask]

        return df_pre

    def create_prehaulback_gearline_by_gcd(self, df_vessel, begin_tow, start_haulback):
        """
        Method to create the df_pre gear trackline from a GCD of the vessel trackline
        :param df_vessel:
        :param begin_tow:
        :param net_off_bottom:
        :return:
        """
        df_pre = df_vessel.copy(deep=True)
        df_pre.drop_duplicates(subset=['times'], keep="first", inplace=True)  # Required, headings are at 4 Hz
        df_pre.loc[:, "gear_lat"] = df_pre.loc[:, "latitude"]
        df_pre.loc[:, "gear_lon"] = df_pre.loc[:, "longitude"]

        # logging.info(f"gcd calc: {begin_tow}, {start_haulback}")
        # logging.info(f"gcd head: {df_pre.head(5)}")
        # logging.info(f"gcd tail: {df_pre.tail(5)}")
        # logging.info(f"index: {df_pre.index.tolist()}")
        # logging.info(f"df_pre size: {len(df_pre)}")

        start_mask = df_pre['times'].isin([begin_tow])
        if len(df_pre.loc[start_mask]) > 0:
            start_rec = df_pre.loc[start_mask]
        else:
            idx = df_pre["times"].searchsorted(begin_tow)
            if idx == len(df_pre):
                if idx > 0:
                    start_rec = df_pre.iloc[idx-1]
                else:
                    # Cannot find a legitimate start_rec
                    return None
            else:
                start_rec = df_pre.iloc[idx]

            # logging.info(f"begin_tow: {begin_tow}")
            # logging.info(f"gcd start idx: {idx}")
            # logging.info(f"start_rec: {start_rec}")

        end_mask = df_pre["times"].isin([start_haulback])
        if len(df_pre.loc[end_mask]) > 0:
            end_rec = df_pre.loc[end_mask]
        else:
            idx = df_pre["times"].searchsorted(start_haulback)
            if idx == len(df_pre):
                if idx > 0:
                    end_rec = df_pre.iloc[idx-1]
                else:
                    return None
            else:
                end_rec = df_pre.iloc[idx]

        df_pre = start_rec.append(end_rec)

        return df_pre

    def create_prehaulback_gearline_by_smoothed_line(self, df_trackline, doors_fully_out, begin_tow, start_haulback, span, padding):
        """
        Method to create a gear trackline using the iti r/b for the prehaulback and the trig method
        for the post haulback
        :param df_trackline: pandas DataFrame
        :param begin_tow: arrow date/time
        :param start_haulback: arrow date/time
        :param span:
        :return:
        """

        # Padding done to ensure that the running mean calculates values at the ending point
        df_trackline.drop_duplicates(subset=["times"], keep="first", inplace=True)
        mask = (df_trackline["times"] >= doors_fully_out) & (df_trackline["times"] < start_haulback)
        df_pre = df_trackline.loc[mask]

        if len(df_pre) == 0:
            return None

        start = df_trackline.index.get_loc(df_pre.head(1).index.tolist()[0]) #- padding  #- 1  # Subtract an extra for waypoint transfer later on
        end = df_trackline.index.get_loc(df_pre.tail(1).index.tolist()[0]) + padding   #+ 1
        df_pre = df_trackline.iloc[start:end].copy(deep=True)

        # df_pre = df_trackline.loc[mask].copy(deep=True)
        df_pre.loc[:, ["latitude", "longitude"]] = \
            df_pre.loc[:, ["latitude", "longitude"]].rolling(window=span, center=True).mean()
        df_pre = df_pre.rename(columns={"latitude": "gear_lat", "longitude": "gear_lon"})

        return df_pre

    def create_posthaulback_gearline_by_trig(self, df_trig, df_post, scope, span, padding):
        """
        Method to calculate the posthaulback gearline using the trig method
        :param df_trig:
        :param df_post:
        :param scope:
        :param span:
        :return:
        """
        # Gather static values for the trig method
        if len(df_trig) < padding+1:
            return None

        time_diff = (df_trig["times"].iloc[-1] - df_trig["times"].iloc[0+padding]).seconds  # Start Haulback to Doors At Surface
        ratio = scope / time_diff                                                   # Ratio of Scope to Time Diff
        trig_end_time = df_trig["times"].iloc[-1]                                   # Time at Doors At Surface

        # Calculate and assign the scope
        df_trig.loc[:, "scope"] = df_trig.apply(lambda x: (trig_end_time - x["times"]).seconds * ratio, axis=1)
        df_post = df_post.assign(scope=df_trig["scope"])

        # path = os.path.expanduser("~/Desktop/df_post_before.csv")
        # df_post.loc[:, ['times', 'latitude', 'longitude', 'a', 'scope', 'depth', 'track made good', 'bearing', 'gear_lat', 'gear_lon']].to_csv(path)

        # Calculate the range from the scope + depth using pythagorean theorem
        df_post.loc[:, "range"] = df_post.apply(lambda x: math.sqrt(x["scope"] ** 2 - x["depth"] ** 2)
                                                if x["scope"] > x["depth"] and x["depth"] >= 0 else None, axis=1)

        # path = os.path.expanduser("~/Desktop/df_post_after.csv")
        # df_post.loc[:, ['times', 'latitude', 'longitude', 'a', 'scope', 'depth', 'range', 'track made good', 'bearing', 'gear_lat', 'gear_lon']].to_csv(path)

        # Check to see if the range values are null or not.  If null, this means that the depth > scope, which can't
        # be true, so discard the trig method in this case if we find such instances
        # mask = (df_post["range"].isnull())
        # df_post = df_post.loc[mask]
        # if len(df_post) > 0:
        #     logging.info(f"Trig method, count where the depth > scope: {len(df_post)}, skipping the trig method")
        #     return None

        # Calculate the gear_lat and gear_lon
        df_post["gear_lat"] = df_post.apply(lambda x: self._functions.get_lat(lat=x["latitude"],
                                                                              lon=x["longitude"],
                                                                              bearing=x["bearing"],
                                                                              range=x["range"]) if x["range"] is not None else None, axis=1)
        df_post["gear_lon"] = df_post.apply(lambda x: self._functions.get_lon(lat=x["latitude"],
                                                                              lon=x["longitude"],
                                                                              bearing=x["bearing"],
                                                                              range=x["range"]) if x["range"] is not None else None, axis=1)

        # Smooth the df_post line using a running mean
        df_post.loc[:, ["gear_lat", "gear_lon"]] = df_post.loc[:, ["gear_lat", "gear_lon"]].rolling(window=span,
                                                                                                    center=True).mean()

        return df_post

    def create_gearline_by_catenary_prior_bearing(self, df_vessel, start_haul, doors_fully_out, begin_tow, start_haulback,
                                    net_off_bottom, doors_at_surface, end_of_haul, padding, scope, span):
        """
        Method to create the catenary gear line using the prior bearing information
        :param df_vessel:
        :param start_haul:
        :param doors_fully_out:
        :param begin_tow:
        :param start_haulback:
        :param net_off_bottom:
        :param doors_at_surface:
        :param end_of_haul:
        :param padding:
        :param scope:
        :param span:
        :param avg_depth:
        :return:
        """
        # Copy the vessel dataframe to the gear dataframe and add in the gear_lat, gear_lon, and range columns
        df_gear = df_vessel.copy(deep=True)
        df_gear.loc[:, "gear_lat"] = df_gear.loc[:, "latitude"]
        df_gear.loc[:, "gear_lon"] = df_gear.loc[:, "longitude"]
        df_gear.loc[:, "range"] = 0

        #################################################################################
        # Get the First Record (or nearest point to it on the vessel track)
        #################################################################################
        initialize_time = start_haul # begin_tow
        mask = (df_gear["times"] == initialize_time)
        record = df_gear.loc[mask]
        if not record.empty:
            record = df_gear.loc[mask].iloc[0]
        else:
            idx = df_gear["times"].searchsorted(initialize_time)
            if idx:
                record = df_gear.iloc[idx[0]-1]
                if record.empty:
                    return
            else:
                if idx == 0:
                    record = df_gear.iloc[0]
                else:
                    return

        # logging.info(f"catenary starting record: {record}")

        #################################################################################
        # Calculate the gear_lat / gear_lon at the starting point - this is passed to the C DLL
        #################################################################################
        track_made_good = record["track made good"]
        bearing = track_made_good - 180 if track_made_good >= 180 else track_made_good + 180
        bearing = track_made_good - 180
        depth = record["depth"]
        if depth > 0:
            a = (scope ** 2 - depth ** 2) / (2 * depth)
            scope_0 = math.sqrt(depth ** 2 + 2 * depth * a)
            distance = a * math.log((scope_0 / a) + math.sqrt((scope_0 / a) ** 2 + 1))
        else:
            distance = 0
        gear_lat, gear_lon = self._functions.get_lat_lon(lat1=record["latitude"], lon1=record["longitude"],
                                                           bearing=bearing, range=distance)
        logging.info(f"starting gear lat/lon: {gear_lat}, {gear_lon}")

        #################################################################################
        # Start Haul > Doors Fully Out Phase
        #
        #   Scope is incrementally increasing from the Start Haul to the Doors Fully Out
        #
        #   Don't start increasing the scope until the depth is greater than 1 meter as well, for
        #   otherwise there can be a lot of values close to the start of haul where the depth is
        #   less than 1 meter, and so no scope has really been let out.
        #
        #################################################################################

        # TODO Todd Hay - Change this to not be hardcoded to 1 meter, but rather once we see it changing at a certain rate

        # Mask when the depth is <= 1 meter
        mask = (df_gear["times"] >= start_haul) & (df_gear["times"] <= doors_fully_out) & (df_gear["depth"] <= 1)
        df_gear.loc[mask, "scope"] = 0
        df_gear.loc[mask, "a"] = 1000000
        df_gear.loc[mask, "range"] = 0

        # Mask when the depth is > 1 meter
        mask = (df_gear["times"] >= start_haul) & (df_gear["times"] <= doors_fully_out) & (df_gear["depth"] > 1)
        num_pts = len(df_gear.loc[mask])
        if num_pts == 1:
            logging.info(f"depth masking greater than 1m returns a single point, which leads to division by zero")
            return
        scope_grow_rate = scope / (num_pts - 1)
        logging.info(f"Number of points before the tow starts: {num_pts}")

        df_gear.loc[mask, "scope"] = [i * scope_grow_rate for i in range(num_pts)]
        df_gear.loc[mask, "a"] = df_gear.loc[mask].apply(lambda x: (x["scope"]**2 - x["depth"]**2) / (2*x["depth"])
                                                         if x["depth"] != 0 and not math.isnan(x["depth"]) else None, axis=1)
        df_gear.loc[mask, "range"] = df_gear.loc[mask] \
            .apply(lambda x: x["a"] * math.log((x["scope"] / x["a"]) + math.sqrt((x["scope"] / x["a"]) ** 2 + 1))
                if not math.isnan(x["a"]) and x["a"] != 0 else None, axis=1)

        #################################################################################
        # Doors Fully Out > Start Haulback Phase
        #
        #   Scope is static during this phase.  The fishing operation occurs during this time, beginning
        #           at the Doors Fully Out waypoint and concluding at the Start Haulback waypoint
        #
        #################################################################################
        mask = (df_gear["times"] >= doors_fully_out) & (df_gear["times"] < start_haulback)
        df_gear.loc[mask, "scope"] = scope
        df_gear.loc[mask, "a"] = df_gear.loc[mask].apply(lambda x: (x["scope"]**2 - x["depth"]**2) / (2*x["depth"])
                                                            if x["depth"] != 0 and not math.isnan(x["depth"]) else None, axis=1)
        df_gear.loc[mask, "range"] = df_gear.loc[mask]\
                .apply(lambda x: x["a"] * math.log((x["scope"]/x["a"]) + math.sqrt((x["scope"]/x["a"])**2 + 1))
                                    if not math.isnan(x["a"]) and x["a"] != 0 else None, axis=1)

        #################################################################################
        # Start Haulback > End of Haul Phase
        #
        #   Scope is incrementally decreasing from Start Haulback to End of Haul
        #
        #################################################################################
        mask = (df_gear["times"] >= start_haulback) & (df_gear["times"] <= end_of_haul)
        post_haulback_size = len(df_gear.loc[mask])

        # path = os.path.expanduser("~/Desktop/df_cat.csv")
        # df_gear.loc[:, ['times', 'latitude', 'longitude', 'a', 'scope', 'depth', 'range', 'track made good', 'bearing', 'gear_lat', 'gear_lon']].to_csv(path)

        # Decrement Scope, then calculate a, approach
        scope_shrink_rate = scope / (post_haulback_size-1)
        df_gear.loc[mask, "scope"] = [scope - i * scope_shrink_rate for i in range(post_haulback_size)]
        df_gear.loc[mask, "a"] = df_gear.loc[mask].apply(lambda x: (x["scope"]**2 - x["depth"]**2) / (2*x["depth"])
                                                     if x["depth"] != 0 and not math.isnan(x["depth"]) else None, axis=1)
        df_gear.loc[mask, "range"] = df_gear.loc[mask]\
                .apply(lambda x: x["a"] * math.log((x["scope"]/x["a"]) + math.sqrt((x["scope"]/x["a"])**2 + 1))
                                    if not math.isnan(x["a"]) and x["a"] != 0 else None, axis=1)

        #################################################################################
        # Calculate all of the gear lat/lon values using the C DLL function
        #################################################################################
        start = arrow.now()

        mask = (df_gear["times"] >= start_haul) & (df_gear["times"] <= end_of_haul)
        num_rows = len(df_gear.loc[mask])

        # Prepare the vessel_data structure for passing into the C DLL
        vessel_data = df_gear.loc[mask, ["latitude", "longitude", "range"]].copy(deep=True).as_matrix()
        vessel_data = np.ascontiguousarray(vessel_data, dtype=np.double)
        vessel_ctypes = [np.ctypeslib.as_ctypes(array) for array in vessel_data]
        vessel_ptr = (POINTER(c_double) * num_rows)(*vessel_ctypes)

        # Prepare gear_data data structure to catch gear lat/lon values calculated via C DLL function
        gear_data = np.zeros([num_rows, 3], dtype=np.double)
        gear_ctypes = [np.ctypeslib.as_ctypes(array) for array in gear_data]
        gear_ptr = (POINTER(c_double) * num_rows)(*gear_ctypes)

        # Calculate gear lat/lon values from the C DLL
        geodll.get_gear_lat_lon(c_int(num_rows), vessel_ptr, c_double(gear_lat), c_double(gear_lon), gear_ptr)

        end = arrow.now()
        logging.info(f"C DLL Iterations:  {num_rows} iterations took {(end-start).total_seconds():.4f}s")

        # Add the gear_data back to the pandas data frame:
        df_gear.loc[mask, "gear_lat"] = [gear_data[i][0] for i in range(num_rows)]
        df_gear.loc[mask, "gear_lon"] = [gear_data[i][1] for i in range(num_rows)]
        df_gear.loc[mask, "bearing"] = [gear_data[i][2] for i in range(num_rows)]

        path = os.path.expanduser("~/Desktop/df_gear_cat.csv")
        # df_gear.loc[:, ['times', 'latitude', 'longitude', 'a', 'scope', 'depth', 'range', 'track made good', 'bearing', 'gear_lat', 'gear_lon']].to_csv(path)

        return df_gear

    def create_gearline_by_smoothed_line(self, df_trackline, span):
        """
        Method to create a gear line from the ITI time series data for the given technique.  These techniques will
        include iti r/b or iti $iigll.
        :param df_trackline: pandas DataFrame
        :param span: int - size of the running mean window
        :return:
        """
        df_gear = df_trackline.copy(deep=True)
        df_gear.drop_duplicates(subset=['times'], keep="first", inplace=True)  # Required, headings are at 4 Hz
        df_gear.loc[:, ["latitude", "longitude"]] = \
            df_gear.loc[:, ["latitude", "longitude"]].rolling(window=span, center=True).mean()
        df_gear = df_gear.rename(columns={"latitude": "gear_lat", "longitude": "gear_lon"})

        return df_gear




    # Utilities only - NOT SURE THAT I'M EVEN USING THESE ANYMORE EITHER
    def calculate_vessel_gcd(self, df_vessel, begin_tow, start_haulback):
        """
        Method to calculate the Great Circle Distance of the vessel from begin tow to start haulback
        :param df_vessel:
        :param begin_tow:
        :param start_haulback:
        :return:
        """
        mask = (df_vessel["times"] >= begin_tow) & (df_vessel["times"] < start_haulback)
        df_gcd = df_vessel.loc[mask].copy(deep=True)
        distance_M = self._functions.get_distance(lat1=df_gcd.iloc[0].loc["latitude"],
                                                lon1=df_gcd.iloc[0].loc["longitude"],
                                                lat2=df_gcd.iloc[-1].loc["latitude"],
                                                lon2=df_gcd.iloc[-1].loc["longitude"])
        distance_N = distance_M * N_PER_M
        speed = distance_N / ((arrow.get(start_haulback) - arrow.get(begin_tow)).total_seconds() / 3600)

        return distance_N, speed


    def calculate_catenary_missing_scope(self, depth, range, initial):
        """
        Calculate the catenary a (shape factor) when we have the depth and range, but no scope
        :param depth: vertical distance from the lowest point of the tow line to the vessel
        :param range: horizontal distance between the supports
        :param initial:
        :return:
        """

        # :param h2: vertical distance between the higher and lower support points, always 0 for us
        h2 = 0

        def f(a):
            return range - a * (np.arccosh((depth + a) / a) + np.arccosh((depth - h2 + a) / a))

        solution = self._functions.newton(f, initial)
        return solution

    def calculate_catenary_missing_range(self, depth, scope, initial):
        """
        Calculate catenary a (shape factor) when we have the depth and the scope.
        :param depth: vertical distance from the lowest point of the tow line to the vessel
        :param L: scope of the tow line (i.e. how much let out)
        :param initial:
        :return:
        """

        # :param h2: vertical distance between the higher and lower support points, always 0 for us
        h2 = depth
        h2 = 0

        def f(a):
            return scope - a * (np.sinh(np.arccosh((depth + a) / a)) + np.sinh(np.arccosh((depth - h2 + a) / a)))

        solution = self._functions.newton(f, initial)
        return solution


    # NOT USED I DON'T THINK
    def create_prehaulback_gearline_by_range_extrapolation(self, df_vessel, doors_fully_out, begin_tow, start_haulback, scope, padding, span):
        """
        Method to calculate the df_pre by the range extrapolation method.  This uses the provided scope
        and the instantaneous depth values to calculate the range, and then assuming a bearing of 180 degrees
        (straight behind) the vessel, calculates the gear position using the vessel lat / lon and the gear range / bearing
        :param df_vessel: pandas DataFrame - contains the vessel track
        :param begin_tow: arrow Date/Time - when the tow started
        :param start_haulback: arrow Date/Time - when the haulback was started
        :param span: int -
        :return:
        """
        # Mask and pad the df_vessel track.  Padding use to buffer the running mean calculation only
        mask = (df_vessel["times"] >= doors_fully_out) & (df_vessel["times"] < start_haulback)
        df_pre = df_vessel.loc[mask]
        start = df_vessel.index.get_loc(df_pre.head(1).index.tolist()[0]) #- padding  #- 1  # Subtract an extra for waypoint transfer later on
        end = df_vessel.index.get_loc(df_pre.tail(1).index.tolist()[0]) + padding   #+ 1
        df_pre = df_vessel.iloc[start:end].copy(deep=True)

        # Calculate the Pre-Haulback Range
        df_pre.loc[:, "range"] = df_pre.apply(lambda x: math.sqrt(scope ** 2 - x["depth"] ** 2) if scope > x["depth"] else None, axis=1)

        # Calculate the gear_lat and gear_lon
        df_pre["gear_lat"] = df_pre.apply(lambda x: self._functions.get_lat(lat=x["latitude"],
                                                                            lon=x["longitude"],
                                                                            bearing=x["bearing"],
                                                                            range=x["range"]), axis=1)
        df_pre["gear_lon"] = df_pre.apply(lambda x: self._functions.get_lon(lat=x["latitude"],
                                                                            lon=x["longitude"],
                                                                            bearing=x["bearing"],
                                                                            range=x["range"]), axis=1)

        # Smooth the df_pre line with a running mean
        df_pre.loc[:, ["gear_lat", "gear_lon"]] = df_pre.loc[:, ["gear_lat", "gear_lon"]].rolling(window=span,
                                                                                                  center=True).mean()

        return df_pre

    def calculate_catenary(self, depth, range, initial):
        """
        Calculate catenary a (openness) value when provided the depth and range, but no scope
        :param depth:
        :param range:
        :param initial:
        :return:
        """
        def f(a):
            return (depth + a) / a - np.cosh(range / (2 * a))

        solution = self._functions.newton(f, initial)
        return solution

    def create_gearline_by_catenary(self, df_vessel, bearing_type, doors_fully_out, begin_tow, start_haulback,
                                    net_off_bottom, doors_at_surface, padding, scope, span):
        """
        This method creates a gear trackline using a pure catenary method.  The basic method is as follows:

        Range Calculation - done using catenary with the current depth
        Bearing Calculation - calculate the bearing via one of two methods:
            a. Get vessel start/end points, and calculate an overall bearing to be used by the catenary
            b. Get vessel bearing and create a running mean to smooth it out and use this for the catenary
        :param df_vessel:
        :param scope:
        :param span:
        :return:
        """
        # Get the df_gear
        mask = (df_vessel["times"] >= doors_fully_out) & (df_vessel["times"] <= doors_at_surface)
        df_gear = df_vessel.loc[mask]

        # Bearing Method A. Vessel start/end points to calculate overall bearing
        if bearing_type == "vessel_gcd":

            mask = (df_vessel["times"] >= begin_tow) & (df_vessel["times"] <= net_off_bottom)
            df_masked_vessel = df_vessel.loc[mask]
            vessel_start = df_masked_vessel.head(1).iloc[0]
            vessel_end = df_masked_vessel.tail(1).iloc[0]

            distance, bearing = self._functions.get_distance_bearing(lat1=vessel_start["latitude"], lon1=vessel_start["longitude"],
                                                                  lat2=vessel_end["latitude"], lon2=vessel_end["longitude"])
            df_gear.loc[:, "bearing"] = bearing - 180 if bearing-180 >= 0 else bearing + 180

        # Bearing Method B. Smoothed vessel bearing
        elif bearing_type == "vessel_smoothed":
            df_gear["bearing"] = df_gear.apply(lambda x: (x["track made good"] - 180)   # Calculate the net bearing from vessel heading
                            if (x["track made good"] - 180) >= 0 else x["track made good"] + 180, axis=1)    # Create Bearing
            df_gear.loc[:, "bearing"] = df_gear.loc[:, "bearing"].rolling(window=span).mean()

        # Bearing Method C. Point-wise bearing, no smoothingn
        elif bearing_type == "standard":
            df_gear["bearing"] = df_gear.apply(lambda x: (x["track made good"] - 180)   # Calculate the net bearing from vessel heading
                            if (x["track made good"] - 180) >= 0 else x["track made good"] + 180, axis=1)    # Create Bearing

        # Bearing Method D. Large Smoothing from Vessel Position to Gear Position.  Take a span of the number of points
        # between where the vessel is at begin_tow and trace backwards by the initial scope size, achieved by going back
        # the number of vessel track points whose segment distance sum equals the size of the initial scope
        elif bearing_type == "vessel_scope_mean":

            df_vessel_dist = df_vessel.copy(deep=True)
            # df_vessel_dist.loc[:, ["latitude", "longitude"]] = df_vessel_dist.loc[:, ["latitude", "longitude"]]\
            #     .rolling(window=span).mean()
            df_vessel_dist["latitude2"] = df_vessel_dist["latitude"].shift()
            df_vessel_dist["longitude2"] = df_vessel_dist["longitude"].shift()
            df_vessel_dist["dist"] = df_vessel_dist.apply(lambda x: self._functions.get_distance(x["latitude"], x["longitude"],
                                                                                   x["latitude2"], x["longitude2"]), axis=1)
            mask = (df_vessel_dist["times"] <= begin_tow)
            df_vessel_dist = df_vessel_dist.loc[mask]
            df_vessel_dist["sum"] = df_vessel_dist.iloc[::-1].loc[:, "dist"].cumsum()[::-1]
            mask = (df_vessel_dist["sum"] <= scope)
            df_vessel_dist = df_vessel_dist.loc[mask]

            vessel_scope_span = len(df_vessel_dist)
            logging.info(f"begin_tow: {begin_tow}, scope: {scope}")
            logging.info(f"number of points to include: {vessel_scope_span}")

            df_vessel.loc[:, "smoothed heading"] = df_vessel.loc[:, "track made good"].rolling(window=vessel_scope_span).mean()
            df_gear = df_gear.assign(smoothed_heading=df_vessel["smoothed heading"])
            df_gear["bearing"] = df_gear.apply(lambda x: (x["smoothed_heading"] - 180)
                                                 if x["smoothed_heading"] - 180 >= 0 else x["smoothed_heading"]+180, axis=1)

        # Begin the catenary calculation.  Find the shape factor, a, of the catenary at begin_tow with the
        # starting depth and starting scope
        mask = (df_gear["times"] == begin_tow)
        depth = df_gear.loc[mask].iloc[0]["depth"]

        # initial_guess = 100
        # a = self.calculate_catenary_missing_range(depth=depth, scope=scope, initial=initial_guess)
        # distance = a * np.arccosh((depth+a) / a)
        # logging.info(f"OLD:  depth: {depth}, scope: {scope}, a: {a}, distance: {distance}")

        a = (scope**2 - depth**2)/(2*depth)
        scope_0 = math.sqrt(depth**2 + 2*depth*a)
        distance = a * math.log((scope_0 / a) + math.sqrt((scope_0/a)**2 + 1))



        # Now start moving the net along the bottom, but the range will stay constant as the scope isn't changing
        # Do this until start_haulback happens
        df_gear.loc[df_gear["times"] < start_haulback, "range"] = distance

        # At start_haulback, start pulling in the scope, and thus calculate the new range from it.  This requires
        # tightening up the shape factor, a, until we hit the net off bottom point
        post_haulback_size = len(df_gear.loc[df_gear["times"] >= start_haulback])

        # df_gear.loc[df_gear["times"] >= start_haulback, "scope"] = [scope - i for i in range(post_haulback_size)]
        # df_gear.loc[df_gear["times"] >= start_haulback, "a"] = df_gear.loc[df_gear["times"] >= start_haulback] \
        #     .apply(lambda x: (x["scope"]**2 - x["depth"]**2) / (2*x["depth"]), axis=1)

        df_gear.loc[df_gear["times"] >= start_haulback, "a"] = [a - i*2 for i in range(post_haulback_size)]


        df_gear.loc[df_gear["times"] >= start_haulback, "scope"] = \
            df_gear.loc[df_gear["times"] >= start_haulback] \
                .apply(lambda x: math.sqrt(x["depth"]**2 + 2*x["depth"]*x["a"]) if x["a"] > 0 else None, axis=1)

        df_gear.loc[df_gear["times"] >= start_haulback, "range"] = \
            df_gear.loc[df_gear["times"] >= start_haulback]\
                .apply(lambda x: x["a"] * math.log((x["scope"]/x["a"]) + math.sqrt((x["scope"]/x["a"])**2 + 1))
                                    if not math.isnan(x["a"]) else None, axis=1)

        # df_gear.loc[df_gear["times"] >= start_haulback, "range"] = \
        #     df_gear.loc[df_gear["times"] >= start_haulback]\
        #         .apply(lambda x: x["a"] * np.arccosh((x["depth"] + x["a"]) / x["a"]) if a > 0 else 0, axis=1)

        logging.info(f"df_gear: {df_gear.loc[:,['times', 'depth', 'a', 'scope', 'range']]}")


        # Determine where the vessel speed starts decreasing and then levels out
        # df_post["speed_mean"] = df_post["speed over ground"].rolling(window=span, center=True).mean()
        # df_post["speed_change"] = df_post["speed_mean"].pct_change()
        # df_post.loc[:, "range"] = None
        # mask = (df_post["speed_change"].abs() >= SPEED_CHANGE_PCT)
        # df_change = df_post.loc[mask]



        # Calculate the gear_lat and gear_lon with the newly generated bearing and range information
        df_gear["gear_lat"] = df_gear.apply(lambda x: self._functions.get_lat(lat=x["latitude"],
                                                                              lon=x["longitude"],
                                                                              bearing=x["bearing"],
                                                                              range=x["range"]), axis=1)
        df_gear["gear_lon"] = df_gear.apply(lambda x: self._functions.get_lon(lat=x["latitude"],
                                                                              lon=x["longitude"],
                                                                              bearing=x["bearing"],
                                                                              range=x["range"]), axis=1)

        # Smooth the df_post line using a running mean
        df_gear.loc[:, ["gear_lat", "gear_lon"]] = df_gear.loc[:, ["gear_lat", "gear_lon"]].rolling(window=span,
                                                                                                    center=True).mean()

        # path = os.path.expanduser("~/Desktop/df_catenary.csv")
        # df_gear.loc[:, ['times', 'depth', 'scope', 'a', 'range', 'speed over ground', 'track made good', 'smoothed_heading', 'bearing', 'gear_lat', 'gear_lon']].to_csv(path)

        # path = r"C:\Users\Todd.Hay\Desktop\depth.csv"
        # df_vessel.loc[:, ['times', 'latitude', 'longitude', 'scope', 'depth', 'range', 'track made good', 'bearing']].to_csv(path)


        logging.info(f"BT: {begin_tow}, SH: {start_haulback}, NOB: {net_off_bottom}")
        # logging.info(f"{df_gear.loc[1618:1699, ['times', 'depth', 'range', 'a', 'gear_lat', 'gear_lon']]}")

        return df_gear

    def create_posthaulback_gearline_by_catenary(self, depth, range, df_trig, df_post, scope, span, padding):
        """
        Method to calculate the posthaulback gearline using a combination of the catengary equation and the trig method

        In this scenario, there are twp\o segments of the post haulback gear line to include:

        --------------------------------------------------------------------------------------------------------
        | Segment       | Description               |   Start           | End           |

        - Slack To Taut - it is assumed that there is some slack in the tow line that must first be pulled taut for the
                            net to then start moving
        - Taut

        :param depth: depth at the end of Pre Haulback - df_pre
        :param range: range at the end of Pre Haulback - df_pre
        :param df_trig:
        :param df_post:
        :param scope:
        :param span:
        :return:
        """
        if df_trig is None or df_post is None:
            return

        # Gather static values for the trig method
        time_diff = (df_trig["times"].iloc[-1] - df_trig["times"].iloc[0+padding]).seconds  # Start Haulback to Doors At Surface
        ratio = scope / time_diff                                                   # Ratio of Scope to Time Diff
        trig_end_time = df_trig["times"].iloc[-1]                                   # Time at Doors At Surface

        # Calculate and assign the scope
        df_trig.loc[:, "scope"] = df_trig.apply(lambda x: (trig_end_time - x["times"]).seconds * ratio, axis=1)
        df_post = df_post.assign(trig_scope=df_trig["scope"])

        # Determine where the vessel speed starts decreasing and then levels out
        # df_speed = df_post.copy(deep=True)
        df_post["speed_mean"] = df_post["speed over ground"].rolling(window=span, center=True).mean()
        df_post["speed_change"] = df_post["speed_mean"].pct_change()
        df_post.loc[:, "range"] = None

        # logging.info(f"speed change: {df_post.loc[:,['times', 'speed over ground', 'speed_mean', 'speed_change']]}")
        mask = (df_post["speed_change"].abs() >= SPEED_CHANGE_PCT)
        df_change = df_post.loc[mask]
        if df_change.empty:
            return None

        start = df_post.index.get_loc(df_change.head(1).index.tolist()[0])
        end = df_post.index.get_loc(df_change.tail(1).index.tolist()[0])
        df_slack = df_post.iloc[start:end].copy(deep=True)

        # Calculate thhe range using the catenary method
        initial_guess = 100
        a = self.calculate_catenary(depth=depth, range=range, initial=initial_guess)
        df_post.loc[:, "range"] = df_post.apply(lambda x: a * math.acosh((x["depth"] + a) / a), axis=1)

        df_slack.loc[:, "range"] = df_slack.apply(lambda x: a * math.acosh((x["depth"] + a) / a), axis=1)

        # Calculate the range from the scope + depth using pythagorean theorem, i.e  Trig Method
        df_taut = df_post.iloc[end+1:].copy(deep=True)
        df_taut.loc[:, "range"] = \
            df_taut.apply(lambda x: math.sqrt(x["trig_scope"] ** 2 - x["depth"] ** 2) if x["trig_scope"] > x["depth"] else None, axis=1)
        df_slack = df_slack.append(df_taut)

        # Calculate the gear_lat and gear_lon
        df_post["gear_lat"] = df_post.apply(lambda x: self._functions.get_lat(lat=x["latitude"],
                                                                              lon=x["longitude"],
                                                                              bearing=x["bearing"],
                                                                              range=x["range"]), axis=1)
        df_post["gear_lon"] = df_post.apply(lambda x: self._functions.get_lon(lat=x["latitude"],
                                                                              lon=x["longitude"],
                                                                              bearing=x["bearing"],
                                                                              range=x["range"]), axis=1)

        # Smooth the df_post line using a running mean
        df_post.loc[:, ["gear_lat", "gear_lon"]] = df_post.loc[:, ["gear_lat", "gear_lon"]].rolling(window=span,
                                                                                                    center=True).mean()

        # logging.info(f"df_post: {df_post.loc[:, ['times', 'scope', 'depth', 'range']]}")
        # logging.info(f"First + Last rows of df_post:\n{pd.concat([df_post['times'].head(3), df_post['times'].tail(3)])}")

        return df_post
