# -*- coding: UTF-8 -*-
import os
from collections import OrderedDict

from PyQt5.QtWidgets import (QDialog, QAbstractItemView, QVBoxLayout,
    QHBoxLayout, QLabel, QWidget, QTreeView, QLineEdit, QTableView, QCompleter)
from PyQt5.QtGui import QStandardItem, QStandardItemModel, QPixmap
from PyQt5.QtCore import Qt, pyqtSlot, QModelIndex

from neox.commons.qt_models import get_simple_model
from neox.commons.forms import GridForm
from neox.commons.buttons import ActionButton

__all__ = ['QuickDialog', 'SearchDialog', 'HelpDialog',
    'CategoryDialog',
    'ProductDialog','FactoryIcons']

current_dir = os.path.dirname(__file__)
par_dir = os.path.dirname(current_dir)

_SIZE = (500, 200)
_TSIZE = (800,200)

class QuickDialog(QDialog):

    def __init__(self, parent, kind, string=None, data=None, widgets=None,
        icon=None, size=None, readonly=False, disable_cancel=False,
        active_widget=None, cancel_message=None, message_bar=None):
        super(QuickDialog, self).__init__(parent)
        # Size arg is in deprecation

        if not size:
            size = _SIZE
        self.factory = None
        self.readonly = readonly
        self.parent = parent
        self.active_widget = active_widget
        self.parent_model = None
        titles = {
            'warning': self.tr('Warning...'),
            'info': self.tr('Information...'),
            'action': self.tr('Action...'),
            'help': self.tr('Help...'),
            'error': self.tr('Error...'),
            'question': self.tr('Question...'),
            'selection': self.tr('Selection...'),
            None: self.tr('Dialog...')
        }
        self.setWindowTitle(titles[kind])
        self.setModal(True)
        self.setParent(parent)
        self.factory = FactoryIcons()
        self.default_widget_focus = None
        self.kind = kind
        self.widgets = widgets
        self.data = data
        self.disable_cancel = disable_cancel
        self.cancel_message = cancel_message
        self.message_bar = message_bar
        string_widget = None
        data_widget = None
        _buttons = None
        row_stretch = 1
        main_vbox = QVBoxLayout()

        self.sub_hbox = QHBoxLayout()

        # Add main message
        if string:
            # For simple dialog
            string_widget = QLabel(string)

        if kind == 'help':
            data_widget = widgets[0]
        elif kind == 'action':
            if widgets:
                data_widget = widgets[0]
            else:
                data_widget = GridForm(parent, OrderedDict(data))
        elif kind == 'selection':
            self.name = data['name']
            data_widget = self.set_selection(parent, data)
        elif widgets:
            data_widget = GridForm(parent, OrderedDict(widgets))

        if string_widget:
            main_vbox.addWidget(string_widget, 0)

        if data_widget:
            if isinstance(data_widget, QWidget):
                row_stretch += 1
                size = (size[0], size[1] + 200)
                self.sub_hbox.addWidget(data_widget, 0)
            else:
                self.sub_hbox.addLayout(data_widget, 0)

        self.ok_button = ActionButton('ok', self.dialog_accepted)
        self.ok_button.setFocus()
        self.cancel_button = ActionButton('cancel', self.dialog_rejected)

        _buttons = []
        if kind in ('info', 'help', 'warning', 'question', 'error'):
            if kind in ('warning', 'question'):
                _buttons.append(self.cancel_button)
            _buttons.append(self.ok_button)
        elif kind in ('action', 'selection'):
            _buttons.extend([self.cancel_button, self.ok_button])

        self.buttonbox = QHBoxLayout()
        for b in _buttons:
            self.buttonbox.addWidget(b, 1)

        main_vbox.addLayout(self.sub_hbox, 0)
        main_vbox.addLayout(self.buttonbox, 1)
        main_vbox.insertStretch(row_stretch, 0)

        self.setLayout(main_vbox)
        self.setMinimumSize(*size)

        if kind in ('info', 'error'):
            self.show()

    def exec_(self, args=None):
        res = None
        self.parent.releaseKeyboard()
        res = super(QuickDialog, self).exec()
        if self.kind == 'action':
            pass
        return res

    def show(self):
        super(QuickDialog, self).show()
        self.parent.releaseKeyboard()
        self.ok_button.setFocus()
        if self.default_widget_focus:
            self.default_widget_focus.setFocus()
            if hasattr(self.default_widget_focus, 'setText'):
                self.default_widget_focus.setText('')
        else:
            self.setFocus()

    def hide(self):
        super(QuickDialog, self).hide()
        self.parent.setFocus()

    def set_info(self, info):
        if hasattr(self, 'label_info'):
            self.label_info.setText(info)

    def set_widgets(self, widgets):
        if widgets:
            # Set default focus to first widget created
            self.default_widget_focus = widgets[0]

    def closeEvent(self, event):
        super(QuickDialog, self).closeEvent(event)
        if self.message_bar and self.cancel_message:
            self.message_bar.set(self.cancel_message)
        if self.active_widget:
            self.active_widget.setFocus()

    def dialog_rejected(self):
        self.parent.setFocus()
        if self.active_widget:
            self.active_widget.setFocus()
        self.setResult(0)
        self.hide()

    def dialog_accepted(self):
        if self.kind in ('action', 'selection', 'warning', 'question'):
            self.setResult(1)
            self.done(1)
        self.hide()

    def keyPressEvent(self, event):
        key = event.key()
        if key == Qt.Key_Escape:
            if self.disable_cancel==False:
                if self.message_bar and self.cancel_message:
                    self.message_bar.set(self.cancel_message)
                if self.active_widget:
                    self.active_widget.setFocus()
                self.dialog_rejected()
            return
        else:
            super(QuickDialog, self).keyPressEvent(event)

    def set_selection(self, obj, data):
        self.set_simple_model()
        setattr(obj, data['name'] + '_model', self.data_model)
        self.parent_model = data.get('parent_model')
        self.treeview = QTreeView()
        self.treeview.setRootIsDecorated(False)
        self.treeview.setColumnHidden(0, True)
        self.treeview.setItemsExpandable(False)
        self.treeview.setAlternatingRowColors(True)
        self.treeview.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.treeview.setModel(self.data_model)
        self.treeview.clicked.connect(self.field_selection_changed)
        self.treeview.activated.connect(self.field_selection_changed)

        self.update_values(self.data['values'])

        # By default first row must be selected
        item = self.data_model.item(0, 0)
        idx = self.data_model.indexFromItem(item)
        self.treeview.setCurrentIndex(idx)
        return self.treeview

    def update_values(self, values):
        self.data_model.removeRows(0, self.data_model.rowCount())
        self._insert_items(self.data_model, values)
        self.treeview.resizeColumnToContents(0)

    def set_simple_model(self):
        self.data_model = QStandardItemModel(0, len(self.data['heads']), self)
        _horizontal = Qt.Horizontal
        for i, h in enumerate(self.data['heads'], 0):
            self.data_model.setHeaderData(i, _horizontal, h)

    def _insert_items(self, model, values):
        for value in values:
            row = []
            for v in value:
                itemx = QStandardItem(v)
                itemx.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
                row.append(itemx)
            self.data_model.insertRow(0, row)
        self.data_model.sort(0, Qt.AscendingOrder)

    @pyqtSlot(QModelIndex)
    def field_selection_changed(self, qm_index):
        if not self.readonly:
            item_id = self.data_model.item(qm_index.row(), 0).text()
            item_name = self.data_model.item(qm_index.row(), 1).text()

            if self.parent_model is not None:
                self.parent_model[self.name] = item_id
            if hasattr(self.parent, 'field_' + self.name):
                field = getattr(self.parent, 'field_' + self.name)
                if hasattr(field, 'setText'):
                    field.setText(item_name)
            else:
                setattr(self.parent, 'field_' + self.name + '_name', item_name)
            setattr(self.parent, 'field_' + self.name + '_id', int(item_id))
            action = getattr(self.parent, 'action_' + self.name + '_selection_changed')
            action()
        self.dialog_accepted()

class SearchDialog(QDialog):

    def __init__(self, parent, headers, values, on_activated,
            hide_headers=False, completion_column=None, title=None):
        super(SearchDialog, self).__init__(parent)
        self.parent = parent
        self.headers = headers
        self.values = values
        if not title:
            title = self.tr('Search Products...')
        self.setWindowTitle(title)

        self._product_line = QLineEdit()
        self.table_view = QTableView()

        button_cancel = ActionButton('cancel', self.on_reject)
        vbox = QVBoxLayout()
        hbox = QHBoxLayout()
        hbox.addWidget(button_cancel)
        vbox.addWidget(self._product_line)
        vbox.addLayout(hbox)
        self.setLayout(vbox)
        self.completer = QCompleter()
        self.treeview_search_product = QTreeView()
        if hide_headers:
            col_headers = self.treeview_search_product.header()
            col_headers.hide()
        self.completer.setPopup(self.treeview_search_product)
        self._product_line.setCompleter(self.completer)
        self.set_model()

        self.completer.activated.connect(self.on_accept)
        self.completer.setFilterMode(Qt.MatchStartsWith)
        self.completer.setCaseSensitivity(Qt.CaseInsensitive)
        self.completer.setCompletionColumn(2)
        self.completer.activated.connect(on_activated)

    def set_model(self):
        headers_name = [h[1] for h in self.headers]
        self.model = get_simple_model(self.parent, self.values, headers_name)
        self.completer.setModel(self.model)

    def get_selected_index(self):
        model_index = self._get_model_index()
        idx = self.model.index(model_index.row(), 0)
        return idx.data()

    def get_selected_data(self):
        model_index = self._get_model_index()
        data = {}
        i = 0
        for h, _ in self.headers:
            data[h] = self.model.index(model_index.row(), i).data()
            i += 1
        return data

    def _get_model_index(self):
        item_view = self.completer.popup()
        index = item_view.currentIndex()
        proxy_model = self.completer.completionModel()
        model_index = proxy_model.mapToSource(index)
        return model_index

    def on_accept(self):
        self.accept()

    def on_reject(self):
        self.reject()

class HelpDialog(QuickDialog):

    def __init__(self, parent):
        self.treeview = QTreeView()
        self.treeview.setRootIsDecorated(False)
        self.treeview.setAlternatingRowColors(True)
        self.treeview.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.treeview.setEditTriggers(QAbstractItemView.NoEditTriggers)
        super(HelpDialog, self).__init__(parent, 'help', widgets=[self.treeview],
            size=(400, 500))
        self.set_info(self.tr('Keys Shortcuts...'))
        self.hide()

    def set_shortcuts(self, shortcuts):
        model = self._help_model(shortcuts)
        self.treeview.setModel(model)
        header = self.treeview.header()
        header.resizeSection(0, 250)

    def _help_model(self, shortcuts):
        model = QStandardItemModel(0, 2, self)
        model.setHeaderData(0, Qt.Horizontal, self.tr('Action'))
        model.setHeaderData(1, Qt.Horizontal, self.tr('Shortcut'))

        for short in shortcuts:
            model.insertRow(0)
            model.setData(model.index(0, 0), short[0])
            model.setData(model.index(0, 1), short[1])
        return model

class ProductDialog(QDialog):
    def __init__(self, parent, methods=None, widget=None, title=None):
        super(ProductDialog, self).__init__(parent)
        vbox = QVBoxLayout()
        self.parent = parent
        width = 1000
        height = 300
        if widget:
            vbox.addLayout(widget.products, 0)

            count = len(widget.products)

            #width = count * 250
            if count > 4:
                #width = 1000
                height = 600

        self.setWindowTitle(title)
        self.setLayout(vbox)
        self.resize(width, height)

    def keyPressEvent(self, event):
        key = event.key()
        if key == Qt.Key_Escape:
            self.dialog_rejected()
            return
        else:
            super(ProductDialog, self).keyPressEvent(event)

    def on_accept(self):
        self.accept()

    def on_reject(self):
        self.reject()

    def dialog_rejected(self):
        self.parent.setFocus()
        self.setResult(0)
        self.hide()

    def dialog_accepted(self):
        self.setResult(1)
        self.done(1)
        self.parent.setFocus()
        self.hide()

    def closeEvent(self, event):
        super(ProductDialog, self).closeEvent(event)

    def exec_(self, args=None):
        res = None
        self.parent.releaseKeyboard()
        res = super(ProductDialog, self).exec_()
        return res

    def show(self):
        super(ProductDialog, self).show()
        self.parent.releaseKeyboard()
        self.cancel_button.setFocus()
        self.setFocus()

    def hide(self):
        super(ProductDialog, self).hide()
        self.parent.setFocus()

