Skip to content

Utility functions

AngleAnnotation

Bases: Arc

Draws an arc between two vectors which appears circular in display space. Implementation of class Arc

Source code in a2gUtils.py
class AngleAnnotation(Arc):
    """
    Draws an arc between two vectors which appears circular in display space. Implementation
    of class Arc
    """
    def __init__(self, xy, p1, p2, size=75, unit="points", ax=None,
                 text="", textposition="inside", text_kw=None, **kwargs):
        """
        Parameters
        ----------
        xy, p1, p2 : tuple or array of two floats
            Center position and two points. Angle annotation is drawn between
            the two vectors connecting *p1* and *p2* with *xy*, respectively.
            Units are data coordinates.

        size : float
            Diameter of the angle annotation in units specified by *unit*.

        unit : str
            One of the following strings to specify the unit of *size*:

            * "pixels": pixels
            * "points": points, use points instead of pixels to not have a
              dependence on the DPI
            * "axes width", "axes height": relative units of Axes width, height
            * "axes min", "axes max": minimum or maximum of relative Axes
              width, height

        ax : `matplotlib.axes.Axes`
            The Axes to add the angle annotation to.

        text : str
            The text to mark the angle with.

        textposition : {"inside", "outside", "edge"}
            Whether to show the text in- or outside the arc. "edge" can be used
            for custom positions anchored at the arc's edge.

        text_kw : dict
            Dictionary of arguments passed to the Annotation.

        **kwargs
            Further parameters are passed to `matplotlib.patches.Arc`. Use this
            to specify, color, linewidth etc. of the arc.

        """
        self.ax = ax or plt.gca()
        self._xydata = xy  # in data coordinates
        self.vec1 = p1
        self.vec2 = p2
        self.size = size
        self.unit = unit
        self.textposition = textposition

        super().__init__(self._xydata, size, size, angle=0.0,
                         theta1=self.theta1, theta2=self.theta2, **kwargs)

        self.set_transform(IdentityTransform())

        if self.ax.patches:
            #for i in self.ax.patches:
            #    i.remove()
            if len(self.ax.patches) > 1:
                self.ax.patches[0].remove()

        if self.ax.texts:
            #for i in range(len(self.ax.texts)-1):
            #    if isinstance(i, matplotlib.text.Annotation):
            #        i.remove()            
            if len(self.ax.texts) > 4:
                for i in self.ax.texts[3:-1]:
                    i.remove()

        self.ax.add_patch(self)

        self.kw = dict(ha="center", va="center",
                       xycoords=IdentityTransform(),
                       xytext=(0, 0), textcoords="offset points",
                       annotation_clip=True)
        self.kw.update(text_kw or {})
        self.text = ax.annotate(text, xy=self._center, **self.kw)

    def get_size(self):
        factor = 1.
        if self.unit == "points":
            factor = self.ax.figure.dpi / 72.
        elif self.unit[:4] == "axes":
            b = TransformedBbox(Bbox.unit(), self.ax.transAxes)
            dic = {"max": max(b.width, b.height),
                   "min": min(b.width, b.height),
                   "width": b.width, "height": b.height}
            factor = dic[self.unit[5:]]
        return self.size * factor

    def set_size(self, size):
        self.size = size

    def get_center_in_pixels(self):
        """return center in pixels"""
        return self.ax.transData.transform(self._xydata)

    def set_center(self, xy):
        """set center in data coordinates"""
        self._xydata = xy

    def get_theta(self, vec):
        vec_in_pixels = self.ax.transData.transform(vec) - self._center
        return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0]))

    def get_theta1(self):
        return self.get_theta(self.vec1)

    def get_theta2(self):
        return self.get_theta(self.vec2)

    def set_theta(self, angle):
        pass

    # Redefine attributes of the Arc to always give values in pixel space
    _center = property(get_center_in_pixels, set_center)
    theta1 = property(get_theta1, set_theta)
    theta2 = property(get_theta2, set_theta)
    width = property(get_size, set_size)
    height = property(get_size, set_size)

    # The following two methods are needed to update the text position.
    def draw(self, renderer):
        self.update_text()
        super().draw(renderer)

    def update_text(self):
        c = self._center
        s = self.get_size()
        angle_span = (self.theta2 - self.theta1) % 360
        angle = np.deg2rad(self.theta1 + angle_span / 2)
        r = s / 2
        if self.textposition == "inside":
            r = s / np.interp(angle_span, [60, 90, 135, 180],
                                          [3.3, 3.5, 3.8, 4])
        self.text.xy = c + r * np.array([np.cos(angle), np.sin(angle)])
        if self.textposition == "outside":
            def R90(a, r, w, h):
                if a < np.arctan(h/2/(r+w/2)):
                    return np.sqrt((r+w/2)**2 + (np.tan(a)*(r+w/2))**2)
                else:
                    c = np.sqrt((w/2)**2+(h/2)**2)
                    T = np.arcsin(c * np.cos(np.pi/2 - a + np.arcsin(h/2/c))/r)
                    xy = r * np.array([np.cos(a + T), np.sin(a + T)])
                    xy += np.array([w/2, h/2])
                    return np.sqrt(np.sum(xy**2))

            def R(a, r, w, h):
                aa = (a % (np.pi/4))*((a % (np.pi/2)) <= np.pi/4) + \
                     (np.pi/4 - (a % (np.pi/4)))*((a % (np.pi/2)) >= np.pi/4)
                return R90(aa, r, *[w, h][::int(np.sign(np.cos(2*a)))])

            bbox = self.text.get_window_extent()
            X = R(angle, r, bbox.width, bbox.height)
            trans = self.ax.figure.dpi_scale_trans.inverted()
            offs = trans.transform(((X-s/2), 0))[0] * 72
            self.text.set_position([offs*np.cos(angle), offs*np.sin(angle)])

