import os
import numpy as np
import rasterio as rio
import rasterio.features
import geopandas as gpd
import rasterstats

def find_underlying_vector_value(starting_objects, starting_objects_identifying_column, objects_to_select,
                                 objects_to_select_attribute_column):
    """
    Find the value of a specified attribute of a vector layer underlying another vector layer.

    - Spatial join between input GeoDataFrames
    - Create DataFrame with only desired results columns
    - Aggregate underlying vector attribute values based on overlying vector identifying values
    - Merge aggregated results column back onto original input starting GeoDataFrame

    Parameters
    ----------
    starting_objects : GeoDataFrame
        Input object(s)
    starting_objects_identifying_column : column label
        Column name that uniquely identifies starting objects
    objects_to_select : GeoDataFrame
        Selecting objects
    objects_to_select_attribute_column : column label
        Desired attribute to find

    Returns
    -------
    GeoDataFrame
        Updated GeoDataFrame
    """
    # create a new GeoDataFrame using gpd.sjoin()
    joined = gpd.sjoin(starting_objects, objects_to_select, how='inner', lsuffix='left', rsuffix='right')
    # the joined GeoDataFrame contains all columns from both input GeoDataFrames so create a new DataFrame with only
    # the columns required for the output (the two identifying columns)
    results = joined[[starting_objects_identifying_column, objects_to_select_attribute_column]].copy()
    # aggregate the results DataFrame based on the starting objects identifying column value combining the vector
    # attribute column values with a semicolon separating them
    aggregated = results.groupby(starting_objects_identifying_column).agg(
                                                        {objects_to_select_attribute_column: '; ' .join}).reset_index()
    # merge the aggregated results columns back onto the starting objects GeoDataFrame based on the shared starting
    # object identifying column
    updated = starting_objects.merge(aggregated, on=[starting_objects_identifying_column])
    return updated  # an updated version of the input GeoDataFrame

def calculate_percentage_underlying_raster_categories_for_polygons(starting_polygons,
                                                                   starting_polygons_identifying_column,
                                                                   raster, raster_category_map, desired_column_names,
                                                                   affine, nodata=0):
    """
    Calculate percentage cover values based on underlying raster values for each polygon in a GeoDataFrame.

    - Calculate zonal statistics using rasterstats function giving pixel counts for each raster value
    - Create dictionary of results assigning zonal statistics to each polygon in polygon GeoDataFrame
    - Create dictionary of raster values and desired column names
    - Assign zonal stats results to new columns in GeoDataFrame using try...except block giving a 0 value for no cover
    - Convert pixel count values to percentages

    Parameters
    ----------
    starting_polygons : GeoDataFrame
        Input polygon(s)
    starting_polygons_identifying_column : column label
        Column name that uniquely identifies starting polygons
    raster : ndarray
        Input raster - must be categorical
    raster_category_map : dict
        Input raster category map dictionary
    desired_column_names : list of str
        Desired output column names
    affine : Affine instance
        The input raster geotransform
    nodata : int, default 0
        The nodata value for the input raster

    Returns
    -------
    GeoDataFrame
        Updated GeoDataFrame
    """
    # calculate zonal statistics using rasterstats.zonal_stats() function
    zonal_stats = rasterstats.zonal_stats(starting_polygons,  # the shapefile to use
                                          raster,  # the raster to use
                                          affine=affine,  # the geotransform for the raster
                                          categorical=True,  # this function only runs on categorised data
                                          category_map=raster_category_map,  # the raster category map dictionary
                                          nodata=nodata  # the nodata value for the raster
                                          )

    # create a dictionary with the zonal results added to each polygon in the input GeoDataFrame
    polygons_dict = dict()
    for ind, row in starting_polygons.iterrows():
        polygons_dict[row[starting_polygons_identifying_column]] = zonal_stats[ind]

    # use dict and zip with the category names to create a dictionary of category names and the names for the results
    # columns
    column_dict = dict(zip(raster_category_map.values(), desired_column_names))

    # add rasterstats results to columns in GeoDataFrame
    for ind, row in starting_polygons.iterrows():  # use iterrows to iterate over each row in the GeoDataFrame
        results_data = polygons_dict[row[starting_polygons_identifying_column]]  # get the category data for row/polygon
        for category in raster_category_map.values():  # iterate over each of the category class names
            # try...except block giving a 0 value for no cover and assigning value if there is cover
            try:
                # add the category count to a new column
                starting_polygons.loc[ind, column_dict[category]] = results_data[category]
            except KeyError:
                # if category name is not present, value should be 0
                starting_polygons.loc[ind, column_dict[category]] = 0

    # convert the counts into percentages
    for ind, row in starting_polygons.iterrows():  # iterate over each row in the GeoDataFrame
        # multiply the pixel count values by 100 and divide by the sum of all cover column values
        starting_polygons.loc[ind, desired_column_names] = 100 * row[desired_column_names] / row[
                                                                                desired_column_names].sum()
    return starting_polygons  # an updated version of the input GeoDataFrame

def calculate_stat_values_underlying_raster_for_polygons(starting_polygons,
                                                         starting_polygons_identifying_column_integer,
                                                         desired_column_names, raster, affine, fill_value=0,
                                                         starting_polygons_geometry_column='geometry'):
    """
    Calculate statistics values of underlying raster for each polygon in a GeoDataFrame.

    - Create list of geometry, value pairs for polygons in GeoDataFrame
    - Rasterize vector polygons using rasterio in order to create masks for each polygon extent
    - Calculate statistics of underlying raster using masks by iterating over each polygon in GeoDataFrame
    - Add values to input polygon GeoDataFrame by creating new columns with desired column names

    Parameters
    ----------
    starting_polygons : GeoDataFrame
        Input polygon(s)
    starting_polygons_identifying_column_integer : column label
        Column name that uniquely identifies starting polygons - values in column must be of type integer and must not
        contain values equal to the fill value used with the vector geometries below
    desired_column_names : dict
        Dictionary of desired output column names - dictionary must have keys as below
                    {'mean': 'desired column name',
                    'min': 'desired column name',
                    'max': 'desired column name',
                    'range': 'desired column name',
                    'median': 'desired column name',
                    'std': 'desired column name'}
    raster : ndarray
        Input raster
    affine : Affine instance
        The input raster geotransform
    fill_value : int, default 0
        The value to use for areas not covered by the polygon geometries
    starting_polygons_geometry_column : column label, default 'geometry'
        Column name of geometry column in polygon GeoDataFrame

    Returns
    -------
    GeoDataFrame
        Updated GeoDataFrame
    """
    # get a list of geometry, value pairs
    shapes = list(zip(starting_polygons[starting_polygons_geometry_column],
                      starting_polygons[starting_polygons_identifying_column_integer]))

    # create a raster based on the vector polygons
    site_mask = rio.features.rasterize(shapes=shapes,  # the list of geometry/value pairs
                                       fill=fill_value,  # the value to use for cells not covered by any geometry
                                       out_shape=raster.shape,  # the shape of the new raster
                                       transform=affine)  # the geotransform of the new raster

    for ind, row in starting_polygons.iterrows():  # iterate over each row in the GeoDataFrame
        # calculate mean value using mask and assign to column using desired column name from column name dictionary
        starting_polygons.loc[ind, desired_column_names['mean']] = np.nanmean(
            raster[site_mask == row[starting_polygons_identifying_column_integer]])
    for ind, row in starting_polygons.iterrows():  # iterate over each row in the GeoDataFrame
        # calculate min value using mask and assign to column using desired column name from column name dictionary
        starting_polygons.loc[ind, desired_column_names['min']] = np.nanmin(
            raster[site_mask == row[starting_polygons_identifying_column_integer]])
    for ind, row in starting_polygons.iterrows():  # iterate over each row in the GeoDataFrame
        # calculate max value using mask and assign to column using desired column name from column name dictionary
        starting_polygons.loc[ind, desired_column_names['max']] = np.nanmax(
            raster[site_mask == row[starting_polygons_identifying_column_integer]])
    for ind, row in starting_polygons.iterrows():  # iterate over each row in the GeoDataFrame
        # calculate range value by finding max and min values and subtracting them using mask and assign to column using
        # desired column name from column name dictionary
        starting_polygons.loc[ind, desired_column_names['range']] = (np.nanmax(
            raster[site_mask == row[starting_polygons_identifying_column_integer]]) - np.nanmin(
            raster[site_mask == row[starting_polygons_identifying_column_integer]]))
    for ind, row in starting_polygons.iterrows():  # iterate over each row in the GeoDataFrame
        # calculate median value using mask and assign to column using desired column name from column name dictionary
        starting_polygons.loc[ind, desired_column_names['median']] = np.nanmedian(
            raster[site_mask == row[starting_polygons_identifying_column_integer]])
    for ind, row in starting_polygons.iterrows():  # iterate over each row in the GeoDataFrame
        # calculate standard deviation value using mask and assign to column using desired column name from column name
        # dictionary
        starting_polygons.loc[ind, desired_column_names['std']] = np.nanstd(
            raster[site_mask == row[starting_polygons_identifying_column_integer]])
    return starting_polygons  # an updated version of the input GeoDataFrame

# load the input shapefile datasets from the data_files folder using gpd.read_file(os.path.abspath())
sites = gpd.read_file(os.path.abspath('data_files/Site_Locations.shp'))
counties = gpd.read_file(os.path.abspath('data_files/Counties.shp'))
LGDs = gpd.read_file(os.path.abspath('data_files/Local_Government_Districts.shp'))

# transform data files to Northern Ireland (NI) Universal Transverse Mercator zone (UTM(29) which has an epsg of 32629)
# which will give measurements in metres using gdf.to_crs() - see here for a list of EPSG codes: https://epsg.io/
# transform all input data_files to ensure all data is on the same reference system using inplace=true as we want to
# transform the datasets here and not create new ones
sites.to_crs(epsg=32629, inplace=True)
counties.to_crs(epsg=32629, inplace=True)
LGDs.to_crs(epsg=32629, inplace=True)

# find the county each site is situated within
# we want to improve the display of the county names in the output by ensuring the names are no longer in all capitals
# and by adding the characters 'County ' to the start of each county name string
for ind, row in counties.iterrows():  # iterate over each row in the GeoDataFrame
    # assign the row's CountyName to a new column called County after converting the values out of uppercase and adding
    # the 'County ' characters to the start of the string
    counties.loc[ind, 'County'] = 'County '+row['CountyName'].title()
# find the underlying county using find_underlying_vector_value() function previously defined and assigning its name to
# a new column called County
sites = find_underlying_vector_value(sites, 'Name', counties, 'County')

# find the Local Government District (LGD) each site is situated within
# we want to improve the display of the LGD name column label in the output by renaming the LGDNAME column to LGD
LGDs = LGDs.rename(columns={'LGDNAME': 'LGD'})
# find the underlying LGD using find_underlying_vector_value() function previously defined and assigning its name to
# a new column called LGD
sites = find_underlying_vector_value(sites, 'Name', LGDs, 'LGD')

# calculate the areas of each site
for ind, row in sites.iterrows():  # iterate over each row in the GeoDataFrame
    # calculate the area of the polygon and assign it to a new column called Area(km2)
    sites.loc[ind, 'Area(km2)'] = row['geometry'].area
# divide the area by 1000000 to convert from metres squared to kilometres squared and round to 2 decimal places
sites['Area(km2)'] = (sites['Area(km2)']/1000000).round(2)

# calculate the perimeter of each site
for ind, row in sites.iterrows():  # iterate over each row in the GeoDataFrame
    # calculate the perimeter of the polygon and assign it to a new column called Perimeter(km)
    sites.loc[ind, 'Perimeter(km)'] = row['geometry'].length
# divide the length by 1000 to convert from metres to kilometres and round to 2 decimal places
sites['Perimeter(km)'] = (sites['Perimeter(km)']/1000).round(2)

# calculate the percentage landcover for each site using the
# calculate_percentage_underlying_raster_categories_for_polygons() function previously defined
# open the landcover raster and read the data - we will use with rio.open() here to read the data and ensure the file
# is then closed
with rio.open('data_files/Landcover.tif') as dataset:
    lc_crs = dataset.crs  # the raster crs
    landcover = dataset.read(1)  # the band the data values are stored in that we want to read (band 1)
    lc_affine_tfm = dataset.transform  # the raster geotransform