class CategoryDialog(QDialog):
    def __init__(self, parent, methods=None, widget=None, title=None):
        super(CategoryDialog, self).__init__(parent)
        vbox = QVBoxLayout()
        self.parent = parent
        width = 500
        height = 300
        if widget:
            vbox.addLayout(widget.categories, 0)
            count = len(widget.categories)
            if count > 4:
                width = 1000
                height = 600
        self.setWindowTitle(title)
        self.setLayout(vbox)
        self.resize(width, height)

    def keyPressEvent(self, event):
        key = event.key()
        if key == Qt.Key_Escape:
            self.dialog_rejected()
            return
        else:
            super(CategoryDialog, self).keyPressEvent(event)

    def on_accept(self):
        self.accept()

    def on_reject(self):
        self.reject()

    def dialog_rejected(self):
        self.parent.setFocus()
        self.setResult(0)
        self.hide()

    def dialog_accepted(self):
        self.setResult(1)
        self.done(1)
        self.parent.setFocus()
        self.hide()

    def closeEvent(self, event):
        super(CategoryDialog, self).closeEvent(event)

    def exec_(self, args=None):
        res = None
        self.parent.releaseKeyboard()
        res = super(CategoryDialog, self).exec_()
        return res

    def show(self):
        super(CategoryDialog, self).show()
        self.parent.releaseKeyboard()
        self.cancel_button.setFocus()
        self.setFocus()

    def hide(self):
        super(CategoryDialog, self).hide()
        self.parent.setFocus()

class AgentDialog(QDialog):
    def __init__(self, parent, methods=None,widget=None,title=None):
        super(AgentDialog, self).__init__(parent)
        vbox = QVBoxLayout()
        self.parent = parent
        if widget:
            vbox.addLayout(widget.agents,0)
        self.setWindowTitle(title)
        self.setLayout(vbox)

    def keyPressEvent(self, event):
        key = event.key()
        if key == Qt.Key_Escape:
            self.dialog_rejected()
            return
        else:
            super(AgentDialog, self).keyPressEvent(event)

    def on_accept(self):
        self.accept()

    def on_reject(self):
        self.reject()

    def dialog_rejected(self):
        self.parent.setFocus()
        self.setResult(0)
        self.hide()

    def dialog_accepted(self):
        self.setResult(1)
        self.done(1)
        self.parent.setFocus()
        self.hide()

    def closeEvent(self, event):
        super(AgentDialog, self).closeEvent(event)

    def exec_(self, args=None):
        res = None
        self.parent.releaseKeyboard()
        res = super(AgentDialog, self).exec_()
        return res

    def show(self):
        super(AgentDialog, self).show()
        self.parent.releaseKeyboard()
        self.cancel_button.setFocus()
        self.setFocus()

    def hide(self):
        super(AgentDialog, self).hide()
        self.parent.setFocus()

class DeliveryDialog(QDialog):
    def __init__(self, parent, methods=None, widget=None, title=None):
        super(DeliveryDialog, self).__init__(parent)
        vbox = QVBoxLayout()
        self.parent = parent
        width = 800
        height = 300
        if widget:
            vbox.addLayout(widget.functions,0)
        self.setWindowTitle(title)
        self.setLayout(vbox)
        self.resize(width, height)

    def keyPressEvent(self, event):
        key = event.key()
        if key == Qt.Key_Escape:
            self.dialog_rejected()
            return
        else:
            super(DeliveryDialog, self).keyPressEvent(event)

    def on_accept(self):
        self.accept()

    def on_reject(self):
        self.reject()

    def dialog_rejected(self):
        self.parent.setFocus()
        self.setResult(0)
        self.hide()

    def dialog_accepted(self):
        self.setResult(1)
        self.done(1)
        self.parent.setFocus()
        self.hide()

    def closeEvent(self, event):
        super(DeliveryDialog, self).closeEvent(event)

    def exec_(self, args=None):
        res = None
        self.parent.releaseKeyboard()
        res = super(DeliveryDialog, self).exec_()
        return res

    def show(self):
        super(DeliveryDialog, self).show()
        self.parent.releaseKeyboard()
        self.cancel_button.setFocus()
        self.setFocus()

    def hide(self):
        super(DeliveryDialog, self).hide()
        self.parent.setFocus()

class FactoryIcons(object):

    def __init__(self):
        name_icons = ['print', 'warning', 'info', 'error', 'question']
        self.icons = {}
        for name in name_icons:
            path_icon = os.path.join(par_dir, 'share', 'icon-' + name + '.png')
            if not os.path.exists(path_icon):
                continue
            _qpixmap_icon = QPixmap()
            _qpixmap_icon.load(path_icon)
            _icon_label = QLabel()
            _icon_label.setAlignment(Qt.AlignCenter | Qt.AlignCenter)
            _icon_label.setPixmap(_qpixmap_icon.scaledToHeight(48))
            self.icons[name] = _icon_label

class TableDialog(QDialog):
    def __init__(self, parent, methods=None, widgets=[],
            table=None, title=None, cols_width=[]):
        super(TableDialog, self).__init__(parent)

        self.on_accepted_method = methods.get('on_accepted_method')

        vbox = QVBoxLayout()
        self.parent = parent
        self.ok_button = ActionButton('ok', self.dialog_accepted)
        self.cancel_button = ActionButton('cancel', self.dialog_canceled)
        self.cancel_button.setFocus()

        _buttons = []
        _buttons.extend([self.cancel_button, self.ok_button])

        buttonbox = QHBoxLayout()
        for b in _buttons:
            buttonbox.addWidget(b, 1)
        vbox.addLayout(buttonbox, 1)

        if widgets:
            vbox.addLayout(widgets, 2)

        if table:
            self.table = table
            vbox.addWidget(table, 3)

        self.setWindowTitle(title)
        self.setLayout(vbox)
        if cols_width:
            #print("cols_width >>", cols_width)
            WIDTH = sum(cols_width) + 75
        else:
            WIDTH = _TSIZE[0]
        size = (WIDTH+30, _TSIZE[1])
        self.setMinimumSize(*size)

    def keyPressEvent(self, event):
        key = event.key()
        if key == Qt.Key_Escape:
            self.dialog_canceled()
            return
        else:
            super(TableDialog, self).keyPressEvent(event)

    def dialog_canceled(self):
        self.parent.setFocus()
        self.setResult(0)
        self.hide()

    def dialog_accepted(self):
        if self.parent and self.on_accepted_method:
            getattr(self.parent, self.on_accepted_method)()
        self.parent.setFocus()
        self.setResult(1)
        self.done(1)
        self.hide()

    def exec_(self, args=None):
        res = None
        self.parent.releaseKeyboard()
        res = super(TableDialog, self).exec_()
        return res

    def show(self):
        super(TableDialog, self).show()
        self.parent.releaseKeyboard()
        self.cancel_button.setFocus()
        self.setFocus()

    def hide(self):
        super(TableDialog, self).hide()
        self.parent.setFocus()


from flask import Blueprint, render_template, abort

blueprint = Blueprint("geotagx-blog", __name__)

@blueprint.route("/", defaults={"page": 1})
@blueprint.route("/page/<int:page>")
def index(page):
    """Renders the blog page with the specified number.

    Args:
        page (int): A page number.

    Returns:
        unicode: The page's rendered HTML.
    """
    from pybossa.model.blogpost import Blogpost
    from sqlalchemy import desc
    from pybossa.util import Pagination

    page = 1 if page < 1 else page
    POSTS_PER_PAGE = 20
    pagination = Pagination(page, POSTS_PER_PAGE, Blogpost.query.count())

    posts_from = (page - 1) * POSTS_PER_PAGE
    if posts_from > pagination.total_count:
        abort(404)

    posts_to = posts_from + POSTS_PER_PAGE
    posts = Blogpost.query.order_by(desc(Blogpost.created)).slice(posts_from, posts_to).all()
    for p in posts:
        p.cover_image = _find_cover_image(p.body) # _find_cover_image must be called before the body is truncated by _summarize.
        p.body = _summarize(p.body)

    return render_template("blog/index.html", posts=posts, pagination=pagination)

@blueprint.route("/post/<int:id>", endpoint="post")
def render_post(id):
    """Renders the blog post with the specified identifier.

    Args:
        id (int): A blog post's unique identifier.

    Returns:
        unicode: The page's rendered HTML.
    """
    return render_template("blog/post.html", post=_get_post(id))

def _get_post(id):
    """Returns the blog post with the specified id.

    Args:
        id (int): A blog post's unique identifier.

    Returns:
        Blogpost | None: If found, an instance of the post with the specified id, None otherwise.
    """
    from pybossa.core import blog_repo
    return blog_repo.get(id)

def _find_cover_image(body):
    """Attempts to find a cover image to use for a summarized blog post.

    The cover image will be the first image found in the specified body.
    Because the body is written in markdown format, the algorithm works by
    looking for the pattern "![<label>](<URL>)" where <label> is an image
    label and <URL> is the URL to the image. If an occurrence of the
    aforementioned pattern is found, <URL> is returned.

    Args:
        body (str): A blog post's body.

    Returns:
        str | None: A URL to an image if successful, None otherwise.
    """
    from re import search

    result = None
    match = search("!\[.*\]\((.*)\)", body)
    if match:
        result = match.group(1)

    return result

def _summarize(body):
    """Summarizes the specified blog post's body.

    This function will extract the first paragraph from the specified body.
    While the function does a reasonable job, it is far from robust as it does not cover
    a few corner-cases of the markdown format. A better solution would be to introduce
    a 'summary' field to the Blogpost class, allowing authors to write their own summaries.

    Args:
        body (str): A blog post's body.

    Returns:
        str: A summary of the specified body.
    """
    summary = ""
    if body:
        # The first summary is at least a quarter of the original body's length.
        # Note that body is truncated after a paragraph.
        summary = body[:body.find("\r\n", len(body)/4)]

        # Remove all images from the summary since the cover image is already in use.
        from re import findall
        for image in findall('(!\[.*\]\(.*\))', summary):
            summary = summary.replace(image, "")

        # With the images removed, get rid of any leading whitespace that may have been introduced.
        summary = summary.lstrip()

        markdown_delimiters = set(["*", "#", "_"])
        limit = 0

        if summary[0] in markdown_delimiters:
            delimiter = summary[0]
            delimiter_length = 1
            while summary[delimiter_length] == delimiter:
                delimiter_length += 1

            delimiter *= delimiter_length
            limit = summary.find(delimiter, delimiter_length + 1) + delimiter_length
        else:
            minimum_length = 200
            limit = 0
            while limit < minimum_length:
                k = summary.find("\r\n", limit + 1)
                if k < limit:
                    break
                else:
                    limit = k

    return summary[:limit]


# -*- coding: utf-8 -*-
"""
Mask Filtering Utils (mask_utils.py)
====================================

Functions for filtering islands from channel masks. Currently in beta.
Also see :mod:`im_utils` for morphologic operations that could be useful.

Created on Mon Jul 6 18:29:23 2020

@author: Jon
"""
import numpy as np
import geopandas as gpd
import rivgraph.im_utils as iu
import rivgraph.geo_utils as gu
from scipy.ndimage import distance_transform_edt
from shapely.geometry import Polygon
from shapely.ops import cascaded_union
from scipy import stats
import rivgraph.im_utils as im
import networkx as nx

def pixagon(c_cent, r_cent, pixlen):
    """
    Returns a shapely polygon
    of a provided center coordinate given the pixel resolution (pixlen).
    """
    halflen = pixlen/2
    c_corner = np.array([c_cent-halflen, c_cent+halflen, c_cent+halflen,
                         c_cent-halflen, c_cent-halflen])
    r_corner = np.array([r_cent+halflen, r_cent+halflen, r_cent-halflen,
                         r_cent-halflen, r_cent+halflen])

    pixgon = Polygon(zip(c_corner, r_corner))

    return pixgon