__init__(xy, p1, p2, size=75, unit='points', ax=None, text='', textposition='inside', text_kw=None, **kwargs)

Parameters

xy, p1, p2 : tuple or array of two floats Center position and two points. Angle annotation is drawn between the two vectors connecting p1 and p2 with xy, respectively. Units are data coordinates.

float

Diameter of the angle annotation in units specified by unit.

str

One of the following strings to specify the unit of size:

  • "pixels": pixels
  • "points": points, use points instead of pixels to not have a dependence on the DPI
  • "axes width", "axes height": relative units of Axes width, height
  • "axes min", "axes max": minimum or maximum of relative Axes width, height
matplotlib.axes.Axes

The Axes to add the angle annotation to.

str

The text to mark the angle with.

{"inside", "outside", "edge"}

Whether to show the text in- or outside the arc. "edge" can be used for custom positions anchored at the arc's edge.

dict

Dictionary of arguments passed to the Annotation.

**kwargs Further parameters are passed to matplotlib.patches.Arc. Use this to specify, color, linewidth etc. of the arc.

Source code in a2gUtils.py
def __init__(self, xy, p1, p2, size=75, unit="points", ax=None,
             text="", textposition="inside", text_kw=None, **kwargs):
    """
    Parameters
    ----------
    xy, p1, p2 : tuple or array of two floats
        Center position and two points. Angle annotation is drawn between
        the two vectors connecting *p1* and *p2* with *xy*, respectively.
        Units are data coordinates.

    size : float
        Diameter of the angle annotation in units specified by *unit*.

    unit : str
        One of the following strings to specify the unit of *size*:

        * "pixels": pixels
        * "points": points, use points instead of pixels to not have a
          dependence on the DPI
        * "axes width", "axes height": relative units of Axes width, height
        * "axes min", "axes max": minimum or maximum of relative Axes
          width, height

    ax : `matplotlib.axes.Axes`
        The Axes to add the angle annotation to.

    text : str
        The text to mark the angle with.

    textposition : {"inside", "outside", "edge"}
        Whether to show the text in- or outside the arc. "edge" can be used
        for custom positions anchored at the arc's edge.

    text_kw : dict
        Dictionary of arguments passed to the Annotation.

    **kwargs
        Further parameters are passed to `matplotlib.patches.Arc`. Use this
        to specify, color, linewidth etc. of the arc.

    """
    self.ax = ax or plt.gca()
    self._xydata = xy  # in data coordinates
    self.vec1 = p1
    self.vec2 = p2
    self.size = size
    self.unit = unit
    self.textposition = textposition

    super().__init__(self._xydata, size, size, angle=0.0,
                     theta1=self.theta1, theta2=self.theta2, **kwargs)

    self.set_transform(IdentityTransform())

    if self.ax.patches:
        #for i in self.ax.patches:
        #    i.remove()
        if len(self.ax.patches) > 1:
            self.ax.patches[0].remove()

    if self.ax.texts:
        #for i in range(len(self.ax.texts)-1):
        #    if isinstance(i, matplotlib.text.Annotation):
        #        i.remove()            
        if len(self.ax.texts) > 4:
            for i in self.ax.texts[3:-1]:
                i.remove()

    self.ax.add_patch(self)

    self.kw = dict(ha="center", va="center",
                   xycoords=IdentityTransform(),
                   xytext=(0, 0), textcoords="offset points",
                   annotation_clip=True)
    self.kw.update(text_kw or {})
    self.text = ax.annotate(text, xy=self._center, **self.kw)

