# -*- coding: utf-8 -*-
import numpy as np
from typing import Tuple, List
from shapely.geometry import LineString
from igp2.opendrive.elements.geometry import (
Geometry,
Line,
Spiral,
ParamPoly3,
Arc,
Poly3,
)
from igp2.opendrive.elements.geometry import normalise_angle, ramer_douglas
[docs]
class PlanView:
"""The plan view record contains a series of geometry records
which define the layout of the road's
reference line in the x/y-plane (plan view).
(Section 5.3.4 of OpenDRIVE 1.4)
"""
def __init__(self):
self._geometries = []
self._precalculation = None
self._midline = None
self.should_precalculate = 0
self._geo_lengths = np.array([0.0])
self.cache_time = 0
self.normal_time = 0
def _add_geometry(self, geometry: Geometry, should_precalculate: bool):
"""
Args:
geometry:
should_precalculate:
"""
self._geometries.append(geometry)
if should_precalculate:
self.should_precalculate += 1
else:
self.should_precalculate -= 1
self._add_geo_length(geometry.length)
[docs]
def add_line(self, start_pos, heading, length):
"""
Args:
start_pos:
heading:
length:
"""
self._add_geometry(Line(start_pos, heading, length), False)
[docs]
def add_spiral(self, start_pos, heading, length, curve_start, curve_end):
"""
Args:
start_pos:
heading:
length:
curve_start:
curve_end:
"""
self._add_geometry(Spiral(start_pos, heading, length, curve_start, curve_end), True)
[docs]
def add_arc(self, start_pos, heading, length, curvature):
"""
Args:
start_pos:
heading:
length:
curvature:
"""
self._add_geometry(Arc(start_pos, heading, length, curvature), True)
[docs]
def add_param_poly3(
self, start_pos, heading, length, aU, bU, cU, dU, aV, bV, cV, dV, pRange
):
"""
Args:
start_pos:
heading:
length:
aU:
bU:
cU:
dU:
aV:
bV:
cV:
dV:
pRange:
"""
self._add_geometry(
ParamPoly3(
start_pos, heading, length, aU, bU, cU, dU, aV, bV, cV, dV, pRange
),
True,
)
[docs]
def add_poly3(self, start_pos, heading, length, a, b, c, d):
"""
Args:
start_pos:
heading:
length:
a:
b:
c:
d:
"""
self._add_geometry(Poly3(start_pos, heading, length, a, b, c, d), True)
def _add_geo_length(self, length: float):
"""Add length of a geometry to the array which keeps track at which position
which geometry is placed. This array is used for quickly accessing the proper geometry
for calculating a position.
Args:
length: Length of geometry to be added.
"""
self._geo_lengths = np.append(self._geo_lengths, length + self._geo_lengths[-1])
@property
def length(self) -> float:
"""Get length of whole plan view"""
return self._geo_lengths[-1]
@property
def start_position(self) -> np.ndarray:
return self._geometries[0].start_position
@property
def end_position(self) -> np.ndarray:
if self._precalculation is not None:
return self._precalculation[-1][1:3]
return np.array(self.calc(self.length)[0])
@property
def midline(self) -> LineString:
""" The midline of the entire road geometry """
return self._midline
@property
def geometries(self) -> List[Geometry]:
""" Return the list of geometric objects that define the road layout. """
return self._geometries
[docs]
def calc(self, s_pos: float) -> Tuple[np.ndarray, float]:
"""Calculate position and tangent at s_pos.
Either interpolate values if it possible or delegate calculation
to geometries.
Args:
s_pos: Position on PlanView in ds.
Returns:
Position (x,y) in cartesian coordinates. Tangent in radians at position s_pos in range [-pi, pi].
"""
if self._precalculation is not None:
result_pos, result_tang = self.interpolate_cached_values(s_pos)
else:
result_pos, result_tang = self.calc_geometry(s_pos)
result_tang = normalise_angle(result_tang)
return result_pos, result_tang
[docs]
def interpolate_cached_values(self, s_pos: float) -> Tuple[np.ndarray, float]:
"""Calc position and tangent at s_pos by interpolating values
in _precalculation array.
Args:
s_pos: Position on PlanView in ds.
Returns:
Position (x,y) in cartesian coordinates.
Angle in radians at position s_pos.
"""
# start = time.time()
# we need idx for angle interpolation
# so idx can be used anyway in the other np.interp function calls
idx = np.abs(self._precalculation[:, 0] - s_pos).argmin()
if s_pos - self._precalculation[idx, 0] < 0 or idx + 1 == len(
self._precalculation
):
idx -= 1
result_pos_x = np.interp(
s_pos,
self._precalculation[idx: idx + 2, 0],
self._precalculation[idx: idx + 2, 1],
)
result_pos_y = np.interp(
s_pos,
self._precalculation[idx: idx + 2, 0],
self._precalculation[idx: idx + 2, 2],
)
result_tang = self.interpolate_angle(idx, s_pos)
result_pos = np.array((result_pos_x, result_pos_y))
# end = time.time()
# self.cache_time += end - start
return result_pos, result_tang
[docs]
def interpolate_angle(self, idx: int, s_pos: float) -> float:
"""Interpolate two angular values using the shortest angle between both values.
Args:
idx: Index where values in _precalculation should be accessed.
s_pos: Position at which interpolated angle should be calculated.
Returns:
Interpolated angle in radians.
"""
angle_prev = self._precalculation[idx, 3]
angle_next = self._precalculation[idx + 1, 3]
pos_prev = self._precalculation[idx, 0]
pos_next = self._precalculation[idx + 1, 0]
shortest_angle = ((angle_next - angle_prev) + np.pi) % (2 * np.pi) - np.pi
return angle_prev + shortest_angle * (s_pos - pos_prev) / (pos_next - pos_prev)
[docs]
def calc_geometry(self, s_pos: float) -> Tuple[np.ndarray, float]:
"""Calc position and tangent at s_pos by delegating calculation to geometry.
Args:
s_pos: Position on PlanView in ds.
Returns:
Position (x,y) in cartesian coordinates.
Angle in radians at position s_pos.
"""
try:
# get index of geometry which is at s_pos
mask = self._geo_lengths > s_pos
sub_idx = np.argmin(self._geo_lengths[mask] - s_pos)
geo_idx = np.arange(self._geo_lengths.shape[0])[mask][sub_idx] - 1
except ValueError:
# s_pos is after last geometry because of rounding error
if np.isclose(s_pos, self._geo_lengths[-1]):
geo_idx = self._geo_lengths.size - 2
else:
raise Exception(
f"Tried to calculate a position outside of the borders of the reference path at s={s_pos}"
f", but path has only length of l={self._geo_lengths[-1]}"
)
# geo_idx is index which geometry to use
return self._geometries[geo_idx].calc_position(
s_pos - self._geo_lengths[geo_idx]
)
[docs]
def precalculate(self, precision: float = 0.25, linestring: bool = False):
"""Precalculate coordinates of planView to save computing resources and time.
Save result in _precalculation array.
Args:
precision: Precision with which to calculate points on the geometry
linestring: True if pre-calculation should also be stored as a LineString into the field midline. Overrides
the should_precalculate flag
"""
if not linestring and self.should_precalculate < 1:
return
num_steps = int(max(2, np.ceil(self.length / precision)))
positions = np.linspace(0, self.length, num_steps)
self._precalculation = np.empty([num_steps, 4])
for i, pos in enumerate(positions):
coord, tang = self.calc_geometry(pos)
self._precalculation[i] = (pos, coord[0], coord[1], tang)
if linestring:
curve = self._precalculation[:, 1:3]
curve = ramer_douglas(curve, 0.005) # Simplify midline
self._midline = LineString(curve)