def get_island_properties(Imask, pixlen, pixarea, crs, gt, props, connectivity=2):
    """Get island properties."""

    # maxwidth is an additional property
    if 'maxwidth' in props:
        props.remove('maxwidth')
        do_maxwidth = True
    else:
        do_maxwidth = False

    # Need perimeter to make island polygons
    if 'perimeter' not in props:
        props.append('perimeter')

    # Pad by one pixel to help identify and remove the outer portion of
    # the channel netowrk
    Imaskpad = np.array(np.pad(Imask, 1, mode='constant'), dtype=bool)
    Imp_invert = np.invert(Imaskpad)

    rp_islands, Ilabeled = iu.regionprops(Imp_invert, props=props, connectivity=connectivity)

    # Make polygons of the island perimeters
    # Also get ids to match the labeled image
    pgons = []
    ids = []
    for ip, p in enumerate(rp_islands['perimeter']):
        ids.append(Ilabeled[p[0][0], p[0][1]])  # Store the index

        p = np.vstack((p, p[0]))  # Close the polygon
        # Adjust for the single-pixel padding we added to the image
        cr = gu.xy_to_coords(p[:, 1] - 1, p[:, 0] - 1, gt)

        # Special cases: where the island is two pixels or less, we use the
        # corner coordinates rather than the center coordinates to define
        # the polygon.
        if len(cr[0]) <= 2:
            pixgon = [pixagon(cc, rc, pixlen) for cc, rc, in zip(cr[0], cr[1])]
            if len(pixgon) > 1:
                pixgon = cascaded_union(pixgon)
            pgons.append(pixgon)
        else:
            pgons.append(Polygon(zip(cr[0], cr[1])))

    # Do maximum width if requested
    if do_maxwidth:
        Idist = distance_transform_edt(Imp_invert)
        maxwids = []
        for i in ids:
            maxwids.append(np.max(Idist[Ilabeled == i])*2*pixlen)

    # Convert requested properties to proper units
    if 'area' in props:
        rp_islands['area'] = rp_islands['area'] * pixarea
    if 'major_axis_length' in props:
        rp_islands['major_axis_length'] = rp_islands['major_axis_length'] * pixlen
    if 'minor_axis_length' in props:
        rp_islands['minor_axis_length'] = rp_islands['minor_axis_length'] * pixlen
    if 'perim_len' in props:
        rp_islands['perim_len'] = rp_islands['perim_len'] * pixlen
    if 'convex_area' in props:
        rp_islands['convex_area'] = rp_islands['convex_area'] * pixarea

    # Need to change 'area' key as it's a function in geopandas
    if 'area' in rp_islands:
        rp_islands['Area'] = rp_islands.pop('area')

    # Create islands geodataframe
    gdf_dict = {k:rp_islands[k] for k in rp_islands if k not in ['coords', 'perimeter', 'centroid']}
    gdf_dict['geometry'] = pgons
    gdf_dict['id'] = ids
    if do_maxwidth:
        gdf_dict['maxwid'] = maxwids
    gdf = gpd.GeoDataFrame(gdf_dict)
    gdf.crs = crs

    # Identify and remove the border blob
    border_id = Ilabeled[0][0]
    Ilabeled[Ilabeled == border_id] = 0
    gdf = gdf[gdf.id.values != border_id]

    # Put 'id' column in front
    colnames = [k for k in gdf.keys()]
    colnames.remove('id')
    colnames.insert(0, 'id')
    gdf = gdf[colnames]

    return gdf, Ilabeled[1:-1, 1:-1]

def surrounding_link_properties(links, nodes, Imask, islands, Iislands,
                                pixlen, pixarea):
    """
    Find the links surrounding each island and computes their properties. This
    function is useful for filtering; e.g. when it is desired to remove islands
    surrounded by very large channels.

    Parameters
    ----------
    links : dict
        Network links.
    nodes : dict
        Network nodes.
    Imask : np.array
        Binary mask of the channel network.
    islands : geopandas.GeoDataframe
        Contains island boundaries and associated properties. Created by
        get_island_properties().
    Iislands : np.array
        Image wherein each island has a unique integer ID.
    pixlen : numeric
        Nominal length of a pixel (i.e. its resolution).
    pixarea : numeric
        Nominal area of a pixel.

    Returns
    -------
    islands : geopandas.GeoDataframe
        DESCRIPTION.

    """
    # obj = d
    # links = obj.links
    # nodes = obj.nodes
    # Imask = obj.Imask
    # pixlen = obj.pixlen
    # pixarea = obj.pixarea
    # gt = obj.gt
    # crs = obj.crs
    # props=['area', 'maxwidth', 'major_axis_length', 'minor_axis_length']
    # islands, Iislands = get_island_properties(obj.Imask, pixlen, pixarea, obj.crs, obj.gt, props)
    # islands.to_file(r"C:\Users\Jon\Desktop\Research\John Shaw\Deltas\GBM\GBM_islands.shp")
    # np.save(r'C:\Users\Jon\Desktop\Research\John Shaw\Deltas\GBM\GBM_Iislands.npy', Iislands)
    # # islands = gpd.read_file(r"C:\Users\Jon\Desktop\Research\eBI\Results\Indus\Indus_islands.shp")
    # # Iislands = np.load(r'C:\Users\Jon\Desktop\Research\eBI\Results\Indus\Indus_Iislands.npy')

    # Rasterize the links and nodes
    Iln = np.zeros(Imask.shape, dtype=int)

    # Burn links into raster
    for lidcs in links['idx']:
        rcidcs = np.unravel_index(lidcs, Iln.shape)
        Iln[rcidcs] = 1
    # Burn nodes into raster, but use their negative so we can find them later
    for nid, nidx in zip(nodes['id'], nodes['idx']):
        rc = np.unravel_index(nidx, Iln.shape)
        Iln[rc] = -nid

    # Pad Ilids and Imask to avoid edge effects later
    npad = 8
    Iln = np.pad(Iln, npad, mode='constant')
    Imask = np.array(np.pad(Imask, npad, mode='constant'), dtype=bool)
    Iislands = np.pad(Iislands, npad, mode='constant')

    # Make a binary version of the network skeleton
    Iskel = np.array(Iln, dtype=bool)
    # Invert the skeleton
    Iskel = np.invert(Iskel)

    # Find the regions of the inverted map
    regions, Ireg = im.regionprops(Iskel, props=['coords', 'area', 'label'],
                                   connectivity=1)
    regions['area'] = regions['area'] * pixarea

    # Dilate each region and get the link ids that encompass it
    # Ensure the set of link ids forms a closed loop; remove link ids that don't
    # Use the loop links to compute the average river width around the island
    # Finally, map the region to its corresponding island and compute the island
    # properties to determine whether or not to fill it
    keys = ['sur_area', 'sur_avg_wid', 'sur_max_wid', 'sur_min_wid']
    for k in keys:
        islands[k] = [np.nan for r in range(len(islands))]
    islands['sur_link_ids'] = ['' for r in range(len(islands))]

    # # Can speed up the calculation by skipping huge regions
    # max_area = np.mean(links['wid_adj'])**2 * 20

    imshape = Ireg.shape
    for idx in range(len(islands)):
        print(idx)

        # Identify the region associated with the island
        i_id = islands.id.values[idx]
        r_id = stats.mode(Ireg[Iislands == i_id])[0][0]

        # It is possible that the corresponding region is a 0 pixel, or one
        # that comprises the network. This usually happens only when the island
        # is one or two pixels. Skip these islands
        if r_id == 0:
            continue
        r_idx = np.where(regions['label'] == r_id)[0][0]

        # Get the region's properties
        ra = regions['area'][r_idx]
        rc = regions['coords'][r_idx]

        # if ra > max_area:
        #     continue

        # Make region blob
        Irblob, cropped = im.crop_binary_coords(rc)

        # Pad and dilate the blob
        Irblob = np.pad(Irblob, npad, mode='constant')
        Irblob = np.array(im.dilate(Irblob, n=2, strel='disk'), dtype=bool)

        # Adjust padded image in case pads extend beyond original image boundary
        if cropped[0] - npad < 0:
            remove = npad - cropped[0]
            Irblob = Irblob[:, remove:]
            cropped[0] = 0
        else:
            cropped[0] = cropped[0] - npad
        if cropped[1] - npad < 0:
            remove = npad - cropped[1]
            Irblob = Irblob[abs(remove):, :]
            cropped[1] = 0
        else:
            cropped[1] = cropped[1] - npad
        if cropped[2] + npad > imshape[1]:
            remove = (cropped[2] + npad) - imshape[1]
            Irblob = Irblob[:, :(-remove-1)]
            cropped[2] = imshape[1]
        else:
            cropped[2] = cropped[2] + npad
        if cropped[3] + npad > imshape[0]:
            remove = (cropped[3] + npad) - imshape[0]
            Irblob = Irblob[:(-remove-1), :]
            cropped[3] = imshape[0]
        else:
            cropped[3] = cropped[3] + npad

        # Get node ids that overlap the dilated blob
        Iln_crop = Iln[cropped[1]:cropped[3]+1, cropped[0]:cropped[2]+1]
        lids = Iln_crop[Irblob]
        overlap_nodes = -np.unique(lids[lids < 0])

        # Get the links connected to the overlap nodes so we can construct the
        # mini-graph
        overlap_links = [li for l in [nodes['conn'][nodes['id'].index(nid)] for nid in overlap_nodes] for li in l]

        # Try to find a loop using the identified link ids
        G = nx.Graph()
        G.add_nodes_from(overlap_nodes)
        lconn = [links['conn'][links['id'].index(lid)] for lid in overlap_links]
        for lc in lconn:
            G.add_edge(lc[0], lc[1])
        surrounding_nodes = nx.cycle_basis(G)

        # Check if we're dealing with a parallel loop
        if len(surrounding_nodes) == 0:
            if len(overlap_nodes) == 2:
                if sum([l in nodes['conn'][nodes['id'].index(overlap_nodes[1])] for l in nodes['conn'][nodes['id'].index(overlap_nodes[0])]]) > 1:
                    surrounding_nodes = [[o for o in overlap_nodes]]
            else:  # We assume that if no loops were found, this must be a parallel loop
                for on in overlap_nodes:
                    conn = nodes['conn'][nodes['id'].index(on)]
                    for on2 in overlap_nodes:
                        if on2 == on:
                            continue
                        else:
                            conn2 = nodes['conn'][nodes['id'].index(on2)]
                            if sum([c in conn2 for c in conn]) == 2:
                                surrounding_nodes = [[on, on2]]
                                break

        # # Check if links are at outlet or inlet
        # if len(surrounding_nodes) == 0:
        #     poss_nodes = np.array([links['conn'][links['id'].index(lid)] for lid in lids]).flatten()
        #     if any(np.in1d(poss_nodes, nodes['inlets'])) or any(np.in1d(poss_nodes, nodes['outlets'])):
        #         # Only keep link ids that have 3 or more occurrences
        #         surrounding_nodes = [[lid for lid, ct in zip(cts[0], cts[1]) if ct > 2]]
        #         print('io:{}'.format(ic))

        if len(surrounding_nodes) == 0:
            Warning('Cant find surrounding links for region {}.'.format(idx))

        # If multiple loops were found
        if len(surrounding_nodes) > 1:
            # Choose the surrounding nodes that contain the highest
            # fraction of overlap with the overlap_nodes
            fracs = []
            for sn in surrounding_nodes:
                in_or_out = [s in overlap_nodes for s in sn]
                fracs.append(sum(in_or_out)/len(overlap_nodes))
            surrounding_nodes = [surrounding_nodes[fracs.index(max(fracs))]]

        # At this point, only one loop should be present
        # if len(surrounding_nodes) != 1:
        #     import pdb
        #     pdb.set_trace()
        assert(len(surrounding_nodes)==1)
        surrounding_nodes = surrounding_nodes[0]
        surrounding_nodes.append(surrounding_nodes[0])

        # Get the links of the loop
        surrounding_links = []
        for i in range(len(surrounding_nodes)-1):
            n1 = surrounding_nodes[i]
            n2 = surrounding_nodes[i+1]
            for lid in overlap_links:
                lconn = links['conn'][links['id'].index(lid)]
                if n1 in lconn and n2 in lconn:
                    surrounding_links.append(lid)
        surrounding_links = list(set(surrounding_links))
        islands.sur_link_ids.values[idx] = str(surrounding_links)

        # Now that links surrounding the island are known, can compute some
        # of their morphologic metrics.
        # Use a length-weighted width. Could alternatively use the 'wid_pix' but
        # that includes the misleading connector pixels
        wids = np.array([links['wid_adj'][links['id'].index(lid)] for lid in surrounding_links])
        lens = np.array([links['len_adj'][links['id'].index(lid)] for lid in surrounding_links])
        avg_wid = np.sum(wids * lens) / np.sum(lens)
        islands.sur_avg_wid.values[idx] = avg_wid
        islands.sur_max_wid.values[idx] = np.max(wids)
        islands.sur_min_wid.values[idx] = np.min(wids)
        islands.sur_area.values[idx] = ra  # already converted to pixarea

    return islands