# we need to ensure the vector layer is in the same crs as the raster before performing the next step
sites.to_crs(lc_crs, inplace=True)

# to find the percentage landcover for each site we first need to define a landcover category map dictionary which
# maps the raster values to the landcover categories
landcover_names = {1: 'Broadleaf woodland',
                   2: 'Coniferous woodland',
                   3: 'Arable',
                   4: 'Improved grassland',
                   5: 'Semi-natural grassland',
                   6: 'Mountain, heath, bog',
                   7: 'Saltwater',
                   8: 'Freshwater',
                   9: 'Coastal',
                   10: 'Built-up areas and gardens'}

# we also need to define a desired column names list (in the same order as the above raster category map)
lc_short_names = ['%Broadleaf',
                  '%Coniferous',
                  '%Arable',
                  '%Imp_grass',
                  '%Nat_grass',
                  '%Mountain',
                  '%Saltwater',
                  '%Freshwater',
                  '%Coastal',
                  '%Built_up']

# calculate the percentage landcover using the calculate_percentage_underlying_raster_categories_for_polygons()
# function previously defined
calculate_percentage_underlying_raster_categories_for_polygons(sites, 'Name', landcover, landcover_names,
                                                               lc_short_names, lc_affine_tfm)

# calculate the percentage underlying superficial geology for each site using the
# calculate_percentage_underlying_raster_categories_for_polygons() function previously defined
# open the geology raster and read the data - we will use with rio.open() here to read the data and ensure the file
# is then closed
with rio.open('data_files/Superficial_Geology.tif') as dataset:
    geol_crs = dataset.crs  # the raster crs
    geology = dataset.read(1)  # the band the data values are stored in that we want to read (band 1)
    geol_affine_tfm = dataset.transform  # the raster geotransform

# we need to ensure the vector layer is in the same crs as the raster before performing the next step
sites.to_crs(geol_crs, inplace=True)

# to find the percentage underlying geology for each site we first need to define a geology category map dictionary
# which maps the raster values to the geology categories
geology_names = {1: 'Alluvium - Sand and Silt',
                 2: 'Glaciolacustrine Deposits - Silt and Clay',
                 3: 'Blown Sand',
                 4: 'Diatomite',
                 5: 'Glaciofluvial Sheet Deposits - Sand, Silt and Clay',
                 6: 'Glacial Sand and Gravel',
                 7: 'Lacustrine Alluvium - Clay, Silt and Sand',
                 8: 'Peat',
                 9: 'Raised Beach Deposits - Gravel, Sand and Silt',
                 10: 'Raised Marine Deposits - Clay, Silt and Sand',
                 11: 'Landslide Deposits - Unknown/Unclassified',
                 12: 'Till - Diamicton'}

# we also need to define a desired column names list (in the same order as the above raster category map)
geol_short_names = ['%Alluv_sand_silt',
                    '%Glaciolac_silt_clay',
                    '%Blown_sand',
                    '%Diatomite',
                    '%Glaciofulv_sand_silt_clay',
                    '%Glacial_sand_gravel',
                    '%Lac_alluv_clay_silt_sand',
                    '%Peat',
                    '%Raised_beach_gravel_sand_silt',
                    '%Raised_marine_clay_silt_sand',
                    '%Landslide_unknown',
                    '%Till_diamicton']

# calculate the percentage underlying geology using the calculate_percentage_underlying_raster_categories_for_polygons()
# function previously defined
calculate_percentage_underlying_raster_categories_for_polygons(sites, 'Name', geology, geology_names, geol_short_names,
                                                               geol_affine_tfm)

# before running the calculate_stat_values_underlying_raster_for_polygons() function previously defined we need to
# ensure we have an integer column that uniquely identifies each polygon in the GeoDataFrame that does not contain a
# value that is the same as the value we will use as our fill value (0)
# create an integer identifier column for the raster analysis called ID_RA which starts with 1 and increments by 1 for
# each row until the end of the sites GeoDataFrame
sites['ID_RA'] = range(1, 1+len(sites))

# calculate wind speeed statistics for each site
# open the wind speed raster and read the data - we will use with rio.open() here to read the data and ensure the file
# is then closed
with rio.open('data_files/Wind_Speed.tif') as dataset:
    ws_crs = dataset.crs  # the raster crs
    wind_speed = dataset.read(1)  # the band the data values are stored in that we want to read (band 1)
    ws_affine_tfm = dataset.transform  # the raster geotransform

# we need to ensure the vector layer is in the same crs as the raster before performing the next step
sites.to_crs(ws_crs, inplace=True)

# before running the function we need to create a dictionary of desired column names for the statistics
wind_speed_stat_columns_dict = {'mean': 'MeanWindSpeed(m/s)',
                                'min': 'MinWindSpeed(m/s)',
                                'max': 'MaxWindSpeed(m/s)',
                                'range': 'WindSpeedRange(m/s)',
                                'median': 'MedianWindSpeed(m/s)',
                                'std': 'WindSpeedStdDev'}

# calculate statistics for the wind speed raster for each site using
# calculate_stat_values_underlying_raster_for_polygons() function previously defined
calculate_stat_values_underlying_raster_for_polygons(sites, 'ID_RA', wind_speed_stat_columns_dict, wind_speed,
                                                     ws_affine_tfm, 0, 'geometry')

# calculate wind power density statistics for each site
# open the wind power density raster and read the data - we will use with rio.open() here to read the data and ensure
# the file is then closed
with rio.open('data_files/Wind_Power_Density.tif') as dataset:
    wpd_crs = dataset.crs  # the raster crs
    wind_power_density = dataset.read(1)  # the band the data values are stored in that we want to read (band 1)
    wpd_affine_tfm = dataset.transform  # the raster geotransform

# we need to ensure the vector layer is in the same crs as the raster before performing the next step
sites.to_crs(wpd_crs, inplace=True)

# before running the function we need to create a dictionary of desired column names for the statistics
wind_power_density_stat_columns_dict = {'mean': 'MeanWindPowerDensity(W/m2)',
                                        'min': 'MinWindPowerDensity(W/m2)',
                                        'max': 'MaxWindPowerDensity(W/m2)',
                                        'range': 'WindPowerDensityRange(W/m2)',
                                        'median': 'MedianWindPowerDensity(W/m2)',
                                        'std': 'WindPowerDensityStdDev'}

# calculate statistics for the wind power density raster for each site using
# calculate_stat_values_underlying_raster_for_polygons() function previously defined
calculate_stat_values_underlying_raster_for_polygons(sites, 'ID_RA', wind_power_density_stat_columns_dict,
                                                     wind_power_density, wpd_affine_tfm, 0, 'geometry')

# calculate elevation statistics for each site
# open the elevation raster and read the data - we will use with rio.open() here to read the data and ensure the file
# is then closed
with rio.open('data_files/DEM.tif') as dataset:
    elev_crs = dataset.crs  # the raster crs
    elevation = dataset.read(1)  # the band the data values are stored in that we want to read (band 1)
    elev_affine_tfm = dataset.transform  # the raster geotransform

# we need to ensure the vector layer is in the same crs as the raster before performing the next step
sites.to_crs(elev_crs, inplace=True)

# before running the function we need to create a dictionary of desired column names for the statistics
elevation_stat_columns_dict = {'mean': 'MeanElevation(m)',
                               'min': 'MinElevation(m)',
                               'max': 'MaxElevation(m)',
                               'range': 'ElevationRange(m)',
                               'median': 'MedianElevation(m)',
                               'std': 'ElevationStdDev'}

# calculate statistics for the elevation raster for each site using
# calculate_stat_values_underlying_raster_for_polygons() function previously defined
calculate_stat_values_underlying_raster_for_polygons(sites, 'ID_RA', elevation_stat_columns_dict, elevation,
                                                     elev_affine_tfm, 0, 'geometry')

# for clarity and so that we can join the site characteristics data with the proximity data we will rename the Name
# column for the sites to Site Name
sites = sites.rename(columns={'Name': 'Site Name'})

# now to save the results - firstly, create a DataFrame to export to CSV by copying only desired columns from
# GeoDataFrame that contains results by dropping unnecessary columns (the geometry column as this is not needed in the
# csv results and the ID_RA column as this was only created for functions in the script and is not needed in the CSV
# results)
site_results = sites.drop(columns=['geometry', 'ID_RA']).copy()

# now that the results are in a DataFrame as opposed to a GeoDataFrame we can round all results to 2 decimal places,
# this step could not be undertaken on the GeoDataFame as geometry cannot be rounded
site_results = site_results.round(2)

# save the results DataFrame as a CSV called Proximity_analysis.csv to the output_files folder with the index removed
site_results.to_csv('output_files/Site_characteristics.csv', index=False)


import sys
import pathlib
import json

def get_settings_dir(name) -> pathlib.Path:
    """
    Returns a parent directory path
    where persistent application data can be stored.

    # linux: ~/.local/share
    # macOS: ~/Library/Application Support
    # windows: C:/Users/<USER>/AppData/Roaming
    """

    home = pathlib.Path.home()

    if sys.platform == "win32":
        config_path = home / "AppData/Roaming"
    elif sys.platform == "linux":
        config_path = home / ".local/share"
    elif sys.platform == "darwin":
        config_path = home / "Library/Application Support"
    else:
        raise OSError("OS not supported")

    config_path = config_path / name / "config.json"
    return config_path

def create_dir(settings_file):
    try:
        settings_file.parent.mkdir(parents=True)
    except FileExistsError:
        pass

class Settings(object):
    def __init__(self, name):
        self._settings = {
            "jira_domain": "arcanys.atlassian.net",
            "jira_username": None,
            "jira_token": None,
            "tempo_token": None,
        }
        self._file = get_settings_dir(name)

    def load(self):
        create_dir(self._file)
        if pathlib.Path.is_file(self._file):
            with open(self._file, "r") as fp:
                data = json.load(fp)
            for k, v in data.items():
                if k in self._settings:
                    self._settings[k] = v

    def save(self):
        create_dir(self._file)
        with open(self._file, "w") as fp:
            json.dump(self._settings, fp)

    def get(self, key):
        return self._settings.get(key)

    def set(self, key, value):
        self._settings[key] = value


'''
Universidad del Valle de Guatemala
Redes - 2021
DVR.py

Roberto Figueroa 18306
Luis Quezada 18028
Esteban del Valle 18221
'''

from slixmpp.basexmpp import BaseXMPP
from node import Node

from asyncio import sleep
from aioconsole import aprint
from time import time
from xml.etree import ElementTree as ET
import json
import asyncio
import numpy as np
from scipy.sparse.csgraph import shortest_path

"""
---------
|   A   |
|  Sec. |
|  Age  |
---------
| B | 0.3 |
| E | 0.5 |
---------
"""

EXPIRATION = 5

