import sys, path
import copy
import numpy
import math
import random
from collections import defaultdict
import itertools
# from typing import NewType
# for JupyterLab
#import import_ipynb

# tree datastructure
from . import avl_tree

# shapely/polygon packages
from shapely.geometry import MultiPolygon, Polygon, LineString, LinearRing, Point
from shapely.geometry.base import BaseGeometry, geos_geom_from_py
from shapely import affinity

# plotting packages
import mpld3
import bokeh
from bokeh.plotting import figure, Figure
from bokeh.io import output_notebook, show
from bokeh.layouts import gridplot, row, layout, column, Column
from bokeh.models import Div
from bokeh.models import Legend
from bokeh.models import BoxAnnotation
from bokeh.models.widgets import Tabs, Panel
from bokeh.models import ColumnDataSource, Label, LabelSet, Range1d
from bokeh.palettes import Dark2_5 as palette
from bokeh.plotting import output_file


# for analysing the data
import pandas as pd

# Type aliases for the type hints
Point_xy = (float, float)


class ConvexPolygon(object):

    def __init__(self, shell: [Point_xy]) -> None:
        self.__shell, self.__area = self.convex_hull_and_area(shell)
        self.__x_values = [vertex[0] for vertex in self.shell]
        self.__y_values = [vertex[1] for vertex in self.shell]
        self.__min_x = min(self.__x_values)
        self.__max_x = max(self.__x_values)
        self.__min_y = min(self.__y_values)
        self.__max_y = max(self.__y_values)
        self.__height = self.__max_y - self.__min_y
        self.__width = self.__max_x - self.__min_x
        self.__spine = self.set_spine()
        self.__vertices_left_visible, self.__vertices_right_visible = self.splitter()
        self.__slope = self.set_slope()
        self.plot_fill = False

    @property
    def area(self):
        return self.__area

    @property
    def shell(self):
        return self.__shell

    @property
    def min_x(self):
        return self.__min_x

    @property
    def max_x(self):
        return self.__max_x

    @property
    def min_y(self):
        return self.__min_y

    @property
    def max_y(self):
        return self.__max_y

    @property
    def x_values(self):
        return self.__x_values

    @property
    def y_values(self):
        return self.__y_values

    @property
    def height(self):
        return self.__height

    @property
    def width(self):
        return self.__width

    @property
    def spine(self):
        return self.__spine

    @property
    def vertices_left_visible(self):
        return self.__vertices_left_visible

    @property
    def vertices_right_visible(self):
        return self.__vertices_right_visible

    @property
    def slope(self):
        return self.__slope

    #     @property
    #     def plot_fill(self):
    #         return self.__plot_fill

    def convex_hull_and_area(self, shell: [Point_xy]) -> [Point_xy]:
        # the Polygon points will be ordered clockweise and the start and end point are connected
        if shell == None:
            raise ValueError("a convex polygon can't intialized with None")
        elif shell == [] or len(shell) < 3:
            raise ValueError("a convex polygon needs at least 3 points")
        shapely_polygon = Polygon(shell)
        convex_polygon = shapely_polygon.convex_hull
        if type(convex_polygon) is not Polygon:
            raise ValueError(
                "couldn't create the convex Polygon, to less points after using the convex hull on the input points")
        shell = list(convex_polygon.exterior.coords)
        area = convex_polygon.area
        return (shell, area)

    def translation(self, x: float, y: float) -> None:
        self.__shell = [(point[0] + x, point[1] + y) for point in self.__shell]
        self.__spine = (
        (self.__spine[0][0] + x, self.__spine[0][1] + y), (self.__spine[1][0] + x, self.__spine[1][1] + y))
        self.__vertices_left_visible = [(point[0] + x, point[1] + y) for point in self.__vertices_left_visible]
        self.__vertices_right_visible = [(point[0] + x, point[1] + y) for point in self.__vertices_right_visible]
        self.__min_x = self.__min_x + x
        self.__max_x = self.__max_x + x
        self.__min_y = self.__min_y + y
        self.__max_y = self.__max_y + y
        self.__x_values = [vertex + x for vertex in self.__x_values]
        self.__y_values = [vertex + y for vertex in self.__y_values]

    def set_spine(self) -> (Point_xy,
                            Point_xy):  # für den spine spielt es keine Rolle in welche Rolle das Polygon aufgebaut ist ob im Uhrzeigersinn oder dagegen
        y_sorted_p_p = sorted(self.__shell, key=lambda k: k[
            1])  # Parser schreibt oder davon ausgeht das konvexe Polygone übergeben werden
        if y_sorted_p_p[0][1] == y_sorted_p_p[1][
            1]:  # falls der niedrige Puntk eine wagerechte ist nehme die linke Ecke
            if y_sorted_p_p[0][0] < y_sorted_p_p[1][0]:
                spine_bottom = y_sorted_p_p[0]
            else:
                spine_bottom = y_sorted_p_p[1]
        else:
            spine_bottom = y_sorted_p_p[0]
        if y_sorted_p_p[-2][1] == y_sorted_p_p[-1][
            1]:  # falls der höchste Punkt eine wagerechte ist nehme die rechte Ecke
            if y_sorted_p_p[-2][0] < y_sorted_p_p[-1][0]:
                spine_top = y_sorted_p_p[-1]
            else:
                spine_top = y_sorted_p_p[-2]
        else:
            spine_top = y_sorted_p_p[-1]
        return (spine_bottom, spine_top)

    def set_slope(self) -> float:
        spine = self.__spine
        if spine[0][0] == spine[1][0]:  # Sonderfall für eine senkrechte
            slope = math.inf
        else:
            slope = (spine[1][1] - spine[0][1]) / (spine[1][0] - spine[0][
                0])  # m = y2-y1/x2-x1 Formel für die Geradensteigung mithilfe aus zwei verschiedenen Punkten der Geraden
        return slope

    def plot_polygon(self, title="", plot_width=400, plot_height=300, color="#1F77B4", render=True) -> Figure:
        """Plotting a Polygon with bokeh"""
        legend_items = []
        height = (int(self.__height * 10)) / 10
        if self.__slope == math.inf:
            slope = math.inf
        else:
            slope = truncate(self.__slope, 1)
        x_data = self.__x_values
        y_data = self.__y_values
        TOOLTIPS = [("index", "$index"), ("(x,y)", "($x{0.0}, $y{0.0})"), ]
        if title == "":
            title = 'height: {}  slope: {}'.format(height, slope)
        else:
            title = '{}  height: {}  slope: {}'.format(title, height, slope)
        fig = figure(title=title, x_axis_label='x', y_axis_label='y', width=plot_width, height=plot_height,
                     tooltips=TOOLTIPS, )
        if self.plot_fill:
            poly_fig = fig.patch(x_data, y_data, line_width=1, color=color, alpha=0.9, line_alpha=1, muted_alpha=0.05)
        else:
            poly_fig = fig.patch(x_data, y_data, line_width=1, alpha=0.1, color=color, line_alpha=1, muted_alpha=0.05)
        fig_points = fig.circle(x_data, y_data, color=color, line_width=1, muted_alpha=0.02, size=4)
        legend_items.append(("shell-lines", [poly_fig]))
        legend_items.append(("vertices", [fig_points]))
        spine_x_values = [x[0] for x in self.__spine]
        spine_y_values = [x[1] for x in self.__spine]
        fig_spine = fig.line(spine_x_values, spine_y_values, line_color="red", line_width=1, muted_alpha=0.2,
                             muted_color="red")
        legend_items.append(("spine", [fig_spine]))
        legend = Legend(items=legend_items)
        legend.click_policy = "mute"
        fig.add_layout(legend, 'right')
        if render:
            show(fig)
        return fig

    def splitter(self) -> ([Point_xy], [Point_xy]):
        left = []
        right = []
        spine_bottom = self.__spine[0]
        spine_top = self.__spine[1]
        help_array = self.__shell[:]
        if help_array[0] == help_array[-1]:  # falls letztes element doppelt
            help_array = help_array[0:-1]
        spine_bottom_found = False
        spine_top_found = False
        spine_bottom_not_horizontal = False
        while spine_bottom_found == False:  # Phase 1 der Funktion sie sucht den bottom spine, dabei werden die Ecken die nicht der Spine Bottom sind ans Ende der Liste geschoben
            if help_array[0] == spine_bottom:
                if spine_bottom[1] != help_array[-1][
                    1]:  # checkt ob Spine bottom ein horizontalen Nachbar hat falls ja wird ein Flag gesetzt damit Spine bottomt später nicht in die Rechte mitgenommen wird
                    spine_bottom_not_horizontal = True
                spine_bottom_found = True
                left.append(help_array.pop(0))
            else:
                help_array.append(help_array.pop(0))
        while spine_top_found == False:  # iterieren die Liste erneut durch bis wir spine top finden, falls spine top gefunden wurde wird auch geschaut ob spine top ein horizontalen linken
            if help_array[0] == spine_top:  # Nachbar hat falls ja wird spine top nicht in die Linke Liste miteingefügt
                spine_top_found = True
                if spine_top[1] != left[-1][1]:
                    left.append(spine_top)
            else:
                left.append(help_array.pop(0))
        right = help_array.copy()
        if spine_bottom_not_horizontal:
            right.append(spine_bottom)
        return (left, right)