def thresholding_set1(islands, apex_width):

    # Thresholding
    remove = set()

    # Global thresholding -- islands smaller than 1/10 the apex_wid^2
    area_thresh = (1/10 * apex_width)**2
    remove.update(np.where(islands.Area.values < area_thresh)[0].tolist())

    # Threshold islands whose major axis length is less than 1/3 of the apex width
    maj_axis_thresh = apex_width/4
    remove.update(np.where((islands.major_axis_length.values < maj_axis_thresh))[0].tolist())

    # Threshold island area/surrounding area
    area_rat_thresh = 0.01
    remove.update(np.where(islands.Area.values/islands.sur_area.values < area_rat_thresh)[0].tolist())

    # # Threshold average island width as a fraction of surrounding channel widths
    avgwid_ratio_thresh = 0.1
    imal = islands.major_axis_length.values
    imal[imal == 0] = np.nan
    avg_island_wid = islands.Area.values / imal
    remove.update(np.where(avg_island_wid/islands.sur_avg_wid.values < avgwid_ratio_thresh)[0].tolist())

    # Keep islands with a major axis length greater than the apex width
    keep = set()
    keep.update(np.where(islands.major_axis_length.values > apex_width)[0].tolist())

    # Do the thresholding
    remove = remove - keep

    return remove


"""Solution to day 07 of the Advent of Code."""

import heapq
import string
from typing import Any, Dict, List, Set, Tuple

import utils

class Graph:
    """Represents a graph.

    Attributes:
        adj_list: A dictionary that maps each vertex of the graph to the
            vertices it is connected to.
    """

    def __init__(self) -> None:
        self._adj_list = {}

    def add_vertex(self, vertex: Any) -> None:
        """Adds a vertex to the graph.

        Args:
            vertex: The vertex to add.
        """
        if vertex not in self._adj_list:
            self._adj_list[vertex] = []

    def add_edge(self, u: Any, v: Any) -> None:  # pylint: disable=C0103
        """Adds an edge to the graph.

        Args:
            u: The vertex the edge comes from.
            v: The vertex the edge goes to.
        """
        self.add_vertex(u)
        self.add_vertex(v)
        self._adj_list[u].append(v)

    def _topological_sort_worker(self, vertex: Any, visited: Set[Any],
                                 stack: List[Any]):
        """A recursive worker function use by topological sort.

        Applies a DFS-like search on the graph, starting at the given vertex.
        Once the search is complete for all of the vertex's children, the vertex
        is added to the shared stack.

        Args:
            vertex: The vertex to start the search from.
            visited: A set containing all vertices that have been visited.
            stack: A stack containing the vertices in reversed topological
                order.
        """
        visited.add(vertex)
        for child in self._adj_list[vertex]:
            if child not in visited:
                self._topological_sort_worker(child, visited, stack)
        stack.append(vertex)

    def topological_sort(self) -> List[Any]:
        """Provides the graph's vertices in topological order.

        There are no guarantees on which topological order will be provided.

        Returns:
            A list of vertices in topological order.
        """
        visited = set()
        stack = []
        for vertex, _ in self._adj_list.items():
            if vertex not in visited:
                self._topological_sort_worker(vertex, visited, stack)
        stack.reverse()
        return stack

    def specific_topological_sort(self) -> List[Any]:
        """Provides the graph's vertices in a very specific topological order.

        See https://adventofcode.com/2018/day/7 Part 1 for details.

        Returns:
            A list of vertices in topological order.
        """
        completed = []
        parent_count = {vertex: 0 for vertex, _ in self._adj_list.items()}
        for _, children in self._adj_list.items():
            for child in children:
                parent_count[child] += 1
        available = [
            vertex for vertex, count in parent_count.items() if count == 0
        ]
        heapq.heapify(available)
        while available:
            vertex = heapq.heappop(available)
            completed.append(vertex)
            for child in self._adj_list[vertex]:
                parent_count[child] -= 1
                if parent_count[child] == 0:
                    heapq.heappush(available, child)
        return completed

    def multiworker_step_sort(self, durations: Dict[Any, int],
                              workers: int = 5) -> List[Any]:
        """Provides the graph's vertices in a very specific topological order.

        See https://adventofcode.com/2018/day/7 Part 2 for details.

        Args:
            durations: A dictionary that maps each vertex to the amount of time
                required to complete the corresponding step.
            workers: The number of workers working on the steps.

        Returns:
            A list of vertices in topological-like order.
        """
        completed = []
        parent_count = {vertex: 0 for vertex, _ in self._adj_list.items()}
        for _, children in self._adj_list.items():
            for child in children:
                parent_count[child] += 1
        available = [
            vertex for vertex, count in parent_count.items() if count == 0
        ]
        progress = [None] * workers
        heapq.heapify(available)
        time_count = -1  # One extra iteration is required to get started.
        while len(completed) < len(self._adj_list):
            time_count += 1
            for worker, work in enumerate(progress):
                # Work on in-progress steps for 1 time unit
                if work is not None:
                    (step, time_left) = work
                    time_left -= 1

                    if time_left > 0:
                        progress[worker] = (step, time_left)
                        continue

                    completed.append(step)
                    for child in self._adj_list[step]:
                        parent_count[child] -= 1
                        if parent_count[child] == 0:
                            heapq.heappush(available, child)
                    progress[worker] = None

            for worker, work in enumerate(progress):
                # Assign steps to free workers
                if work is None:
                    if available:
                        step = heapq.heappop(available)
                        progress[worker] = (step, durations[step])
                    continue

        return completed, time_count

def _read_dependencies(input_string: str) -> List[Tuple[str, str]]:
    """Reads step dependencies from a given input string.

    Args:
        input_string: A string containing the day's input.

    Returns:
        A list of dependencies represented as tuples of step names. The first
        step must be completed before the second.
    """
    dependencies = []
    for line in input_string.split('\n'):
        words = line.split()
        dependencies.append((words[1], words[7]))
    return dependencies

def _build_dependency_graph(dependencies: List[Tuple[str, str]]) -> Graph:
    """Builds a directed acyclic graph from a list of dependencies.

    Args:
        dependencies: A list dependencies represented as pairs of step names.

    Returns:
        A directed acyclic graph representing the dependencies.
    """
    graph = Graph()
    for dependency in dependencies:
        graph.add_edge(*dependency)
    return graph

def _get_step_durations(  # pylint: disable=W0102
        steps: List[Any] = list(string.ascii_uppercase),
        offset: int = 61) -> Dict[Any, int]:
    return {step: offset + i for i, step in enumerate(steps)}

def get_step_order(input_string: str) -> str:
    """Finds the order the instructions should be completed in.

    Args:
        input_string: The puzzle input.

    Returns:
        A string with the step names written in order.
    """
    dependencies = _read_dependencies(input_string)
    graph = _build_dependency_graph(dependencies)
    return "".join(graph.specific_topological_sort())

def get_multiworker_total_time(input_string: str,
                               offset: int = 61,
                               workers: int = 5) -> str:
    """Finds the total time required by multiple workers to complete all steps.

    Args:
        input_string: The puzzle input.
        offset: The offset in seconds of the step durations. Step A will take
            offset seconds, step B offset+1, etc.
        workers: The number of workers working on the steps in parallel.

    Returns:
        An integer representing the total time required in seconds.
    """
    dependencies = _read_dependencies(input_string)
    graph = _build_dependency_graph(dependencies)
    step_durations = _get_step_durations(offset=offset)
    _, total_time = graph.multiworker_step_sort(step_durations, workers)
    return total_time

def _run_tests() -> None:
    """Tests solution."""
    assert get_step_order(
        'Step C must be finished before step A can begin.\n'
        'Step C must be finished before step F can begin.\n'
        'Step A must be finished before step B can begin.\n'
        'Step A must be finished before step D can begin.\n'
        'Step B must be finished before step E can begin.\n'
        'Step D must be finished before step E can begin.\n'
        'Step F must be finished before step E can begin.') == 'CABDFE'
    assert get_multiworker_total_time(
        'Step C must be finished before step A can begin.\n'
        'Step C must be finished before step F can begin.\n'
        'Step A must be finished before step B can begin.\n'
        'Step A must be finished before step D can begin.\n'
        'Step B must be finished before step E can begin.\n'
        'Step D must be finished before step E can begin.\n'
        'Step F must be finished before step E can begin.',
        offset=1,
        workers=2) == 15

def _print_answers(step_order: str, multiworker_total_time: str) -> None:
    """Prints answers.

    Args:
        step_order: The order to complete the instructions in.
        multiworker_total_time: The total number of seconds required to complete
            all steps with multiple workers.
    """
    print('Answers for day 07:')
    print(f'  Step order: {step_order}')
    print(f'  Multi-worker total time: {multiworker_total_time}')

def main() -> None:
    """Runs tests and prints answers to day's puzzle."""
    _run_tests()
    input_string = utils.read_input(7)
    step_order = get_step_order(input_string)
    multiworker_total_time = get_multiworker_total_time(input_string)
    _print_answers(step_order, multiworker_total_time)

if __name__ == '__main__':
    main()


from __future__ import annotations
from typing import overload
from typing import Union, List

import numpy as np

import Component
import CalcGraph

class Circuit:
    def __init__(self):
        self.node: List[Node] = []
        self.component: List[Component.ComponentBase] = []

    def __getitem__(self, label: str):
        return self.find_by_label(label)

    def find_by_label(self, target_label: str) -> None | Component.ComponentBase | Node:
        if target_label == '':
            return None

        for comp in self.component:
            if comp.label == target_label:
                return comp

        for n in self.node:
            if n.label == target_label:
                return n

        return None

    def add_node(self, n: Node) -> bool:
        if self.find_by_label(n.label) is not None:
            return False

        self.node.append(n)
        return True

    def remove_node(self, n: Node) -> bool:
        if n is None:
            return False

        if type(n) != Node:
            return False

        self.node.remove(n)
        return True

    def add_component(self, comp: Component.ComponentBase) -> bool:
        if self.find_by_label(comp.label) is not None:
            return False

        self.component.append(comp)
        return True

    def remove_component(self, comp: Component.ComponentBase) -> bool:
        # Detach from circuit
        node_buf = comp.pin.copy()
        for i in range(len(comp.pin)):
            comp.pin[i] = None

        # Remove component from nodes
        for i in range(len(node_buf)):
            node_buf[i].remove_component_if_not_connected(comp)

        self.component.remove(comp)
        del comp
        return True

    def remove_component_by_label(self, label: str) -> bool:
        comp = self.find_by_label(label)
        if comp is None:
            return False

        return self.remove_component(comp)

class Node:
    def __init__(self, parent: Circuit, init_comp: Component.ComponentBase):
        self.label = ''
        self.parent = parent
        self.component: List[Component.ComponentBase] = [init_comp]

        self.prev_voltage = CalcGraph.Number(np.NaN)

        parent.add_node(self)

    def set_label(self, new_label: str):
        if self.parent.find_by_label(new_label) is not None:
            return False

        self.label = new_label
        return True

    def connect(self, n: Node):
        n.component += self.component
        n.component = list(set(n.component))

        for comp in self.component:
            for pin_idx in range(len(comp.pin)):
                if comp.pin[pin_idx] is self:
                    comp.pin[pin_idx] = n

        self.parent.remove_node(self)
        del self

    def merge_with(self, n: Node):
        self.connect(n)

    def remove_component_if_not_connected(self, comp: Component.ComponentBase) -> bool:
        for p in comp.pin:
            if p is self:
                # Still connected with component
                return False

        self.component.remove(comp)
        if len(self.component) == 0:
            del self

        return True

    def update_component_list(self):
        for comp in self.component:
            self.remove_component_if_not_connected(comp)


