Source code for util.geometry

# -*- coding: utf-8 -*-

"""
© Copyright Institut National de l'Information Géographique et Forestière (2020)
Contributors: 
    Yann Méneroux
Creation date: 1th november 2020

tracklib library provides a variety of tools, operators and 
functions to manipulate GPS trajectories. It is a open source contribution 
of the LASTIG laboratory at the Institut National de l'Information 
Géographique et Forestière (the French National Mapping Agency).
See: https://tracklib.readthedocs.io
 
This software is governed by the CeCILL-C license under French law and
abiding by the rules of distribution of free software. You can  use, 
modify and/ or redistribute the software under the terms of the CeCILL-C
license as circulated by CEA, CNRS and INRIA at the following URL
"http://www.cecill.info". 

As a counterpart to the access to the source code and rights to copy,
modify and redistribute granted by the license, users are provided only
with a limited warranty  and the software's author,  the holder of the
economic rights,  and the successive licensors  have only  limited
liability. 

In this respect, the user's attention is drawn to the risks associated
with loading,  using,  modifying and/or developing or reproducing the
software by the user in light of its specific status of free software,
that may mean  that it is complicated to manipulate,  and  that  also
therefore means  that it is reserved for developers  and  experienced
professionals having in-depth computer knowledge. Users are therefore
encouraged to load and test the software's suitability as regards their
requirements in conditions enabling the security of their systems and/or 
data to be ensured and,  more generally, to use and operate it in the 
same conditions as regards security. 

The fact that you are presently reading this means that you have had
knowledge of the CeCILL-C license and that you accept its terms.



Definition of some geometric functions

# -------------------------- Geometry Functions -------------------------------
# Geometry Functions  like:
#    - cartesienne
#    - __eval
#    - __right

#    - dist_point_droite
#    - dist_point_to_segment
#    - distance_to_segment

#    - projection_droite
#    - proj_segment
#    - proj_polyligne

#    - triangle_area
#    - aire_visval

#    - isSegmentIntersects
#    - intersection
#    - intersects

#    - inclusion

#    - transform
#    - transform_inverse
#
#    - angleBetweenThreePoints
#    -
# ----------------------------------------------------------------
"""


# For type annotation
from __future__ import annotations   

import math
import numpy as np

import tracklib as tracklib
from tracklib.core import ObsTime, Obs, makeCoords


def cartesienne(segment) -> list[float, float, float, float]:
    """Fonction equation cartesienne

    :return: liste de paramètres
    """

    parametres = list()

    x1 = segment[0]
    y1 = segment[1]
    x2 = segment[2]
    y2 = segment[3]

    u1 = x2 - x1
    u2 = y2 - y1

    b = -u1
    a = u2

    c = -(a * x1 + b * y1)

    parametres.append(a)
    parametres.append(b)
    parametres.append(c)

    return parametres


# ----------------------------------------
# Fonction de test d'equation de droite
# Entrees : paramatres et coords (x,y)
# Sortie : en particulier 0 si le point
# appartient a la droite
# ----------------------------------------
def __eval(param, x, y):

    a = param[0]
    b = param[1]
    c = param[2]

    return a * x + b * y + c


def right(a, b, c):
    return (a == c) or (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]) < 0


def direction(a, b, c):
    return (c[0] - a[0]) * (b[1] - a[1]) - (b[0] - a[0]) * (c[1] - a[1])


# ----------------------------------------
# Fonction distance point-droite
# Entree : paramètres a,b,c d'une droite,
# coordonnées x et y du point
# Sortie : distance du point à la droite
# ----------------------------------------
def dist_point_droite(param, x, y):

    a = param[0]
    b = param[1]
    c = param[2]

    distance = math.fabs(a * x + b * y + c)
    
    # TODO: ajouter test si a = 0 et b = 0
    #if a * a + b * b == 0:
    #    return -1
    distance /= math.sqrt(a * a + b * b)

    return distance


def dist_point_to_segment(point, segment):
    return dist_point_droite(cartesienne(segment), point.getX(), point.getY())