get_center_in_pixels()

return center in pixels

Source code in a2gUtils.py
def get_center_in_pixels(self):
    """return center in pixels"""
    return self.ax.transData.transform(self._xydata)

set_center(xy)

set center in data coordinates

Source code in a2gUtils.py
def set_center(self, xy):
    """set center in data coordinates"""
    self._xydata = xy

GpsOnMap

Bases: object

Source code in a2gUtils.py
class GpsOnMap(object):

    def __init__(self, path_to_osmpbf, canvas=None,fig=None, ax=None, air_coord=None, gnd_coord=None):
        """
        This is a handler for the canvas element where gps coords are plot

        It requires an .osm.pbf picture of the map get from https://extract.bbbike.org/

        Args:
            path_to_osmpbf (str): path to .osm.pbf file
            canvas (widget, optional): canvas widget from app. Defaults to None.
            fig (fig, optional): _description_. Defaults to None.
            ax (ax, optional): _description_. Defaults to None.
            air_coord (dictionary, optional): the keys of the dictionary should be "LAT" and "LON". Defaults to None.
            gnd_coord (dictionary, optional): the keys of the dictionary should be "LAT" and "LON". Defaults to None.
        """

        #plt.rcParams['animation.html'] = 'jshtml'
        if ax is None and fig is None:
            fig, ax = plt.subplots()
        elif ax is not None and fig is None:
            print('\n[DEBUG]: figure handle not provided')
        elif fig is not None and ax is None:
            print('\n[DEBUG]: axes handle not provided')

        self.ax = ax
        self.fig = fig
        self.canvas = canvas

        # Initialize the OSM parser object
        osm = OSM(path_to_osmpbf)

        # Plot cycling, driving and walking layers, and also buildings
        cycling_net = osm.get_network(network_type="cycling")
        drive_net = osm.get_network(network_type="driving")
        walking_net = osm.get_network(network_type="walking")
        buildings = osm.get_buildings()

        cycling_net.plot(ax=self.ax)
        buildings.plot(ax=self.ax)
        walking_net.plot(ax=self.ax)

        self.air_coord = air_coord

        if air_coord is not None:
            self.air_pos, =self.ax.plot(air_coord['LON'], air_coord['LAT'], 'b+', markersize=15)

        self.test_cnt = 1
        self.fut_hub = {'LAT': 60.18650, 'LON': 24.81350}
        self.air_pos.set_data(self.air_coord['LON'], self.air_coord['LAT'])

        if canvas is None:
            plt.show()
        else:
            self.canvas.draw()

    def show_air_moving(self, lat, lon):
        """
        Updates the plot with the new positions
        """

        self.air_pos.set_data(lon, lat)

        if self.canvas is None:
            plt.show()
        else:
            self.canvas.draw()

__init__(path_to_osmpbf, canvas=None, fig=None, ax=None, air_coord=None, gnd_coord=None)

This is a handler for the canvas element where gps coords are plot

It requires an .osm.pbf picture of the map get from https://extract.bbbike.org/

Parameters:

Name Type Description Default
path_to_osmpbf str

path to .osm.pbf file

required
canvas widget

canvas widget from app. Defaults to None.

None
fig fig

description. Defaults to None.

None
ax ax

description. Defaults to None.

None
air_coord dictionary

the keys of the dictionary should be "LAT" and "LON". Defaults to None.

None
gnd_coord dictionary

the keys of the dictionary should be "LAT" and "LON". Defaults to None.