"""Bubble Sort
Bubble Sort is the simplest sorting algorithm that works by repeatedly swapping the adjacent elements if they are in wrong order.
Example:
First Pass:
( 5 1 4 2 8 ) –> ( 1 5 4 2 8 ), Here, algorithm compares the first two elements, and swaps since 5 > 1.
( 1 5 4 2 8 ) –>  ( 1 4 5 2 8 ), Swap since 5 > 4
( 1 4 5 2 8 ) –>  ( 1 4 2 5 8 ), Swap since 5 > 2
( 1 4 2 5 8 ) –> ( 1 4 2 5 8 ), Now, since these elements are already in order (8 > 5), algorithm does not swap them.

Second Pass:
( 1 4 2 5 8 ) –> ( 1 4 2 5 8 )
( 1 4 2 5 8 ) –> ( 1 2 4 5 8 ), Swap since 4 > 2
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –>  ( 1 2 4 5 8 )
Now, the array is already sorted, but our algorithm does not know if it is completed. The algorithm needs one whole pass without any swap to know it is sorted.

Third Pass:
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )"""

# Python program for implementation of Bubble Sort

def bubbleSort(arr):
    n = len(arr)

    # Traverse through all array elements
    for i in range(n):
        not_swap = True
        # Last i elements are already in place
        for j in range(0, n - i - 1):

            # traverse the array from 0 to n-i-1
            # Swap if the element found is greater
            # than the next element
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                not_swap = False
        if not_swap:
            break

# Driver code to test above
arr = [64, 34, 25, 12, 22, 11, 90]

bubbleSort(arr)

print("Sorted array is:")
for i in range(len(arr)):
    print("%d" % arr[i]),


# -*- coding: utf-8 -*-
import torch
from shapely.geometry import Polygon
import numpy as np

def point_form(boxes):
    """ Convert prior_boxes to (xmin, ymin, xmax, ymax)
    representation for comparison to point form ground truth data.
    Args:
        boxes: (tensor) center-size default boxes from priorbox layers.
    Return:
        boxes: (tensor) Converted xmin, ymin, xmax, ymax form of boxes.
    """
    return torch.cat((boxes[:, :2] - boxes[:, 2:]/2,     # xmin, ymin
                     boxes[:, :2] + boxes[:, 2:]/2), 1)  # xmax, ymax
def point_form_2(boxes):
    """ 将 prior_boxes 转换为按顺时针从左上到左下的坐标(x1,y1,x2,y2,x3,y3,x4,y4)
    representation for comparison to point form ground truth data.
    Args:
        boxes: (tensor) center-size default boxes from priorbox layers.
    Return:
        boxes: (tensor) Converted x1,y1,x2,y2,x3,y3,x4,y4 form of boxes.
    """
    reboxes = torch.zeros((boxes.shape[0], 8))
    reboxes[:, 0] = boxes[:, 0] - boxes[:, 2] / 2
    reboxes[:, 1] = boxes[:, 1] - boxes[:, 3] / 2
    reboxes[:, 2] = boxes[:, 0] + boxes[:, 2] / 2
    reboxes[:, 3] = boxes[:, 1] - boxes[:, 3] / 2
    reboxes[:, 4] = boxes[:, 0] + boxes[:, 2] / 2
    reboxes[:, 5] = boxes[:, 1] + boxes[:, 3] / 2
    reboxes[:, 6] = boxes[:, 0] - boxes[:, 2] / 2
    reboxes[:, 7] = boxes[:, 1] + boxes[:, 3] / 2
    return reboxes

def center_size(boxes):
    """ Convert prior_boxes to (cx, cy, w, h)
    representation for comparison to center-size form ground truth data.
    Args:
        boxes: (tensor) point_form boxes
    Return:
        boxes: (tensor) Converted xmin, ymin, xmax, ymax form of boxes.
    """
    return torch.cat((boxes[:, 2:] + boxes[:, :2])/2,  # cx, cy
                     boxes[:, 2:] - boxes[:, :2], 1)  # w, h

def intersect(box_a, box_b):
    """ We resize both tensors to [A,B,2] without new malloc:
    [A,2] -> [A,1,2] -> [A,B,2]
    [B,2] -> [1,B,2] -> [A,B,2]
    Then we compute the area of intersect between box_a and box_b.
    Args:
      box_a: (tensor) bounding boxes, Shape: [A,4].
      box_b: (tensor) bounding boxes, Shape: [B,4].
    Return:
      (tensor) intersection area, Shape: [A,B].
    """
    A = box_a.size(0)
    B = box_b.size(0)

    max_xy = torch.min(box_a[:, 2:].unsqueeze(1).expand(A, B, 2),
                       box_b[:, 2:].unsqueeze(0).expand(A, B, 2))
    min_xy = torch.max(box_a[:, :2].unsqueeze(1).expand(A, B, 2),
                       box_b[:, :2].unsqueeze(0).expand(A, B, 2))
    inter = torch.clamp((max_xy - min_xy), min=0)
    return inter[:, :, 0] * inter[:, :, 1]

def jaccard(box_a, box_b):
    """Compute the jaccard overlap of two sets of boxes.  The jaccard overlap
    is simply the intersection over union of two boxes.  Here we operate on
    ground truth boxes and default boxes.
    E.g.:
        A ∩ B / A ∪ B = A ∩ B / (area(A) + area(B) - A ∩ B)
    Args:
        box_a: (tensor) Ground truth bounding boxes, Shape: [num_objects,4]
        box_b: (tensor) Prior boxes from priorbox layers, Shape: [num_priors,4]
    Return:
        jaccard overlap: (tensor) Shape: [box_a.size(0), box_b.size(0)]
    """
    box_a_4 = torch.zeros(box_a.shape[0], 4)
    box_a_4[:, 0] = box_a[:, 0:7:2].min(dim=1)[0]
    box_a_4[:, 1] = box_a[:, 1:8:2].min(dim=1)[0]
    box_a_4[:, 2] = box_a[:, 0:7:2].max(dim=1)[0]
    box_a_4[:, 3] = box_a[:, 1:8:2].max(dim=1)[0]

    inter = intersect(box_a_4, box_b)
    area_a = ((box_a_4[:, 2]-box_a_4[:, 0]) *
              (box_a_4[:, 3]-box_a_4[:, 1])).unsqueeze(1).expand_as(inter)  # [A,B]
    area_b = ((box_b[:, 2]-box_b[:, 0]) *
              (box_b[:, 3]-box_b[:, 1])).unsqueeze(0).expand_as(inter)  # [A,B]
    union = area_a + area_b - inter
    return inter / union  # [A,B]

def intersect_ploy(box_a, box_b):
    """
    任意两个图形的相交面积的计算
    :param data1: 当前物体[(x1,y1), (x2, y2), .., ..]
    :param data2: 待比较的物体
    :return: 当前物体与待比较的物体的面积交集, box_a的面积
    """
    box_a = box_a.reshape(4, 2)
    box_b = box_b.reshape(4, 2)

    poly1 = Polygon(box_a).convex_hull      # Polygon：多边形对象
    poly2 = Polygon(box_b).convex_hull

    if not poly1.intersects(poly2):
        inter_area = 0  # 如果两四边形不相交
    else:
        inter_area = poly1.intersection(poly2).area  # 相交面积
    return inter_area, poly1.union(poly2).area

def jaccard_2(box_a, box_b, overlaps):
    """Compute the jaccard overlap of two sets of boxes.  The jaccard overlap
    is simply the intersection over union of two boxes.  Here we operate on
    ground truth boxes and default boxes.
    E.g.:
        A ∩ B / A ∪ B = A ∩ B / (area(A) + area(B) - A ∩ B)
    Args:
        box_a: (tensor) Ground truth bounding boxes, Shape: [num_objects,4], [num_objects,8] point-offset
        box_b: (tensor) Prior boxes from priorbox layers, Shape: [num_priors,4], [num_objects,8] point-offset
    Return:
        jaccard overlap: (tensor) Shape: [box_a.size(0), box_b.size(0)]
    """
    # inter = intersect(box_a, box_b)
    A = box_a.size(0)
    B = box_b.size(0)

    box_a_index,  box_b_index = torch.where(overlaps>0)
    inter = torch.zeros(A, B)
    union = torch.zeros(A, B)
    inter_union = np.array([intersect_ploy(box_a[box_a_index[i]], box_b[box_b_index[i]]) for i in range(len(box_a_index))])
    for j in range(len(box_a_index)):
        inter[box_a_index[j], box_b_index[j]] = inter_union[j, 0]
        union[box_a_index[j], box_b_index[j]] = inter_union[j, 1]
    return inter / union  # [A,B]

def match(threshold, truths, priors, variances, labels, loc_t, conf_t, idx):
    """Match each prior box with the ground truth box of the highest jaccard
    overlap, encode the bounding boxes, then return the matched indices
    corresponding to both confidence and location preds.
    Args:
        threshold: (float) The overlap threshold used when mathing boxes.
        truths: (tensor) Ground truth boxes, Shape: [num_obj, num_priors]. point-form
        priors: (tensor) Prior boxes from priorbox layers, Shape: [n_priors,4]. center-form
        variances: (tensor) Variances corresponding to each prior coord,
            Shape: [num_priors, 4].
        labels: (tensor) All the class labels for the image, Shape: [num_obj].
        loc_t: (tensor) Tensor to be filled w/ endcoded location targets.
        conf_t: (tensor) Tensor to be filled w/ matched indices for conf preds.
        idx: (int) current batch index
    Return:
        The matched indices corresponding to 1)location and 2)confidence preds.
    """
    # jaccard index
    overlaps = jaccard(truths, point_form(priors))

    # (Bipartite Matching)
    # [1,num_objects] best prior for each ground truth
    best_prior_overlap, best_prior_idx = overlaps.max(1, keepdim=True) # (A, 1) 每一行的最大值，即单一真实框和预选框IOU最大的一个下标
    # [1,num_priors] best ground truth for each prior
    best_truth_overlap, best_truth_idx = overlaps.max(0, keepdim=True) # (1, B) 每一列的最大值，即单一预选框和真实框IOU最大的一个下标
    best_truth_idx.squeeze_(0)
    best_truth_overlap.squeeze_(0)
    best_prior_idx.squeeze_(1)
    best_prior_overlap.squeeze_(1)
    best_truth_overlap.index_fill_(0, best_prior_idx, 2)  # ensure best prior
    # TODO refactor: index  best_prior_idx with long tensor
    # ensure every gt matches with its prior of max overlap
    for j in range(best_prior_idx.size(0)):
        best_truth_idx[best_prior_idx[j]] = j
    matches = truths[best_truth_idx]          # Shape: [num_priors,4] [num_priors,8], point-form
    conf = labels[best_truth_idx] + 1         # Shape: [num_priors]
    conf[best_truth_overlap < threshold] = 0  # label as background
    pos = torch.where(conf>0)
    loc = encode(matches, priors, pos, variances)
    loc_t[idx] = loc    # [num_priors,4] encoded offsets to learn
    conf_t[idx] = conf  # [num_priors] top class label for each prior

