""" Road shape drawing. """ from dataclasses import dataclass from typing import List import numpy as np import svgwrite from shapely.geometry import LineString, Point from roentgen.flinger import Flinger, angle, turn_by_angle, norm from roentgen.osm_reader import OSMNode @dataclass class Lane: """ Road lane specification. """ width: float # Width in meters class RoadPart: """ Line part of the road. """ def __init__( self, point_1: np.array, point_2: np.array, left_offset: float, right_offset: float, lanes: List[Lane], ): """ :param point_1: start point of the road part :param point_2: end point of the road part :param left_offset: offset from the central line to the left border :param right_offset: offset from the central line to the right border :param lanes: lane specification """ self.point_1: np.array = point_1 self.point_2: np.array = point_2 self.left_offset: float = left_offset self.right_offset: float = right_offset self.lanes: List[Lane] = lanes self.turned: np.array = norm( turn_by_angle(self.point_2 - self.point_1, np.pi / 2) ) self.right_vector: np.array = self.turned * self.right_offset self.left_vector: np.array = -self.turned * self.left_offset self.right_connection: np.array = None self.left_connection: np.array = None self.right_projection: np.array = None self.left_projection: np.array = None @classmethod def from_nodes( cls, node_1: OSMNode, node_2: OSMNode, flinger: Flinger, left_offset: float, right_offset: float, lanes: List[Lane], ) -> "RoadPart": """ Construct road part from OSM nodes. """ return cls( flinger.fling(node_1.coordinates), flinger.fling(node_2.coordinates), left_offset, right_offset, lanes, ) def update(self) -> None: """ Compute additional points. """ if ( self.right_connection is not None and self.left_connection is not None ): self.right_projection = ( self.left_connection + self.right_vector - self.left_vector ) self.left_projection = ( self.right_connection - self.right_vector + self.left_vector ) def get_angle(self) -> float: """ Get an angle between line and x axis. """ return angle(self.point_2 - self.point_1) def draw_debug(self, drawing: svgwrite.Drawing): """ Draw some debug lines. """ line = drawing.path( ("M", self.point_1, "L", self.point_2), fill="none", stroke="#000000", ) drawing.add(line) line = drawing.path( ( "M", self.point_1 + self.right_vector, "L", self.point_2 + self.right_vector, ), fill="none", stroke="#FF0000", stroke_width=0.5, ) drawing.add(line) line = drawing.path( ( "M", self.point_1 + self.left_vector, "L", self.point_2 + self.left_vector, ), fill="none", stroke="#0000FF", stroke_width=0.5, ) drawing.add(line) if self.right_connection is not None: circle = drawing.circle(self.right_connection, 2) drawing.add(circle) if self.left_connection is not None: circle = drawing.circle(self.left_connection, 2) drawing.add(circle) if self.right_projection is not None: circle = drawing.circle(self.right_projection, 2, fill="#FF0000") drawing.add(circle) if self.left_projection is not None: circle = drawing.circle(self.left_projection, 2, fill="#0000FF") drawing.add(circle) self.draw_entrance(drawing, True) def draw(self, drawing: svgwrite.Drawing): """ Draw road part. """ path_commands = [ "M", self.point_2 + self.right_vector, "L", self.point_2 + self.left_vector, "L", self.left_connection, "L", self.right_connection, "Z", ] drawing.add(drawing.path(path_commands, fill="#CCCCCC")) self.draw_entrance(drawing, False) def draw_entrance(self, drawing: svgwrite.Drawing, is_debug: bool): path_commands = [ "M", self.right_projection, "L", self.right_connection, "L", self.left_projection, "L", self.left_connection, "Z", ] if is_debug: path = drawing.path( path_commands, fill="none", stroke="#880088", stroke_width=0.5 ) drawing.add(path) else: drawing.add(drawing.path(path_commands, fill="#BBBBBB")) def draw_lanes(self, drawing: svgwrite.Drawing): """ Draw lane delimiters. """ for lane in self.lanes: a = self.right_vector - self.turned * lane.width path = drawing.path( ["M", self.point_2 + a, "L", self.point_1 + a], fill="none", stroke="#FFFFFF", stroke_width=2, stroke_dasharray="7,7", ) drawing.add(path) class Intersection: """ An intersection of the roads, that is described by its parts. All first points of the road parts should be the same. """ def __init__(self, parts: List[RoadPart]): self.parts: List[RoadPart] = sorted(parts, key=lambda x: x.get_angle()) for index in range(len(self.parts)): next_index: int = 0 if index == len(self.parts) - 1 else index + 1 part_1 = self.parts[index] part_2 = self.parts[next_index] line_1 = LineString( [ part_1.point_1 + part_1.right_vector, part_1.point_2 + part_1.right_vector, ] ) line_2 = LineString( [ part_2.point_1 + part_2.left_vector, part_2.point_2 + part_2.left_vector, ] ) a = line_1.intersection(line_2) if isinstance(a, Point): part_1.right_connection = np.array((a.x, a.y)) part_2.left_connection = np.array((a.x, a.y)) part_1.update() part_2.update() def draw(self, drawing: svgwrite.Drawing, is_debug: bool): """ Draw all road parts and intersection. """ path_commands = ["M"] for part in self.parts: path_commands += [part.left_connection, "L"] path_commands[-1] = "Z" if is_debug: drawing.add(drawing.path(path_commands, fill="#DDFFDD")) for part in self.parts: if is_debug: part.draw_debug(drawing) else: part.draw(drawing) if not is_debug: for part in self.parts: part.draw_lanes(drawing) drawing.add(drawing.path(path_commands, fill="#AAAAAA"))