[docs] def distance_to_segment(x0: float, y0: float, x1: float, y1: float, x2: float, y2: float) -> float: """Function to compute distance between a point and a segment :param x0: point coordinate X :param y0: point coordinate Y :param x1: segment first point X :param y1: segment first point Y :param x2: segment second point X :param y2: segment second point Y :return: Distance between point and projection coordinates """ # Segment length l = math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) # Normalized scalar product psn = ((x0 - x1) * (x2 - x1) + (y0 - y1) * (y2 - y1)) / l X = max(x1, x2) Y = max(y1, y2) x = min(x1, x2) y = min(y1, y2) xproj = x1 + psn / l * (x2 - x1) yproj = y1 + psn / l * (y2 - y1) xproj = min(max(xproj, x), X) yproj = min(max(yproj, y), Y) # Compute distance d = math.sqrt((x0 - xproj) * (x0 - xproj) + (y0 - yproj) * (y0 - yproj)) return d
def projection_droite(param: tuple[float, float, float], x: float, y: float) -> tuple[float, float]: """ Fonction projection orthogonale sur une droite :param param: Paramètres a,b,c d'une droite :param x: X du point à projeter :param y: Y du point à projeter :return: coordonnée xproj et yproj du point projeté """ a = param[0] b = param[1] c = param[2] if b == 0: return (x, a) xv = -b yv = a norm = math.sqrt(xv * xv + yv * yv) xb = 0 yb = -c / b BH = ((x - xb) * xv + (y - yb) * yv) / norm xproj = xb + BH * xv / norm yproj = yb + BH * yv / norm return xproj, yproj # ---------------------------------------- # Fonction complète de projection # Entree : segment, coordonnées x et y du # point # Sortie : distance du point au segment et # coordonnées du projeté # ---------------------------------------- def proj_segment(segment, x, y): param = cartesienne(segment) a = param[0] b = param[1] c = param[2] distance = math.fabs(a * x + b * y + c) distance /= math.sqrt(a * a + b * b) # Récupération des coordonnées du projeté xproj, yproj = projection_droite(param, x, y) # Test d'inclusion dans le segment x1 = segment[0] y1 = segment[1] x2 = segment[2] y2 = segment[3] boolx1 = (xproj >= x1) and (xproj <= x2) boolx2 = (xproj <= x1) and (xproj >= x2) boolx = boolx1 or boolx2 booly1 = (yproj >= y1) & (yproj <= y2) booly2 = (yproj <= y1) & (yproj >= y2) booly = booly1 or booly2 bool_include = boolx and booly # Si le projeté est dans le segment if bool_include: a = param[0] b = param[1] c = param[2] xv = -b yv = a norm = math.sqrt(xv * xv + yv * yv) xb = 0 yb = -c / b BH = ((x - xb) * xv + (y - yb) * yv) / norm xproj = xb + BH * xv / norm yproj = yb + BH * yv / norm return distance, xproj, yproj else: distance1 = math.sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1)) distance2 = math.sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)) if distance1 <= distance2: return distance1, x1, y1 else: return distance2, x2, y2 # ----------------------------------------------- # Fonction complète de projection entre un # point et une polyligne # # Entree : polyligne, coordonnées x et y du point # Sortie : distance du point ) la polyligne # et coordonnées du projeté # ----------------------------------------------- def proj_polyligne(Xp, Yp, x, y): distmin = 1e400 xproj = None yproj = None iproj = -1 for i in range(len(Xp) - 1): x1 = Xp[i] y1 = Yp[i] x2 = Xp[i + 1] y2 = Yp[i + 1] if (abs(x1-x2) + abs(y1-y2) < 1e-16): continue dist, xp, yp = proj_segment([x1, y1, x2, y2], x, y) if dist < distmin: distmin = dist xproj = xp yproj = yp iproj = i return distmin, xproj, yproj, iproj # -------------------------------------------------------------------------- # Function to detect if a point is on left or right side of track # -------------------------------------------------------------------------- # Input : # - track :: Track # - x,y :: coordinate of the point we want to detect the side # -------------------------------------------------------------------------- # Output : list of sides # 0 if P is on the line # +1 if P is on left side of the track # -1 if P is on right side of the track # -------------------------------------------------------------------------- def detect_side(track, x, y, seuilMemeProj=0.1): SIDES = [] Xp = track.getX() Yp = track.getY() distmin = 1e400 INDICES = [] for i in range(len(Xp) - 1): x1 = Xp[i] y1 = Yp[i] x2 = Xp[i + 1] y2 = Yp[i + 1] if (abs(x1-x2) + abs(y1-y2) < 1e-16): continue dist, xp, yp = proj_segment([x1, y1, x2, y2], x, y) if abs(dist - distmin) < seuilMemeProj: if dist < distmin: distmin = dist INDICES.append(i) elif dist < distmin: distmin = dist INDICES = [] INDICES.append(i) for iproj in INDICES: xa = track[iproj].position.getX() ya = track[iproj].position.getY() xb = track[iproj+1].position.getX() yb = track[iproj+1].position.getY() pdt = (xb-xa)*(y-ya) - (yb-ya)*(x-xa) dist, xp, yp = proj_segment([xa, ya, xb, yb], x, y) # abscurv = tracklib.computeAbsCurvAnotherPoint(track, xp, yp) if iproj == 0: abscurv = track.getFirstObs().distanceTo(tracklib.Obs(tracklib.ENUCoords(xp, yp), tracklib.ObsTime())) else: d = track[iproj].distanceTo(tracklib.Obs(tracklib.ENUCoords(xp, yp), tracklib.ObsTime())) abscurv = tracklib.computeCurvAbsBetweenTwoPoints(track, 0, iproj) + d if pdt > 0: SIDES.append((dist, xp, yp, 1, abscurv)) elif pdt < 0: SIDES.append((dist, xp, yp, -1, abscurv)) else: SIDES.append((dist, xp, yp, 0, abscurv)) return SIDES # -------------------------------------------------------------------------- # Function to compute area of triangle with cross product # -------------------------------------------------------------------------- # Input : # - x0, y0 :: point 1 coordinates # - x1, y1 :: point 2 coordinates # - x2, y2 :: point 3 coordinates # -------------------------------------------------------------------------- # Output : area of P1P2P3 in coordinate units # -------------------------------------------------------------------------- def triangle_area(x0, y0, x1, y1, x2, y2): return 0.5 * abs((x1 - x0) * (y2 - y1) - (x2 - x1) * (y1 - y0)) # ============================================================================= """ Pour calculer l'aire des triangles dans Visvalingam """ def aire_visval(track, i): x0 = track.getObs(i - 1).position.getX() y0 = track.getObs(i - 1).position.getY() x1 = track.getObs(i).position.getX() y1 = track.getObs(i).position.getY() x2 = track.getObs(i + 1).position.getX() y2 = track.getObs(i + 1).position.getY() return triangle_area(x0, y0, x1, y1, x2, y2) # ---------------------------------------- # Fonction booleenne d'intersection # Entrees : segment1 et segment2 # Sortie : true s'il y a intersection # ---------------------------------------- def isSegmentIntersects(segment1, segment2): param_1 = cartesienne(segment1) param_2 = cartesienne(segment2) # a1 = param_1[0] # b1 = param_1[1] # c1 = param_1[2] # a2 = param_2[0] # b2 = param_2[1] # c2 = param_2[2] x11 = segment1[0] y11 = segment1[1] x12 = segment1[2] y12 = segment1[3] x21 = segment2[0] y21 = segment2[1] x22 = segment2[2] y22 = segment2[3] val11 = __eval(param_1, x21, y21) val12 = __eval(param_1, x22, y22) val21 = __eval(param_2, x11, y11) val22 = __eval(param_2, x12, y12) val1 = val11 * val12 val2 = val21 * val22 return (val1 <= 0) & (val2 <= 0) # ---------------------------------------- # Intersection between 2 tracks # withTime: time constraint (in secs) # (-1 if no time constraint) # ---------------------------------------- def intersection(track1, track2, withTime=-1): if not (track1.getSRID() == track2.getSRID()): print("Error: tracks must have same SRID to compute intersections") exit() I = tracklib.Track() TMP_I = [] TMP_J = [] TMP_TPS2 = [] for i in range(len(track1) - 1): x11 = track1[i].position.getX() y11 = track1[i].position.getY() x12 = track1[i + 1].position.getX() y12 = track1[i + 1].position.getY() seg1 = [x11, y11, x12, y12] for j in range(len(track2) - 1): x21 = track2[j].position.getX() y21 = track2[j].position.getY() x22 = track2[j + 1].position.getX() y22 = track2[j + 1].position.getY() seg2 = [x21, y21, x22, y22] if isSegmentIntersects(seg1, seg2): P1 = cartesienne(seg1) P2 = cartesienne(seg2) A = np.zeros((2, 2)) B = np.zeros((2, 1)) A[0, 0] = P1[0] A[0, 1] = P1[1] B[0, 0] = -P1[2] A[1, 0] = P2[0] A[1, 1] = P2[1] B[1, 0] = -P2[2] det = np.linalg.det(A) if abs(det) < 1e-12: # print("Parallel lines") continue X = np.linalg.solve(A, B) x = X[0, 0] y = X[1, 0] p = makeCoords(x, y, 0, track1.getSRID()) # Linear interpolation on track 1 w1 = p.distance2DTo(track1[i].position) w2 = p.distance2DTo(track1[i + 1].position) p.setZ( ( w1 * track1[i + 1].position.getZ() + w2 * track1[i].position.getZ() ) / (w1 + w2) ) t1 = track1[i].timestamp.toAbsTime() t2 = track1[i].timestamp.toAbsTime() ta = (w1 * t2 + w2 * t1) / (w1 + w2) # Linear interpolation on track 2 w1 = p.distance2DTo(track2[j].position) w2 = p.distance2DTo(track2[j + 1].position) t1 = track2[j].timestamp.toAbsTime() t2 = track2[j+1].timestamp.toAbsTime() tb = (w1 * t2 + w2 * t1) / (w1 + w2) # Add intersection if (withTime == -1) or (abs(tb - ta) < withTime): I.addObs(Obs(p, ObsTime.readUnixTime(ta))) TMP_TPS2.append(ObsTime.readUnixTime(tb)) TMP_I.append(i) TMP_J.append(j) if I.size() > 0: I.createAnalyticalFeature("timestamp2", TMP_TPS2) I.createAnalyticalFeature("id1", TMP_I) I.createAnalyticalFeature("id2", TMP_J) return I # ---------------------------------------- # Intersection between 2 tracks (boolean) # ---------------------------------------- def intersects(track1, track2): for i in range(len(track1) - 1): x11 = track1[i].position.getX() y11 = track1[i].position.getY() x12 = track1[i + 1].position.getX() y12 = track1[i + 1].position.getY() seg1 = [x11, y11, x12, y12] for j in range(len(track2) - 1): x21 = track2[j].position.getX() y21 = track2[j].position.getY() x22 = track2[j + 1].position.getX() y22 = track2[j + 1].position.getY() seg2 = [x21, y21, x22, y22] if isSegmentIntersects(seg1, seg2): return True return False # -------------------------------------------------------- # Fonction booleenne d'inlcusion d'un pt dans un polygone # Un polygone est defini comme une liste de points : # -------------------------------------------------------- def inclusion(X, Y, x, y): cmax = 2 * max(np.max(X), np.max(Y)) segment = list() segment.append(x) segment.append(y) segment.append(cmax) segment.append(cmax) n = 0 # Number of intersections for i in range(len(X) - 1): edge = list() edge.append(X[i]) edge.append(Y[i]) edge.append(X[i + 1]) edge.append(Y[i + 1]) if isSegmentIntersects(segment, edge): n += 1 return n % 2 == 1 def transform(theta, tx, ty, X, Y): XR = [0] * len(X) YR = [0] * len(Y) ct = math.cos(theta) st = math.sin(theta) for j in range(len(X)): XR[j] = ct * (X[j] - tx) + st * (Y[j] - ty) YR[j] = -st * (X[j] - tx) + ct * (Y[j] - ty) return XR, YR def transform_inverse(theta, tx, ty, X, Y): XR = [0] * len(X) YR = [0] * len(Y) ct = math.cos(theta) st = math.sin(theta) for j in range(len(X)): XR[j] = ct * X[j] - st * Y[j] + tx YR[j] = st * X[j] + ct * Y[j] + ty return XR, YR # Fonction de calcul de l'azimut (en °) # Entrées : coordonnées de P1 et P2 # Sortie : azimut (en °) de P1 vers P2 def azimut(x1, y1, x2, y2): dx = x2 - x1 dy = y2 - y1 if (dx == 0) and (dy == 0): print("Erreur : les points p1 et p2 doivent être distincts") return 0 q = math.sqrt(dx * dx + dy * dy) + dy if q == 0: azimut = 0 else: azimut = 2 * math.atan(dx / q) * 180 / math.pi if azimut < 0: azimut += 360 return azimut def collinear(p1, p2, p3): """ Parameters ---------- p1, p2, p3 : [x, y] Returns ------- bool DESCRIPTION. """ x1 = p1[0] y1 = p1[1] x2 = p2[0] y2 = p2[1] x3 = p3[0] y3 = p3[1] xVectP1P2 = x2 - x1 yVectP1P2 = y2 - y1 xVectP1P3 = x3 - x1 yVectP1P3 = y3 - y1 if xVectP1P2 * yVectP1P3 - xVectP1P3 * yVectP1P2 == 0: # points are collinear return True else: # Not collinear return False def angleBetweenThreePoints(o1, o2, o3): """ Compute angle between three points (the angle is calculated for the middle point). :param float o_1: first point :param float o_2: second point :param float o_3: third point :return: angle in radian """ x1 = o1.position.getX() x2 = o2.position.getX() x3 = o3.position.getX() y1 = o1.position.getY() y2 = o2.position.getY() y3 = o3.position.getY() # 3 points confondus if x1 == x2 and x2 == x3 and y1 == y2 and y2 == y3: return 0 # 2 points confondus if x1 == x2 and y1 == y2: return 0 if x1 == x3 and y1 == y3: return 0 if x2 == x3 and y2 == y3: return 0 # print (x1, x2, x3, y1, y2, y3) num = (x1 - x2) * (x3 - x2) + (y1 - y2) * (y3 - y2) den = math.sqrt((x1-x2)**2 + (y1-y2)**2) * math.sqrt((x3-x2)**2 + (y3-y2)**2) r = num / den if r > 1: r = 1 if r < -1: r = -1 return math.acos(r)