def define_orient(matched, horizontal_rectangle):
    '''
    确定多边形四个角的顺序，采用欧几里得距离，求最小的排列
    :param matched: [num_priors, 8], point-form
    :param horizontal_rectangle: [num_priors, 8], point-form
    :return:
    '''
    # 1234-2341-3412-4123
    distance_min_1 = torch.sum((matched[:, 0:2] - horizontal_rectangle[:, 0:2]) ** 2, dim=1) + \
                   torch.sum((matched[:, 2:4] - horizontal_rectangle[:, 2:4]) ** 2, dim=1) + \
                   torch.sum((matched[:, 4:6] - horizontal_rectangle[:, 4:6]) ** 2, dim=1) + \
                   torch.sum((matched[:, 6:8] - horizontal_rectangle[:, 6:8]) ** 2, dim=1) # (num_priors)

    distance_min_2 = torch.sum((matched[:, 2:4] - horizontal_rectangle[:, 0:2]) ** 2, dim=1) + \
                     torch.sum((matched[:, 4:6] - horizontal_rectangle[:, 2:4]) ** 2, dim=1) + \
                     torch.sum((matched[:, 6:8] - horizontal_rectangle[:, 4:6]) ** 2, dim=1) + \
                     torch.sum((matched[:, 0:2] - horizontal_rectangle[:, 6:8]) ** 2, dim=1)  # (num_priors)

    distance_min_3 = torch.sum((matched[:, 4:6] - horizontal_rectangle[:, 0:2]) ** 2, dim=1) + \
                     torch.sum((matched[:, 6:8] - horizontal_rectangle[:, 2:4]) ** 2, dim=1) + \
                     torch.sum((matched[:, 0:2] - horizontal_rectangle[:, 4:6]) ** 2, dim=1) + \
                     torch.sum((matched[:, 2:4] - horizontal_rectangle[:, 6:8]) ** 2, dim=1)  # (num_priors)

    distance_min_4 = torch.sum((matched[:, 6:8] - horizontal_rectangle[:, 0:2]) ** 2, dim=1) + \
                     torch.sum((matched[:, 0:2] - horizontal_rectangle[:, 2:4]) ** 2, dim=1) + \
                     torch.sum((matched[:, 2:4] - horizontal_rectangle[:, 4:6]) ** 2, dim=1) + \
                     torch.sum((matched[:, 4:6] - horizontal_rectangle[:, 6:8]) ** 2, dim=1)  # (num_priors)

    origin = torch.cat((distance_min_1.unsqueeze(1),
                        distance_min_2.unsqueeze(1),
                        distance_min_3.unsqueeze(1),
                        distance_min_4.unsqueeze(1)), 1) # (num_priors, 4)

    origin_indices = origin.min(dim=1).indices  # (num_priors)
    for i, box in enumerate(matched):
        if origin_indices[i] == 1:
            index = [2, 3, 4, 5, 6, 7, 0, 1]
            matched[i] = matched[i][index]
        if origin_indices[i] == 2:
            index = [4, 5, 6, 7, 0, 1, 2, 3]
            matched[i] = matched[i][index]
        if origin_indices[i] == 3:
            index = [6, 7, 0, 1, 2, 3, 4, 5]
            matched[i] = matched[i][index]

    return matched

def encode(matched, priors, pos, variances):
    """Encode the variances from the priorbox layers into the ground truth boxes
    we have matched (based on jaccard overlap) with the prior boxes.
    Args:
        matched: (tensor) Coords of ground truth for each prior in point-form
            Shape: [num_priors, 4].[num_priors, 8], point-form
        priors: (tensor) Prior boxes in center-offset form
            Shape: [num_priors,4].
        variances: (list[float]) Variances of priorboxes
    Return:
        encoded boxes (tensor), Shape: [num_priors, 4]
    """

    # dist b/t match center and prior's center
    # 多边形文本框外接水平矩形
    x0 = matched[:, 0:7:2].min(-1, keepdim=True).values
    y0 = matched[:, 1:8:2].min(-1, keepdim=True).values
    x1 = matched[:, 0:7:2].max(-1, keepdim=True).values
    y1 = matched[:, 1:8:2].max(-1, keepdim=True).values
    horizontal_rectangle = torch.cat((x0, y0, x1, y0, x1, y1, x0, y1), 1)
    matched[pos] = define_orient(matched[pos], horizontal_rectangle[pos])

    w = x1 - x0
    h = y1 - y0

    delta_x = ((x1+x0)/2 - priors[:, 0].unsqueeze(1))/priors[:, 2].unsqueeze(1)
    delta_y = ((y1+y0)/2 - priors[:, 1].unsqueeze(1))/priors[:, 3].unsqueeze(1)
    delta_w = torch.log(w/priors[:, 2].unsqueeze(1))
    delta_h = torch.log(h/priors[:, 3].unsqueeze(1))

    # 预选框
    priors_8 = point_form_2(priors) # center-offset form to 8point-form
    matched[:, 0:7:2] = (matched[:, 0:7:2] - priors_8[:, 0:7:2]) / priors[:, 2].unsqueeze(1)
    matched[:, 1:8:2] = (matched[:, 1:8:2] - priors_8[:, 1:8:2]) / priors[:, 3].unsqueeze(1)

    target_delta = torch.cat((delta_x, delta_y, delta_w, delta_h), 1)
    target_delta = torch.cat((target_delta, matched), 1)

    # return target for smooth_l1_loss
    return target_delta  # [num_priors,12]

# Adapted from https://github.com/Hakuyume/chainer-ssd
def decode(loc, priors, variances):
    """Decode locations from predictions using priors to undo
    the encoding we did for offset regression at train time.
    Args:
        loc (tensor): location predictions for loc layers,
            Shape: [num_priors,4]
        priors (tensor): Prior boxes in center-offset form.
            Shape: [num_priors,4].
        variances: (list[float]) Variances of priorboxes
    Return:
        decoded bounding box predictions. center-offset form+point-form
    """

    boxes = torch.cat((
        priors[:, :2] + loc[:, :2] * priors[:, 2:],
        priors[:, 2:4] * torch.exp(loc[:, 2:4])), 1)

    priors_8 = point_form_2(priors)
    loc[:, 4:11:2] = priors_8[:, 0:7:2] + priors[:, 2].unsqueeze(1) * loc[:, 4:11:2]
    loc[:, 5:12:2] = priors_8[:, 1:8:2] + priors[:, 3].unsqueeze(1) * loc[:, 5:12:2]

    boxes = torch.cat((boxes, loc[:, 4:12]), 1)
    return boxes

def log_sum_exp(x):
    """Utility function for computing log_sum_exp while determining
    This will be used to determine unaveraged confidence loss across
    all examples in a batch.
    Args:
        x (Variable(tensor)): conf_preds from conf layers
    """
    x_max = x.data.max()
    return torch.log(torch.sum(torch.exp(x-x_max), 1, keepdim=True)) + x_max

# Original author: Francisco Massa:
# https://github.com/fmassa/object-detection.torch
# Ported to PyTorch by Max deGroot (02/01/2017)
def nms(boxes, scores, overlap=(0.5, 0.2), top_k=200):
    """Apply non-maximum suppression at test time to avoid detecting too many
    overlapping bounding boxes for a given object.方框+四边形nms
    Args:
        boxes: (tensor) The location preds for the img, Shape: [num_priors,4].point-form,[num_priors,12]
        scores: (tensor) The class predscores for the img, Shape:[num_priors].
        overlap: (float) The overlap thresh for suppressing unnecessary boxes.
        top_k: (int) The Maximum number of box preds to consider.
    Return:
        The indices of the kept boxes with respect to num_priors.
    """
    # First NMS with a relatively high IOU threshold (e.g. 0.5) on the minimum horizontal rectangles
    keep = scores.new(scores.size(0)).zero_().long()
    # 返回元素数目
    if boxes.numel() == 0:
        return keep
    x1 = boxes[:, 0:7:2].min(-1).values
    y1 = boxes[:, 1:8:2].min(-1).values
    x2 = boxes[:, 0:7:2].max(-1).values
    y2 = boxes[:, 1:8:2].max(-1).values

    area_f = torch.mul(x2 - x1, y2 - y1)
    # 预选框排名，升序
    v, idx_f = scores.sort(0)  # sort in ascending order
    idx_f = idx_f[-top_k:]
    # boxes.new()该Tensor的type和device都和原有Tensor一致，且无内容
    xx1 = boxes.new()
    yy1 = boxes.new()
    xx2 = boxes.new()
    yy2 = boxes.new()
    w = boxes.new()
    h = boxes.new()

    # keep = torch.Tensor()
    count = 0
    while idx_f.numel() > 0:
        i = idx_f[-1]  # index of current largest val
        # keep.append(i)
        keep[count] = i
        count += 1
        if idx_f.size(0) == 1:
            break
        idx_f = idx_f[:-1]  # remove kept element from view
        # load bboxes of next highest vals
        torch.index_select(x1, 0, idx_f, out=xx1)
        torch.index_select(y1, 0, idx_f, out=yy1)
        torch.index_select(x2, 0, idx_f, out=xx2)
        torch.index_select(y2, 0, idx_f, out=yy2)
        # store element-wise max with next highest score
        xx1 = torch.clamp(xx1, min=x1[i])
        yy1 = torch.clamp(yy1, min=y1[i])
        xx2 = torch.clamp(xx2, max=x2[i])
        yy2 = torch.clamp(yy2, max=y2[i])
        w.resize_as_(xx2)
        h.resize_as_(yy2)
        w = xx2 - xx1
        h = yy2 - yy1
        # check sizes of xx1 and xx2.. after each iteration
        w = torch.clamp(w, min=0.0)
        h = torch.clamp(h, min=0.0)
        inter = w * h
        # IoU = i / (area(a) + area(b) - i)
        rem_areas = torch.index_select(area_f, 0, idx_f)  # load remaining areas)
        union = (rem_areas - inter) + area_f[i]
        IoU_f = inter / union  # store result in iou
        # keep only elements with an IoU <= overlap
        # input<=other，坐标位置为1，否则为0
        idx_f = idx_f[IoU_f.le(overlap[0])]

    # sencod NMS on quadrilaterals or rotated rectangles is applied to a few remaining
    # candidate boxes with a lower IOU threshold (e.g. 0.2).
    x1 = boxes[:, 4]
    y1 = boxes[:, 5]
    x2 = boxes[:, 6]
    y2 = boxes[:, 7]
    x3 = boxes[:, 8]
    y3 = boxes[:, 9]
    x4 = boxes[:, 10]
    y4 = boxes[:, 11]
    xx1 = boxes.new()
    yy1 = boxes.new()
    xx2 = boxes.new()
    yy2 = boxes.new()
    xx3 = boxes.new()
    yy3 = boxes.new()
    xx4 = boxes.new()
    yy4 = boxes.new()

    count_s = 0
    keep_s = scores.new(scores.size(0)).zero_().long()
    idx_s = keep[:count]
    while idx_s.numel() > 0:
        i = idx_s[0]
        keep_s[count_s] = i
        count_s += 1
        if idx_s.size(0) == 1:
            break
        max_box = boxes[i]
        idx_s = idx_s[1:]
        torch.index_select(x1, 0, idx_s, out=xx1)
        torch.index_select(y1, 0, idx_s, out=yy1)
        torch.index_select(x2, 0, idx_s, out=xx2)
        torch.index_select(y2, 0, idx_s, out=yy2)
        torch.index_select(x3, 0, idx_s, out=xx3)
        torch.index_select(y3, 0, idx_s, out=yy3)
        torch.index_select(x4, 0, idx_s, out=xx4)
        torch.index_select(y4, 0, idx_s, out=yy4)

        inter_union = np.array([intersect_ploy(max_box[4:12], np.array([xx1[i], yy1[i],
                                                         xx2[i], yy2[i],
                                                         xx3[i], yy3[i],
                                                         xx4[i], yy4[i]])) for i in range(xx1.shape[0])])
        inter, union = inter_union[:, 0], inter_union[:, 1]
        IoU_s = torch.tensor(inter / union)
        idx_s = idx_s[IoU_s.le(overlap[1])]
    return keep_s, count_s


import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import dash_table
import pandas as pd
import dash_bootstrap_components as dbc
from app import app
import numpy as np
import plotly.graph_objs as go
import dash_html_components as html
from dash.dependencies import Input, Output, State
import plotly.figure_factory as ff

dft = pd.read_csv('./timesData.csv')
nb_data = 50
n_comp = 8

dfs = dft[dft.year==2016].iloc[:nb_data,:]
dfs = dfs.dropna()
dfs.world_rank = [each.replace('=','') for each in dfs.world_rank]
# dfs.world_rank = pd.to_numeric(dft.world_rank, errors='coerce')
# dfs.income = pd.to_numeric(dft.income, errors='coerce')
# dfs.international = pd.to_numeric(dft.international, errors='coerce')
# dfs.total_score = pd.to_numeric(dft.total_score, errors='coerce')
# dfs.num_students = [str(each).replace(',','') for each in dft.num_students]
# dfs.international_students = [each.replace('%','') for each in dft.international_students]
# dfs.female_male_ratio = [str(each).split() for each in dft.female_male_ratio]
# dfs.female_male_ratio = [round((float(each[0]) / float(each[2])),2)*100 for each in dft.female_male_ratio]
# dfs.female_male_ratio = pd.to_numeric(dft.female_male_ratio, errors='coerce')