class DVR(Node):

    def __init__(self, jid, password, entity, asoc_nodes = None, t_keys = None):
        super().__init__(jid, password)
        self.DVR_seqnum = 0
        self.DVR = {}
        self.entity = entity
        self.basexmpp = BaseXMPP()
        self.neighbors = asoc_nodes #should be a dict
        self.neighbors_niknames = self.neighbors.keys() if self.neighbors != None else []

        self.topo = []
        self.all_nodes = [self.entity]
        self.ady_matrix = []
        self.prev_matrix = []
        self.build_topo_package()

        # ---------
        self.topo_vector = t_keys.sort()

    def send_hello(self, hto, hfrom):
        """
        Function for neighbor discovery
        """
        self.send_message(hto,
            "<hello>",
            mfrom=hfrom)
        print("Sending hello to neighbor ...")

    def eco(self, eco_to, eco_from):
        """
        Function for measure cost between neighbors
        """
        # print("Sending eco to {}".format(eco_to))
        self.send_message(
            mto=eco_to,
            mbody="<eco time='%f' ></eco>" % time(),
            mfrom=eco_from
        )

    def build_topo_package(self):
        """
        Function for package build about the network
        destination | dist | next hop
        """
        for i in self.topo_keys:
            if i == self.entity:
                self.topo.append((i , 0, None))
            self.topo.append((i , float('inf'), None))

    def update_topo_package(self, node, weight):
        """
        Function for package weights update+
        """

        for i in self.topo:
            if i[0] == node:
                i[1] = weight

    def send_topo_package(self, to):
        """
        Send the topo package to neighbors
        """
        dvr_json = json.dumps(self.topo)
        self.send_message(to,
            "<pack dvr='%s' from='%s'></pack>" % (dvr_json, self.entity),
            mfrom=self.boundjid,
        )

    def recieve_topo_package(self, nfrom, topo_package):
        """
        Recieve topo package from a neighbor,
        this function must process the timestamp and
        the sequence number in order to drop or send
        package
        """
        return "This should be a message stanza"

    def shortest_path(self):
        """
        Must be a Bellman-Ford algorithm implementation
        """
        path = []
        return path.reverse()

    async def update_tables(self):
        while True:
            for router in self.neighbors_niknames:
                self.eco(self.neighbors[router], self.boundjid)

            await asyncio.sleep(5)
            # print("Sending packages to neighbors ... ")
            for router in self.neighbors_niknames:
                self.send_topo_package(self.neighbors[router])

    def get_nickname(self, jid):
        key_list = list(self.neighbors.keys())
        val_list = list(self.neighbors.values())

        return key_list[val_list.index(jid)]

    def init_listener(self):
        self.loop.create_task(self.update_tables())

    def flood(self, to, package):

        self.send_message(to,
            "<pack dvr='%s'></pack>" % package,
            mfrom=self.boundjid,
        )

    def save_prev_matrix(self):
    	self.prev_matrix = self.ady_matrix

    def update_ady_matrix(self):
        self.save_prev_matrix()
        length = len(self.all_nodes)
        self.ady_matrix = np.zeros((length, length),dtype=np.float16)
        for row_node in self.all_nodes:
            for col_node in self.topo[row_node]['weights'].keys():
                row = self.all_nodes.index(row_node)
                if col_node in self.all_nodes:
                    col = self.all_nodes.index(col_node)
                else:
                    return
                self.ady_matrix[row][col] = self.topo[row_node]['weights'][col_node]

        # compare tables, update if diff with Bellman Ford
        optimized_matrix = shortest_path(self.ady_matrix,directed=True,method='BF',return_predecessors=False)
        if np.allclose(self.ady_matrix,optimized_matrix) == False:
            self.ady_matrix = optimized_matrix

    def bellmanFord(self, destiny):
        D, Pr = shortest_path(self.ady_matrix,directed=True,method='BF',return_predecessors=True)
        _from = self.all_nodes.index(self.entity)
        path = [destiny]
        k = destiny

    def parse_path(self, path):
        return [self.all_nodes[i] for i in path]

    def get_shortest_path(self, destiny): #should be a character
        _from = self.all_nodes.index(self.entity)
        destiny = self.all_nodes.index(destiny)
        path = [destiny]
        k = destiny
        while self.ady_matrix[_from, k] != -9999:
            path.append(self.ady_matrix[_from, k])
            k = self.ady_matrix[_from, k]
        return self.parse_path(path[::-1])

    def send_msg(self, to, msg): # to should be a character
        path = self.get_shortest_path(to)
        print("%s: my best path: %s" %(self.entity,path))
        if len(path) > 1:
            self.send_message(
                mto=self.neighbors[path[1]],
                mbody="<msg chat='%s' to='%s' ></msg>" %(msg, to),
                mfrom=self.boundjid
            )

    async def message(self, msg):
        if msg['type'] in ('normal', 'chat'):
            if msg['body'][:7] in ("<hello>"):
                msg.reply(self.boundjid).send()
                print("Recieved hello from neighbor, sending answer ...")
            elif msg['body'][1:4] == "eco":
                xml_parse = ET.fromstring(msg['body'])
                timestamp = xml_parse.attrib['time']
                msg.reply("<a_eco time='%s'></a_eco>" % timestamp).send()
            elif msg['body'][1:6] == "a_eco":
                pack_from = msg['from'].bare
                node_entity = self.get_nickname(pack_from)
                end_time = time()
                msg_parse = ET.fromstring(msg['body'])
                start_time = float(msg_parse.attrib['time'])
                delta_time = (end_time - start_time) / 2
                delta_time = round(delta_time, 1)
                self.update_topo_package(node_entity, delta_time)
            elif msg['body'][1:5] == "pack":
                # p_from = msg['from'].bare
                # n_entity = self.get_nickname(p_from)
                parse = ET.fromstring(msg['body'])
                pack_json = parse.attrib['dvr']
                dvr = json.loads(pack_json)
                n_entity = parse.attrib['from']

            elif msg['body'][1:4] == "msg":
                msg_parse = ET.fromstring(msg['body'])
                bare_msg = msg_parse.attrib['chat']
                msg_to = msg_parse.attrib['to']
                if msg_to != self.entity:
                    self.send_msg(msg_to, bare_msg)
                else:
                    print("Incoming message: %s" % bare_msg)
            else:
                pass


from __future__ import absolute_import, division, print_function

import os
import sys
import glob
import argparse
import numpy as np
import PIL.Image as pil
import PIL.ImageOps
import matplotlib as mpl
import matplotlib.cm as cm
import matplotlib.pyplot as plt

from skimage.segmentation import mark_boundaries
from scipy.optimize import linear_sum_assignment

import torch
import torch.nn.functional as F
from torchvision import transforms, datasets

import networks
from layers import disp_to_depth
from utils import download_model_if_doesnt_exist
#from fullcrf import fullcrf

#labels = ['unlabeled','ego vehicle','rectification border','out of roi','static','dynamic','ground','road','sidewalk','parking','rail track',
#    'building','wall','fence','guard rail','bridge','tunnel','pole','polegroup','traffic light','traffic sign','vegetation','terrain',
#    'sky','person','rider','car','truck','bus','caravan','trailer','train','motorcycle','bicycle','license plate']

labels = ['road','sidewalk','building','wall','fence','pole','traffic light','traffic sign','vegetation','terrain',
    'sky','person','rider','car','truck','bus','train','motorcycle','bicycle']

eval_cls = [7,8,11,12,13,17,19,20,21,22,23,24,25,26,27,28,31,32,33]
void_cls = [0, 1, 2, 3, 4, 5, 6, 9, 10, 14, 15, 16, 18, 29, 30, -1]

class_map = dict(zip(eval_cls, range(len(eval_cls))))
seg_class_map = dict(zip(range(len(eval_cls)), range(len(eval_cls))))

color_seg = [[128, 64, 128],[244, 35, 232],[70, 70, 70],[102, 102, 156],[190, 153, 153],
        [153, 153, 153],[250, 170, 30],[220, 220, 0],[107, 142, 35],[152, 251, 152],
        [0, 130, 180],[220, 20, 60],[255, 0, 0],[0, 0, 142],[0, 0, 70],[0, 60, 100],
        [0, 80, 100],[0, 0, 230],[119, 11, 32]]
color_seg = np.array(color_seg).astype(np.uint8)

def load_data(config):

    inputs = []
    gts = []

    output_dirs = {"input":[],"seg":[],"depth":[]}

    input_paths = sorted(glob.glob(os.path.join(config.input_path, '*.png')))[1::2]
    if config.gt_path != None:
        gt_paths = sorted(glob.glob(os.path.join(config.gt_path, '*.png')))[1::2]
    else:
        gt_paths = []

    model_name = config.model_name
    output_dir_base = os.path.join(config.output_dir, model_name)
    if not os.path.exists(output_dir_base):
        os.makedirs(output_dir_base)

    print("-> Predicting on {:d} eval images".format(len(input_paths)))

    print("-> {:d} gt images".format(len(gt_paths)))

    for image_path in input_paths:
        image = pil.open(image_path).convert('RGB')
        inputs.append(image)

        output_name = os.path.splitext(os.path.basename(image_path))[0]
        output_dir_input = os.path.join(output_dir_base, "{}_input.png".format(output_name))
        output_dir_seg = os.path.join(output_dir_base, "{}_seg.png".format(output_name))
        output_dir_depth = os.path.join(output_dir_base, "{}_zepth.png".format(output_name))
        output_dirs["input"].append(output_dir_input)
        output_dirs["seg"].append(output_dir_seg)
        output_dirs["depth"].append(output_dir_depth)

    for image_path in gt_paths:
        image = pil.open(image_path)
        original_width, original_height = image.size
        gt = np.array(image,dtype=np.uint8)
        for c in void_cls:
            gt[gt==c] = 255
        for c in eval_cls:
            gt[gt==c] = class_map[c]
        gts.append(gt)

    return inputs, gts, output_dirs

def predict_seg(models, inputs, output_dirs, config):
    """Function to predict for a single image or folder of images
    """

    for key, model in models.items():
        model.eval()
        model.to('cuda')

    segs = []

    with torch.no_grad():

        for i, image in enumerate(inputs):

            original_width, original_height = image.size
            image.save(output_dirs["input"][i])
            # Load image and preprocess

            input_image = image.resize((config.width, config.height), pil.LANCZOS)
            input_image = transforms.ToTensor()(input_image).unsqueeze(0)

            # PREDICTION
            input_image = input_image.to('cuda')

            if config.architecture == "share":
                seg_features = models["encoder"](input_image)
                seg = models["seg"](seg_features)[("output", 0)]
                depth = models["depth"](seg_features)[("output", 0)]

            if config.architecture == "byol":
                features = models["encoder"](input_image)
                representation = models["decoder"](features)
                representation = F.normalize(representation, dim=1, p=2)
                seg = F.softmax(models["head"](representation.detach(), features)[("output", 0)],dim=1)
                depth = None

            seg = torch.nn.functional.interpolate(
                seg, (original_height, original_width), mode="bilinear", align_corners=False)

            seg = seg.squeeze()
            seg = torch.argmax(seg, dim=0).cpu().numpy()

            segs.append(seg)

            if depth != None:
                disp_resized = torch.nn.functional.interpolate(depth, (original_height, original_width), mode="bilinear", align_corners=False)
                disp_resized_np = disp_resized.squeeze().cpu().numpy()
                vmax = np.percentile(disp_resized_np, 95)
                normalizer = mpl.colors.Normalize(vmin=disp_resized_np.min(), vmax=vmax)
                mapper = cm.ScalarMappable(norm=normalizer, cmap='magma')
                colormapped_im = (mapper.to_rgba(disp_resized_np)[:, :, :3] * 255).astype(np.uint8)
                im = pil.fromarray(colormapped_im)
                im.save(output_dirs["depth"][i])

    return segs

def compute_metrics(segs, gts, output_dirs, h_match = False):
    num_cls = len(eval_cls)
    intersection = np.zeros((num_cls, num_cls))
    for seg, gt in zip(segs, gts):
        mask = (gt >= 0) & (gt < num_cls)
        hist = np.bincount(
            num_cls * seg[mask] + gt[mask], minlength=num_cls ** 2
        ).reshape(num_cls, num_cls)
        intersection += hist
    i = j = range(num_cls)
    if h_match:
        union = -intersection + intersection.sum(axis=1,keepdims=True) + intersection.sum(axis=0,keepdims=True)
        cost = -intersection/union
        cost[np.isnan(cost)] = -1
        i, j = linear_sum_assignment(cost)

    hist = intersection
    print(j)

    acc = hist[i,j].sum() / hist.sum()
    acc_cls = hist[i,j] / hist.sum(axis=0)[j]
    acc_cls = np.nanmean(acc_cls)
    iu = hist[i,j] / (hist.sum(axis=1) + hist.sum(axis=0)[j] - hist[i,j])
    mean_iu = np.nanmean(iu)
    freq = hist.sum(axis=0)[j] / hist.sum()
    fwavacc = (freq[freq > 0] * iu[freq > 0]).sum()
    gt_labels = [labels[index] for index in j]
    cls_iu = dict(zip(gt_labels, iu))

    metrics = {
            "Overall Acc": acc,
            "Mean Acc": acc_cls,
            "FreqW Acc": fwavacc,
            "Mean IoU": mean_iu
        }
    metrics.update(cls_iu)

    for s, seg in enumerate(segs):
        seg_img = color_seg[j][seg]
        seg_img = pil.fromarray(seg_img)
        seg_img.save(output_dirs["seg"][s])

    return metrics