None
Source code in a2gUtils.py
def __init__(self, path_to_osmpbf, canvas=None,fig=None, ax=None, air_coord=None, gnd_coord=None):
    """
    This is a handler for the canvas element where gps coords are plot

    It requires an .osm.pbf picture of the map get from https://extract.bbbike.org/

    Args:
        path_to_osmpbf (str): path to .osm.pbf file
        canvas (widget, optional): canvas widget from app. Defaults to None.
        fig (fig, optional): _description_. Defaults to None.
        ax (ax, optional): _description_. Defaults to None.
        air_coord (dictionary, optional): the keys of the dictionary should be "LAT" and "LON". Defaults to None.
        gnd_coord (dictionary, optional): the keys of the dictionary should be "LAT" and "LON". Defaults to None.
    """

    #plt.rcParams['animation.html'] = 'jshtml'
    if ax is None and fig is None:
        fig, ax = plt.subplots()
    elif ax is not None and fig is None:
        print('\n[DEBUG]: figure handle not provided')
    elif fig is not None and ax is None:
        print('\n[DEBUG]: axes handle not provided')

    self.ax = ax
    self.fig = fig
    self.canvas = canvas

    # Initialize the OSM parser object
    osm = OSM(path_to_osmpbf)

    # Plot cycling, driving and walking layers, and also buildings
    cycling_net = osm.get_network(network_type="cycling")
    drive_net = osm.get_network(network_type="driving")
    walking_net = osm.get_network(network_type="walking")
    buildings = osm.get_buildings()

    cycling_net.plot(ax=self.ax)
    buildings.plot(ax=self.ax)
    walking_net.plot(ax=self.ax)

    self.air_coord = air_coord

    if air_coord is not None:
        self.air_pos, =self.ax.plot(air_coord['LON'], air_coord['LAT'], 'b+', markersize=15)

    self.test_cnt = 1
    self.fut_hub = {'LAT': 60.18650, 'LON': 24.81350}
    self.air_pos.set_data(self.air_coord['LON'], self.air_coord['LAT'])

    if canvas is None:
        plt.show()
    else:
        self.canvas.draw()

show_air_moving(lat, lon)

Updates the plot with the new positions

Source code in a2gUtils.py
def show_air_moving(self, lat, lon):
    """
    Updates the plot with the new positions
    """

    self.air_pos.set_data(lon, lat)

    if self.canvas is None:
        plt.show()
    else:
        self.canvas.draw()

azimuth_difference_between_coordinates(heading, lat_origin, lon_origin, lat_dest, lon_dest)

This is the angle difference between the heading direction (angle w.r.t the North) of the node behaving as the origin and the destination node direction (w.r.t the origin node).

The following picture provides an illustration of the angle to be computed (named here theta).

Image title
Illustration of the angle difference theta

Parameters:

Name Type Description Default
heading float

angle between [0, 2*pi] (rads) corresponding to the heading direction of the line between the two antennas connected to Septentrio's receiver in the origin node. Defaults to None.

required
lat_origin float

latitude of the origin node.

required
lon_origin float

longitude of the origin node.

required
lat_dest float

latitude of the destination node.

required
lon_dest float

longitude of the destination node.

required

Returns: yaw_to_set (int): azimuth angle difference.

Source code in a2gUtils.py
def azimuth_difference_between_coordinates(heading, lat_origin, lon_origin, lat_dest, lon_dest):
    """
    This is the angle difference between the **heading** direction (angle w.r.t the North) of the node behaving as the origin and the destination node direction (w.r.t the origin node).

    The following picture provides an illustration of the angle to be computed (named here theta).

    <figure markdown="span">
    ![Image title](assets/azimuth_difference_btw_coods.PNG){ width="400" }
    <figcaption>Illustration of the angle difference theta</figcaption>
    </figure>

    Args:
        heading (float): angle between [0, 2*pi] (rads) corresponding to the heading direction of the line between the two antennas connected to Septentrio's receiver in the origin node. Defaults to None.
        lat_origin (float): latitude of the origin node. 
        lon_origin (float): longitude of the origin node.
        lat_dest (float): latitude of the destination node.
        lon_dest (float): longitude of the destination node. 
    Returns:
        yaw_to_set (int): azimuth angle difference.
    """

    wgs84_geod = Geod(ellps='WGS84')

    ITFA, _, _ = wgs84_geod.inv(lon_origin, lat_origin, lon_dest, lat_dest)

    # Restrict heading to [-pi, pi] interval. No need for < -2*pi check, cause it won't happen
    if heading > 180:
        heading = heading - 360

    yaw_to_set = ITFA - heading

    if yaw_to_set > 180:
        yaw_to_set = yaw_to_set - 360
    elif yaw_to_set < -180:
        yaw_to_set = yaw_to_set + 360

    yaw_to_set = int(yaw_to_set*10)

    return yaw_to_set