dfs.research = pd.to_numeric(dft.research, errors="coerce")
dfs.world_rank  = pd.to_numeric(dft.world_rank, errors="coerce")
TirParGroup = dfs.groupby('university_name')
# ParGroup = dftableau.groupby('country')
y1=TirParGroup['research'].agg(np.mean).sort_values(ascending=False)
y2=TirParGroup['world_rank'].agg(np.mean)

trace1 = {
  'x': y1.index.get_level_values(0).tolist(),

  'y': y1,
  'name': 'Research',
  'type': 'bar'
}
trace2 = {
  'x':  y1.index.get_level_values(0).tolist(),

  'y': y2,
  'name': 'World Rank',
  'type': 'bar'
}
data = [trace1, trace2]

layout_graph = {
  'xaxis': {'title': 'Université'},
  'barmode': 'relative',
  'title': 'Correlation',

}
fig = go.Figure( data = data, layout = layout_graph)
fig.update_layout(
    plot_bgcolor='#181B1E' ,
    paper_bgcolor='#181B1E',
    font_color='#DCDCDC',

)

markdown_text = '''
* Réaliser une veille sur la librairie Dash.
* Faire une analyse du jeu de données correspondant au classement des 50 meilleures universités en 2016.
* Réaliser une Analyse en Composantes Principales (vous pourrez vous appuyer sur la librairie Scikit-Learn) https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html
* Mettre en place un Dashbord Dash multi-pages permettant de répondre à la question initiale : De toutes les universités du monde, quelles sont les meilleures ? La première page de votre Dashbord mettra en évidence l'analyse des données des 50 meilleures universités de l'année 2016 (avant L'ACP).

#### 1ère page

* Dans cette première page se trouvera notamment une table des données des 50 meilleurs universités de l'année 2016 avec un bouton de téléchargement permettant de télécharger un tableau .csv des données.
* Plusieurs graphiques mettant en évidences des corrélations entre certains critères (par exemple : qualité de la recherche en fonction du rand mondial du classement des universités etc..)
#### 2eme page

Cette page permettra d'afficher les résultats issus de l'ACP. On pourra ainsi y trouver des graphiques ainsi que des paragraphes de textes mettant en évidence des variables explicatives (par exemple la valeur propre, la proportion, le cumulé, les composantes principales (CP), les scores, les distances).

Vous mettrez en ligne votre Dashboard Dash sur le serveur Cloud Heroku

'''

modal = html.Div(
    [
        dbc.Button(style={'margin-bottom':'5vh','margin-left':'2vw','backgroundColor':'#181B1E'}, children=["Détail du brief"], id="open-xl"),
        dbc.Modal(
            [
                dbc.ModalHeader(""),
                dbc.ModalBody(style={}, children=[
                    html.H5(style={'text-align': 'center',}, children=['Contexte du projet']),
                    html.H5(style={'text-align': 'center',}, children=['Classement des universités']),
                    html.P(style={}, children=[
                        "Le classement des universités est une pratique difficile, politique et controversée. Il existe des centaines de systèmes de classement universitaires nationaux et internationaux différents, dont beaucoup sont en désaccord les uns avec les autres.Le Times Higher Education World University Ranking est largement considéré comme l'une des mesures universitaires les plus influentes et les plus largement observées. Fondée au Royaume-Uni en 2010, elle a été critiquée pour sa commercialisation et pour avoir 'affaibli' les établissements non anglophones. "
                    ]),
                    html.H5(style={'text-align': 'center',}, children=['Analyse en Composantes Principales']),
                    html.P(style={}, children=[
                        " Pour vous aider dans votre analyse du jeux de données, vous réaliserez une Analyse en Composantes Principales.Cette analyse permettra de répondre à certaines questions du type : quelles ressemblances peut-il y avoir d'une université à une autre. Quelles ressemblances existent-il entre différents critères d'évaluation des universités ? Vous pourrez ainsi définir quand est-ce que 2 universités se ressemblent et quand est-ce qu'elles se ressemblent du point de vue de l'ensemble des colonnes (c'est-à-dire des critères d'évaluation du Times Higher Education World University Ranking).Est-il possible de faire un bilan des ressemblances ? ( Vous chercherez ici à faire une typologie, une partition des universités, c'est-à-dire à construire des groupes d'universités homogènes du point de vue de l'ensemble des variables. A l'intérieur d'un groupe, les individus se ressemblent et d'un groupe à l'autre ils sont différents."
                    ]),
                    html.H5(style={'text-align': 'center',}, children=['Analyse en Composantes Principales']),
                    html.P(style={}, children=[
                        dcc.Markdown(children=markdown_text),

                    ]),

                ]),
                dbc.ModalFooter(
                    dbc.Button(style={'backgroundColor':'#181B1E'},children=["Fermer"], id="close-xl", className="ml-auto")
                ),
            ],
            id="modal-xl",
            size="xl",
        ),
    ]
)

# Modal 2 Corrélation
colonnes = ['world_rank', 'total_score', 'research', 'teaching', ]
data_pca = dfs[colonnes]
lay1_fig2 = ff.create_scatterplotmatrix(data_pca, diag='histogram',
                                  colormap='Viridis',
                                  colormap_type='cat',
                                  height=500, width=1000)

nav_menu = dbc.Nav(
    [
        dbc.NavLink(style={'color':'#DCDCDC'},children=["Home "], active=True, href="/"),
        dbc.NavLink(style={'color':'#DCDCDC'},children=["Page 2"], href="/acp"),

    ]
)

layout = html.Div(style={'backgroundColor':'#212529', 'padding-top':'2vh', 'padding-botton':'4vh'}, children=[

  nav_menu,

  html.H1(style={'text-align': 'center', 'color':'#DCDCDC', 'margin-top':'2vh', 'margin-bottom':'3vh'} , children=[ "Dashboard"]),
  html.Section(style={'height':'70vh', 'display':'flex', 'justify-content':'space-around'}, children=[
      html.Article(style={ 'backgroundColor':'#181B1E', 'height':'70vh','width':'66vw', 'padding-top':'1.2vh','border-radius':'15px'}, children=[
           dcc.Graph(

        figure=fig
    ),
      ]),
      html.Article(style={'backgroundColor':'#212529', 'height':'70vh', 'width':'30vw'}, children=[
        html.Div(style={'backgroundColor':'#181B1E', 'height':'33vh', 'width':'30vw','border-radius':'15px'}, children=[]),
        html.Div(style={'backgroundColor':'#181B1E', 'margin-top':'4vh','height':'33vh', 'width':'30vw','border-radius':'15px'}, children=[])
      ]),
  ]

  ),
  html.H2(style={'text-align': 'center','color':'#DCDCDC', 'margin-top':'5vh','margin-bottom':'5vh'},children=["Les 50 meilleurs universités de l'année 2016 "]),
  html.Section(style={'display':'flex', 'margin-bottom':'5vh','justify-content':'space-around'},children=[
      html.Article(style={'backgroundColor':'#181B1E', 'height':'75vh','width':'25vw','border-radius':'15px'},children=[]),
      html.Article(style={'backgroundColor':'#181B1E','margin-left':'2vw', 'height':'75vh', 'width':'70vw','border-radius':'15px'},children=[

          dash_table.DataTable(
            id='table',
          export_format='csv',
            columns=[{"name": i, "id": i} for i in dfs.columns],
            data=dfs.to_dict('records'),
            page_size=15,
            style_table={'height': '71vh', 'overflowY': 'auto', 'width':'69vw'},

            style_header={'backgroundColor': '#151819', 'color':'white'},
            style_cell={
                'backgroundColor': '#1C1F23',
                'color': '#DCDCDC'
            },),

      ])
  ]),
  html.Br(),
  html.Br(),
  html.Section([

  ]),
  modal,

])

def toggle_modal(n1, n2, is_open):
    if n1 or n2:
        return not is_open
    return is_open

app.callback(
    Output("modal-xl", "is_open"),
    [Input("open-xl", "n_clicks"), Input("close-xl", "n_clicks")],
    [State("modal-xl", "is_open")],
)(toggle_modal)


# queries all students and assigns them a ranking based on:
#   grade level and advancement level

import statistics, random
from statistics import mean
from random import shuffle

import queries, classes, usefulFunctions
from classes import TieredStudent
from usefulFunctions import shuffleArray

tieredStudents = []
firstTier = []
secondTier = []
round = 0

def init():
    global tieredStudents
    tieredStudents = withinTiers(False, 0)
    return tieredStudents

def withinTiers(balanceTiers, roundNum):
    global tieredStudents, round
    round = roundNum
    for i in range (12, 8, -1):
        tiered = detLevel(i, balanceTiers)
        for student in tiered:
            tieredStudents.append(student)
    return tieredStudents

def detLevel(grade, balanceTiers):
    global firstTier, secondTier
    firstTier = []
    secondTier = []
    for student in queries.getStudentsByGrade(grade):
        courses = queries.getStudentCoursePreferencesByID(student.id)
        overallLevel = []
        for course in courses:
            courseInfo = queries.getCourseByID(course.course_id)
            overallLevel.append(getAllowedGrades(courseInfo.allowed_grades, student.year))
        #     levels = []
        #     levels = courseInfo.allowed_grades
        #     for level in levels:
        #         overallLevel.append(int(level))
        #         print("appending %s to student %s" %(level, student))
        studentAdvancement = mean(overallLevel)
        if abs(studentAdvancement - student.year) <= .25:
            firstTier.append(TieredStudent(student.id, student.name, True, student.gender))
        else :
            secondTier.append(TieredStudent(student.id, student.name, False, student.gender))
    if firstTier :
        firstTier =(firstTier)
        if not secondTier :
            return firstTier
        secondTier = shuffleArray(secondTier, 3)
        if balanceTiers:
            temp = firstTier
            firstTier = secondTier
            secondTier = temp
        for student in secondTier:
            firstTier.append(student)
        return firstTier
    if secondTier :
        secondTier = shuffleArray(secondTier, 3)
        return secondTier

def getAllowedGrades(gradeString, studentGrade):
    allowedGrades = gradeString.split(',')
    if len(allowedGrades) == 1:
        return int(allowedGrades[0])
    if studentGrade in allowedGrades:
        return int(studentGrade)
    else :
        sumGrades = []
        for grade in allowedGrades:
            sumGrades.append(int(grade))
        return mean(sumGrades)


# Extract the best image based on dicom window width and center from each of the structural multi_parametric MRI scans, save that image into PNG and use a pretrained [timm](https://github.com/rwightman/pytorch-image-models/tree/master/timm) model `ig_resnext101_32x16d`.  Inference involves extracting the best image (again based on dicom window width and center) and averaging the prediction across all structural multi_parametric scans.
# This notebook uses various functions from the [fmi](https://github.com/asvcode/fmi) library which is a package that adds additional functionality to [fastai's](https://www.fast.ai/) medical imaging module.
# To learn more about medical imaging view my [blog](https://asvcode.github.io/MedicalImaging/)
#!git clone https://github.com/asvcode/fmi.git
# Since internet is off in this case you can use the [fmipackage](https://www.kaggle.com/avirdee/fmipackage) dataset
#!cp -r ./fmi/* ./
from fmi.explore import *
from fmi.preprocessing import *
from fmi.pipeline import *
#!pip install timm #'../input/timm034/timm-0.3.4-py3-none-any.whl' -qq
from fastai.vision.all import *
from fastai.medical.imaging import *
import pydicom
from torchvision.utils import save_image
from glob import glob
from skimage import exposure
from pydicom.pixel_data_handlers.util import apply_voi_lut
from timm import create_model
from fastai.vision.learner import _update_first_layer
matplotlib.rcParams['image.cmap'] = 'gist_ncar'

#system_info()

source = '../input/rsna-miccai-brain-tumor-radiogenomic-classification'
train_files = get_dicom_files(f'{source}/train/00000')
train_path = f'{source}/train'
labels = pd.read_csv(f'{source}/train_labels.csv', header=0, names=['id','value'], dtype=object)
#print(os.listdir(source))

