#!/usr/bin/python
# -*- coding: utf-8 -*-

# The MIT License (MIT)

# This code is part of the CityGML2OBJs package

# Copyright (c) 2014 
# Filip Biljecki
# Delft University of Technology
# fbiljecki@gmail.com

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import math
import markup3dmodule
from lxml import etree
import copy
import triangle
import numpy as np
import shapely
from sklearn.decomposition import PCA


def getAreaOfGML(poly, height=True):
    """Function which reads <gml:Polygon> and returns its area.
    The function also accounts for the interior and checks for the validity of the polygon."""
    exteriorarea = 0.0
    interiorarea = 0.0
    # -- Decompose the exterior and interior boundary
    e, i = markup3dmodule.polydecomposer(poly)
    # -- Extract points in the <gml:LinearRing> of <gml:exterior>
    epoints = markup3dmodule.GMLpoints(e[0])
    if isPolyValid(epoints):
        if height:
            exteriorarea += get3DArea(epoints)
        else:
            exteriorarea += get2DArea(epoints)
    for idx, iring in enumerate(i):
        # -- Extract points in the <gml:LinearRing> of <gml:interior>
        ipoints = markup3dmodule.GMLpoints(iring)
        if isPolyValid(ipoints):
            if height:
                interiorarea += get3DArea(ipoints)
            else:
                interiorarea += get2DArea(ipoints)
    # -- Account for the interior
    area = exteriorarea - interiorarea
    # -- Area in dimensionless units (coordinate units)
    return area


# -- Validity of a polygon ---------
def isPolyValid(polypoints, output=True):
    """Checks if a polygon is valid. Second option is to supress output."""
    # -- Number of points of the polygon (including the doubled first/last point)
    npolypoints = len(polypoints)
    # -- Assume that it is valid, and try to disprove the assumption
    valid = True
    # -- Check if last point equal
    if polypoints[0] != polypoints[-1]:
        if output:
            print("\t\tA degenerate polygon. First and last points do not match.")
        valid = False
    # -- Check if it has at least three points
    if npolypoints < 4:  # -- Four because the first point is doubled as the last one in the ring
        if output:
            print("\t\tA degenerate polygon. The number of points is smaller than 3.")
        valid = False
    # -- Check if the points are planar
    if not isPolyPlanar(polypoints):
        if output:
            print("\t\tA degenerate polygon. The points are not planar.")
        valid = False
    # -- Check if some of the points are repeating
    for i in range(1, npolypoints):
        if polypoints[i] == polypoints[i - 1]:
            if output:
                print("\t\tA degenerate polygon. There are identical points.")
            valid = False
    # -- Check if the polygon does not have self-intersections
    # -- Disabled, something doesn't work here, will work on this later.
    # if not isPolySimple(polypoints):
    #    print "A degenerate polygon. The edges are intersecting."
    #    valid = False
    return valid


def isPolyPlanar(polypoints):
    """Checks if a polygon is planar."""
    # -- Normal of the polygon from the first three points
    try:
        normal = unit_normal(polypoints[0], polypoints[1], polypoints[2])
    except:
        return False
    # -- Number of points
    npolypoints = len(polypoints)
    # -- Tolerance
    eps = 0.01
    # -- Assumes planarity
    planar = True
    for i in range(3, npolypoints):
        vector = [polypoints[i][0] - polypoints[0][0], polypoints[i][1] - polypoints[0][1],
                  polypoints[i][2] - polypoints[0][2]]
        if math.fabs(dot(vector, normal)) > eps:
            planar = False
    return planar