class HighClass(object):
    def __init__(self, i: int, alpha: float, h_max_polygon: float, w_max_polygon: float) -> None:
        self.i = i
        self.alpha = alpha
        self.h_max_polygon = h_max_polygon
        self.w_max_polygon = w_max_polygon
        self.min_border = alpha ** (i + 1) * h_max_polygon
        self.max_border = alpha ** (i) * h_max_polygon
        self.polygons = []
        self.spine_ordered_polygons = []

    def set_polygons(self, polygons: [ConvexPolygon]) -> None:
        self.polygons = polygons
        spine_ordered_polygons = sorted(self.polygons, key=lambda polygon: polygon.slope)
        self.spine_ordered_polygons = spine_ordered_polygons

    def plot_hc(self, ordered=False, render=True, plot_width=300, plot_height=300) -> Column:
        plots = []
        if ordered:
            polygon_list = self.spine_ordered_polygons
        else:
            polygon_list = self.polygons
        for counter, polygon in enumerate(polygon_list):
            title = "Polygon {}".format(counter)
            plots.append(polygon.plot_polygon(title=title, render=False))
        grid = gridplot(plots, ncols=4, plot_width=plot_width, plot_height=plot_height, toolbar_location="left")
        grid_title = Div(text="<b>Hc_{}&nbsp;&nbsp;height:&nbsp;{}-{}&nbsp;&nbsp;alpha: {}<b>".format(self.i, truncate(
            self.min_border, 1), truncate(self.max_border, 1), self.alpha))
        grid_layout = column(grid_title, grid)
        if render:
            show(grid_layout)
        return grid_layout