def build_models(config):
    models = {}
    if config.architecture == "share":
        models["encoder"] = networks.ResnetEncoder(
            num_layers = config.ResX,
            pretrained = True)
        models["seg"] = networks.Decoder(
            out_channels = config.cls_num,
            scales = config.seg_scales,
            activate = 'softmax',
            in_channels = models["encoder"].num_ch_enc)
        models["depth"] = networks.Decoder(
            out_channels = 1,
            scales = config.seg_scales,
            activate = 'sigmoid',
            in_channels = models["encoder"].num_ch_enc)

        models_to_load = ["encoder","seg","depth"]

    if config.architecture == "byol":
        models["encoder"] = networks.ResnetEncoder(
            num_layers = config.ResX,
            pretrained = False)
        models["decoder"] = networks.Decoder_BYOL(
            in_channels = models["encoder"].num_ch_enc)
        models["head"] = networks.FullHead(
            out_channels=19,
            activate = 'softmax',
        in_channels=models["encoder"].num_ch_enc)
        models_to_load = ["encoder","decoder","head"]

    config.load_weights_folder = os.path.expanduser(config.load_weights_folder)
    assert os.path.isdir(config.load_weights_folder), \
        "Cannot find folder {}".format(config.load_weights_folder)
    print("loading model from folder {}".format(config.load_weights_folder))

    for n in models_to_load:
        print("Loading {} weights...".format(n))
        path = os.path.join(config.load_weights_folder, "{}.pth".format(n))
        model_dict = models[n].state_dict()
        pretrained_dict = torch.load(path)
        pretrained_dict = {k: v for k, v in pretrained_dict.items() if k in model_dict}
        model_dict.update(pretrained_dict)
        models[n].load_state_dict(model_dict)

    return models

def model_eval(config, models = None):
    if models == None:
        models = build_models(config)
    inputs, gts, output_dirs = load_data(config)
    segs = predict_seg(models, inputs, output_dirs, config)
    metrics = compute_metrics(segs, gts, output_dirs, config.h_match)

    return metrics


#!/usr/bin/env python

import os
import json
import argparse
from pprint import pprint
from pathlib import Path

import requests
from requests.auth import HTTPBasicAuth
import jwt
from dotenv import find_dotenv, load_dotenv

DOTENV_PATH = find_dotenv()
if DOTENV_PATH:
    load_dotenv(DOTENV_PATH)

KEYCLOAK_HOST = os.environ.get("KEYCLOAK_HOST") or "localhost"
KEYCLOAK_PORT = os.environ.get("KEYCLOAK_PORT") or "8080"
KEYCLOAK_ISSUER = (
    os.environ.get("KEYCLOAK_ISSUER") or
    f"http://{KEYCLOAK_HOST}:{KEYCLOAK_PORT}/realms/fhir-dev"
)
KEYCLOAK_CLIENT_ID = (
    os.environ.get("KEYCLOAK_CLIENT_ID") or "fhir-superuser-client"
)
KEYCLOAK_CLIENT_SECRET = os.environ.get("KEYCLOAK_CLIENT_SECRET") or "none"
SMILECDR_HOST = os.environ.get("SMILECDR_HOST") or "localhost"
SMILECDR_PORT = os.environ.get("SMILECDR_PORT") or "8000"
SMILECDR_FHIR_ENDPOINT = "http://{SMILECDR_HOST}:{SMILECDR_PORT}"
SMILECDR_AUDIENCE = "https://kf-api-fhir-smilecdr-dev.org"

def send_request(method, *args, **kwargs):
    print("\n***** Sending request ******")
    try:
        requests_op = getattr(requests, method)
        resp = requests_op(*args, **kwargs)
        resp.raise_for_status()
    except requests.exceptions.HTTPError as e:
        print("Problem sending request to endpoint")
        print(resp.text)
        raise e

    return resp

def get_access_token(
    client_id=KEYCLOAK_CLIENT_ID, client_secret=KEYCLOAK_CLIENT_SECRET,
    issuer=KEYCLOAK_ISSUER, decoded=True
):
    """
    Test OAuth2 stuff
    """
    headers = {
        "Content-Type": "application/json",
    }
    # Get OIDC configuration
    print("\n****** Get OIDC Configuration *************")
    openid_config_endpoint = (
        f"{issuer}/.well-known/openid-configuration"
    )
    resp = send_request("get", openid_config_endpoint, headers=headers)
    openid_config = resp.json()

    pprint(openid_config)

    # Authorize to get access token
    print("\n****** Get Access Token *************")
    token_endpoint = openid_config["token_endpoint"]
    payload = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret,
        "audience": SMILECDR_AUDIENCE
    }
    params = {
        "scope": "fhir"
    }
    resp = send_request("post", token_endpoint, data=payload, params=params)
    token_payload = resp.json()
    access_token = token_payload["access_token"]

    pprint(token_payload)

    print("\n****** Introspect Token *************")
    decoded_token = jwt.decode(
        access_token, options={"verify_signature": False}
    )

    pprint(decoded_token)

    token_payload.update({
        "decoded_token": decoded_token
    })
    return token_payload

def cli():
    """
    CLI for running this script
    """
    parser = argparse.ArgumentParser(
        description='Get access token for client'
    )
    parser.add_argument(
        "--client_id",
        default=KEYCLOAK_CLIENT_ID,
        help="Keycloak Client ID",
    )
    parser.add_argument(
        "--client_secret",
        default=KEYCLOAK_CLIENT_SECRET,
        help="Keycloak Client secret",
    )
    parser.add_argument(
        "--issuer",
        default=KEYCLOAK_ISSUER,
        help="Keycloak Issuer URL",
    )
    args = parser.parse_args()

    get_access_token(args.client_id, args.client_secret, args.issuer)

if __name__ == "__main__":
    cli()


# Video Capture, Classification and labeling

from keras.models import load_model
import cv2
import numpy as np

# Model from epoch 17 selected as it has the lowest validation loss and highest accuracy in contrast to the other epochs
#Video capture frame activated

model = load_model('model-017.model')
face_clsfr=cv2.CascadeClassifier('haarcascade_frontalface_default.xml')

source=cv2.VideoCapture(0)

# Binary label assigned to frame, No mask labeled with red and with mask labeled with green
labels_dict={0:'MASK',1:'NO MASK'}
color_dict={0:(0,255,0),1:(0,0,255)}

# Loop will capture each frame, extract region of interest (face),
# Process the ROI in the same way it was done in training (convert to grayscale and resize)
# Processed image is normalized and fed into the imported Neural model for prediction (0 :Mask | 1: No Mask)

while(True):

    ret,img=source.read()
    gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
    faces=face_clsfr.detectMultiScale(gray,1.3,5)

    for x,y,w,h in faces:

        face_img=gray[y:y+w,x:x+w]
        resized=cv2.resize(face_img,(100,100))
        normalized=resized/255.0
        reshaped=np.reshape(normalized,(1,100,100,1))
        result=model.predict(reshaped)

        label=np.argmax(result,axis=1)[0]

  #Draw a rectangle on the frame over the ROI with the approriate color obtained from dict ( Green= Mask , red=no mask )
        cv2.rectangle(img,(x,y),(x+w,y+h),color_dict[label],2)
        cv2.rectangle(img,(x,y-40),(x+w,y),color_dict[label],-1)
        cv2.putText(img, labels_dict[label], (x, y-10),cv2.FONT_HERSHEY_SIMPLEX,0.8,(255,255,255),2) # add Binary label on display

    cv2.imshow('LIVE',img)
    key=cv2.waitKey(1)

    if(key==27):
        break

cv2.destroyAllWindows()
source.release()


# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Path setup --------------------------------------------------------------

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import re
import zipfile
from pathlib import Path
from datetime import date
import importlib
import json

import sphinx_rtd_theme
import git
import yaml

TMP_FOLDER = Path(__file__).parent / "tmp"
TMP_FOLDER.mkdir(exist_ok=True)

JSON_COMPATIBILITY_TABLE_FILE = TMP_FOLDER / "releases.json"

with open("additional/releases.yaml") as f:
    compat_table = yaml.safe_load(f)
    with open(JSON_COMPATIBILITY_TABLE_FILE, "w") as f:
        json.dump(compat_table, f)

repo = git.Repo(search_parent_directories=True)
current_commit = repo.head.commit
tagged_commits = [tag.commit for tag in repo.tags]

if os.environ.get("READTHEDOCS_VERSION_TYPE") == "tag" or current_commit in tagged_commits:
    # Index 0 means latest release
    SUBSTRA_VERSION = compat_table["releases"][0]["components"]["substra"]["version"]
    TOOLS_VERSION = compat_table["releases"][0]["components"]["substra-tools"]["version"]
    SUBSTRAFL_VERSION = compat_table["releases"][0]["components"]["substrafl"]["version"]

else:
    SUBSTRA_VERSION = "main"
    TOOLS_VERSION = "main"
    SUBSTRAFL_VERSION = "main"

print(
    f"Versions of the components used:"
    f"\n - substra: {SUBSTRA_VERSION}"
    f"\n - substra-tools: {TOOLS_VERSION}"
    f"\n - substrafl: {SUBSTRAFL_VERSION}"
)

class SubSectionTitleOrder:
    """Sort example gallery by title of subsection.
    Assumes README.txt exists for all subsections and uses the subsection with
    dashes, '---', as the adornment.

    This class is adapted from sklearn
    """

    def __init__(self, src_dir):
        self.src_dir = src_dir
        self.regex = re.compile(r"^([\w ]+)\n-", re.MULTILINE)

    def __repr__(self):
        return "<%s>" % (self.__class__.__name__,)

    def __call__(self, directory):
        src_path = os.path.normpath(os.path.join(self.src_dir, directory))

        # Forces Release Highlights to the top
        if os.path.basename(src_path) == "release_highlights":
            return "0"

        readme = os.path.join(src_path, "README.txt")

        try:
            with open(readme, "r") as f:
                content = f.read()
        except FileNotFoundError:
            return directory

        title_match = self.regex.search(content)
        if title_match is not None:
            return title_match.group(1)
        return directory

# Nbsphinx config

nbsphinx_thumbnails = {
    "examples/substra_core/diabetes_example/run_diabetes": "_static/example_thumbnail/diabetes.png",
    "examples/substra_core/titanic_example/run_titanic": "_static/example_thumbnail/titanic.jpg",
    "examples/substrafl/get_started/run_mnist_torch": "_static/example_thumbnail/mnist.png",
    "examples/substrafl/go_further/run_diabetes_substrafl": "_static/example_thumbnail/diabetes.png",
    "examples/substrafl/go_further/run_iris_sklearn": "_static/example_thumbnail/iris.jpg",
    "examples/substrafl/go_further/run_mnist_cyclic": "_static/example_thumbnail/cyclic-mnist.png",
}

nbsphinx_prolog = r"""
{% set docname = 'docs/source/' + env.doc2path(env.docname, base=None) %}

.. raw:: html

    Launch notebook online
     or download it

"""

nbsphinx_epilog = nbsphinx_prolog

# zip the assets directory found in the examples directory and place it in the current dir
def zip_dir(source_dir, zip_file_name):
    # Create archive with compressed files
    with zipfile.ZipFile(file=TMP_FOLDER / zip_file_name, mode="w", compression=zipfile.ZIP_DEFLATED) as ziph:
        for root, _, files in os.walk(source_dir):
            for file in files:
                ziph.write(
                    os.path.join(root, file),
                    os.path.relpath(os.path.join(root, file), os.path.join(source_dir, "..")),
                )

assets_dir_titanic = Path(__file__).parent / "examples" / "substra_core" / "titanic_example" / "assets"
zip_dir(assets_dir_titanic, "titanic_assets.zip")

assets_dir_diabetes = Path(__file__).parent / "examples" / "substra_core" / "diabetes_example" / "assets"
zip_dir(assets_dir_diabetes, "diabetes_assets.zip")