def isPolySimple(polypoints): #todo: this function has to be adapted
    """Checks if the polygon is simple, i.e. it does not have any self-intersections.
    Inspired by http://www.win.tue.nl/~vanwijk/2IV60/2IV60_exercise_3_answers.pdf"""
    npolypoints = len(polypoints)
    # -- Check if the polygon is vertical, i.e. a projection cannot be made.
    # -- First copy the list so the originals are not modified
    temppolypoints = copy.deepcopy(polypoints)
    newpolypoints = copy.deepcopy(temppolypoints)
    # -- If the polygon is vertical
    #if math.fabs(unit_normal(temppolypoints[0], temppolypoints[1], temppolypoints[2])[2]) < 10e-6:
    #    vertical = True

    #else:
    #    vertical = False

    normal = calculate_polygon_normal(temppolypoints)
    if math.fabs(normal[2]) < 10e-6:
        vertical = True
        print("The polygon is vertical 2")
        print("math.fabs(normal[2]): ", math.fabs(normal[2]))
    else:
        vertical = False
        print("Not vertical 2")

    # -- We want to project the vertical polygon to the XZ plane
    # -- If a polygon is parallel with the YZ plane that will not be possible
    YZ = True
    for i in range(1, npolypoints):
        if temppolypoints[i][0] != temppolypoints[0][0]:
            YZ = False
            continue
    # -- Project the plane in the special case
    if YZ:
        for i in range(0, npolypoints):
            newpolypoints[i][0] = temppolypoints[i][1]
            newpolypoints[i][1] = temppolypoints[i][2]
    # -- Project the plane
    elif vertical:
        for i in range(0, npolypoints):
            newpolypoints[i][1] = temppolypoints[i][2]
    else:
        pass  # -- No changes here
    # -- Check for the self-intersection edge by edge
    for i in range(0, npolypoints - 3):
        if i == 0:
            m = npolypoints - 3
        else:
            m = npolypoints - 2
        for j in range(i + 2, m):
            if intersection(newpolypoints[i], newpolypoints[i + 1], newpolypoints[j % npolypoints],
                            newpolypoints[(j + 1) % npolypoints]):
                return False
    return True


def intersection(p, q, r, s):
    """Check if two line segments (pq and rs) intersect. Computation is in 2D.
    Inspired by http://www.win.tue.nl/~vanwijk/2IV60/2IV60_exercise_3_answers.pdf"""

    eps = 10e-6

    V = [q[0] - p[0], q[1] - p[1]]
    W = [r[0] - s[0], r[1] - s[1]]

    d = V[0] * W[1] - W[0] * V[1]

    if math.fabs(d) < eps:
        return False
    else:
        return True


# ------------------------------------------

def collinear(p0, p1, p2):
    # -- http://stackoverflow.com/a/9609069
    x1, y1 = p1[0] - p0[0], p1[1] - p0[1]
    x2, y2 = p2[0] - p0[0], p2[1] - p0[1]
    return x1 * y2 - x2 * y1 < 1e-12


# -- Area and other handy computations
def det(a):
    """Determinant of matrix a."""
    return a[0][0] * a[1][1] * a[2][2] + a[0][1] * a[1][2] * a[2][0] + a[0][2] * a[1][0] * a[2][1] - a[0][2] * a[1][1] * \
           a[2][0] - a[0][1] * a[1][0] * a[2][2] - a[0][0] * a[1][2] * a[2][1]


def unit_normal(a, b, c):
    """Unit normal vector of plane defined by points a, b, and c."""
    x = det([[1, a[1], a[2]],
             [1, b[1], b[2]],
             [1, c[1], c[2]]])
    y = det([[a[0], 1, a[2]],
             [b[0], 1, b[2]],
             [c[0], 1, c[2]]])
    z = det([[a[0], a[1], 1],
             [b[0], b[1], 1],
             [c[0], c[1], 1]])
    magnitude = (x ** 2 + y ** 2 + z ** 2) ** .5
    if magnitude == 0.0:
        raise ValueError(
            "The normal of the polygon has no magnitude. Check the polygon. The most common cause for this are two identical sequential points or collinear points.")
    return (x / magnitude, y / magnitude, z / magnitude)


def dot(a, b):
    """Dot product of vectors a and b."""
    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]


def cross(a, b):
    """Cross product of vectors a and b."""
    x = a[1] * b[2] - a[2] * b[1]
    y = a[2] * b[0] - a[0] * b[2]
    z = a[0] * b[1] - a[1] * b[0]
    return (x, y, z)