class Container(object):
    def __init__(self, hc: HighClass) -> None:
        self.hc = hc  # am ende nur zuweiseung ohne copy
        self.y_boundary = self.hc.max_border
        self.x_boundary = 0
        self.Tree = avl_tree.AVLTree()
        self.root = None
        self.sigma, self.plot_steps = self.packing_container(self.hc.spine_ordered_polygons)

    def distance(self, edge_points: (Point_xy, Point_xy), point: Point_xy) -> float:
        # y = m*x+n
        # (y-n)/m=x
        # n = y-m*x
        edge_p1 = edge_points[0]
        edge_p2 = edge_points[1]
        if edge_p1[0] == edge_p2[0]:  # Sonderfall für eine senkrechte
            distance = math.sqrt((edge_p1[0] - point[
                0]) ** 2)  # da der edge_points eine senkrechte ist -> der eintreffpunkt hat den gleichen y wert wie der punkt, in der Abstand formel fällt dieser wert weg
        elif edge_p1[1] == point[1]:
            distance = math.sqrt((edge_p1[0] - point[0]) ** 2 + (edge_p1[1] - point[1]) ** 2)
        else:
            slope = (edge_p2[1] - edge_p1[1]) / (edge_p2[0] - edge_p1[
                0])  # m = y2-y1/x2-x1 Formel für die Geradensteigung mithilfe aus zwei verschiedenen Punkten der Geraden
            n = edge_p1[1] - (slope * edge_p1[0])
            intersection_point_x = (point[1] - n) / slope  # x=(y-n)/m
            intersection_point = (intersection_point_x, point[1])
            distance = math.sqrt((intersection_point[0] - point[0]) ** 2 + (intersection_point[1] - point[1]) ** 2)
        return distance

    def find_successor(self, vertex: Point_xy, vertices_visible_left: [Point_xy]) -> (Point_xy, Point_xy):
        for counter, top_edge_vertex in enumerate(vertices_visible_left):
            if top_edge_vertex[1] >= vertex[1]:
                bottom_edge_vertex = vertices_visible_left[counter - 1]
                return (bottom_edge_vertex, top_edge_vertex)

    def packing_container(self, hc_spine_ordered_polygons: [ConvexPolygon]) -> ([ConvexPolygon], [Figure]):
        hc_polygons = copy.deepcopy(hc_spine_ordered_polygons)  # n
        sigma_plot_helper = []  # holds the Polygons before their translation to next polygon in sigma
        step_counter = 0
        sigma = []
        plot_steps = []
        for polygon in hc_polygons:
            sigma_plot_helper.append(polygon)
            if sigma == []:
                transform_x = -polygon.min_x
                transform_y = -polygon.min_y
                polygon.translation(transform_x, transform_y)
                polygon_vertices = len(polygon.vertices_right_visible)
                # filling the tree with all vertices visible form right of the first polygon
                for count in range(0, polygon_vertices - 1):
                    vertice = polygon.vertices_right_visible[count]
                    vertice_neighbour = polygon.vertices_right_visible[count + 1]
                    self.root = self.Tree.insert_node(self.root, vertice[1], vertice, vertice_neighbour)
                sigma.append(polygon)
                self.x_boundary += polygon.width
                plot_steps.append(
                    self.plot_container(sigma_plot_helper, render=False, title="Step {}".format(step_counter)))
                step_counter += 1
            else:
                transform_x = (self.x_boundary + abs(polygon.min_x)) + 10
                transform_y = -polygon.min_y
                polygon.translation(transform_x, transform_y)
                min_horizontal_distance = math.inf
                distance_rl_plot_helper = []
                # distanzen von rechts nach links 
                for vertex in polygon.vertices_left_visible:
                    vertex_y = vertex[1]
                    successor_vertex = self.Tree.find_edges(self.root, vertex_y, self.root)
                    corresponding_edge_points = (successor_vertex.vertice, successor_vertex.vertice_neighbour)
                    distance = self.distance(corresponding_edge_points, vertex)
                    if distance <= min_horizontal_distance:
                        min_horizontal_distance = distance
                    distance_rl_plot_helper.append((vertex, distance))
                key = polygon.spine[1][1]
                tree_array = self.Tree.preOrder_array((self.root), [],
                                                      key)  # alle Ecken bis zum höchsten Punkt von dem neuen Polygon
                distance_lr_plot_helper = []
                # distanzen von links nach rechts
                for vertex in tree_array:
                    successor = self.find_successor(vertex, polygon.vertices_left_visible)
                    distance = self.distance(successor, vertex)
                    if distance <= min_horizontal_distance:
                        min_horizontal_distance = distance
                    self.root = self.Tree.delete_node(self.root, vertex[1])  # deleting vertices from B
                    distance_lr_plot_helper.append((vertex, distance))
                plot_steps.append(
                    self.plot_container(sigma_plot_helper, distance_rl_plot_helper, distance_lr_plot_helper,
                                        min_horizontal_distance, render=False, title="Step {}".format(step_counter)))
                step_counter += 1
                polygon.translation(-(min_horizontal_distance), 0)
                for counter, vertex in enumerate(polygon.vertices_right_visible[0:-1]):
                    self.root = self.Tree.insert_node(self.root, vertex[1], vertex,
                                                      (polygon.vertices_right_visible[counter + 1]))
                x_boundary = max([vertex[0] for vertex in polygon.vertices_right_visible] + [self.x_boundary])
                self.x_boundary = x_boundary
                sigma.append(polygon)
        plot_steps.append(self.plot_container(sigma, render=False, title="Finished Container"))
        return (sigma, plot_steps)

    def plot_container(self, sigma=None, r_l_distances=None, l_r_distances=None, min_distance=None, render=True,
                       title="", box_boundarys_x_values_colors_tuple=None) -> Figure:
        # sigma als argument statt self.sigma dient dazu auch zwischen schritte plotten zu lassen
        legend_items = []
        legend_polygons = []
        legend_spine = []
        legend_lr = []
        legend_rl = []
        legend_vertices = []
        if sigma == None:
            sigma = self.sigma
        TOOLTIPS = [("index", "$index"), ("(x,y)", "($x{0.0}, $y{0.0})"), ]
        fig = figure(title=title, x_axis_label='x', y_axis_label='y', tooltips=TOOLTIPS, toolbar_location="left")
        for counter, polygon in enumerate(sigma):
            x_values = polygon.x_values
            y_values = polygon.y_values
            poly_fig = fig.patch(x_values, y_values, line_width=1, alpha=0.1, muted_alpha=0.05, line_alpha=1)
            circle_fig = fig.circle(x_values, y_values, line_width=1, muted_alpha=0.01)
            legend_label = "Polygon{}".format(counter)
            legend_polygons.append((legend_label, [poly_fig]))
            legend_vertices.append(circle_fig)
            x_spine = [x[0] for x in polygon.spine]
            y_spine = [y[1] for y in polygon.spine]
            spine_fig = fig.line(x_spine, y_spine, line_color="red", line_width=1, alpha=0.01, muted_color="red",
                                 muted_alpha=1)
            legend_spine.append(spine_fig)
        if box_boundarys_x_values_colors_tuple != None:
            box_boundarys_x_values = box_boundarys_x_values_colors_tuple[0]
            background_c_list = box_boundarys_x_values_colors_tuple[1]
            for counter, boundary in enumerate(box_boundarys_x_values[0:-1]):
                next_boundary = box_boundarys_x_values[counter + 1]
                color_box = BoxAnnotation(bottom=0, top=self.y_boundary, left=box_boundarys_x_values[counter],
                                          right=next_boundary, fill_color=background_c_list[counter], fill_alpha=0.1)
                fig.add_layout(color_box)
                fig.line([next_boundary, next_boundary], [0, self.y_boundary], line_dash="dotted")
            fig.title.text = 'Hc-Container{}  height: {}  -> Mini Containers'.format(self.hc.i,
                                                                                     truncate(self.y_boundary, 1))
        if title == "":
            fig.title.text = 'Hc-Container{} height: {} '.format(self.hc.i, truncate(self.y_boundary, 1))
        if r_l_distances != None:
            for vertex_distance_tuple in r_l_distances:
                vertex_r = vertex_distance_tuple[0]
                distance = vertex_distance_tuple[1]
                x = vertex_r[0]
                corresponding_point_x = x - distance
                y = vertex_r[1]
                text_point_x, text_point_y = (x - distance / 2, y)
                distance_int = int(distance)
                color = "blue"
                if distance_int == int(min_distance):
                    line_dash = "solid"
                else:
                    line_dash = "dotted"
                fig_rl = fig.line([vertex_r[0], corresponding_point_x], [y, y], line_color=color, line_width=1,
                                  muted_alpha=0.2, line_dash=line_dash)
                source = ColumnDataSource(data=dict(x=[vertex_r[0] - (distance / 2)], y=[y], names=[distance_int]))
                labels = LabelSet(x='x', y='y', text='names', level='glyph', x_offset=0, y_offset=0, source=source,
                                  render_mode='canvas')
                fig.add_layout(labels)
                legend_rl.append(fig_rl)
        if l_r_distances != None:
            for vertex_distance_tuple in l_r_distances:
                vertex_l = vertex_distance_tuple[0]
                distance = vertex_distance_tuple[1]
                x = vertex_l[0]
                corresponding_point_x = x + distance
                y = vertex_l[1]
                text_point_x, text_point_y = (x + distance / 2, y)
                distance_int = int(distance)
                color = "green"
                if distance_int == int(min_distance):
                    line_dash = 'solid'
                else:
                    line_dash = "dotted"
                fig_lr = fig.line([vertex_l[0], corresponding_point_x], [y, y], line_color=color, line_width=1,
                                  muted_alpha=0.2, line_dash=line_dash)
                source = ColumnDataSource(data=dict(x=[vertex_l[0] + (distance / 2)], y=[y], names=[distance_int]))
                labels = LabelSet(x='x', y='y', text='names', level='glyph', x_offset=0, y_offset=0, source=source,
                                  render_mode='canvas')
                fig.add_layout(labels)
                legend_lr.append(fig_lr)
        # building the custom legend
        fig_boundary = fig.line([0, self.x_boundary, self.x_boundary, 0, 0],
                                [0, 0, self.y_boundary, self.y_boundary, 0], line_color="black", alpha=0.7,
                                line_width=1, muted_alpha=0.2, )
        legend_items.append(("spine", legend_spine))
        if l_r_distances != None:
            legend_items.append(("distance-lr", legend_lr))
        if r_l_distances != None:
            legend_items.append(("distance-rl", legend_rl))
        legend_items.append(("boundary", [fig_boundary]))
        legend_items.append(("vertices", legend_vertices))
        legend_items = legend_items + legend_polygons
        legend = Legend(items=legend_items)
        legend.click_policy = "mute"
        fig.add_layout(legend, 'right')
        if render:
            show(fig)
        return fig

    def plot_container_steps(self, render=True, colums=2, plot_width=600, plot_height=600) -> Column:
        grid = gridplot(self.plot_steps, ncols=colums, plot_width=plot_width, plot_height=plot_height,
                        toolbar_location="left")
        min_border = (int(self.hc.min_border * 10)) / 10
        max_border = (int(self.hc.max_border * 10)) / 10
        # text="<b>Hc_{}&nbsp;&nbsp;height:&nbsp;{}-{}&nbsp;&nbsp;alpha: {}<b>
        title = Div(
            text="<b>Highclass Container&nbsp{},&nbspPolygon&nbspheight:&nbsp{}-{} <b>".format(self.hc.i, min_border,
                                                                                               max_border))
        grid_layout = column(title, grid)
        if render:
            show(grid_layout)
        return grid_layout