assets_dir_substrafl_torch_fedavg = (
    Path(__file__).parent / "examples" / "substrafl" / "get_started" / "torch_fedavg_assets"
)
zip_dir(assets_dir_substrafl_torch_fedavg, "torch_fedavg_assets.zip")

assets_dir_substrafl_diabetes = (
    Path(__file__).parent / "examples" / "substrafl" / "go_further" / "diabetes_substrafl_assets"
)
zip_dir(assets_dir_substrafl_diabetes, "diabetes_substrafl_assets.zip")

assets_dir_substrafl_sklearn_fedavg = (
    Path(__file__).parent / "examples" / "substrafl" / "go_further" / "sklearn_fedavg_assets"
)
zip_dir(assets_dir_substrafl_sklearn_fedavg, "sklearn_fedavg_assets.zip")

assets_dir_substrafl_sklearn_fedavg = (
    Path(__file__).parent / "examples" / "substrafl" / "go_further" / "torch_cyclic_assets"
)
zip_dir(assets_dir_substrafl_sklearn_fedavg, "torch_cyclic_assets.zip")

# Copy the source documentation files from substra and substrafl to their right place
# in the substra-documentation repository
from dataclasses import dataclass
from distutils.dir_util import copy_tree
import subprocess
import shutil
import sys
import typing

EDITABLE_LIB_PATH = Path(__file__).resolve().parents[1] / "src"

@dataclass
class Repo:
    pkg_name: str
    repo_name: str
    installation_cmd: str
    version: str
    doc_dir: typing.Optional[str] = None
    dest_doc_dir: typing.Optional[str] = None

SUBSTRA_REPOS = [
    Repo(
        pkg_name="substra",
        repo_name="substra",
        installation_cmd="#egg=substra",
        version=SUBSTRA_VERSION,
        doc_dir="references",
        dest_doc_dir="documentation/references",
    ),
    Repo(
        pkg_name="substrafl",
        repo_name="substrafl",
        installation_cmd="#egg=substrafl[dev]",
        version=SUBSTRAFL_VERSION,
        doc_dir="docs/api",
        dest_doc_dir="substrafl_doc/api",
    ),
    Repo(
        pkg_name="substratools",
        repo_name="substra-tools",
        installation_cmd="#egg=substratools",
        version=TOOLS_VERSION,
    ),
]

def install_dependency(library_name, repo_name, repo_args, version):
    try:
        subprocess.run(
            args=[
                sys.executable,
                "-m",
                "pip",
                "install",
                "--src",
                str(EDITABLE_LIB_PATH),
                "--editable",
                f"git+https://github.com/substra/{repo_name}.git@{version}{repo_args}",
            ],
            check=True,
            capture_output=True,
        )
    except subprocess.CalledProcessError as e:
        print(e.stderr)
        print(e.stdout)
        raise
    importlib.invalidate_caches()
    sys.path.insert(0, str(EDITABLE_LIB_PATH / library_name))

def copy_source_files(src, dest):
    full_dest_path = Path(__file__).resolve().parent / dest
    if full_dest_path.exists():
        shutil.rmtree(full_dest_path)

    copy_tree(str(src), str(full_dest_path))

for repo in SUBSTRA_REPOS:
    install_dependency(
        library_name=repo.pkg_name,
        repo_name=repo.repo_name,
        repo_args=repo.installation_cmd,
        version=repo.version,
    )

    if repo.doc_dir is not None:
        imported_module = importlib.import_module(repo.pkg_name)
        source_path = Path(imported_module.__file__).resolve().parents[1] / repo.doc_dir
        copy_source_files(source_path, repo.dest_doc_dir)

# reformat links to a section in a markdown files (not supported by myst_parser)
def reformat_md_section_links(file_path: Path):
    # Read in the file
    with open(file_path, "r") as file:
        filedata = file.read()

    # replace ".md#" by ".html#"
    filedata = filedata.replace(".md#", ".html#")
    filedata = re.sub(r"#(.*)\)", lambda m: m.group().lower(), filedata)

    # Write the file out again
    with open(file_path, "w") as file:
        file.write(filedata)

for file_path in Path(".").rglob("*.md"):
    reformat_md_section_links(file_path)
# -- Project information -----------------------------------------------------

project = "Substra"
copyright = f"{date.today().year}, OWKIN"
author = "Owkin"

# parse the current doc version to display it in the menu
_doc_version = re.sub("^v", "", os.popen("git describe --tags").read().strip())
# The full version, including alpha/beta/rc tags
version = _doc_version
release = _doc_version

# -- General configuration ---------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    "nbsphinx",
]

extensions.extend(
    [
        "sphinx.ext.intersphinx",
        "sphinx.ext.autodoc",
        "sphinx_rtd_theme",
        "sphinx.ext.napoleon",
        "sphinx.ext.ifconfig",
        "sphinx_click",
        "sphinx.ext.autosectionlabel",
        "sphinx.ext.todo",
        "sphinx_fontawesome",
        "myst_parser",  # we need it for links between md files. Recommanded by sphinx : https://www.sphinx-doc.org/en/master/usage/markdown.html
        "sphinx_copybutton",
    ]
)

sys.path.append(os.path.abspath("./_ext"))
extensions.append("compatibilitytable")

todo_include_todos = False

intersphinx_mapping = {
    "python": ("https://docs.python.org/3", None),
    "numpy": ("https://numpy.org/doc/stable/", None),
    "pandas": ("https://pandas.pydata.org/docs/", None),
    "torch": ("https://pytorch.org/docs/stable/", None),
}

################
# Substrafl API
################

# generate autosummary even if no references
autosummary_generate = True

autosectionlabel_prefix_document = True

# autodoc settings
autodoc_default_options = {
    "show-inheritance": True,
    "members": True,
}
autoclass_content = "both"
autodoc_typehints = "both"

# Napoleon settings
# https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
napoleon_google_docstring = True
napoleon_numpy_docstring = False

# Remove the prompt when copying examples
copybutton_prompt_text = ">>> "

# As we defined the type of our args, auto doc is trying to find a link to a
# documentation for each type specified
# The following elements are the link that auto doc were not able to do

nitpick_ignore = [
    ("py:class", "pydantic.main.BaseModel"),
    ("py:class", "BaseModel"),
    ("py:class", "torch.nn.modules.module.Module"),
    ("py:class", "torch.nn.modules.Module"),
    ("py:class", "torch.nn.modules.loss._Loss"),
    ("py:class", "torch.optim.optimizer.Optimizer"),
    ("py:class", "torch.optim.lr_scheduler._LRScheduler"),
    ("py:class", "torch.utils.data.dataset.Dataset"),
    ("py:class", "torch.nn.modules.module.T"),
    ("py:class", "string"),
    ("py:class", "Module"),
    ("py:class", "optional"),
    ("py:class", "Dropout"),
    ("py:class", "BatchNorm"),
    ("py:class", "torch.utils.hooks.RemovableHandle"),
    ("py:class", "torch.nn.Parameter"),
    ("py:class", "Parameter"),
    ("py:class", "Tensor"),
    ("py:class", "Path"),
    ("py:class", "module"),
    ("py:attr", "persistent"),
    ("py:attr", "grad_input"),
    ("py:attr", "strict"),
    ("py:attr", "grad_output"),
    ("py:attr", "requires_grad"),
    ("py:attr", "device"),
    ("py:attr", "non_blocking"),
    ("py:attr", "dst_type"),
    ("py:attr", "dtype"),
    ("py:attr", "device"),
    ("py:func", "register_module_forward_hook"),
    ("py:func", "register_module_forward_pre_hook"),
    ("py:func", "register_module_full_backward_hook"),
    ("py:func", "register_module_full_backward_pre_hook"),
    ("py:class", "substra.sdk.schemas.Permissions"),
    ("py:class", "substra.Client"),
    ("py:class", "substra.sdk.client.Client"),
    ("py:class", "substra.sdk.models.ComputePlan"),
    ("py:class", "substra.sdk.schemas.FunctionOutputSpec"),
    ("py:class", "substra.sdk.schemas.FunctionInputSpec"),
    ("py:class", "ComputePlan"),
]

# This must be the name of an image file (path relative to the configuration
# directory) that is the favicon of the docs. Modern browsers use this as
# the icon for tabs, windows and bookmarks. It should be a Windows-style
# icon file (.ico).
html_favicon = "static/favicon.png"

# Add any paths that contain templates here, relative to this directory.
templates_path = ["templates/"]

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**/description.md"]

rst_epilog = f"""
.. |substra_version| replace:: {importlib.import_module('substra').__version__}
.. |substrafl_version| replace:: {importlib.import_module('substrafl').__version__}
"""

# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
html_theme = "sphinx_rtd_theme"

# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["./static"]

html_extra_path = [str(JSON_COMPATIBILITY_TABLE_FILE)]

html_css_files = [
    "owkin.css",
]

html_logo = "static/logo.svg"
html_show_sourcelink = False
html_show_sphinx = False

html_context = {
    "display_github": False,
}


from django.shortcuts import render
from article.models import Article

def HomePage(request):

    articles = Article.objects.all().order_by('-created_date')[:3]
    first_article = None
    second_article = None
    third_article = None

    if len(articles) >= 1:
        first_article = articles[0]
    if len(articles) >= 2:
        second_article = articles[1]
    if len(articles) >= 3:
        third_article = articles[2]

    context = {
        'first_article': first_article,
        'second_article': second_article,
        'third_article': third_article
    }

    return render(request, 'main/home.html', context)


from flask import current_app
from sqlalchemy import asc, desc
from sqlalchemy.exc import SQLAlchemyError

from app import db
from app.businesses.exceptions import EntityNotFoundException
from app.businesses.models import Business, Category, Tag, BusinessUpload

CONTAINS = '%{}%'

class BaseRepository(object):
    model = None
    _session = None

    def __init__(self):
        self._session = db.session

    @property
    def session(self):
        return self._session or current_app.session()

    @property
    def query(self):
        assert self.model, "A model is required to use the query property."
        return self.session.query(self.model)

    def get(self, id, strict=False):
        entity = self.query.get(id)
        if strict and not entity:
            raise EntityNotFoundException
        return entity

    def filter(self, **kwargs):
        query = self.query.filter_by(**kwargs)
        return query

    def save(self, entity):
        self.session.add(entity)
        try:
            self.session.commit()
        except SQLAlchemyError:
            self.session.rollback()
            raise
        return entity

    def save_many(self, entities):
        self.session.add_all(entities)
        self.session.commit()
        return entities

    def update(self, id_, **kwargs):
        db_entity = self.get(id_, strict=True)
        self._update_fields(db_entity, **kwargs)
        self.session.commit()
        return db_entity

    @classmethod
    def _update_fields(cls, db_entity, **kwargs):
        for key, value in kwargs.items():
            setattr(db_entity, key, value)

    def _delete(self, id_):
        try:
            db_entity = self.get(id_, strict=True)
            self.session.delete(db_entity)
            self.session.commit()
        except EntityNotFoundException:
            raise

class BusinessRepository(BaseRepository):
    model = Business

    def save(self, entity):
        entity = self.save_tags(entity)
        super(BusinessRepository, self).save(entity)
        return entity

    def save_tags(self, entity):
        tag_repository = TagRepository()
        existing_tag = {t.name: t for t in tag_repository.query.all()}
        tag_to_be_add = []
        for t in entity.tags:
            if t.name in existing_tag:
                tag_to_be_add.append(existing_tag[t.name])
            else:
                tag_to_be_add.append(t)
        entity.tags = tag_to_be_add
        return entity

    def filter(self, id=None, querySearch=None, accepted_at=None, status=None, order=None, order_by=None,
               exclude_deleted=None,
               **kwargs):
        query = self.query

        if exclude_deleted:
            query = query.filter(Business.deleted_at.is_(None))

        if id:
            return query.filter(Business.id == id)

        if querySearch:
            querySearch = CONTAINS.format(querySearch)
            query = query.filter(Business.name.ilike(querySearch))

        if accepted_at:
            query = query.filter(Business.accepted_at == accepted_at)

        if status:
            for s in status:
                query = query.filter(Business.status == s)

        if order_by:
            order = asc if order == "ASC" else desc
            if order_by == "name":
                query = query.order_by(order(Business.name))

        return query

    def delete(self, id):
        business = self.get(id=id)
        business.delete()
        self.save(business)