def get3DArea(polypoints):
    """Function which reads the list of coordinates and returns its area.
    The code has been borrowed from http://stackoverflow.com/questions/12642256/python-find-area-of-polygon-from-xyz-coordinates"""
    # -- Compute the area
    total = [0, 0, 0]
    for i in range(len(polypoints)):
        vi1 = polypoints[i]
        if i is len(polypoints) - 1:
            vi2 = polypoints[0]
        else:
            vi2 = polypoints[i + 1]
        prod = cross(vi1, vi2)
        total[0] += prod[0]
        total[1] += prod[1]
        total[2] += prod[2]
    result = dot(total, unit_normal(polypoints[0], polypoints[1], polypoints[2]))
    return math.fabs(result * .5)


def get2DArea(polypoints):
    """Reads the list of coordinates and returns its projected area (disregards z coords)."""
    flatpolypoints = copy.deepcopy(polypoints)
    for p in flatpolypoints:
        p[2] = 0.0
    return get3DArea(flatpolypoints)


def getNormal(polypoints):
    """Get the normal of the first three points of a polygon. Assumes planarity."""
    return unit_normal(polypoints[0], polypoints[1], polypoints[2])


def getAngles(normal):
    """Get the azimuth and altitude from the normal vector."""
    # -- Convert from polar system to azimuth
    azimuth = 90 - math.degrees(math.atan2(normal[1], normal[0]))
    if azimuth >= 360.0:
        azimuth -= 360.0
    elif azimuth < 0.0:
        azimuth += 360.0
    t = math.sqrt(normal[0] ** 2 + normal[1] ** 2)
    if t == 0:
        tilt = 0.0
    else:
        tilt = 90 - math.degrees(math.atan(normal[2] / t))  # 0 for flat roof, 90 for wall
    tilt = round(tilt, 3)

    return azimuth, tilt


def GMLstring2points(pointstring):
    """Convert list of points in string to a list of points. Works for 3D points."""
    listPoints = []
    # -- List of coordinates
    coords = pointstring.split()
    # -- Store the coordinate tuple
    assert (len(coords) % 3 == 0)
    for i in range(0, len(coords), 3):
        listPoints.append([float(coords[i]), float(coords[i + 1]), float(coords[i + 2])])
    return listPoints


def smallestPoint(list_of_points):
    "Finds the smallest point from a three-dimensional tuple list."
    smallest = []
    # -- Sort the points
    sorted_points = sorted(list_of_points, key=lambda x: (x[0], x[1], x[2]))
    # -- First one is the smallest one
    smallest = sorted_points[0]
    return smallest


def highestPoint(list_of_points, a=None):
    "Finds the highest point from a three-dimensional tuple list."
    highest = []
    # -- Sort the points
    sorted_points = sorted(list_of_points, key=lambda x: (x[0], x[1], x[2]))
    # -- Last one is the highest one
    if a is not None:
        equalZ = True
        for i in range(-1, -1 * len(list_of_points), -1):
            if equalZ:
                highest = sorted_points[i]
                if highest[2] != a[2]:
                    equalZ = False
                    break
            else:
                break
    else:
        highest = sorted_points[-1]
    return highest


def centroid(list_of_points):
    """Returns the centroid of the list of points."""
    sum_x = 0
    sum_y = 0
    sum_z = 0
    n = float(len(list_of_points))
    for p in list_of_points:
        sum_x += float(p[0])
        sum_y += float(p[1])
        sum_z += float(p[2])
    return [sum_x / n, sum_y / n, sum_z / n]

# This function delivers very unsatisfying results for some reason.
# The returned point lies on the contour of the polygon sometimes, wich then messes up the triangulation
def point_inside(list_of_points):
    """Returns a point that is guaranteed to be inside the polygon, thanks to Shapely."""
    #  Th_Fr: new function that actually works
    representative_point_tmp = centroid(list_of_points)
    representative_point = shapely.geometry.Point(representative_point_tmp)
    #  End of the changes by Th_Fr
    return representative_point.coords


