Source code for igp2.opendrive.elements.road

# -*- coding: utf-8 -*-
import logging
from typing import Union, Tuple

import numpy as np

from shapely.geometry import JOIN_STYLE, Point, LineString
from shapely.ops import unary_union, substring
from shapely.geometry.polygon import Polygon

from igp2.opendrive.elements.geometry import normalise_angle, ramer_douglas
from igp2.opendrive.elements.road_plan_view import PlanView
from igp2.opendrive.elements.road_link import RoadLink
from igp2.opendrive.elements.road_lanes import Lanes
from igp2.opendrive.elements.road_elevation_profile import (
    ElevationProfile,
)
from igp2.opendrive.elements.road_lateral_profile import LateralProfile
from igp2.opendrive.elements.junction import Junction

logger = logging.getLogger(__name__)


[docs]class Road: """ Road object of the OpenDrive standard (OpenDrive 1.6.1 - Section 8) """ def __init__(self): self._id = None self._name = None self._junction = None self._length = None self._boundary = None self._header = None # TODO self._link = RoadLink() self._types = [] self._planView = PlanView() self._elevation_profile = ElevationProfile() self._lateral_profile = LateralProfile() self._lanes = Lanes() def __eq__(self, other): return other.__class__ is self.__class__ and self.__dict__ == other.__dict__ def __repr__(self): return f"{self.name} from {self.plan_view.start_position} with length {self.plan_view.length}" @property def id(self) -> int: """ Unique ID of the Road """ return self._id @id.setter def id(self, value): self._id = int(value) @property def name(self) -> str: """ Name of the Road""" return self._name @name.setter def name(self, value): self._name = str(value) @property def junction(self): """ Junction object if the Road is part of a junction """ return self._junction @junction.setter def junction(self, value): if not isinstance(value, (Junction, int)) and value is not None: raise TypeError("Property must be a Junction or NoneType") if value == -1: value = None self._junction = value @property def link(self): """ """ return self._link @property def types(self): """ """ return self._types @property def plan_view(self) -> PlanView: """ PlanView describing the RoadGeometry of the Road in the OpenDrive standard (OpenDrive 1.6.1 - Section 7) """ return self._planView @property def length(self): return self._planView.length @property def midline(self) -> LineString: """ The Road midline """ return self.plan_view.midline
[docs] def distance_at(self, point: Union[Point, Tuple[float, float], np.ndarray]) -> float: """ Return the distance along the Road midline at the given point. Args: point: The point to check Returns: distance float """ p = Point(point) return self._planView.midline.project(p)
[docs] def point_at(self, distance: float) -> np.ndarray: """ Return the point along the Road midline at the given distance. Args: distance: The point to check Returns: 1d numpy array """ return self._planView.calc(distance)[0]
[docs] def calculate_road_geometry(self, resolution: float = 0.25, fix_eps: float = 1e-2): """ Calculate the boundary Polygon of the road. Calculates boundaries of lanes as a sub-function. Args: resolution: Sampling resolution for geometries fix_eps: If positive, then the algorithm attempts to fix sliver geometry in the map with this threshold """ if self.lanes is None or self.lanes.lane_sections == []: return self.calculate_center_lane(resolution) boundary = Polygon() for ls in self.lanes.lane_sections: if ls.length < resolution: logger.debug(f"Road {self.id} skipping too short lane-section {ls.idx}. This will likely cause errors " f"downstream. Consider removing the lane-section from the XODR file.") continue start_segment = ls.center_lanes[0].reference_line sample_distances = np.linspace(0.0, ls.length, int(ls.length / resolution) + 1) previous_direction = None reference_segment = start_segment reference_widths = np.zeros_like(sample_distances) for lane in ls.all_lanes: current_direction = np.sign(lane.id) if previous_direction is None or previous_direction != current_direction: reference_segment = start_segment reference_widths = np.zeros_like(sample_distances) lane_boundary, reference_segment, segment_widths = \ lane.sample_geometry(sample_distances, start_segment, reference_segment, reference_widths) boundary = unary_union([boundary, lane_boundary]) previous_direction = current_direction reference_widths += segment_widths if fix_eps > 0.0: boundary = boundary.buffer(fix_eps, 1, join_style=JOIN_STYLE.mitre) \ .buffer(-fix_eps, 1, join_style=JOIN_STYLE.mitre) if not boundary.boundary.geom_type == "LineString": logger.warning(f"Boundary of road ID {self.id} is not a closed a loop!") self._boundary = boundary
[docs] def calculate_center_lane(self, resolution: float): """ Calculate center lane of the road by applying lane offsets and store them in the respective center lanes. """ ref_line = self.plan_view.midline if not self.lanes.lane_offsets: center_lane = ref_line else: sample_distances = np.linspace(0.0, ref_line.length, int(ref_line.length / resolution) + 1) offsets = [] for offset_idx, offset in enumerate(self.lanes.lane_offsets): if offset_idx == len(self.lanes.lane_offsets) - 1: section_distances = sample_distances[offset.start_offset <= sample_distances] else: next_offset = self.lanes.lane_offsets[offset_idx + 1] indices = ((offset.start_offset <= sample_distances) & ( sample_distances < next_offset.start_offset)).nonzero()[0] section_distances = sample_distances[indices] coefficients = list(reversed(offset.polynomial_coefficients)) section_offset = np.polyval(coefficients, section_distances - offset.start_offset) offsets.append(section_offset) offsets = np.hstack(offsets) points = [] for i, d in enumerate(sample_distances): point = np.array(ref_line.interpolate(d).coords[0]) theta = normalise_angle(self.plan_view.calc(d)[1] + np.pi / 2) normal = np.array([np.cos(theta), np.sin(theta)]) points.append(tuple(point + offsets[i] * normal)) center_lane = LineString(ramer_douglas(points, dist=0.01)) if not center_lane.is_simple: coords_list = [] for non_intersecting_ls in unary_union(center_lane).geoms: if non_intersecting_ls.length > 0.5: coords_list.extend(non_intersecting_ls.coords) center_lane = LineString(coords_list) # Assign midlines for each center lane for every lane section for ls in self.lanes.lane_sections: lane = ls.center_lanes[0] lane._ref_line = substring(center_lane, ls.start_distance / ref_line.length, (ls.start_distance + ls.length) / ref_line.length, normalized=True)
@property def boundary(self): """ Get the outer boundary of the road with all lanes """ return self._boundary @property def elevation_profile(self) -> ElevationProfile: return self._elevation_profile @property def lateral_profile(self) -> LateralProfile: return self._lateral_profile @property def lanes(self) -> Lanes: """ Container object for all LaneSections of the road""" return self._lanes @property def drivable(self) -> bool: """ True if at least one lane is drivable in the road. """ return any([ls.drivable for ls in self.lanes.lane_sections]) @property def all_lane_forwards(self) -> bool: """ True if all lanes are forwards directed on the road (e.g., all-right lanes in right-handed traffic). """ return all([all([ll.id < 0 for ll in ls.all_lanes if ll.id != 0]) for ls in self.lanes.lane_sections]) @property def all_lane_backwards(self) -> bool: """ True if all lanes are backwards directed on the road (e.g., all-left lanes in right-handed traffic). """ return all([all([ll.id > 0 for ll in ls.all_lanes if ll.id != 0]) for ls in self.lanes.lane_sections])