class Mini_Container(object):
    def __init__(self, max_width: float, max_height: float, max_polygon_width: float, hc_i: int, c=2.214) -> None:
        self.max_polygon_width = max_polygon_width
        self.current_right_boundary = 0
        self.height = 0
        self.hc_i = hc_i
        self.hc_height = max_height
        self.max_width = max_width
        self.c = c
        self.polygons = []
        self.y_offset = 0
        self.plot_steps = []

    def plot_container(self, render=True, title="", background_c=None) -> Figure:
        y_offset = self.y_offset
        legend_polygons = []
        legend_spine = []
        legend_lr = []
        legend_rl = []
        legend_vertices = []
        TOOLTIPS = [("index", "$index"), ("(x,y)", "($x{0.0}, $y{0.0})"), ]
        fig = figure(title=title, x_axis_label='x', y_axis_label='y', tooltips=TOOLTIPS, toolbar_location="left")
        if background_c != None:
            color_box = BoxAnnotation(left=0, right=self.max_width, bottom=y_offset,
                                      top=y_offset + self.height, fill_color=background_c,
                                      fill_alpha=0.1)
            fig.add_layout(color_box)
        for counter, polygon in enumerate(self.polygons):
            x_values = polygon.x_values
            y_values = polygon.y_values
            poly_fig = fig.patch(x_values, y_values, line_width=1, alpha=0.1, line_alpha=1, muted_alpha=0.05)
            circle_fig = fig.circle(x_values, y_values, line_width=1, muted_alpha=0.01)
            legend_label = "Polygon{}".format(counter)
            legend_polygons.append((legend_label, [poly_fig]))
            legend_vertices.append(circle_fig)
            x_spine = [x[0] for x in polygon.spine]
            y_spine = [y[1] for y in polygon.spine]
            spine_fig = fig.line(x_spine, y_spine, line_color="red", line_width=1, alpha=0.1, muted_alpha=1,
                                 muted_color="red")
            legend_spine.append(spine_fig)
        # building the custom legend
        fig_boundary = fig.line([0, self.max_width, self.max_width, 0, 0],
                                [0 + y_offset, 0 + y_offset, self.height + y_offset, self.height + y_offset,
                                 0 + y_offset]
                                , line_color="black", alpha=0.7, line_width=1, muted_alpha=0.2, )
        fig_polygon_boundary = fig.line([self.current_right_boundary, self.current_right_boundary],
                                        [0 + y_offset, self.height + y_offset]
                                        , line_color="black", line_dash="dotted", alpha=0.7, line_width=1,
                                        muted_alpha=0.2, )
        items = legend_polygons + [("spine", legend_spine)]
        items.append(("container-boundary", [fig_boundary]))
        items.append(("last polygon-boundary", [fig_polygon_boundary]))
        items.append(("vertices", legend_vertices))
        legend = Legend(items=items)
        legend.click_policy = "mute"
        fig.add_layout(legend, 'right')
        if render:
            show(fig)
        return fig