class CategoryRepository(BaseRepository):
    model = Category

    def save(self, entity):
        super(CategoryRepository, self).save(entity)
        return entity

    def filter(self, *args, **kwargs):
        query = super(CategoryRepository, self).filter(**kwargs)

        return query

    def delete(self, id_):
        self._delete(id_)

class TagRepository(BaseRepository):
    model = Tag

    def save(self, entity):
        super(TagRepository, self).save(entity)
        return entity

    def filter(self, *args, **kwargs):
        query = super(TagRepository, self).filter(**kwargs)

        return query

    def delete(self, id_):
        self._delete(id_)

    def get_tags_with_id(self, new_tags):
        existing_tag_names = {t.name: t for t in self.query.all()}
        tags_to_be_add = []
        for t in new_tags:
            if t.name in existing_tag_names:
                tags_to_be_add.append(existing_tag_names[t.name])
            else:
                tags_to_be_add.append(t)
        return tags_to_be_add

class BusinessUploadRepository(BaseRepository):
    model = BusinessUpload


""" Module to analyze vessel pulsatility during the heart cycle in ecg-gated CT
radius change - area change - volume change
Authors: Almar Klein and Maaike Koenrades. Created 2019.
"""

import os
import sys
import time

import openpyxl
import pirt
import numpy as np
import visvis as vv

from stentseg.utils.datahandling import select_dir, loadvol, loadmodel, loadmesh
from stentseg.stentdirect.stentgraph import create_mesh
from stentseg.motion.vis import create_mesh_with_abs_displacement
from stentseg.utils import PointSet, fitting

from lspeas.utils.vis import showModelsStatic
from lspeas.utils.deforminfo import DeformInfo
from lspeas.utils.curvature import measure_curvature #todo: check xyz vs zyx!
from lspeas.utils import meshlib

assert openpyxl.__version__ < "2.4", "Do pip install openpyxl==2.3.5"

#todo: move function to lspeas utils?
def load_excel_centerline(basedirCenterline, vol, ptcode, ctcode, filename=None):
    """ Load centerline data from excel
    Centerline exported from Terarecon; X Y Z coordinates in colums
    """
    if filename is None:
        filename = '{}_{}_centerline.xlsx'.format(ptcode,ctcode)
    excel_location = os.path.join(basedirCenterline,ptcode)
    #read sheet
    try:
        wb = openpyxl.load_workbook(os.path.join(excel_location,filename),read_only=True)
    except FileNotFoundError:
        wb = openpyxl.load_workbook(os.path.join(basedirCenterline,filename),read_only=True)
    sheet = wb.get_sheet_by_name(wb.sheetnames[0]) # data on first sheet
    colStart = 2 # col C
    rowStart = 1 # row 2 excel
    coordx = sheet.columns[colStart][rowStart:]
    coordy = sheet.columns[colStart+1][rowStart:]
    coordz = sheet.columns[colStart+2][rowStart:]
    #convert to values
    coordx = [obj.value for obj in coordx]
    coordy = [obj.value for obj in coordy]
    coordz = [obj.value for obj in coordz]
    #from list to array
    centerlineX = np.asarray(coordx, dtype=np.float32)
    centerlineY = np.asarray(coordy, dtype=np.float32)
    centerlineZ = np.asarray(coordz, dtype=np.float32)
    centerlineZ = np.flip(centerlineZ, axis=0) # z of volume is also flipped

    # convert centerline coordinates to world coordinates (mm)
    origin = vol1.origin # z,y,x
    sampling = vol1.sampling # z,y,x

    centerlineX = centerlineX*sampling[2] +origin[2]
    centerlineY = centerlineY*sampling[1] +origin[1]
    centerlineZ = (centerlineZ-0.5*vol1.shape[0])*sampling[0] + origin[0]

    return centerlineX, centerlineY, centerlineZ

# Select the ssdf basedir
basedir = select_dir(
    os.getenv('LSPEAS_BASEDIR', ''),
    r'D:\LSPEAS\LSPEAS_ssdf',
    r'F:\LSPEAS_ssdf_backup',
    r'F:\LSPEAS_ssdf_BACKUP')

basedirMesh = select_dir(
    r'D:\Profiles\koenradesma\SURFdrive\UTdrive\MedDataMimics\LSPEAS_Mimics',
    r'C:\Users\Maaike\SURFdrive\UTdrive\MedDataMimics\LSPEAS_Mimics',
    r"C:\stack\data\lspeas\vaatwand")

basedirCenterline = select_dir(
    r"C:\stack\data\lspeas\vaatwand",
    r'D:\Profiles\koenradesma\SURFdrive\UTdrive\LSPEAS_centerlines_terarecon',
    r'C:\Users\Maaike\SURFdrive\UTdrive\LSPEAS_centerlines_terarecon',
    r"C:\stack\data\lspeas\vaatwand")

# Select dataset
ptcode = 'LSPEAS_003'
ctcode1 = 'discharge'
cropname = 'ring'
modelname = 'modelavgreg'
cropvol = 'stent'

drawModelLines = False  # True or False
drawRingMesh, ringMeshDisplacement = True, False
meshColor = [(1,1,0,1)]
removeStent = True # for iso visualization
dimensions = 'xyz'
showAxis = False
showVol  = 'ISO'  # MIP or ISO or 2D or None
showvol2D = False
drawVessel = True

clim = (0,2500)
clim2D = -200,500 # MPR
clim2 = (0,2)
isoTh = 180 # 250

## Load data

# Load CT image data for reference, and deform data to measure motion
try:
    # If we run this script without restart, we can re-use volume and deforms
    vol1
    deforms
except NameError:
    vol1 = loadvol(basedir, ptcode, ctcode1, cropvol, 'avgreg').vol
    s_deforms = loadvol(basedir, ptcode, ctcode1, cropvol, 'deforms')
    deforms = [s_deforms[key] for key in dir(s_deforms) if key.startswith('deform')]
    deforms = [pirt.DeformationFieldBackward(*fields) for fields in deforms]

# Load vessel mesh (mimics)
# We make sure that it is a mesh without faces, which makes our sampling easier
try:
    ppvessel
except NameError:
    # Load mesh with visvis, then put in our meshlib.Mesh() and let it ensure that
    # the mesh is closed, check the winding, etc. so that we can cut it with planes,
    # and reliably calculate volume.
    filename = '{}_{}_neck.stl'.format(ptcode,ctcode1)
    vesselMesh = loadmesh(basedirMesh, ptcode[-3:], filename) #inverts Z
    vv.processing.unwindFaces(vesselMesh)
    vesselMesh = meshlib.Mesh(vesselMesh._vertices)
    vesselMesh.ensure_closed()
    ppvessel = PointSet(vesselMesh.get_flat_vertices())  # Must be flat!

# Load ring model
try:
    modelmesh1
except NameError:
    s1 = loadmodel(basedir, ptcode, ctcode1, cropname, modelname)
    if drawRingMesh:
        if not ringMeshDisplacement:
            modelmesh1 = create_mesh(s1.model, 0.7)  # Param is thickness
        else:
            modelmesh1 = create_mesh_with_abs_displacement(s1.model, radius = 0.7, dim=dimensions)

# Load vessel centerline (excel terarecon) (is very fast)
centerline = PointSet(np.column_stack(
    load_excel_centerline(basedirCenterline, vol1, ptcode, ctcode1, filename=None)))

## Setup visualization

# Show ctvolume, vessel mesh, ring model - this uses figure 1 and clears it
axes1, cbars = showModelsStatic(ptcode, ctcode1, [vol1], [s1], [modelmesh1],
              [vv.BaseMesh(*vesselMesh.get_vertices_and_faces())],
              showVol, clim, isoTh, clim2, clim2D, drawRingMesh,
              ringMeshDisplacement, drawModelLines, showvol2D, showAxis,
              drawVessel, vesselType=1,
              climEditor=True, removeStent=removeStent, meshColor=meshColor)
axes1 = axes1[0]
axes1.position = 0, 0, 0.6, 1

# Show or hide the volume (showing is nice, but also slows things down)
tex3d = axes1.wobjects[1]
tex3d.visible = False

# VesselMeshes
vesselVisMesh1 = axes1.wobjects[4]
vesselVisMesh1.cullFaces = "front"  # Show the back
# vesselVisMesh2 = vv.Mesh(axes1, *vesselMesh.get_vertices_and_faces())
vesselVisMesh2 = vv.Mesh(axes1, np.zeros((6, 3), np.float32), np.zeros((3, 3), np.int32))
vesselVisMesh2.cullFaces = "back"
vesselVisMesh2.faceColor = "red"

# Show the centerline
vv.plot(centerline, ms='.', ls='', mw=8, mc='b', alpha=0.5)

# Initialize 2D view
axes2 = vv.Axes(vv.gcf())
axes2.position = 0.65, 0.05, 0.3, 0.4
axes2.daspectAuto = False
axes2.camera = '2d'
axes2.axis.showGrid = True
axes2.axis.axisColor = 'k'

# Initialize axes to put widgets and labels in
container = vv.Wibject(vv.gcf())
container.position = 0.65, 0.5, 0.3, 0.5

# Create labels to show measurements
labelpool = []
for i in range(16):
    label = vv.Label(container)
    label.fontSize = 11
    label.position = 10, 100 + 25 * i, -20, 25
    labelpool.append(label)

# Initialize sliders and buttons
slider_ref = vv.Slider(container, fullRange=(1, len(centerline)-2), value=10)
slider_ves = vv.Slider(container, fullRange=(1, len(centerline)-2), value=10)
button_go = vv.PushButton(container, "Take all measurements (incl. volume)")
slider_ref.position = 10, 5, -20, 25
slider_ves.position = 10, 40, -20, 25
button_go.position = 10, 70, -20, 25
button_go.bgcolor = slider_ref.bgcolor = slider_ves.bgcolor = 0.8, 0.8, 1.0

# Initialize line objects for showing the plane orthogonal to centerline
slider_ref.line_plane = vv.plot([], [], [], axes=axes1, ls='-', lw=3, lc='w', alpha = 0.9)
slider_ves.line_plane = vv.plot([], [], [], axes=axes1, ls='-', lw=3, lc='y', alpha = 0.9)
# Initialize line objects for showing selected points close to that plane
slider_ref.line_3d = vv.plot([], [], [], axes=axes1, ms='.', ls='', mw=8, mc='w', alpha = 0.9)
slider_ves.line_3d = vv.plot([], [], [], axes=axes1, ms='.', ls='', mw=8, mc='y', alpha = 0.9)

# Initialize line objects for showing selected points and ellipse in 2D
line_2d = vv.plot([], [], axes=axes2,  ms='.', ls='', mw=8, mc='y')
line_ellipse1 = vv.plot([], [], axes=axes2,  ms='', ls='-', lw=2, lc='b')
line_ellipse2 = vv.plot([], [], axes=axes2,  ms='', ls='+', lw=2, lc='b')

## Functions to update visualization and do measurements

def get_plane_points_from_centerline_index(i):
    """ Get a set of points that lie on the plane orthogonal to the centerline
    at the given index. The points are such that they can be drawn as a line for
    visualization purposes. The plane equation can be obtained via a plane-fit.
    """

    if True:
        # Cubic fit of the centerline

        i = max(1.1, min(i, centerline.shape[0] - 2.11))
        # Sample center point and two points right below/above, using
        # "cardinal" interpolating (C1-continuous), or "basic" approximating (C2-continious).
        pp = []
        for j in [i - 0.1, i, i + 0.1]:
            index = int(j)
            t = j - index
            coefs = pirt.interp.get_cubic_spline_coefs(t, "basic")
            samples = centerline[index - 1], centerline[index], centerline[index + 1], centerline[index + 2]
            pp.append(samples[0] * coefs[0] + samples[1] * coefs[1] + samples[2] * coefs[2] + samples[3] * coefs[3])
        # Get center point and vector pointing down the centerline
        p = pp[1]
        vec1 = (pp[2] - pp[1]).normalize()

    else:
        # Linear fit of the centerline

        i = max(0, min(i, centerline.shape[0] - 2))
        index = int(i)
        t = i - index
        # Sample two points of interest
        pa, pb = centerline[index], centerline[index + 1]
        # Get center point and vector pointing down the centerline
        p = t * pb + (1 - t) * pa
        vec1 = (pb - pa).normalize()

    # Get two orthogonal vectors that define the plane that is orthogonal
    # to the above vector. We can use an arbitrary vector to get the first,
    # but there is a tiiiiiny chance that it is equal to vec1 so that the
    # normal collapese.
    vec2 = vec1.cross([0, 1, 0])
    if vec2.norm() == 0:
        vec2 = vec1.cross((1, 0, 0))
    vec3 = vec1.cross(vec2)

    # Sample some point on the plane and get the plane's equation
    pp = PointSet(3)
    radius = 6
    pp.append(p)
    for t in np.linspace(0, 2 * np.pi, 12):
        pp.append(p + np.sin(t) * radius * vec2 + np.cos(t) * radius * vec3)
    return pp