compute_block_mean_2d_array(array, block_length)

Compute the block mean of a matrix by assuming the matrix consists of blocks of size block_length-by-array.shape[1].

'array' should be a matrix, and 'block_length' should be less than array.shape[1].

Parameters:

Name Type Description Default
array ndarray

description

required
block_length int

description

required
Source code in a2gUtils.py
def compute_block_mean_2d_array(array, block_length):
    """
    Compute the block mean of a matrix by assuming the matrix consists of blocks of size
    block_length-by-array.shape[1].

    'array' should be a matrix, and 'block_length' should be less than array.shape[1].

    Args:
        array (ndarray): _description_
        block_length (int): _description_
    """

    tmp = array.reshape((-1, block_length, array.shape[1]//array.shape[1], array.shape[1]))
    tmp = tmp.transpose((0,2,1,3))
    tmp = np.mean(tmp, axis=(2,))
    tmp = np.squeeze(tmp)
    return tmp      

convert_dB_to_valid_hex_sivers_register_values(rx_bb_gain_1, rx_bb_gain_2, rx_bb_gain_3, rx_bfrf_gain, tx_bb_gain, tx_bb_iq_gain, tx_bb_phase, tx_bfrf_gain)

Converts the dB gain values (all of them) the user has input in the "Sivers settings" panel to the actual values required for the Sivers EVK registers.

Returns:

Name Type Description
tx_signal_values dict

dictionary with the Tx gain values to be set at the Tx Sivers EVK registers.

rx_signal_values dict

dictionary with the Rx gain values to be set at the RX Sivers EVK registers.

Source code in a2gUtils.py
def convert_dB_to_valid_hex_sivers_register_values(rx_bb_gain_1, rx_bb_gain_2, rx_bb_gain_3, rx_bfrf_gain, tx_bb_gain, tx_bb_iq_gain, tx_bb_phase, tx_bfrf_gain):
        """
        Converts the dB gain values (all of them) the user has input in the "Sivers settings" panel to the actual values required for the Sivers EVK registers.

        Returns:
            tx_signal_values (dict): dictionary with the Tx gain values to be set at the Tx Sivers EVK registers.
            rx_signal_values (dict): dictionary with the Rx gain values to be set at the RX Sivers EVK registers.
        """

        rxbb1 = float(rx_bb_gain_1)
        rxbb2 = float(rx_bb_gain_2)
        rxbb3 = float(rx_bb_gain_3)
        rxbfrf = float(rx_bfrf_gain)

        txbb = tx_bb_gain
        txbbiq = float(tx_bb_iq_gain)
        txbbphase = tx_bb_phase
        txbf = float(tx_bfrf_gain)

        valid_values_rx_bb = [0x00, 0x11, 0x33, 0x77, 0xFF]
        valid_values_rx_bb_dB = np.linspace(-6, 0, 5)

        valid_values_rx_bb3_bf = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]
        valid_values_rx_bb3_dB = np.linspace(0,6,16)
        valid_values_rx_bf_dB = np.linspace(0,15,16)

        valid_values_tx_bbiq_bf = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]
        valid_values_tx_bbiq_dB = np.linspace(0,6,16)
        valid_values_tx_bf_dB = np.linspace(0,15,16)        

        tx_signal_values = {'tx_bb_gain':int(txbb,16), 
                            'tx_bb_iq_gain':valid_values_tx_bbiq_bf[np.abs(txbbiq - valid_values_tx_bbiq_dB).argmin()],
                            'tx_bb_phase':int(txbbphase,16), 
                            'tx_bfrf_gain':valid_values_tx_bbiq_bf[np.abs(txbf - valid_values_tx_bf_dB).argmin()]}

        rx_signal_values = {'rx_gain_ctrl_bb1': valid_values_rx_bb[np.abs(rxbb1 - valid_values_rx_bb_dB).argmin()],
                'rx_gain_ctrl_bb2': valid_values_rx_bb[np.abs(rxbb2 - valid_values_rx_bb_dB).argmin()],
                'rx_gain_ctrl_bb3': valid_values_rx_bb3_bf[np.abs(rxbb3 - valid_values_rx_bb3_dB).argmin()],
                'rx_gain_ctrl_bfrf': valid_values_rx_bb3_bf[np.abs(rxbfrf - valid_values_rx_bf_dB).argmin()]}

        return tx_signal_values, rx_signal_values