class End_Container(object):
    def __init__(self, mini_container_array: [Mini_Container], angle=0) -> None:
        self.mini_containers = copy.deepcopy(mini_container_array)
        self.initial_mini_containers = mini_container_array
        self.polygons, self.max_width, self.max_height, self.container_not_clipped_area = self.pack_container()
        self.x_values_border = [0, 0, self.max_width, self.max_width, 0]
        self.y_values_border = [0, self.max_height, self.max_height, 0, 0]
        self.container_area = self.max_width * self.max_height
        self.angle = angle
        self.plot_steps_all = None

    def pack_container(self) -> ([ConvexPolygon], float, float):
        y_offset = 0
        end_c_polygons = []
        container_polygon_boundary_width = 0
        container_not_clipped_area = 0
        for mini_container in self.mini_containers:
            container_not_clipped_area += mini_container.max_width * mini_container.hc_height
            for polygon in mini_container.polygons:
                polygon.translation(0, y_offset)
            mini_container.y_offset = y_offset
            y_offset += mini_container.height
            end_c_polygons = end_c_polygons + mini_container.polygons
            if container_polygon_boundary_width < mini_container.current_right_boundary:
                container_polygon_boundary_width = mini_container.current_right_boundary
        return (end_c_polygons, container_polygon_boundary_width, y_offset, container_not_clipped_area)

    def plot_polygons(self, title="", plot_width=500, plot_height=500, render=True) -> Figure:
        if title == "":
            title = 'End-Container  area: {}  not-clipped-area: {}   angle:{}'.format(truncate(self.container_area, 1),
                                                                                      truncate(
                                                                                          self.container_not_clipped_area,
                                                                                          1), self.angle)
        fig = plot_polygons_in_single_plot(self.polygons, title=title, plot_width=plot_width, plot_height=plot_height,
                                           render=render, border=(self.x_values_border, self.y_values_border))
        return fig

    def plot_steps(self, render=True):
        if render:
            show(self.plot_steps_all)
        return self.plot_steps_all

    def plot_container(self, title="", plot_width=600, plot_height=600, solo_box_border=False, render=True,
                       reverse_legend=True) -> Figure:
        legend_mini_containers = []
        legend_spine = []
        legend_lr = []
        legend_rl = []
        legend_items = []
        legend_vertices = []
        TOOLTIPS = [("index", "$index"), ("(x,y)", "($x{0.0}, $y{0.0})"), ]
        if title == "":
            title = 'End-Container   area: {}   not-clipped-area: {}   angle:{}'.format(
                truncate(self.container_area, 1), truncate(self.container_not_clipped_area, 1), self.angle)
        fig = figure(title=title, x_axis_label='x', y_axis_label='y', width=plot_width, height=plot_height,
                     toolbar_location="left", tooltips=TOOLTIPS)
        y_offset = 0
        colors = itertools.cycle(palette)
        for counter, mini_container in enumerate(self.mini_containers):
            color = next(colors)
            legend_polygons = []
            for polygon in mini_container.polygons:
                source = ColumnDataSource(data=dict(x=polygon.x_values, y=polygon.y_values))
                fig_polygon = fig.patch(x="x", y="y", line_width=1, color=color, line_color=color, muted_color=color,
                                        alpha=0.1, muted_alpha=0.05, line_alpha=1, source=source)
                fig_circle = fig.circle(x="x", y="y", line_width=1, color=color, fill_color=color, muted_color=color,
                                        muted_alpha=0.01, size=4,
                                        source=source)
                spine_x_values = [x[0] for x in polygon.spine]
                spine_y_values = [x[1] for x in polygon.spine]
                fig_spine = fig.line(spine_x_values, spine_y_values, line_color="red", line_width=1, alpha=0.01,
                                     muted_color="red",
                                     muted_alpha=1)
                legend_spine.append(fig_spine)
                legend_polygons = legend_polygons + [fig_polygon]
                legend_vertices.append(fig_circle)
            if solo_box_border:
                mini_box_border = mini_container.current_right_boundary
                fig.title.text = 'End-Container mini-containers with solo boundarys'
            else:
                mini_box_border = self.max_width
            color_box = BoxAnnotation(left=0, right=mini_box_border, bottom=mini_container.y_offset,
                                      top=mini_container.y_offset + mini_container.height, fill_color=color,
                                      fill_alpha=0.1)
            fig.add_layout(color_box)
            fig_border = fig.line([0, mini_box_border, mini_box_border, 0, 0],
                                  [0 + y_offset, 0 + y_offset, mini_container.height + y_offset,
                                   mini_container.height + y_offset,
                                   0 + y_offset], line_color=color, muted_alpha=0.2)
            y_offset += mini_container.height
            legend_label = "MC{} height:{} (hc{})".format(counter, int(mini_container.height), mini_container.hc_i)
            legend_polygons.append(fig_border)
            legend_mini_containers.append((legend_label, legend_polygons))
        spine_legend = [("spine", legend_spine)]
        vertices_legend = [("vertices", legend_vertices)]
        legend_items = legend_items + legend_mini_containers + spine_legend + vertices_legend
        if reverse_legend:
            legend_items.reverse()
        legend = Legend(items=legend_items)
        fig.add_layout(legend, 'right')
        fig.legend.click_policy = "mute"
        if render:
            show(fig)
        return fig