def plane(a, b, c):
    """Returns the equation of a three-dimensional plane in space by entering the three coordinates of the plane."""
    p_a = (b[1] - a[1]) * (c[2] - a[2]) - (c[1] - a[1]) * (b[2] - a[2])
    p_b = (b[2] - a[2]) * (c[0] - a[0]) - (c[2] - a[2]) * (b[0] - a[0])
    p_c = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])
    p_d = -1 * (p_a * a[0] + p_b * a[1] + p_c * a[2])
    return p_a, p_b, p_c, p_d

# added by Th_Fr
def planeAdjusted(points):
    """
    Returns the equation of a plane in three dimensions using PCA.

    Parameters:
    points: list of lists or numpy array of shape (n, 3)
        List of points in 3D space [x, y, z] through which the plane should pass.
        At least 3 points are required to define a plane uniquely.

    Returns:
    p_a, p_b, p_c, p_d: float
        Parameters of the plane equation ax + by + cz + d = 0.
    """
    # Convert points to numpy array for easier manipulation
    points = np.array(points)

    # Check if at least 3 points are provided
    if points.shape[0] < 3:
        raise ValueError("At least 3 points are required to define a plane.")

    # Use PCA to fit the plane
    pca = PCA(n_components=3)
    pca.fit(points)
    normal = pca.components_[2]  # The normal vector to the plane

    # Extract coefficients
    p_a, p_b, p_c = normal
    p_d = -np.dot(normal, pca.mean_)  # Calculate d using the mean of points

    return p_a, p_b, p_c, p_d


def get_height(plane, x, y):
    """Get the missing coordinate from the plane equation and the partial coordinates."""
    p_a, p_b, p_c, p_d = plane
    z = (-p_a * x - p_b * y - p_d) / p_c
    return z


def get_y(plane, x, z):
    """Get the missing coordinate from the plane equation and the partial coordinates."""
    p_a, p_b, p_c, p_d = plane
    y = (-p_a * x - p_c * z - p_d) / (p_b)
    return y


def compare_normals(n1, n2):
    """Compares if two normals are equal or opposite. Takes into account a small tolerance to overcome floating point errors."""
    tolerance = 10e-2
    # -- Assume equal and prove otherwise
    equal = True
    # -- i
    if math.fabs(n1[0] - n2[0]) > tolerance:
        equal = False
    # -- j
    elif math.fabs(n1[1] - n2[1]) > tolerance:
        equal = False
    # -- k
    elif math.fabs(n1[2] - n2[2]) > tolerance:
        equal = False
    return equal


def reverse_vertices(vertices):
    """Reverse vertices. Useful to reorient the normal of the polygon."""
    reversed_vertices = []
    nv = len(vertices)
    for i in range(nv - 1, -1, -1):
        reversed_vertices.append(vertices[i])
    return reversed_vertices

# Added by Th_FR, inspirde by https://pythonseminar.de/prufen-ob-die-liste-doppelte-elemente-enthalt-in-python/
def has_duplicates(seq):
    seen = []
    unique_list = [x for x in seq if x not in seen and not seen.append(x)]
    return len(seq) != len(unique_list)
#  End of changes

# Added by Th_Fr
def weighted_centroid(vertices):
    """
    Calculate the weighted centroid of a polygon defined by vertices.

    Arguments:
    vertices (numpy array): Array of vertices of the polygon.

    Returns:
    numpy array: Weighted centroid [x, y, z].
    """
    total_area = 0.0
    centroid = np.zeros(3)
    num_vertices = len(vertices)

    for i in range(num_vertices):
        j = (i + 1) % num_vertices
        cross = np.cross(vertices[i], vertices[j])
        area = np.linalg.norm(cross)
        centroid += (vertices[i] + vertices[j]) * area
        total_area += area


    return centroid / (3 * total_area)

