mirror of
https://github.com/enzet/map-machine.git
synced 2025-05-02 19:56:39 +02:00
248 lines
7.4 KiB
Python
248 lines
7.4 KiB
Python
"""
|
|
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"))
|