class ConvexContainer(object):
    def __init__(self, polygons: [ConvexPolygon], steps=90, build_plots=True) -> None:
        self.angle_0_end_container = pack_polygons(polygons)
        self.angle_0_end_container_polygons = self.angle_0_end_container.polygons
        self.rotate_center = (self.angle_0_end_container.max_width / 2, self.angle_0_end_container.max_height / 2)
        self.rotated_end_container_list = []  # Liste aller Endcontainer
        self.rotated_container_plots = []
        self.back_rotated_polygons_plots = []
        self.smallest_end_container, self.polygons, self.angle, self.area = self.find_convex_container(steps,
                                                                                                       build_plots)
        self.width = self.smallest_end_container.max_width
        self.height = self.smallest_end_container.max_height
        self.boundarys_x_values, self.boundarys_y_values = self.set_boundarys()
        self.plot_steps_all = self.build_plot_steps()

    def build_plot_steps(self):
        convex_c_panels = []
        smallest_c = self.plot_container(render=False, plot_width=800, plot_height=700)
        smallest_c_tab = Panel(child=smallest_c, title="Smallest-Convex-Container")
        convex_c_panels.append(smallest_c_tab)

        rotated_c = self.plot_rotated_containers(render=False, plot_width=800, plot_height=700)
        rotated_c_tab = Panel(child=rotated_c, title="Rotated-End-Containers")
        convex_c_panels.append(rotated_c_tab)

        back_rotated_c = self.plot_back_rotated_polygons(render=False, plot_width=800, plot_height=700)
        back_rotated_c_tab = Panel(child=back_rotated_c, title="Convex-Containers (back rotated)")
        convex_c_panels.append(back_rotated_c_tab)

        tabs = Tabs(tabs=convex_c_panels)
        t_header = Panel(child=tabs, title="Convex-Container")  #
        tab_header = self.smallest_end_container.plot_steps(render=False)
        tab_header.tabs.append(t_header)
        return tab_header

    def plot_steps(self, render=True):
        if render:
            show(self.plot_steps_all)
        return self.plot_steps_all

    def find_convex_container(self, steps: int, build_plots=True) -> (End_Container, [ConvexPolygon], int, float):
        list_of_area = []
        polygons = []
        for angle in range(0, 360, steps):
            polygons = rotate_polygons(self.angle_0_end_container_polygons, -angle, origin=self.rotate_center)
            rotated_end_container = pack_polygons(polygons, -angle)
            # rotated_end_container.angle=angle
            self.rotated_end_container_list.append(rotated_end_container)
            list_of_area.append(rotated_end_container)
            if build_plots:  # das Flag ist aus Performance gründen da sonst wird jeder End Container zurück rotiert
                title = 'End-Container  area: {}  not-clipped-area: {} angle: {}'.format(
                    truncate(rotated_end_container.container_area, 1),
                    truncate(rotated_end_container.container_not_clipped_area, 1), rotated_end_container.angle)
                self.rotated_container_plots.append(rotated_end_container.plot_container(render=False, title=title))
                back_rotated_polygons = rotate_polygons(rotated_end_container.polygons, angle,
                                                        origin=self.rotate_center)  # kreiert neue Polygone
                box_x_v = rotated_end_container.x_values_border
                box_y_v = rotated_end_container.y_values_border
                boundarys = list(zip(box_x_v, box_y_v))
                rotated_x_values, rotated_y_values = rotate_points_and_split(boundarys, angle,
                                                                             origin=self.rotate_center)
                title2 = 'Convex-Container  area: {}  not-clipped-area: {} angle: {}'.format(
                    truncate(rotated_end_container.container_area, 1),
                    truncate(rotated_end_container.container_not_clipped_area, 1), -rotated_end_container.angle)
                back_rotated_polygon_plot = plot_polygons_in_single_plot(back_rotated_polygons, render=False,
                                                                         border=(rotated_x_values, rotated_y_values),
                                                                         title=title2)
                self.back_rotated_polygons_plots.append(back_rotated_polygon_plot)
        sorted_container = sorted(list_of_area, key=lambda k: k.container_area)
        smallest_container = sorted_container[0]
        angle = smallest_container.angle
        smallest_container_polygons = sorted_container[0].polygons
        smallest_back_rotated_polygons = rotate_polygons(smallest_container_polygons, angle, origin=self.rotate_center)
        smallest_area = smallest_container.container_area
        return (smallest_container, smallest_back_rotated_polygons, abs(angle), smallest_area)

    def set_boundarys(self) -> ([Point_xy], [Point_xy]):
        container = self.smallest_end_container
        boundarys_x_values = [0, 0, self.width, self.width, 0]
        boundarys_y_values = [0, self.height, self.height, 0, 0]
        boundarys = list(zip(boundarys_x_values, boundarys_y_values))
        rotated_x_values, rotated_y_values = rotate_points_and_split(boundarys, container.angle,
                                                                     origin=self.rotate_center)
        return (rotated_x_values, rotated_y_values)

    def plot_rotated_containers(self, render=True, colums=9, plot_width=600, plot_height=600) -> Column:
        grid = gridplot(self.rotated_container_plots, ncols=colums, plot_width=plot_width, plot_height=plot_height,
                        toolbar_location="left")
        if render:
            show(grid)
        return grid

    def plot_back_rotated_polygons(self, render=True, colums=9, plot_width=700, plot_height=600) -> Column:
        grid = gridplot(self.back_rotated_polygons_plots, ncols=colums, plot_width=plot_width, plot_height=plot_height,
                        toolbar_location="left")
        if render:
            show(grid)
        return grid

    def plot_container(self, title="", plot_width=600, plot_height=600, render=True, reverse_legend=True) -> Figure:
        if title == "":
            title = 'Convex Container  area: {}  not-clipped-area: {}  angle: {}'.format(
                truncate(self.smallest_end_container.container_area, 1),
                truncate(self.smallest_end_container.container_not_clipped_area, 1), self.angle)
        fig = plot_polygons_in_single_plot(self.polygons, title=title, plot_width=plot_width, plot_height=plot_height,
                                           render=render, border=(self.boundarys_x_values, self.boundarys_y_values))
        return fig


def rotate_points_and_split(point_list: [Point_xy], angle, origin=(0, 0)) -> ([float], [float]):
    poly_wrapper = Polygon(point_list)
    poly_wrapper_rotated = affinity.rotate(poly_wrapper, angle, origin=origin)
    return poly_wrapper_rotated.exterior.xy


def rotate_and_create_new_convex_polygon(polygon: ConvexPolygon, angle: int, origin=(0, 0)) -> ConvexPolygon:
    poly_wrapper = Polygon(polygon.shell)
    poly_wrapper_rotated = affinity.rotate(poly_wrapper, angle, origin=origin)
    rotated_convex_polygon = ConvexPolygon(poly_wrapper_rotated.exterior.coords)
    return rotated_convex_polygon


def rotate_polygons(polygons: [ConvexPolygon], angle: int, origin=(0, 0)) -> [ConvexPolygon]:
    rotated_polygons = []
    for polygon in polygons:
        rotated_polygon = rotate_and_create_new_convex_polygon(polygon, angle, origin)
        rotated_polygons.append(rotated_polygon)
    return rotated_polygons


def build_mini_containers_and_plots(container_array: [Container], c=2.214) -> ([Mini_Container], [[Figure]]):
    mini_container_array = []
    mini_container_plots_list = []
    colors = itertools.cycle(palette)
    background_color_list = [next(colors)]
    for container in container_array:
        container_sigma = copy.deepcopy(container.sigma)
        container_and_mini_container_plots = []
        box_width = c * container.hc.w_max_polygon
        box_width_helper = 0
        box_counter = 1  # hilft dabei wie oft der Container in Mini-Container geteilt wird
        box_boundarys_x_values = []
        # für den plot die grenzen der miniboxen herauszufinden
        while box_width_helper < container.x_boundary:
            box_boundarys_x_values.append(box_width_helper)
            box_width_helper += box_width
            background_color_list.append(next(colors))
        box_boundarys_x_values_colors_tuple = (box_boundarys_x_values, background_color_list)
        container_and_mini_container_plots.append(container.plot_container(container_sigma, render=False,
                                                                           box_boundarys_x_values_colors_tuple=box_boundarys_x_values_colors_tuple))  # will added to the plots
        max_width_mini_container = box_width + container.hc.w_max_polygon
        background_counter = 0
        # wichtig zu kucken ob der container noch Polygone enthält da die linkesten Punkte noch in der anderen Box liegen können und eine Box leer sein kann
        # while len(container_sigma) > 0 and (box_width * box_counter) < (container.x_boundary):
        while len(container_sigma) > 0:
            mini_container = Mini_Container(max_width_mini_container, (container.y_boundary),
                                            container.hc.w_max_polygon, container.hc.i)
            translation = box_width * (box_counter - 1)
            min_translation = math.inf
            mini_container_y_border = 0
            mini_container_x_border = 0
            # new
            pop_list = []  # enthält die counter der Polygone die zur box gehören
            for counter, polygon in enumerate(container_sigma):
                if polygon.min_x < (box_width * box_counter):
                    pop_list.append(counter)
                    if polygon.min_x < min_translation:
                        min_translation = polygon.min_x
                        # selbst wenn man das 0 Element poped gibt es keine Probleme
            pop_helper = 0
            if pop_list != []:
                for counter in pop_list:
                    container_sigma[counter - pop_helper].translation(-min_translation, 0)  # new
                    if mini_container_y_border < container_sigma[counter - pop_helper].max_y:
                        mini_container_y_border = container_sigma[counter - pop_helper].max_y
                    if mini_container_x_border < container_sigma[counter - pop_helper].max_x:
                        mini_container_x_border = container_sigma[counter - pop_helper].max_x
                    mini_container.polygons.append(container_sigma.pop(counter - pop_helper))
                    pop_helper += 1
                # mini_container.plot_container()
            mini_container.height = mini_container_y_border
            mini_container.current_right_boundary = mini_container_x_border
            mini_container_array.append(mini_container)
            if (box_width * box_counter) < (container.x_boundary):
                title = "Mini-Container{}  height: {}  (hc_{})".format(box_counter, truncate(mini_container.height, 1),
                                                                       container.hc.i)
                b_color = background_color_list[background_counter]  # ----
                container_and_mini_container_plots.append(
                    mini_container.plot_container(title=title, render=False, background_c=b_color))
                box_counter += 1
                background_counter += 1
            # für die kürzere box
            else:
                title = "Mini-Container{}  height: {}  (hc_{})".format(box_counter, truncate(mini_container.height, 1),
                                                                       container.hc.i)
                # mini_container.plot_container(title)-
                container_and_mini_container_plots.append(mini_container.plot_container(title=title, render=False))
        mini_container_plots_list.append(container_and_mini_container_plots)
    return (mini_container_array, mini_container_plots_list)