def get_vessel_points_from_plane_points(pp):
    """ Select points from the vessel points that are very close to the plane
    defined by the given plane points. Returns a 2D and a 3D point set.
    """
    abcd = fitting.fit_plane(pp)

    # Get 2d and 3d coordinates of points that lie (almost) on the plane
    # pp2 = fitting.project_to_plane(ppvessel, abcd)
    # pp3 = fitting.project_from_plane(pp2, abcd)
    signed_distances = fitting.signed_distance_to_plane(ppvessel, abcd)
    distances = np.abs(signed_distances)

    # Select points to consider. This is just to reduce the search space somewhat.
    selection = np.where(distances < 5)[0]

    # We assume that each tree points in ppvessel makes up a triangle (face)
    # We make sure of that when we load the mesh.
    # Select first index of each face (every 3 vertices is 1 face), and remove duplicates
    selection_faces = set(3 * (selection // 3))

    # Now iterate over the faces (triangles), and check each edge. If the two
    # points are on different sides of the plane, then we interpolate on the
    # edge to get the exact spot where the edge intersects the plane.
    sampled_pp3 = PointSet(3)
    visited_edges = set()
    for fi in selection_faces:  # for each face index
        for edge in [(fi + 0, fi + 1), (fi + 0, fi + 2), (fi + 1, fi + 2)]:
            if signed_distances[edge[0]] * signed_distances[edge[1]] < 0:
                if edge not in visited_edges:
                    visited_edges.add(edge)
                    d1, d2 = distances[edge[0]], distances[edge[1]]
                    w1, w2 = d2 / (d1 + d2), d1 / (d1 + d2)
                    p = w1 * ppvessel[edge[0]] + w2 * ppvessel[edge[1]]
                    sampled_pp3.append(p)

    return fitting.project_to_plane(sampled_pp3, abcd), sampled_pp3

def get_distance_along_centerline():
    """ Get the distance along the centerline between the two reference points,
    (using linear interpolation at the ends).
    """
    i1 = slider_ref.value
    i2 = slider_ves.value

    index1 = int(np.ceil(i1))
    index2 = int(np.floor(i2))
    t1 = i1 - index1  # -1 < t1 <= 0
    t2 = i2 - index2  # 0 <= t2 < 1

    dist = 0
    dist += -t1 * (centerline[index1] - centerline[index1 - 1]).norm()
    dist += +t2 * (centerline[index2] - centerline[index2 + 1]).norm()

    for index in range(index1, index2):
        dist += (centerline[index + 1] - centerline[index]).norm()

    return float(dist)

def triangle_area(p1, p2, p3):
    """ Calcualate triangle area based on its three vertices.
    """
    # Use Heron's formula to calculate a triangle's area
    # https://www.mathsisfun.com/geometry/herons-formula.html
    a = p1.distance(p2)
    b = p2.distance(p3)
    c = p3.distance(p1)
    s = (a + b + c) / 2
    return (s * (s - a) * (s - b) * (s - c)) ** 0.5

def deform_points_2d(pp2, plane):
    """ Given a 2D pointset (and the plane that they are on),
    return a list with the deformed versions of that pointset.
    """
    pp3 = fitting.project_from_plane(pp2, plane) #todo: shouldn't origin be subtracted?! see dynamic.py
    deformed = []
    for phase in range(len(deforms)):
        deform = deforms[phase]
        dx = deform.get_field_in_points(pp3, 0) #todo: shouldn't this be z=0 y=1 x=2! see dynamic.py; adapt in all functions?!
        dy = deform.get_field_in_points(pp3, 1)
        dz = deform.get_field_in_points(pp3, 2)
        deform_vectors = PointSet(np.stack([dx, dy, dz], 1))
        pp3_deformed = pp3 + deform_vectors
        deformed.append(fitting.project_to_plane(pp3_deformed, plane))
    return deformed

def measure_centerline_strain():
    """ Measure the centerline strain.
    """
    i1 = slider_ref.value
    i2 = slider_ves.value

    # Get the centerline section of interest
    index1 = int(np.ceil(i1))
    index2 = int(np.floor(i2))
    section = centerline[index1:index2 + 1]

    # get this section of the centerline for each phase
    sections = []
    for phase in range(len(deforms)):
        deform = deforms[phase]
        dx = deform.get_field_in_points(section, 0)
        dy = deform.get_field_in_points(section, 1)
        dz = deform.get_field_in_points(section, 2)
        deform_vectors = PointSet(np.stack([dx, dy, dz], 1))
        sections.append(section + deform_vectors)

    # Measure the strain of the full section, by measuring the total length in each phase.
    lengths = []
    for phase in range(len(deforms)):
        section = sections[phase]
        length = sum(float(section[i].distance(section[i + 1]))
                     for i in range(len(section) - 1))
        lengths.append(length)

    if min(lengths) == 0:
        return 0
    else:
        # Strain as delta-length divided by initial length
        return (max(lengths) - min(lengths)) / min(lengths)
        # ... or as what Wikipedia calls "stretch ratio">
        # return max(lengths) / min(lengths)

def take_measurements(measure_volume_change):
    """ This gets called when the slider is releases. We take measurements and
    update the corresponding texts and visualizations.
    """

    # Get points that form the contour of the vessel in 2D
    pp = get_plane_points_from_centerline_index(slider_ves.value)
    pp2, pp3 = get_vessel_points_from_plane_points(pp)
    plane = pp2.plane

    # Collect measurements in a dict. That way we can process it in one step at the end
    measurements = {}

    # Store slider positions, so we can reproduce this measurement later
    measurements["centerline indices"] = slider_ref.value, slider_ves.value

    # Early exit?
    if len(pp2) == 0:
        line_2d.SetPoints(pp2)
        line_ellipse1.SetPoints(pp2)
        line_ellipse2.SetPoints(pp2)
        vesselVisMesh2.SetFaces(np.zeros((3, 3), np.int32))
        vesselVisMesh2.SetNormals(None)
        process_measurements(measurements)
        return

    # Measure length of selected part of the centerline and the strain in that section
    measurements["centerline distance"] = get_distance_along_centerline()
    measurements["centerline strain"] = measure_centerline_strain()

    # Measure centerline curvature
    curvature_mean, curvature_max, curvature_max_pos, curvature_max_change = measure_curvature(centerline, deforms)
    measurements["curvature mean"] = DeformInfo(curvature_mean)
    measurements["curvature max"] = DeformInfo(curvature_max)
    measurements["curvature max pos"] = DeformInfo(curvature_max_pos)
    measurements["curvature max change"] = curvature_max_change

    # Get ellipse and its center point
    ellipse = fitting.fit_ellipse(pp2)
    p0 = PointSet([ellipse[0], ellipse[1]])

    # Sample ellipse to calculate its area
    pp_ellipse = fitting.sample_ellipse(ellipse, 256)  # results in N + 1 points
    area = 0
    for i in range(len(pp_ellipse)-1):
        area += triangle_area(p0, pp_ellipse[i], pp_ellipse[i + 1])
    # measurements["reference area"] = float(area)
    # Do a quick check to be sure that this triangle-approximation is close enough
    assert abs(area - fitting.area(ellipse)) < 2, "area mismatch"  # mm2  typically ~ 0.1 mm2

    # Measure ellipse area (and how it changes)
    measurements["ellipse area"] = DeformInfo(unit="mm2")
    for pp_ellipse_def in deform_points_2d(pp_ellipse, plane):
        area = 0
        for i in range(len(pp_ellipse_def)-1):
            area += triangle_area(p0, pp_ellipse_def[i], pp_ellipse_def[i + 1])
        measurements["ellipse area"].append(area)

    # # Measure expansion of ellipse in 256 locations?
    # # Measure distances from center to ellipse edge. We first get the distances
    # # in each face, for each point. Then we aggregate these distances to
    # # expansion measures. So in the end we have 256 expansion measures.
    # distances_per_point = [[] for i in range(len(pp_ellipse))]
    # for pp_ellipse_def in deform_points_2d(pp_ellipse, plane):
    #     # todo: Should distance be measured to p0 or to p0 in that phase?
    #     for i, d in enumerate(pp_ellipse_def.distance(p0)):
    #         distances_per_point[i].append(float(d))
    # distances_per_point = distances_per_point[:-1]  # Because pp_ellipse[-1] == pp_ellipse[0]
    # #
    # measurements["expansions"] = DeformInfo()  # 256 values, not 10
    # for i in range(len(distances_per_point)):
    #     distances = distances_per_point[i]
    #     measurements["expansions"].append((max(distances) - min(distances)) / min(distances))

    # Measure radii of ellipse major and minor axis (and how it changes)
    pp_ellipse4 = fitting.sample_ellipse(ellipse, 4)  # major, minor, major, minor
    measurements["ellipse expansion major1"] = DeformInfo(unit="mm")
    measurements["ellipse expansion minor1"] = DeformInfo(unit="mm")
    measurements["ellipse expansion major2"] = DeformInfo(unit="mm")
    measurements["ellipse expansion minor2"] = DeformInfo(unit="mm")
    for pp_ellipse4_def in deform_points_2d(pp_ellipse4, plane):
        measurements["ellipse expansion major1"].append(float( pp_ellipse4_def[0].distance(p0) ))
        measurements["ellipse expansion minor1"].append(float( pp_ellipse4_def[1].distance(p0) ))
        measurements["ellipse expansion major2"].append(float( pp_ellipse4_def[2].distance(p0) ))
        measurements["ellipse expansion minor2"].append(float( pp_ellipse4_def[3].distance(p0) ))

    # Measure how the volume changes - THIS BIT IS COMPUTATIONALLY EXPENSIVE
    submesh = meshlib.Mesh(np.zeros((3, 3)))
    if measure_volume_change:
        # Update the submesh
        plane1 = fitting.fit_plane(get_plane_points_from_centerline_index(slider_ref.value))
        plane2 = fitting.fit_plane(get_plane_points_from_centerline_index(slider_ves.value))
        plane2 = [-x for x in plane2]  # flip the plane upside doen
        submesh = vesselMesh.cut_plane(plane1).cut_plane(plane2)
        # Measure its motion
        measurements["volume"] = DeformInfo(unit="mm3")
        submesh._ori_vertices = submesh._vertices.copy()
        for phase in range(len(deforms)):
            deform = deforms[phase]
            submesh._vertices = submesh._ori_vertices.copy()
            dx = deform.get_field_in_points(submesh._vertices, 0)
            dy = deform.get_field_in_points(submesh._vertices, 1)
            dz = deform.get_field_in_points(submesh._vertices, 2)
            submesh._vertices[:, 0] += dx
            submesh._vertices[:, 1] += dy
            submesh._vertices[:, 2] += dz
            measurements["volume"].append(submesh.volume())

    # Show measurements
    process_measurements(measurements)

    # Update line objects
    line_2d.SetPoints(pp2)
    line_ellipse1.SetPoints(fitting.sample_ellipse(ellipse))
    major_minor = PointSet(2)
    for p in [p0, pp_ellipse4[0], p0, pp_ellipse4[2], p0, pp_ellipse4[1], p0, pp_ellipse4[3]]:
        major_minor.append(p)
    line_ellipse2.SetPoints(major_minor)
    axes2.SetLimits(margin=0.12)

    # Update submesh object
    vertices, faces = submesh.get_vertices_and_faces()
    vesselVisMesh2.SetVertices(vertices)
    vesselVisMesh2.SetFaces(np.zeros((3, 3), np.int32) if len(faces) == 0 else faces)
    vesselVisMesh2.SetNormals(None)

# Global value that will be a dictionary with measurements
mm = {}

def process_measurements(measurements):
    """ Show measurements. Now the results are shown in a label object, but we could do anything here ...
    """
    # Store in global for further processing
    mm.clear()
    mm.update(measurements)

    # Print in shell
    print("Measurements:")
    for key, val in measurements.items():
        val = val.summary if isinstance(val, DeformInfo) else val
        print(key.rjust(16) + ": " + str(val))

    # Show in labels
    index = 0
    for key, val in measurements.items():
        val = val.summary if isinstance(val, DeformInfo) else val
        val = "{:0.4g}".format(val) if isinstance(val, float) else val
        labelpool[index].text = key + ": " + str(val)
        index += 1
    # Clean remaining labels
    for index in range(index, len(labelpool)):
        labelpool[index].text = ""

def on_sliding(e):
    """ When the slider is moved, update the centerline position indicator.
    """
    slider = e.owner
    pp = get_plane_points_from_centerline_index(slider.value)
    slider.line_plane.SetPoints(pp)

def on_sliding_done(e):
    """ When the slider is released, update the whole thing.
    """
    slider = e.owner
    pp = get_plane_points_from_centerline_index(slider.value)
    slider.line_plane.SetPoints(pp)
    take_measurements(False)  # dont do volume

def on_button_press(e):
    """ When the button is pressed, take measurements.
    """
    take_measurements(True)

def set_sliders(value_ref, value_ves):
    """ Set the sliders to a specific position, e.g. to reproduce a measurement.
    """
    slider_ref.value = value_ref
    slider_ves.value = value_ves

# Connect!
slider_ref.eventSliding.Bind(on_sliding)
slider_ves.eventSliding.Bind(on_sliding)
slider_ref.eventSliderChanged.Bind(on_sliding_done)
slider_ves.eventSliderChanged.Bind(on_sliding_done)
button_go.eventMouseDown.Bind(on_button_press)

#todo: visualize mesh with motion and use colors to represent radius change
#todo: add torsion and angular rotation of centerline


from pants.base.exceptions import TargetDefinitionException
from pants.base.payload import Payload
from pants.base.payload_field import PrimitiveField
from pants.fs import archive as archive_lib

from pants.contrib.node.targets.node_package import NodePackage

class NodeBundle(NodePackage):
  """A bundle of node modules."""

  def __init__(self, node_module=None, archive='tgz', address=None, payload=None, **kwargs):
    """
    :param dependencies: a list of node_modules

    :param archive: a string, select from tar, tgz, tbz2, default to tgz
    """
    if archive not in archive_lib.TYPE_NAMES_PRESERVE_SYMLINKS:
      raise TargetDefinitionException(
        self, '{} is not a valid archive type. Allowed archive types are {}'.format(
          archive,
          ', '.join(sorted(list(archive_lib.TYPE_NAMES_PRESERVE_SYMLINKS)))))

    if not node_module:
      raise TargetDefinitionException(self, 'node_module can not be empty.')

    payload = payload or Payload()
    payload.add_fields({
      'archive': PrimitiveField(archive),
      'node_module': PrimitiveField(node_module),
    })
    super().__init__(address=address, payload=payload, **kwargs)

  @classmethod
  def compute_dependency_specs(cls, kwargs=None, payload=None):
    for spec in super().compute_dependency_specs(kwargs, payload):
      yield spec

    target_representation = kwargs or payload.as_dict()
    spec = target_representation.get('node_module')
    if spec:
      yield spec

  @property
  def node_module(self):
    if len(self.dependencies) != 1:
      raise TargetDefinitionException(
        self,
        'A node_bundle must define exactly one node_module dependency, have {}'.format(
          self.dependencies))
    else:
      return self.dependencies[0]


import bluetooth
import time
import serial  # 导入模块
import zxing
import numpy as np
import time
import cv2 as cv

reader = zxing.BarCodeReader()
log_counter = 0
start_time = time.time()
IMG_PATH='/home/pi/Documents/gx/0003.jpg'
# set blue thresh 设置HSV中蓝色、天蓝色范围
lower_red = np.array([0,43,46])
upper_red = np.array([15,255,255])
# lower_blue
lower_blue=np.array([95, 43, 46])
upper_blue=np.array([ 124, 255, 255])

lower_g=np.array([35, 43, 46])
upper_g=np.array([ 77, 255, 255])

UPPER_BOUND=600#130#600
LOWER_BOUND=1700#400#1700
def voice(txt, portx="/dev/ttyUSB0", bps=9600, timex=5,):
    try:
        # #端口，GNU / Linux上的/ dev / ttyUSB0 等 或 Windows上的 COM3 等
        # portx="/dev/ttyUSB0"
        # #波特率，标准值之一：50,75,110,134,150,200,300,600,1200,1800,2400,4800,9600,19200,38400,57600,115200
        # bps=9600
        # #超时设置,None：永远等待操作，0为立即返回请求结果，其他值为等待超时时间(单位为秒）
        # timex=5
        # # 打开串口，并得到串口对象
        ser = serial.Serial(portx, bps, timeout=timex)
        # 写数据
        result = ser.write(("<G>"+txt).encode("gbk"))
        print("写总字节数:", result)
        ser.close()  # 关闭串口
    except Exception as e:
        print("---异常---：", e)
    return

def scanQRCode(ImgPAth):
    barcode = reader.decode(ImgPAth)
    # logPrinter()
    print(barcode)
    print(barcode.raw)
    print(barcode.parsed)
    return barcode.parsed

reader = zxing.BarCodeReader()
log_counter = 0
time_start = time.time()

def logPrinter(logtxt):
    global log_counter
    # global start_time
    end_time = time.time()
    print("log: %d time %.3f  --  %s" %
          (end_time-start_time, log_counter, logtxt))
    log_counter += 1
    return

import numpy as np
import cv2
import time
def catch_picture():
    #调用笔记本内置摄像头，所以参数为0，如果有其他的摄像头可以调整参数为1，2
    start_time=time.time()
    cap=cv2.VideoCapture(2)
    while True:
        #从摄像头读取图片
        sucess,img=cap.read()
        #转为灰度图片
        # gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
        #显示摄像头，背景是灰度。
        # cv2.imshow("img",gray)
        #保持画面的持续。
        # k=cv2.waitKey(1)
        end_time=time.time()
        if end_time-start_time>2:
            cv2.imwrite(IMG_PATH,img)
            # cv2.destroyAllWindows()
            break
    return
# def if_qrcode():

#     return
def classify_color():
    frame=cv.imread(IMG_PATH)

    frame=frame[UPPER_BOUND:LOWER_BOUND][:][:]
    frame=cv2.resize(frame,(480,100))
    # cv.imshow('Capture', frame)
    # change to hsv model
    hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)

    # get mask 利用inRange()函数和HSV模型中蓝色范围的上下界获取mask，mask中原视频中的蓝色部分会被弄成白色，其他部分黑色。

    mask_b = cv.inRange(hsv, lower_blue, upper_blue)
    print(mask_b.shape)
    mask_r = cv.inRange(hsv, lower_red, upper_red)
    mask_g = cv.inRange(hsv, lower_g,upper_g)
    res_r = cv.bitwise_and(frame, frame, mask=mask_r)
    res_g = cv.bitwise_and(frame, frame, mask=mask_g)
    res_b = cv.bitwise_and(frame, frame, mask=mask_b)
    cv.imwrite('/home/pi/Documents/gx/r.jpg',res_r)
    cv.imwrite('/home/pi/Documents/gx/g.jpg',res_g)
    cv.imwrite('/home/pi/Documents/gx/b.jpg',res_b)
    kernel=np.ones((5,5),np.uint8)
    mask_b=cv.erode(mask_b,kernel,iterations=5)
    mask_r=cv.erode(mask_r,kernel,iterations=5)
    mask_g=cv.erode(mask_g,kernel,iterations=5)
    # mask_b=1-mask_b
    # mask_r=1-mask_r
    # mask_g=1-mask_g
    X_axis=np.arange(0,frame.shape[1],1)
    # mask=cv.erode(mask,kernel,iterations=1)
    mask_r=mask_r>0
    mask_g=mask_g>0
    mask_b=mask_b>0
    r_sum=(np.sum(np.nonzero(mask_r))-1)
    g_sum=(np.sum(np.nonzero(mask_g))-1)
    b_sum=(np.sum(np.nonzero(mask_b))-1)
    x_r=(np.multiply(X_axis,mask_r[:]).sum()/r_sum)if r_sum>200 else -1
    x_g=(np.multiply(X_axis,mask_g[:]).sum()/g_sum)if g_sum>200 else -1
    x_b=(np.multiply(X_axis,mask_b[:]).sum()/b_sum)if b_sum>200 else -1
    # res = cv.bitwise_and(frame, frame, mask=mask1)
    # cv.imwrite('det000.jpg',res)
    # # if __name__ == '__main__':
    # while True:
    #     cv.imshow('img', mask)
    #     if cv.waitKey() == ord('q'):
    #         break
    # cv.destroyAllWindows()

    return (x_r,x_g,x_b)
# def catch_

HOST_MAC_ADDR = 'DC:A6:32:AA:75:A3'  # 目的蓝牙的地址
sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
port = 1

if __name__ == "__main__":
    # try:
    print(classify_color())
    # return
#     sock.connect((HOST_MAC_ADDR, port))  # 连接蓝牙

#     while True:
#         now_time=time.time()
#         now_time-=start_time
#         print('now time')
#         # 使用计时或者串口回传
#         if now_time<10:
#             logPrinter('直线')
#         elif now_time<15:
#             logPrinter('拍照')
#             sock.send(('<G>'+scanQRCode(IMG_PATH)).encode())
#         elif now_time<30:
#             logPrinter('直线')
#         elif now_time<45:
#             logPrinter('左横')
#         else:
#             break
#         # sock.send('hello!'.encode()) #每隔三秒发送一个字符串
#             # time.sleep(3)
#     # except:
#     #     sock.close()
#     #     logPrinter('disconnect')
#     sock.close()
#     logPrinter('disconnect')
#     logPrinter('task finished')
# # if __name__ == "__main__":
# #     reader = zxing.BarCodeReader()
# #     log_counter=0
# #     time_start=time.time()
# #     # voice("绿色蓝色")
# #     logPrinter("000")


import cv2
from djitellopy import Tello

def initialize():
    drone = Tello()
    drone.connect()
    drone.for_back_velocity = 0
    drone.left_right_velocity = 0
    drone.up_down_velocity = 0
    drone.yaw_velocity = 0
    drone.speed = 0

    print(drone.get_battery())

    drone.streamon()
    return drone

def shutdown(drone):
    drone.streamoff()
    drone.land()
    exit(0)

def detect(drone, mtcnn, width=360, height=240):

    while True:

        # read stream from drone
        got_frame = drone.get_frame_read()
        frame = got_frame.frame
        vid = cv2.resize(frame, (width, height))
        vid = cv2.resize(vid, (width, height))

        box, prob, lms = mtcnn.detect(vid, landmarks=True)
        draw(vid, box, prob, lms)
        cv2.imshow("Drone Feed", vid)

        if cv2.waitKey(1) & 0xFF == ord("q"):
            break

    cv2.destroyAllWindows()

def draw(frame, box, prob, lms):

    if box is not None and prob is not None:
        for b, p, ld in zip(box, prob, lms):
            x, y, w, h = int(b[0]), int(b[1]), int(b[2]), int(b[3])

            # draw box
            cv2.rectangle(frame, (x, y), (w, h), (0, 255, 0), 2)

            # display probability
            cv2.putText(frame, str(p), (w, h), cv2.FONT_HERSHEY_SIMPLEX, 1,
                        (0, 255, 0), 2, cv2.LINE_AA)