elevation_difference_between_coordinates(lat_origin, lon_origin, h_origin, lat_dest, lon_dest, h_dest)

Elevation angle difference between the origin node and the destination node.

The following picture provides an illustration of the angle to be computed (named here theta).

Image title
Illustration of the angle difference phi

Parameters:

Name Type Description Default
lat_origin float

latitude of the origin node.

required
lon_origin float

longitude of the origin node.

required
h_origin float

height of the origin node.

required
lat_dest float

latitude of the destination node.

required
lon_dest float

longitude of the destination node.

required
h_dest float

height of the destination node.

required

Returns: pitch_to_set (int): elevation angle difference.

Source code in a2gUtils.py
def elevation_difference_between_coordinates(lat_origin, lon_origin, h_origin, lat_dest, lon_dest, h_dest):
    """
    Elevation angle difference between the origin node and the destination node.

    The following picture provides an illustration of the angle to be computed (named here theta).

    <figure markdown="span">
    ![Image title](assets/elevation_difference_btw_coods.PNG){ width="400" }
    <figcaption>Illustration of the angle difference phi</figcaption>
    </figure>

    Args:
        lat_origin (float): latitude of the origin node. 
        lon_origin (float): longitude of the origin node.
        h_origin (float): height of the origin node.
        lat_dest (float): latitude of the destination node.
        lon_dest (float): longitude of the destination node. 
        h_dest (float): height of the destination node.
    Returns:
        pitch_to_set (int): elevation angle difference.
    """

    wgs84_geod = Geod(ellps='WGS84')

    # dist_proj_2D is the distance between the origin node and the projection of the destination node to the plane at the height of the origin node.
    _, _, dist_proj_2D = wgs84_geod.inv(lon_origin, lat_origin, lon_dest, lat_dest)

    pitch_to_set = np.arctan2(h_dest - h_origin, dist_proj_2D) 
    pitch_to_set = int(np.rad2deg(pitch_to_set)*10)

    return pitch_to_set

geocentric2geodetic(X, Y, Z, EPSG_GEODETIC=4979, EPSG_GEOCENTRIC=4978)

Given Geocentric coordinates referred to a datum (given by EPSG_GEOCENTRIC), convert them to Geodetic (lat, lon, height) in the datum given by EPSG_GEODETIC.

Parameters:

Name Type Description Default
X float

geocentric X coordinate.

required
Y float

geocentric Y coordinate.

required
Z float

geocentric Z coordinate.

required
EPSG_GEODETIC int

description. Defaults to 4979, that corresponds to WSG84 (geodetic)

4979
EPSG_GEOCENTRIC int

description. Defaults to 4978, that corresponds to WSG84 (geocentric).

4978
Source code in a2gUtils.py
def geocentric2geodetic(X, Y, Z, EPSG_GEODETIC=4979, EPSG_GEOCENTRIC=4978):
    """
    Given Geocentric coordinates referred to a datum (given by EPSG_GEOCENTRIC), convert them
    to Geodetic (lat, lon, height) in the datum given by EPSG_GEODETIC.    

    Args:
        X (float): geocentric X coordinate. 
        Y (float): geocentric Y coordinate.
        Z (float): geocentric Z coordinate.
        EPSG_GEODETIC (int, optional): _description_. Defaults to 4979, that corresponds to WSG84 (geodetic)
        EPSG_GEOCENTRIC (int, optional): _description_. Defaults to 4978, that corresponds to WSG84 (geocentric).
    """

    geodet_crs = CRS.from_epsg(4979) # Geodetic (lat,lon,h) system
    geocent_crs = CRS.from_epsg(4978) # Geocentric (X,Y,Z) system

    geocent_to_geodet = Transformer.from_crs(geocent_crs, geodet_crs)

    lat, lon, height = geocent_to_geodet.transform(X, Y, Z)

    return lat, lon, height

geodetic2geocentric(lat, lon, height, EPSG_GEODETIC=4979, EPSG_GEOCENTRIC=4978)

Given Geodetic coordinates (lat, lon, h), convert them to Geocentric in the datum given by EPSG_GEOCENTRIC.

Parameters:

Name Type Description Default
lat float

latitude (N)

required
lon float

longitude (E).

required
height float

height in meters.