def plot_mini_containers(plot_steps: [[Figure]], render=True, colums=3, plot_width=600, plot_height=600) -> Column:
    plots = []
    for plot in plot_steps:
        grid = gridplot(plot, ncols=colums, plot_width=plot_width, plot_height=plot_height, toolbar_location="left")
        plots.append(grid)
    if render:
        show(layout(plots))
    return layout(plots)


def create_multiple_convex_polygons(number: int, max_ngon: int, max_rand=1000) -> [ConvexPolygon]:
    polygon_list = []
    for count in range(0, number):
        polygon_list.append(create_convex_polygon(max_ngon, max_rand))
    return polygon_list


def create_convex_polygon(max_ngon: int,
                          max_rand=1000) -> ConvexPolygon:  # die erste Koordinate der konvexen Polygone wird dupliziert zum schließen des Polygons
    ngon = numpy.random.randint(3, max_ngon + 1)
    # polygon_points=[]
    convex_polygon = None
    while type(convex_polygon) is not Polygon:  # da bei der Convexen Hülle ein Punkt oder String rauskommen kann
        polygon_points = []
        while len(polygon_points) < ngon:
            x = numpy.random.randint(0, max_rand + 1)
            y = numpy.random.randint(0, max_rand + 1)
            while (x, y) in polygon_points:
                x = numpy.random.randint(0, max_rand + 1)
                y = numpy.random.randint(0, max_rand + 1)
            polygon_points.append((x, y))
        convex_polygon = Polygon(polygon_points).convex_hull
    c_polygon = ConvexPolygon(list(convex_polygon.exterior.coords))
    return c_polygon


def plot_polygons(polygon_list: [ConvexPolygon], render=True, plot_width=450, plot_height=450, ncols=4) -> Column:
    plots = []
    colors = itertools.cycle(palette)
    for counter, polygon in enumerate(polygon_list):
        color = next(colors)
        title = "Polygon {}".format(counter)
        plots.append(polygon.plot_polygon(title=title, color=color, render=False))
    grid = gridplot(plots, ncols=ncols, plot_width=plot_width, plot_height=plot_height, toolbar_location="left")
    if render:
        show(grid)
    return grid


def plot_figures_as_grid(plot_list: [Figure], render=True, plot_width=600, plot_height=500, ncols=4) -> Column:
    grid = gridplot(plot_list, ncols=ncols, plot_width=plot_width, plot_height=plot_height, toolbar_location="left")
    if render:
        show(grid)
    return grid


def plot_polygons_in_single_plot(polygon_list: [ConvexPolygon], title="", plot_width=750, plot_height=500, render=True,
                                 border=None, reverse_legend=False) -> Figure:
    polygon_number = len(polygon_list)

    TOOLTIPS = [("index", "$index"), ("(x,y)", "($x{0.0}, $y{0.0})"), ]
    fig = figure(title=title, x_axis_label='x', y_axis_label='y', width=plot_width, height=plot_height,
                 tooltips=TOOLTIPS, toolbar_location="left")
    colors = itertools.cycle(palette)
    legend_items = []
    legend_polygons = []
    legend_all_polygons = []
    legend_vertices = []
    for counter, polygon in enumerate(polygon_list):
        color = next(colors)
        x = polygon.x_values
        y = polygon.y_values
        if polygon.plot_fill:
            legend_label = "polygon {} filled".format(counter)
            poly_fig = fig.patch(x, y, color=color, line_width=1, alpha=0.9, line_alpha=1, muted_alpha=0.05)
        else:
            legend_label = "polygon {}".format(counter)
            poly_fig = fig.patch(x, y, color=color, line_width=1, alpha=0.1, line_alpha=1, muted_alpha=0.05)
        circle_fig = fig.circle(x, y, color=color, line_width=1, muted_alpha=0.01, size=4)
        legend_polygons.append((legend_label, [poly_fig]))
        legend_vertices.append(circle_fig)
        legend_all_polygons = legend_all_polygons + [poly_fig]
    if border != None:
        fig_border = fig.line(border[0], border[1], line_color="red", line_width=1, muted_alpha=0.2)
        legend_items.append(("border", [fig_border]))

    legend_items.append(("vertices", legend_vertices))
    legend_items.append(("all polygons", legend_all_polygons))
    legend_items = legend_items + legend_polygons
    if reverse_legend:
        legend_items.reverse()
    legend = Legend(items=legend_items)
    legend.label_text_font_size = "10px"
    legend.click_policy = "mute"
    fig.add_layout(legend, 'right')
    if render:
        show(fig)
    return fig


def build_height_classes(polygon_list: [ConvexPolygon]) -> [HighClass]:
    ordered_polygons = sorted(polygon_list, key=lambda polygon: polygon.height, reverse=True)
    h_max_polygon = ordered_polygons[0].height
    w_max_polygon = max([polygon.width for polygon in polygon_list])
    alpha = 0.407
    i = 0
    height_classes = []
    polygon_count = len(ordered_polygons)
    while polygon_count > 0:
        hc = HighClass(i, alpha, h_max_polygon, w_max_polygon)
        hc_polygons = []
        while polygon_count > 0 and hc.min_border < ordered_polygons[0].height and ordered_polygons[
            0].height <= hc.max_border:
            hc_polygons.append(ordered_polygons.pop(0))
            polygon_count -= 1
        hc.set_polygons(hc_polygons)
        if len(hc.polygons) > 0:
            height_classes.append(hc)  # will man höhen Klassen ohne Polygone drinne ?
        i += 1
    return height_classes


