map-machine/roentgen/road.py
2021-08-17 09:30:55 +03:00

378 lines
12 KiB
Python

"""
WIP: road shape drawing.
"""
from dataclasses import dataclass
from typing import List, Optional
import numpy as np
import svgwrite
from roentgen.flinger import Flinger
from roentgen.osm_reader import OSMNode
from roentgen.vector import Line, compute_angle, norm, turn_by_angle
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
@dataclass
class Lane:
"""
Road lane specification.
"""
width: Optional[float] = None # Width in meters
is_forward: Optional[bool] = None # Whether lane is forward or backward
min_speed: Optional[float] = None # Minimal speed on the lane
# "none", "merge_to_left", "slight_left", "slight_right"
turn: Optional[str] = None
change: Optional[str] = None # "not_left", "not_right"
destination: Optional[str] = None # Lane destination
def set_forward(self, is_forward: bool) -> None:
self.is_forward = is_forward
def get_width(self, scale: float):
"""Get lane width. We use standard 3.7 m lane."""
if self.width is None:
return 3.7 * scale
return self.width * scale
class RoadPart:
"""
Line part of the road.
"""
def __init__(
self,
point_1: np.array,
point_2: np.array,
lanes: List[Lane],
scale: False,
):
"""
:param point_1: start point of the road part
:param point_2: end point of the road part
:param lanes: lane specification
"""
self.point_1: np.array = point_1
self.point_2: np.array = point_2
self.lanes: List[Lane] = lanes
if lanes:
self.width = sum(map(lambda x: x.get_width(scale), lanes))
else:
self.width = 1
self.left_offset: float = self.width / 2
self.right_offset: float = self.width / 2
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
self.left_outer = None
self.right_outer = None
self.point_a = None
@classmethod
def from_nodes(
cls, node_1: OSMNode, node_2: OSMNode, flinger: Flinger, road, scale
) -> "RoadPart":
"""
Construct road part from OSM nodes.
"""
lanes = [Lane(road.width / road.lanes)] * road.lanes
return cls(
flinger.fling(node_1.coordinates),
flinger.fling(node_2.coordinates),
lanes,
scale,
)
def update(self) -> None:
"""
Compute additional points.
"""
if self.left_connection is not None:
self.right_projection = (
self.left_connection + self.right_vector - self.left_vector
)
if self.right_connection is not None:
self.left_projection = (
self.right_connection - self.right_vector + self.left_vector
)
if (
self.left_connection is not None
and self.right_connection is not None
):
a = np.linalg.norm(self.right_connection - self.point_1)
b = np.linalg.norm(self.right_projection - self.point_1)
if a > b:
self.right_outer = self.right_connection
self.left_outer = self.left_projection
else:
self.right_outer = self.right_projection
self.left_outer = self.left_connection
self.point_middle = self.right_outer - self.right_vector
max_: float = 100
if np.linalg.norm(self.point_middle - self.point_1) > max_:
self.point_a = self.point_1 + max_ * norm(
self.point_middle - self.point_1
)
self.right_outer = self.point_a + self.right_vector
self.left_outer = self.point_a + self.left_vector
else:
self.point_a = self.point_middle
def get_angle(self) -> float:
"""
Get an angle between line and x axis.
"""
return compute_angle(self.point_2 - self.point_1)
def draw_normal(self, drawing: svgwrite.Drawing):
"""
Draw some debug lines.
"""
line = drawing.path(
("M", self.point_1, "L", self.point_2),
fill="none",
stroke="#8888FF",
stroke_width=self.width,
)
drawing.add(line)
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,
) # fmt: skip
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,
) # fmt: skip
drawing.add(line)
opacity: float = 0.4
radius: float = 2
if self.right_connection is not None:
circle = drawing.circle(
self.right_connection, 2.5, fill="#FF0000", opacity=opacity
)
drawing.add(circle)
if self.left_connection is not None:
circle = drawing.circle(
self.left_connection, 2.5, fill="#0000FF", opacity=opacity
)
drawing.add(circle)
if self.right_projection is not None:
circle = drawing.circle(
self.right_projection, 1.5, fill="#FF0000", opacity=opacity
)
drawing.add(circle)
if self.left_projection is not None:
circle = drawing.circle(
self.left_projection, 1.5, fill="#0000FF", opacity=opacity
)
drawing.add(circle)
if self.right_outer is not None:
circle = drawing.circle(
self.right_outer,
3.5,
stroke_width=0.5,
fill="none",
stroke="#FF0000",
opacity=opacity,
)
drawing.add(circle)
if self.left_outer is not None:
circle = drawing.circle(
self.left_outer,
3.5,
stroke_width=0.5,
fill="none",
stroke="#0000FF",
opacity=opacity,
)
drawing.add(circle)
if self.point_a is not None:
circle = drawing.circle(self.point_a, radius, fill="#000000")
drawing.add(circle)
# self.draw_entrance(drawing, True)
def draw(self, drawing: svgwrite.Drawing):
"""
Draw road part.
"""
if self.left_connection is not None:
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",
] # fmt: skip
drawing.add(drawing.path(path_commands, fill="#CCCCCC"))
def draw_entrance(self, drawing: svgwrite.Drawing, is_debug: bool = False):
"""
Draw intersection entrance part.
"""
if (
self.left_connection is not None
and self.right_connection is not None
):
path_commands = [
"M", self.right_projection,
"L", self.right_connection,
"L", self.left_projection,
"L", self.left_connection,
"Z",
] # fmt: skip
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="#88FF88"))
def draw_lanes(self, drawing: svgwrite.Drawing, scale: float):
"""
Draw lane delimiters.
"""
for lane in self.lanes:
shift = self.right_vector - self.turned * lane.get_width(scale)
path = drawing.path(
["M", self.point_middle + shift, "L", self.point_2 + shift],
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: RoadPart = self.parts[index]
part_2: RoadPart = self.parts[next_index]
line_1: Line = Line(
part_1.point_1 + part_1.right_vector,
part_1.point_2 + part_1.right_vector,
)
line_2: Line = Line(
part_2.point_1 + part_2.left_vector,
part_2.point_2 + part_2.left_vector,
)
intersection: np.array = line_1.get_intersection_point(line_2)
# if np.linalg.norm(intersection - part_1.point_2) < 300:
part_1.right_connection = intersection
part_2.left_connection = intersection
part_1.update()
part_2.update()
for index in range(len(self.parts)):
next_index: int = 0 if index == len(self.parts) - 1 else index + 1
part_1: RoadPart = self.parts[index]
part_2: RoadPart = self.parts[next_index]
part_1.update()
part_2.update()
if (
part_1.right_connection is None
and part_2.left_connection is None
):
part_1.left_connection = part_1.right_projection
part_2.right_connection = part_2.left_projection
part_1.left_outer = part_1.right_projection
part_2.right_outer = part_2.left_projection
part_1.update()
part_2.update()
def draw(
self, drawing: svgwrite.Drawing, scale: float, is_debug: bool = False
) -> None:
"""
Draw all road parts and intersection.
"""
inner_commands = ["M"]
for part in self.parts:
inner_commands += [part.left_connection, "L"]
inner_commands[-1] = "Z"
outer_commands = ["M"]
for part in self.parts:
outer_commands += [part.left_connection, "L"]
outer_commands += [part.left_outer, "L"]
outer_commands += [part.right_outer, "L"]
outer_commands[-1] = "Z"
# for part in self.parts:
# part.draw_normal(drawing)
if is_debug:
drawing.add(
drawing.path(outer_commands, fill="#0000FF", opacity=0.2)
)
drawing.add(
drawing.path(inner_commands, fill="#FF0000", opacity=0.2)
)
for part in self.parts:
if is_debug:
part.draw_debug(drawing)
else:
part.draw_entrance(drawing)
if not is_debug:
# for part in self.parts:
# part.draw_lanes(drawing, scale)
drawing.add(drawing.path(inner_commands, fill="#FF8888"))