def calculate_polygon_normal_old(poly):
    """
       Calculate the normal vector of a polygon using a weighted centroid and cross product approach.

       Arguments:
       poly (list of lists): List of vertices of the polygon, where each vertex is [x, y, z].

       Returns:
       numpy array: Normal vector (nx, ny, nz) of the polygon's plane.
       """
    vertices = np.array(poly)
    num_vertices = len(vertices)

    # Calculate weighted centroid
    #print("here b")
    centroid1 = centroid(vertices)

    # Compute the normal vector using cross product of edges
    normal = np.zeros(3)
    for i in range(num_vertices):
        j = (i + 1) % num_vertices
        vi = vertices[i]
        vj = vertices[j]
        normal[0] += (vi[1] - centroid1[1]) * (vj[2] - centroid1[2]) - (vi[2] - centroid1[2]) * (vj[1] - centroid1[1])
        normal[1] += (vi[2] - centroid1[2]) * (vj[0] - centroid1[0]) - (vi[0] - centroid1[0]) * (vj[2] - centroid1[2])
        normal[2] += (vi[0] - centroid1[0]) * (vj[1] - centroid1[1]) - (vi[1] - centroid1[1]) * (vj[0] - centroid1[0])

    norm = np.linalg.norm(normal)
    if norm != 0:
        normal /= norm
    else:
        normal = np.array([0.0, 0.0, 0.0])

    return normal


def calculate_polygon_normal(polygon):
    """
    Calculate the surface normal of a polygon.

    Parameters:
    polygon (list): A list of vertices, where each vertex is a list of three coordinates [x, y, z].

    Returns:
    np.array: A normalized vector representing the surface normal.
    """

    normal = np.array([0.0, 0.0, 0.0])
    num_verts = len(polygon)

    for i in range(num_verts):
        current = np.array(polygon[i])
        next_vert = np.array(polygon[(i + 1) % num_verts])

        normal[0] += (current[1] - next_vert[1]) * (current[2] + next_vert[2])
        normal[1] += (current[2] - next_vert[2]) * (current[0] + next_vert[0])
        normal[2] += (current[0] - next_vert[0]) * (current[1] + next_vert[1])

    normal = normalize(normal)
    return normal


def normalize(vector):
    """
    Normalize a vector.

    Parameters:
    vector (np.array): A vector to normalize.

    Returns:
    np.array: A normalized vector.
    """
    norm = np.linalg.norm(vector)
    if norm == 0:
        return vector
    return vector / norm