required
EPSG_GEODETIC int

description. Defaults to 4979, that corresponds to WSG84 (geodetic)

4979
EPSG_GEOCENTRIC int

description. Defaults to 4978, that corresponds to WSG84 (geocentric).

4978
Source code in a2gUtils.py
def geodetic2geocentric(lat, lon, height, EPSG_GEODETIC=4979, EPSG_GEOCENTRIC=4978):
    """
    Given Geodetic coordinates (lat, lon, h), convert them 
    to Geocentric  in the datum given by EPSG_GEOCENTRIC.    

    Args:
        lat (float): latitude (N)
        lon (float): longitude (E).
        height (float): height in meters.
        EPSG_GEODETIC (int, optional): _description_. Defaults to 4979, that corresponds to WSG84 (geodetic)
        EPSG_GEOCENTRIC (int, optional): _description_. Defaults to 4978, that corresponds to WSG84 (geocentric).
    """

    geodet_crs = CRS.from_epsg(4979) # Geodetic (lat,lon,h) system
    geocent_crs = CRS.from_epsg(4978) # Geocentric (X,Y,Z) system

    geodet_to_geocent = Transformer.from_crs(geodet_crs, geocent_crs)

    X, Y, Z = geodet_to_geocent.transform(lat, lon, height)

    return X, Y, Z

make_flight_graph_coordinates(flight_graph, number_stops_per_edge)

Calculates the intermediate coordinates for the flight graph provided with the given number of stops per edge.

Parameters:

Name Type Description Default
flight_graph numpy 2d-array

the provided array rows MUST be ordered according to the order of the planned stops of the drone. For example: 1st row corresponds to the first stop of the drone, 2nd row corresponds to the second stop of the drone, and so on.

required
number_stops_per_edge int

number of stops per edge. It includes both vertexes of the edge.

required

Raises:

Type Description
Exception

description

Returns:

Name Type Description
intermediate_coords dict

a dictionary whose structure is as follows: {'EDGE_1': {'LAT': [], 'LON':[]}, 'EDGE_2': {'LAT': [], 'LON':[]}, ...}

Source code in a2gUtils.py
def make_flight_graph_coordinates(flight_graph, number_stops_per_edge):
        """
        Calculates the intermediate coordinates for the flight graph provided with the given number of stops per edge.


        Args:
            flight_graph (numpy 2d-array): the provided array rows MUST be ordered according to the 
                                           order of the planned stops of the drone. For example:
                                           1st row corresponds to the first stop of the drone,
                                           2nd row corresponds to the second stop of the drone, and so on.
            number_stops_per_edge (int): number of stops per edge. It includes both vertexes of the edge.

        Raises:
            Exception: _description_

        Returns:
            intermediate_coords (dict): a dictionary whose structure is as follows:
                                        {'EDGE_1': {'LAT': [], 'LON':[]}, 'EDGE_2': {'LAT': [], 'LON':[]}, ...}
        """

        flight_graph_2nd_end = flight_graph[1:, :]
        flight_graph_1st_before_end = flight_graph[:-1, :]

        wgs84_geod = Geod(ellps='WGS84')

        az12, _, dist = wgs84_geod.inv(flight_graph_1st_before_end[:, 1], 
                                          flight_graph_1st_before_end[:, 0], 
                                          flight_graph_2nd_end[:, 1], 
                                          flight_graph_2nd_end[:, 0])

        intermediate_coords = {}
        for i in range(flight_graph_2nd_end.shape[0]):
            intermediate_coords['EDGE_'+str(i+1)] = {'LAT': [], 'LON': []}
            lon_2, lat_2, _ = wgs84_geod.fwd(flight_graph_1st_before_end[i, 1], flight_graph_1st_before_end[i, 0], az12[i], dist[i]/(number_stops_per_edge-1))
            intermediate_coords['EDGE_'+str(i+1)]['LAT'].append(lat_2)
            intermediate_coords['EDGE_'+str(i+1)]['LON'].append(lon_2)

            for j in range(number_stops_per_edge-3):
                lon_2, lat_2, _ = wgs84_geod.fwd(lon_2, lat_2, az12[i], dist[i]/(number_stops_per_edge-1))
                intermediate_coords['EDGE_'+str(i+1)]['LAT'].append(lat_2)
                intermediate_coords['EDGE_'+str(i+1)]['LON'].append(lon_2)

        return intermediate_coords