Issue #83: add preliminary support for placement.

This commit is contained in:
Sergey Vartanov 2021-11-08 00:18:31 +03:00
parent edfdd386cf
commit 3b83cf15e6
2 changed files with 113 additions and 49 deletions

View file

@ -1,6 +1,7 @@
"""
WIP: road shape drawing.
"""
import logging
from dataclasses import dataclass
from typing import Any, Optional, Union
@ -75,15 +76,18 @@ class RoadPart:
self.point_1: np.ndarray = point_1
self.point_2: np.ndarray = point_2
self.lanes: list[Lane] = lanes
self.width: float
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.width = 1.0
self.left_offset: float = self.width / 2.0
self.right_offset: float = self.width / 2.0
self.turned: np.ndarray = norm(
turn_by_angle(self.point_2 - self.point_1, np.pi / 2)
turn_by_angle(self.point_2 - self.point_1, np.pi / 2.0)
)
self.right_vector: np.ndarray = self.turned * self.right_offset
self.left_vector: np.ndarray = -self.turned * self.left_offset
@ -122,7 +126,7 @@ class RoadPart:
self.left_outer = self.left_connection
self.point_middle = self.right_outer - self.right_vector
max_: float = 100
max_: float = 100.0
if np.linalg.norm(self.point_middle - self.point_1) > max_:
self.point_a = self.point_1 + max_ * norm(
@ -385,6 +389,8 @@ class Road(Tagged):
self.width: Optional[float] = matcher.default_width
self.lanes: list[Lane] = []
self.scale: float = flinger.get_scale(self.nodes[0].coordinates)
if "lanes" in tags:
try:
self.width = int(tags["lanes"]) * 3.7
@ -417,10 +423,36 @@ class Road(Tagged):
except ValueError:
pass
self.layer: float = 0
self.layer: float = 0.0
if "layer" in tags:
self.layer = float(tags["layer"])
self.placement_offset: float = 0.0
if "placement" in tags:
value: str = tags["placement"]
if ":" in value:
place, lane = value.split(":")
lane_number: int = int(lane)
self.placement_offset = (
sum(
x.get_width(self.scale)
for x in self.lanes[: lane_number - 1]
)
- self.width * self.scale / 2
)
if place == "left_of":
pass
elif place == "middle_of":
self.placement_offset += (
self.lanes[lane_number - 1].get_width(self.scale) * 0.5
)
elif place == "right_of":
self.placement_offset += self.lanes[
lane_number - 1
].get_width(self.scale)
else:
logging.error(f"Unknown placement `{place}`.")
def get_style(
self, flinger: Flinger, is_border: bool, is_for_stroke: bool = False
) -> dict[str, Union[int, float, str]]:
@ -457,7 +489,7 @@ class Road(Tagged):
"stroke-width": scale * width + extra_width + border_width,
}
if is_for_stroke:
style["stroke-width"] = 2 + extra_width
style["stroke-width"] = 2.0 + extra_width
if is_border and self.tags.get("embankment") == "yes":
style["stroke-dasharray"] = "1,3"
if self.tags.get("tunnel") == "yes":
@ -485,7 +517,7 @@ class Road(Tagged):
style: dict[str, Union[int, float, str]] = self.get_style(
flinger, is_border
)
path_commands: str = self.line.get_path()
path_commands: str = self.line.get_path(self.placement_offset)
path: Path
if filter_:
path = Path(d=path_commands, filter=filter_.get_funciri())
@ -513,18 +545,18 @@ class Road(Tagged):
color = Color("#666666")
return color
def draw_lanes(self, svg: Drawing, flinger: Flinger, color: Color) -> None:
def draw_lanes(self, svg: Drawing, color: Color) -> None:
"""Draw lane separators."""
scale: float = flinger.get_scale(self.nodes[0].coordinates)
if len(self.lanes) < 2:
return
for index in range(1, len(self.lanes)):
parallel_offset: float = scale * (
lane_offset: float = self.scale * (
-self.width / 2 + index * self.width / len(self.lanes)
)
path: Path = Path(d=self.line.get_path(parallel_offset))
path: Path = Path(
d=self.line.get_path(self.placement_offset + lane_offset)
)
style: dict[str, Any] = {
"fill": "none",
"stroke": color.hex,
@ -541,7 +573,9 @@ class Road(Tagged):
if not name:
return
path: Path = svg.path(d=self.line.get_path(3), fill="none")
path: Path = svg.path(
d=self.line.get_path(self.placement_offset + 3), fill="none"
)
svg.add(path)
text = svg.add(svg.text.Text(""))
@ -558,7 +592,12 @@ class Road(Tagged):
def get_curve_points(
road: Road, scale: float, center: np.ndarray, road_end: np.ndarray
road: Road,
scale: float,
center: np.ndarray,
road_end: np.ndarray,
placement_offset: float,
is_end: bool,
) -> list[np.ndarray]:
"""
:param road: road segment
@ -568,11 +607,17 @@ def get_curve_points(
"""
width: float = road.width / 2.0 * scale
direction: np.ndarray = (road_end - center) / np.linalg.norm(
road_end - center
direction: np.ndarray = (center - road_end) / np.linalg.norm(
center - road_end
)
if is_end:
direction = -direction
left: np.ndarray = turn_by_angle(direction, np.pi / 2.0) * (
width + placement_offset
)
right: np.ndarray = turn_by_angle(direction, -np.pi / 2.0) * (
width - placement_offset
)
left: np.ndarray = turn_by_angle(direction, np.pi / 2.0) * width
right: np.ndarray = turn_by_angle(direction, -np.pi / 2.0) * width
return [road_end + left, center + left, center + right, road_end + right]
@ -647,9 +692,7 @@ class SimpleConnector(Connector):
class ComplexConnector(Connector):
"""
Connection between two roads that change width.
"""
"""Connection between two roads that change width."""
def __init__(
self,
@ -670,28 +713,39 @@ class ComplexConnector(Connector):
point: np.ndarray = flinger.fling(node.coordinates)
points_1: list[np.ndarray] = get_curve_points(
self.road_1, scale, point, self.road_1.line.points[self.index_1]
self.road_1,
scale,
point,
self.road_1.line.points[self.index_1],
self.road_1.placement_offset,
self.index_1 != 0,
)
points_2: list[np.ndarray] = get_curve_points(
self.road_2, scale, point, self.road_2.line.points[self.index_2]
self.road_2,
scale,
point,
self.road_2.line.points[self.index_2],
self.road_2.placement_offset,
self.index_2 != 0,
)
# fmt: off
self.curve_1: PathCommands = [
points_1[0], "C", points_1[1], points_2[2], points_2[3]
points_1[0], "C", points_1[1], points_2[1], points_2[0]
]
self.curve_2: PathCommands = [
points_2[0], "C", points_2[1], points_1[2], points_1[3]
points_2[3], "C", points_2[2], points_1[2], points_1[3]
]
# fmt: on
def draw(self, svg: Drawing) -> None:
def draw(self, svg: Drawing, draw_circle: bool = False) -> None:
"""Draw connection fill."""
if draw_circle:
for road, index in [
(self.road_1, self.index_1),
(self.road_2, self.index_2),
]:
circle: Circle = svg.circle(
road.line.points[index],
road.line.points[index] - road.placement_offset,
road.width * self.scale / 2,
fill=road.get_color(),
)
@ -757,9 +811,7 @@ class SimpleIntersection(Connector):
class Roads:
"""
Whole road structure.
"""
"""Whole road structure."""
def __init__(self) -> None:
self.roads: list[Road] = []
@ -807,8 +859,9 @@ class Roads:
else:
connector = ComplexConnector(connected, flinger, scale)
else:
# We can also use SimpleIntersection(connected, flinger, scale)
# here.
continue
# connector = SimpleIntersection(connected, flinger, scale)
if connector.min_layer not in layered_connectors:
layered_connectors[connector.min_layer] = []
@ -828,20 +881,26 @@ class Roads:
else:
connectors = []
# Draw borders.
for road in roads:
road.draw(svg, flinger, True)
for connector in connectors:
if connector.min_layer == layer:
connector.draw_border(svg)
# Draw inner parts.
for road in roads:
road.draw(svg, flinger, False)
for connector in connectors:
if connector.max_layer == layer:
connector.draw(svg)
# Draw lane separators.
for road in roads:
road.draw_lanes(svg, flinger, road.matcher.border_color)
road.draw_lanes(svg, road.matcher.border_color)
if draw_captions:
for road in self.roads:

View file

@ -51,9 +51,14 @@ class Polyline:
def get_path(self, parallel_offset: float = 0) -> str:
"""Construct SVG path commands."""
points: list[np.ndarray]
if np.allclose(parallel_offset, 0):
points = self.points
else:
try:
points = (
LineString(self.points).parallel_offset(parallel_offset).coords
LineString(self.points)
.parallel_offset(parallel_offset)
.coords
if parallel_offset
else self.points
)