import math
import numpy as np
from math import sqrt
from shapely.geometry import Point, LineString, MultiLineString
"""
Readapted for LineStrings from Abhinav Ramakrishnan's post in https://stackoverflow.com/a/28261304/7375309.
"""
def _dot(vA, vB):
return vA[0]*vB[0]+vA[1]*vB[1]
[docs]def get_coord_angle(origin, distance, angle):
"""
The function returns the coordinates of the line starting from a tuple of coordinates, which forms with the y axis an angle in degree of a certain magnitude,
given the distance from the origin.
Parameters
----------
origin: tuple
Tuple of coordinates.
distance: float
The distance from the origin coordinates.
angle: float
The desired angle.
Returns:
----------
coords: tuple
The resulting coordinates.
"""
(disp_x, disp_y) = (distance * math.sin(math.radians(angle)), distance * math.cos(math.radians(angle)))
coord = (origin[0] + disp_x, origin[1] + disp_y)
return coord
class Error(Exception):
"""Base class for other exceptions"""
class AngleError(Error):
"""Raised when not-intersecting lines are provided for computing angles"""
class Settings():
"""
A class to store and compare the coordinates of two line geometries.
Attributes
----------
x_originA: float
The x-coordinate of the first point of the first line geometry.
y_originA: float
The y-coordinate of the first point of the first line geometry.
x_secondA: float
The x-coordinate of the second point of the first line geometry.
y_secondA: float
The y-coordinate of the second point of the first line geometry.
x_destinationA: float
The x-coordinate of the last point of the first line geometry.
y_destinationA: float
The y-coordinate of the last point of the first line geometry.
x_second_lastA: float
The x-coordinate of the second-last point of the first line geometry.
y_second_lastA: float
The y-coordinate of the second-last point of the first line geometry.
x_originB: float
The x-coordinate of the first point of the second line geometry.
y_originB: float
The y-coordinate of the first point of the second line geometry.
x_secondB: float
The x-coordinate of the second point of the second line geometry.
y_secondB: float
The y-coordinate of the second point of the second line geometry.
x_destinationB: float
The x-coordinate of the last point of the second line geometry.
y_destinationB: float
The y-coordinate of the last point of the second line geometry.
x_second_lastB: float
The x-coordinate of the second-last point of the second line geometry.
y_second_lastB: float
The y-coordinate of the second-last point of the second line geometry.
lineA (Tuple):
The coordinates of the first line used to calculate the angle.
lineB (Tuple):
The coordinates of the second linne used to calculate the angle.
"""
def set_coordinates(self, coords, prefix):
"""
Set the coordinates for a line.
Parameters:
----------
coords: list
A list of coordinates in the form [(x1, y1), (x2, y2), ...].
prefix: str
A string prefix to be added to the variable names for the coordinates.
For example, if "A" is passed as the prefix, the coordinates will be stored as
self.x_originA, self.y_originA, etc.
"""
setattr(self, 'x_origin'+ prefix, float("{0:.10f}".format(coords[0][0])))
setattr(self, 'y_origin'+ prefix, float("{0:.10f}".format(coords[0][1])))
setattr(self, 'x_second'+ prefix, float("{0:.10f}".format(coords[1][0])))
setattr(self, 'y_second'+ prefix, float("{0:.10f}".format(coords[1][1])))
setattr(self, 'x_destination'+ prefix, float("{0:.10f}".format(coords[-1][0])))
setattr(self, 'y_destination'+ prefix, float("{0:.10f}".format(coords[-1][1])))
setattr(self, 'x_second_last'+ prefix, float("{0:.10f}".format(coords[-2][0])))
setattr(self, 'y_second_last'+ prefix, float("{0:.10f}".format(coords[-2][1])))
def set_conditions(self, calculation_type):
"""
Given a Setting object and a calculation type, this function returns the lines that will be used to compute the angle.
Parameters
----------
Setting: object
An object of the Setting class, which contains information about the lines.
calculation_type: str
One of: 'vectors', 'angular_change', 'deflection':
- 'vectors': computes angle between vectors.
- 'angular_change': computes angle of incidence between the two lines.
- 'deflection': computes angle of incidence between the two lines, on the basis of the vertex in common and the second following(intermediate, if existing)
vertexes forming each of the line.
Raises
------
AngleError: If the lines do not have a common vertex.
"""
if calculation_type == "angular_change":
if (self.x_destinationA, self.y_destinationA) == (self.x_destinationB, self.y_destinationB):
self.lineA = ((self.x_second_lastA, self.y_second_lastA), (self.x_destinationA, self.y_destinationA))
self.lineB = ((self.x_destinationB, self.y_destinationB), (self.x_second_lastB, self.y_second_lastB))
elif (self.x_destinationA, self.y_destinationA) == (self.x_originB, self.y_originB):
self.lineA = ((self.x_second_lastA, self.y_second_lastA), (self.x_destinationA, self.y_destinationA))
self.lineB = ((self.x_originB, self.y_originB), (self.x_secondB, self.y_secondB))
elif (self.x_originA, self.y_originA) == (self.x_originB, self.y_originB):
self.lineA = ((self.x_secondA, self.y_secondA), (self.x_originA, self.y_originA))
self.lineB = ((self.x_originB, self.y_originB), (self.x_secondB, self.y_secondB))
elif (self.x_originA, self.y_originA) == (self.x_destinationB, self.y_destinationB):
self.lineA = ((self.x_secondA, self.y_secondA), (self.x_originA, self.y_originA))
self.lineB = ((self.x_destinationB, self.y_destinationB), (self.x_second_lastB, self.y_second_lastB))
# no common vertex
else:
raise AngleError("The lines do not intersect! provide lines wich have a common vertex")
# deflection on the entire lines
elif calculation_type == "deflection":
if (self.x_destinationA, self.y_destinationA) == (self.x_destinationB, self.y_destinationB):
self.lineA = ((self.x_originA, self.y_originA), (self.x_destinationA, self.y_destinationA))
self.lineB = ((self.x_destinationB, self. y_destinationB), (self.x_originB, self.y_originB))
elif (self.x_destinationA, self.y_destinationA) == (self.x_originB, self.y_originB):
self.lineA = ((self.x_originA, self.y_originA), (self.x_destinationA, self.y_destinationA))
self.lineB = ((self.x_originB, self.y_originB), (self.x_destinationB, self.y_destinationB))
elif (self.x_originA, self.y_originA) == (self.x_originB, self.y_originB):
self.lineA = ((self.x_destinationA, self.y_destinationA), (self.x_originA, self.y_originA))
self.lineB = ((self.x_originB, self.y_originB), (self.x_destinationB, self.y_destinationB))
elif (self.x_originA, self.y_originA) == (self.x_destinationB, self.y_destinationB):
self.lineA = ((self.x_destinationA, self.y_destinationA), (self.x_originA, self.y_originA))
self.lineB = ((self.x_destinationB, self.y_destinationB), (self.x_originB, self.y_originB))
# no common vertex
else:
raise AngleError("The lines do not intersect! provide lines wich have a common vertex")
else: # calculation_type == "vectors"
if (self.x_destinationA, self.y_destinationA) == (self.x_destinationB, self.y_destinationB):
self.lineA = ((self.x_destinationA, self.y_destinationA), (self.x_originA, self.y_originA))
self.lineB = ((self.x_destinationB, self.y_destinationB), (self.x_originB, self.y_originB))
elif (self.x_destinationA, self.y_destinationA) == (self.x_originB, self.y_originB):
self.lineA = ((self.x_destinationA, self.y_destinationA), (self.x_originA, self.y_originA))
self.lineB = ((self.x_originB, self.y_originB), (self.x_destinationB, self.y_destinationB))
elif (self.x_originA, self.y_originA) == (self.x_originB, self.y_originB):
self.lineA = ((self.x_originA, self.y_originA), (self.x_destinationA, self.y_destinationA))
self.lineB = ((self.x_originB, self.y_originB), (self.x_destinationB, self.y_destinationB))
elif (self.x_originA, self.y_originA) == (self.x_destinationB, self.y_destinationB):
self.lineA = ((self.x_originA, self.y_originA), (self.x_destinationA, self.y_destinationA))
self.lineB = ((self.x_destinationB, self.y_destinationB),(self.x_originB, self.y_originB))
# no common vertex
else:
raise AngleError("The lines do not intersect! provide lines wich have a common vertex")
def __init__(self, coordsA, coordsB, calculation_type):
"""
Initializes the class with the coordinates of two line geometries.
Args
----------
coordsA: list
A list of coordinates of the first line geometry.
coordsB: list
A list of coordinates of the second line geometry.
"""
self.set_coordinates(coordsA, 'A')
self.set_coordinates(coordsB, 'B')
self.set_conditions(calculation_type)
[docs]def angle_line_geometries(line_geometryA, line_geometryB, degree = False, calculation_type = 'vector'):
"""
Given two LineStrings it computes the angle between them. Returns value in degrees or radians.
Parameters
----------
line_geometryA: LineString
The first line.
line_geometryB: LineString
The other line; it must share a vertex with line_geometryA.
degree: boolean
If True it returns value in degree, otherwise in radians.
calculation_type: string
One of: 'vectors', 'angular_change', 'deflection':
-'vectors': computes angle between vectors.
-'angular_change': computes angle of incidence between the two lines.
-'deflection': computes angle of incidence between the two lines, on the basis of the vertex in common and the second following(intermediate, if existing)
vertexes forming each of the line.
Returns:
----------
angle: float
The resulting angle in radians or degrees.
"""
valid_calculation_types = ['vectors', 'angular_change', 'deflection']
if not isinstance(line_geometryA, LineString) or not isinstance(line_geometryB, LineString):
raise TypeError("Both input must be of type shapely.geometry.LineString")
if calculation_type not in valid_calculation_types:
raise ValueError(f"Invalid calculation type. Choose one of: {valid_calculation_types}.")
# extracting coordinates and createing lines
coordsA = list(line_geometryA.coords)
coordsB = list(line_geometryB.coords)
if len(coordsA) < 2 or len(coordsB) < 2:
raise ValueError("Both LineString must have at least 2 coordinates")
settings = Settings(coordsA, coordsB, calculation_type)
lineA, lineB = settings.lineA, settings.lineB
# Get nicer vector form
vA = [(lineA[0][0]-lineA[1][0]), (lineA[0][1]-lineA[1][1])]
vB = [(lineB[0][0]-lineB[1][0]), (lineB[0][1]-lineB[1][1])]
try:
# Get dot prod
dot_prod = _dot(vA, vB)
# Get magnitudes
magA = _dot(vA, vA)**0.5
magB = _dot(vB, vB)**0.5
# Get cosine value
cos_ = dot_prod/magA/magB
# Get angle in radians and then convert to degrees
angle_rad = math.acos(dot_prod/magB/magA)
# Basically doing angle <- angle mod 360
angle_deg = math.degrees(angle_rad)%360
except:
angle_deg = 0.0
angle_rad = 0.0
angle = angle_rad
if degree:
angle = angle_deg
return angle