def triangulation(e, i):
    """Triangulate the polygon with the exterior and interior list of points. Works only for convex polygons.
    Assumes planarity. Projects to a 2D plane and goes back to 3D."""
    vertices = []
    holes = []
    segments = []
    index_point = 0
    # -- Slope computation points
    a = [[], [], []]
    b = [[], [], []]
    for ip in range(len(e) - 1):
        vertices.append(e[ip])
        if a == [[], [], []] and index_point == 0:
            a = [e[ip][0], e[ip][1], e[ip][2]]
        if index_point > 0 and (e[ip] != e[ip - 1]):
            if b == [[], [], []]:
                b = [e[ip][0], e[ip][1], e[ip][2]]
        if ip == len(e) - 2:
            segments.append([index_point, 0])
        else:
            segments.append([index_point, index_point + 1])
        index_point += 1

    for hole in i:
        first_point_in_hole = index_point
        for p in range(len(hole) - 1):
            if p == len(hole) - 2:
                segments.append([index_point, first_point_in_hole])
            else:
                segments.append([index_point, index_point + 1])
            index_point += 1
            vertices.append(hole[p])
            # -- A more robust way to get the point inside the hole, should work for non-convex interior polygons
        # alt: holes.append(point_inside(hole[:-1]))
        # -- Alternative, use centroid
        holes.append(centroid(hole[:-1])) # This should be useful!
    # -- Project to 2D since the triangulation cannot be done in 3D with the library that is used
    npolypoints = len(vertices)
    nholes = len(holes)
    # -- Check if the polygon is vertical, i.e. a projection cannot be made.
    # -- First copy the list so the originals are not modified
    temppolypoints = copy.deepcopy(vertices)
    newpolypoints = copy.deepcopy(vertices)
    tempholes = copy.deepcopy(holes)
    newholes = copy.deepcopy(holes)

    # -- Compute the normal of the polygon for detecting vertical polygons and
    # -- for the correct orientation of the new triangulated faces
    # -- If the polygon is vertical
    #normal = unit_normal(temppolypoints[0], temppolypoints[1], temppolypoints[2])
    normal = calculate_polygon_normal(temppolypoints)

    if math.fabs(normal[2]) < 10e-2:
        vertical = True
        #print("The polygon is vertical")
        #print("math.fabs(normal[2]): ", math.fabs(normal[2]))
    else:
        vertical = False
        #print("Not vertical")
    # -- We want to project the vertical polygon to the XZ plane
    # -- If a polygon is parallel with the YZ plane that will not be possible


    YZ = True
    for i in range(1, npolypoints):
        if temppolypoints[i][0] != temppolypoints[0][0]:
            YZ = False
            continue


    # -- Project the plane in the special case
    if YZ == True:
        for i in range(0, npolypoints):
            newpolypoints[i][0] = temppolypoints[i][1]
            newpolypoints[i][1] = temppolypoints[i][2]
        for i in range(0, nholes):
            newholes[i][0] = tempholes[i][1]
            newholes[i][1] = tempholes[i][2]
    # -- Project the plane
    elif vertical == True:
        for i in range(0, npolypoints):
            newpolypoints[i][1] = temppolypoints[i][2]
        for i in range(0, nholes):
            newholes[i][1] = tempholes[i][2]
    else:
        pass  # -- No changes here

    # -- Drop the last point (identical to first)
    for p in newpolypoints:
        # print("p: ", p)
        p.pop(-1)
        # print("p: ", p)
    # -- If there are no holes
    if len(newholes) == 0:
        newholes = None
    else:
        counter = 0
        for h in newholes:
            counter = counter + 1
            h = h.pop(-1)

    # -- Plane information (assumes planarity) #todo: hier muss noch etwas angepasst werden; erledigt?
    a = e[0]
    b = e[1]
    c = e[2]
    # -- Construct the plane
    pl = planeAdjusted(e)

    # -- Prepare the polygon to be triangulated
    # Change by Th_Fr: Distinguishing different cases!
    # There are two cases distinguished here: 1. A Polygon without holes, 2. A polygon with holes
    if newholes == None:
        poly = {'vertices': np.array(newpolypoints), 'segments': np.array(segments)}
        # For some reason this if.case sometimes fails, this is why there is a second version of the
        # Trinangulation without the optional 'pQjz' parameter
        if has_duplicates(newpolypoints) == False:
            t = triangle.triangulate(poly, 'pQjz')
        else:
            t = triangle.triangulate(poly)

    else:
        poly = {'vertices': np.array(newpolypoints), 'segments': np.array(segments), 'holes': np.array(newholes)}
        t = triangle.triangulate(poly, 'pQjz')

    # End of changes by Th_Fr

    # -- Get the triangles and their vertices
    try:
        tris = t['triangles']
    except:
        print("strange error")
        tris = []

    try:
        vert = t['vertices'].tolist()
    except:
        vert = []
    # -- Store the vertices of each triangle in a list
    tri_points = []
    for tri in tris:
        tri_points_tmp = []
        for v in tri.tolist():
            vert_adj = [[], [], []]
            if YZ:
                vert_adj[0] = temppolypoints[0][0]
                vert_adj[1] = vert[v][0]
                vert_adj[2] = vert[v][1]
            elif vertical:
                vert_adj[0] = vert[v][0]
                vert_adj[2] = vert[v][1]
                vert_adj[1] = get_y(pl, vert_adj[0], vert_adj[2])
            else:
                vert_adj[0] = vert[v][0]
                vert_adj[1] = vert[v][1]
                vert_adj[2] = get_height(pl, vert_adj[0], vert_adj[1])
            tri_points_tmp.append(vert_adj)
            try:
                tri_normal = unit_normal(tri_points_tmp[0], tri_points_tmp[1], tri_points_tmp[2])
            except:
                continue
            if compare_normals(normal, tri_normal):
                tri_points.append(tri_points_tmp)
            else:
                tri_points_tmp = reverse_vertices(tri_points_tmp)
                tri_points.append(tri_points_tmp)
    return tri_points