def building_containers(height_classes: [HighClass]) -> [Container]:
    containers = []
    for height_class in height_classes:
        container = Container(height_class)
        containers.append(container)
    return containers


def pack_polygons(polygons: [ConvexPolygon], angle=0) -> End_Container:
    # building the endcontainer
    list_hc = build_height_classes(polygons)
    list_containers = building_containers(list_hc)
    list_mini_containers, mini_container_plot_steps = build_mini_containers_and_plots(list_containers)
    end_container = End_Container(list_mini_containers, angle)

    # plots
    p_tab = polygons_to_tab_plot(polygons)
    p_header_tab = Panel(child=p_tab, title="Polygons")
    hc_tab = hc_to_tab_plot(list_hc)
    hc_header_tab = Panel(child=hc_tab, title="Height-Classes")
    c_tab = containers_to_tab_plot(list_containers)
    c_header_tab = Panel(child=c_tab, title="Containers")
    mc_tab = mini_container_plots_to_tab_plot(mini_container_plot_steps)
    mc_header_tab = Panel(child=mc_tab, title="Mini-Containers")
    ec_tab = end_container_to_tab_plot(end_container)
    ec_header_tab = Panel(child=ec_tab, title="End-Container")
    all_tabs_with_header = Tabs(tabs=[p_header_tab, hc_header_tab, c_header_tab, mc_header_tab, ec_header_tab])

    end_container.plot_steps_all = all_tabs_with_header
    return end_container


def polygons_to_tab_plot(polygons: [ConvexPolygon], tab_poly_count=9, ncols=3, plot_width=450, plot_height=450) -> Tabs:
    polygon_tab_list = []
    polygons_in_one_tab = []
    colors = itertools.cycle(palette)
    for counter, polygon in enumerate(polygons):
        color = next(colors)
        polygon_title = "Polygon{}".format(counter)
        polygon_plot = polygon.plot_polygon(title=polygon_title, color=color, render=False)
        polygons_in_one_tab.append(polygon_plot)
        if len(polygons_in_one_tab) >= tab_poly_count or (counter + 1 == len(polygons)):
            # tab_layout= ([polygons_in_one_tab])
            if counter + 1 - tab_poly_count < 0:
                title = "Polygons {}-{}".format(0, counter)
            else:
                title = "Polygons {}-{}".format(counter + 1 - len(polygons_in_one_tab), counter)
            grid_title = Div(text="<b>{}<b>".format(title))
            polygons_grid = gridplot(polygons_in_one_tab, ncols=ncols, plot_width=plot_width, plot_height=plot_height,
                                     toolbar_location="left")
            tab_layout = layout(grid_title, polygons_grid)
            tab = Panel(child=tab_layout, title=title)
            polygon_tab_list.append(tab)
            polygons_in_one_tab = []
    polygon_tabs = Tabs(tabs=polygon_tab_list)
    return polygon_tabs


def hc_to_tab_plot(list_hc: [HighClass], plot_width=450, plot_height=450) -> Tabs:
    hc_tab_list = []
    for counter, hc in enumerate(list_hc):
        hc_plot = hc.plot_hc(render=False, plot_width=plot_width, plot_height=plot_height)
        tab = Panel(child=hc_plot, title="High-Class {}".format(counter))
        hc_tab_list.append(tab)
    hc_tabs = Tabs(tabs=hc_tab_list)
    return hc_tabs


def containers_to_tab_plot(list_containers: [Container], plot_width=700, plot_height=700) -> Tabs:
    container_tab_list = []
    for counter, container in enumerate(list_containers):
        container_plot = container.plot_container_steps(plot_width=plot_width, colums=3, plot_height=plot_height,
                                                        render=False)
        tab = Panel(child=container_plot, title="Container {}".format(counter, counter))
        container_tab_list.append(tab)
    container_tabs = Tabs(tabs=container_tab_list)
    return container_tabs


def mini_container_plots_to_tab_plot(mini_container_plot_steps: [Figure], plot_width=700, plot_height=700,
                                     ncols=3) -> Tabs:
    mini_plots_tabs = []
    for counter, m_plot in enumerate(mini_container_plot_steps):
        m_plot_grid = gridplot(m_plot, ncols=3, plot_width=plot_width, plot_height=plot_height, toolbar_location="left")
        grid_title = Div(text="<b>Hc{} to Mini-Containers<b>".format(counter))
        mc_layout = layout(grid_title, m_plot_grid)
        tab = Panel(child=mc_layout, title="Hc{}  Mini-Containers".format(counter))
        mini_plots_tabs.append(tab)
    mini_tabs = Tabs(tabs=mini_plots_tabs)
    return mini_tabs


def end_container_to_tab_plot(end_container: End_Container, plot_width=800, plot_height=800) -> Tabs:
    end_container_tabs = []
    polygons_plot = end_container.plot_polygons(render=False, plot_width=plot_width, plot_height=plot_height)
    tab = Panel(child=polygons_plot, title="End-Container")
    end_container_tabs.append(tab)
    container_plot = end_container.plot_container(render=False, plot_width=plot_width, plot_height=plot_height)
    tab = Panel(child=container_plot, title="Show Mini-Containers")
    end_container_tabs.append(tab)
    end_tabs = Tabs(tabs=end_container_tabs)
    return end_tabs


def plot_containers(container_list: [Container], render=True, colums=3, plot_width=500, plot_height=500) -> Column:
    container_plots = []
    for container in container_list:
        container_plots.append(container.plot_container(render=False))
    grid = gridplot(container_plots, ncols=colums, plot_width=plot_width, plot_height=plot_height,
                    toolbar_location="left")
    if render:
        show(grid)
    return grid


# https://kodify.net/python/math/truncate-decimals/
# Python.org (n.d.). math — Mathematical functions. Retrieved on October 22, 2019, from https://docs.python.org/3.8/library/math.html
def truncate(number: float, decimals=0) -> float:
    """
    Returns a value truncated to a specific number of decimal places.
    """
    if not isinstance(decimals, int):
        raise TypeError("decimal places must be an integer.")
    elif decimals < 0:
        raise ValueError("decimal places has to be 0 or more.")
    elif decimals == 0:
        return math.trunc(number)
    factor = 10.0 ** decimals
    return math.trunc(number * factor) / factor

# output_notebook()  # for using Bokeh in Jupyter Lab