"""
# [fmi](https://github.com/asvcode/fmi) has a number of handy features that breakdown the metadata into useful chunks. Firstly you may want to quickly see what image information is contained in the metadata. You can access this using get_image_info
get_image_info(train_files[7])
# Check to see if there is any personally identifiable data in the metadata
get_pii(train_files[7])
# ## Explore Data
# Each dedicated folder is identified by a five-digit number which is further broken down into 4 subfolders:
#
# - Fluid Attenuated Inversion Recovery (FLAIR)
# - T1-weighted pre-contrast (T1w)
# - T1-weighted post-contrast (T1Gd)
# - T2-weighted (T2)
#
# Using folder `00000` as an example
source_00000 = f'{source}/train/00000/'
print(os.listdir(source_00000))
# You could view the images within the `T2w` folder like this:
sort_items = get_dicom_files(source_00000, folders='T2w')

imgs = []
for filename in sort_items:
    file = filename.dcmread().pixel_array
    img = TensorDicom(file)
    imgs.append(img)
show_images(imgs, nrows=12)

# Although this displays the images within the folder they are not displayed in sequence. [fmi](https://github.com/asvcode/fmi) provides a handy function that easily sorts the images by `instance number`
instance_show(sort_items, nrows=20)

# MRI scans are made up of numerous 2D slices taken from a number of angles(or planes). The 3 planes are axial, sagittal and cornonal and the image below show what angels the planes represent. [Image credit](https://www.ipfradiologyrounds.com/hrct-primer/image-reconstruction/)
# `show_aspects` is a handy function that easily lets you view the various planes.
# **View all images**
show_aspects(source_00000, show=True, save=False, figsize=(20,10))

# **T2-weighted (T2)**
T2w_00000 = f'{source_00000}/T2w'
show_aspects(T2w_00000, show=True, save=False, figsize=(17,12))

# You can also easily extract the metadata from the dicom images using `from_dicoms`
flair_00000_files = get_dicom_files(FLAIR_00000)

dicom_dataframe = pd.DataFrame.from_dicoms(flair_00000_files, window=dicom_windows.brain, px_summ=True)
dicom_dataframe[:10]
"""

# `get_dicom_image` is another handy feature that lets you view images within a dataframe and sort them by various items such as `img_pct_window`, `img_mean` or `img_std`.
# We will use this later in extracting the best images from each of the folders.
def get_dicom_image(df, key, nrows=1, source=None, folder_val=None, instance_val=None, figsize=(7,7)):
    "Helper to view images by key"
    imgs=[]
    title=[]
    for i in df.index:
        file_path = Path(f"{df.iloc[i]['fname']}")
        dcc = file_path.dcmread().pixel_array
        imgs.append(dcc)
        pct = df.iloc[i][key]
        title.append(pct)
    return show_images(imgs, titles=title, nrows=nrows)

"""
# **sort by `img_pct_window`**
pct = dicom_dataframe[['PatientID', 'InstanceNumber', 'img_pct_window', 'img_mean', 'img_std', 'fname']].sort_values(by=['img_pct_window'], ascending=False).reset_index(drop=True)
get_dicom_image(pct[:10], 'img_pct_window', source=source, nrows=2, figsize=(20,20))

# **sort by `img_mean`**
pct = dicom_dataframe[['PatientID', 'InstanceNumber', 'img_pct_window', 'img_mean', 'img_std', 'fname']].sort_values(by=['img_mean'], ascending=False).reset_index(drop=True)
get_dicom_image(pct[:10], 'img_mean', source=source, nrows=2, figsize=(20,20))

# **sort by `img_std`**
pct = dicom_dataframe[['PatientID', 'InstanceNumber', 'img_pct_window', 'img_mean', 'img_std', 'fname']].sort_values(by=['img_std'], ascending=False).reset_index(drop=True)
get_dicom_image(pct[:10], 'img_std', source=source, nrows=2, figsize=(20,20))

# ### Extract best image from each of the structural multi-parametric MRI (mpMRI) scans
# In order to iterate quickly and get some baseline results the aim is to get the best images from each folder.  In order to do this:
#  - we will extract the dicom metadata from each sub folder
#  - sort the metadata based on dicom window width and center
#  - Create a mask based on the dicom window width and center and get rid of unwanted pixels
#  - Save the image that best matches our criteria by converting it into PNG format
#  - Create a dataframe with columns that contain the `path` to that image (we will need this when constructing the `DataBlock`)
#
# Note that the `cmap` color used above is purely for aesthetics reasons and actually does not have any impact from a training perspective.  However moving forward we change it to a more appropriate color map
matplotlib.rcParams['image.cmap'] = 'bone'

# `process_dicom` is a handy `fmi` function that crops a dicom image (dependant on dicom window, sigma and thresh values) and saves it in .png format. You have to specify the outpath (where the .png files will be stored), the window width and center, sigma and thresh values.  By default displaying the differences in the images and sanity check is set to `False`
outpath = './test1.png'
window = dicom_windows.brain_soft
sigma = 0.1
thresh = 0.7
remove_max = False

# Have a test run with the first 5 independant cases and using the `T1wCE` folder, we create a dicom dataframe using a `brain_soft` dicom window for each case in the `T1wCE` folder, sort it by `img_pct_window` (img_pct_window is a handy `fastai` function that displays the percentage of pixels within a specified dicom window in an image), and then use `process_dicom` to save the image.
# This is how I created the datasets for each of the structural multi_parametric scans.  To generate the correct paths to save the images I used the code below, which also solves for duplicate file names across the independant cases
"""

def get_outpath(input_dir, dataset, subject, mri):
    img_id = input_dir.split('-')[-1].split('.')[0]
    outpath = os.path.join(f'./{dataset}/{mri}/{subject}.png')

    check = os.path.isfile(outpath)
    if check is not True:
        process_dicom(input_dir, outpath, window=dicom_windows.brain_soft, sigma=0.1, thresh=0.7, remove_max=False, show=False, sanity=True)

    return outpath

# We also have to save the image names and file paths so that we can use it in training later
def dicom_to_png(subject):
    fpath = []
    for mri in ['FLAIR', 'T1w', 'T1wCE', 'T2w']:
        path = f'{train_path}/{subject}/{mri}/'
        files = get_dicom_files(path)
        dicom_dataframe = pd.DataFrame.from_dicoms(files, window=dicom_windows.brain_soft, px_summ=True)
        pct = dicom_dataframe[['PatientID', 'InstanceNumber', 'img_pct_window', 'img_mean', 'img_std', 'fname']].sort_values(by=['img_pct_window'], ascending=True).reset_index(drop=True)
        ff = pct['fname'][0]
        outfile = get_outpath(ff, 'train', subject, mri)
        fpath.append(outfile)
    return fpath

labels['t1w_path']= 'path'
labels['t1wce_path'] = 'path'
labels['t2w_path'] = 'path'
labels['flare_path'] = 'path'

#if not os.path.exists('./train/T1wCE'):
for mri in ['FLAIR' ,'T1w', 'T1wCE', 'T2w']:
    if not os.path.exists(f'./train/{mri}'):
        os.makedirs(f'./train/{mri}')
labels['t1wce_path'] = labels.id.apply(lambda x: dicom_to_png(x))


import json
import os.path

config_file = "/home/pi/.thermostat.json"

KEY_TEMP_DAY = "temp.day"
KEY_TEMP_NIGHT = "temp.night"
KEY_HUMIDITY = "humidity"
KEY_GOVERNOR = "governor"

def read_config():
    if os.path.isfile(config_file):
        with open(config_file, 'rb') as f:
            return json.loads(f.read().decode("utf8"))
    else:
        return dict()

def write_config(d):
    with open(config_file, 'wb') as f:
        f.write(json.dumps(d).encode("utf8"))

def read_config_value(key):
    return read_config().get(key, None)

def write_config_value(key, value):
    c = read_config()
    c[key] = value
    write_config(c)


import subprocess
def execute_wait(com):
    proc = subprocess.Popen(com, shell=False, stdout=subprocess.PIPE, universal_newlines=True)
    # windows Warning : Using shell = True can be a security hazard
    # Note Do not use stdout=PIPE or stderr=PIPE with this function as that can deadlock based on the child process output volume. Use Popen with the communicate() method when you need pipes.
    # subprocess.run(["ls", "-l", "/dev/null"], capture_output=True)
    for stdout_line in iter(proc.stdout.readline, ""):
        yield stdout_line
    proc.stdout.close()
    return_code = proc.wait()
    if return_code:
        print(f"proc failed - {subprocess.CalledProcessError(return_code, com)}")


import math
import os
import numpy as np
import random
from math import e, inf
from numpy.core.fromnumeric import sort
from utils import get_map, map_to_graph
from config import *
from tqdm import tqdm

def cal_cost(graph, traj_list):
    cost = 0
    for i in range(len(traj_list) - 1):
        cost = cost + graph[traj_list[i]][traj_list[i+1]]
    return cost + graph[traj_list[-1]][traj_list[0]]

def generate_random_seeds(max_num):
    x = random.sample(range(max_num + 1), 2)
    return min(x[0], x[1]), max(x[0], x[1])

def is_equal(a, b, error):
    if abs(a - b) < error:
        return True
    else:
        return False

def SA(num_city, map, origin_temp, iter_times, temp_decay):
    """
    Simulated annealing algorithm
    Parameters:
    graph: distance map
    map: coordinates map
    origin_traj_list: original number list [1, 2, 3, ..., num_city]
    origin_temp: temperature at time 0
    iter_times: max times of iteration
    temp_decay: the decay of temperature
    Return:
    None
    """
    graph = map_to_graph(map, num_city)
    last_cost = inf
    last_list = [i for i in range(num_city)]
    now_cost = cal_cost(graph, traj_list=last_list)
    t = origin_temp
    step = 0
    while not is_equal(now_cost, last_cost, 1e-3):
        last_cost = now_cost
        last_iter_cost = now_cost
        for _ in range(iter_times):
            step = step + 1
            u, v = generate_random_seeds(num_city)
            new_list_1 = last_list[:u]
            new_list_2 = list(reversed(last_list[u:v]))
            new_list_3 = last_list[v:]
            new_list = new_list_1 + new_list_2 + new_list_3
            now_iter_cost = last_iter_cost + graph[
                new_list[u-1]][new_list[u]] + graph[new_list[v-1]][new_list[v % num_city]] - graph[
                    last_list[u-1]][last_list[u]] - graph[last_list[v-1]][last_list[v % num_city]]
            # now_iter_cost = cal_cost(graph, new_list)
            if now_iter_cost <  last_iter_cost:
                last_list = new_list
                last_iter_cost = now_iter_cost
            elif now_iter_cost > last_iter_cost:
                pro = math.exp(-(now_iter_cost-last_iter_cost)/t)
                random_bond = np.random.rand()
                if pro > random_bond:
                    # state convert
                    last_list = new_list
                    last_iter_cost = now_iter_cost

        # balance at temperature t
        now_cost = last_iter_cost
        t = t * temp_decay

    return round(now_cost, 2), last_list

if __name__ == '__main__':
    result_list = []
    result = []
    time = []
    for _ in tqdm(range(iters)):
        loss, traj = SA(num_city=num_city, map=map,
                        origin_temp=t0, iter_times=l, temp_decay=alpha)
        result_list.append([loss, traj])
    for item in result_list:
        if item not in result:
            result.append(item)
            time.append(1)
        else:
            id = result.index(item)
            time[id] = time[id] + 1
    for i in range(len(time)):
        result[i].append(time[i])
    result = sorted(result, key=lambda x: x[0])
    if SA_mode == "demo":
        for i, item in enumerate(result):
            get_map(np.array([map[index] for index in item[1]]), rank=i)
            print("The top_{} distance is {}. The trajactory is {}. The repeat time is {}".format(i + 1, item[0], item[1], item[2]))
    elif SA_mode == "exp":
        exp_result = []
        exp_times = []
        for i, item in enumerate(result):
            if item[0] not in exp_result:
                exp_result.append(item[0])
                exp_times.append(item[2])
            else:
                idx = exp_result.index(item[0])
                exp_times[idx] = exp_times[idx] + 1
        for i in range(len(exp_result)):
            print("Loss: {}; Ratio:{}".format(exp_result[i], round(exp_times[i] / iters, 4)))